The intent of this builder is to create a Windows OVA from a Windows ISO fully scripted and without any user interaction. This includes the automation to configure network settings and enable the configuration and services to allow ssh or win-rm based automation tools to finalize customization.
This builder is entirely programatic. This builder starts with a plain Windows 2019 ISO and customizes from there.
Features:
- Formats: Creates both OVF and OVA files.
- Duplicate SID Handling: Sysprep automatically ran on initial boot of the OVA deployed Virtual machine. This regenerates the Windows SID to avoid issues when joining to a domain.
- OVF Deployment Options: OVF modified to include options to allow setting the IP, subnet, gateway, name servers, and domain name, when deploying into VMware. This can be utilized by either filling in the form when importing the OVA or with injecting OVF properties with tools such as Ansible's "vmware_deploy_ovf" module or with CLI tools such as govc.
- OVF Property Monitoring: A script set to run on boot that checks the OVF properties. It will automatically apply changes on the initial boot, or when VApp properties have changed post-deployment.
- Ansible compatibility:
- Win RM Hotfix KB2842230 applied
- Win RM configured to allow Ansible connections
- Remote Access:
- Remote Desktop Services are enabled to allow connection through Microsoft Windows Terminal Services Client.
- OpenSSH Service enabled during first boot sysprep to ensure non-duplicate ssh host keys.
Requires the following in your <path>
:
- Packer CLI Tool: Packer is a image creation tool by HashiCorp that easily allows scripting of image builds. (v1.5.5 is required for "vcenter" provisioner)
- VMWare ovftool: Tool from VMware that converts between various Virtual Machine formats.
You must downloaded the required Windows 2019 ISO image for the packer script
to work. The path to this is specified in the packer vars file (vars.json
).
Samples for the config files are found in the project folder with the filename
suffix _sample
.
The following config files must be created before running.
- Image configuration can be found in file(s):
vars.json
- Provisioner configuration can be found in file(s):
<provisioner>-config.json
(local does not require a provisioner).
The image type created is defined in the autounattend.xml
file.
Known types are:
- Windows Server 2019 SERVERDATACENTER (default)
- Windows Server 2019 SERVERDATACENTERCORE
- Windows Server 2019 SERVERSTANDARD
- Windows Server 2019 SERVERSTANDARDCORE
This can be changed in the following section of the autounattend.xml
file.
<InstallFrom>
<MetaData wcm:action="add">
<Key>/IMAGE/NAME</Key>
<Value>Windows Server 2019 SERVERDATACENTER</Value>
</MetaData>
</InstallFrom>
The Windows License key can be specified in the autounattend.xml
file. This
can be updated in the following section:
<UserData>
<AcceptEula>true</AcceptEula>
<ProductKey>
<!-- <Key>11111-22222-33333-44444-55555</Key> -->
<WillShowUI>Never</WillShowUI>
</ProductKey>
</UserData>
By default the license is not defined. This causes the image to built with a evaluation license of 180 days. This evaluation period begins when the OVA/OVF is deployed as a VM and NOT when the OVA is initially generated.
This allows short term environments to be stood up for lab and test purposes.
To verify how much time is left on the trial, run the following commands from powershell:
Get-CimInstance SoftwareLicensingProduct
Look for GracePeriodRemaining
. This is the number of minutes remaining.
[...]
GracePeriodRemaining : 259166
[...]
This script modifies the OVF to include options to allow setting the IP, subnet, gateway, name servers, and domain name when deploying into VMware. This can be utilized by either filling in the form when importing the OVA or when using OVF properties of tools such as the Ansible "vmware_deploy_ovf" module or the GOVC command line tool. (see examples at end)
This allows the Hypervisor to include a form to configure network properties when deploying the OVF/OVA.
If this OVF/OVA is deployed to a VMWare VCenter managed environment, it is possible to change the VApp properties of the Virtual Machine after it has been deployed.
When this happens, this will be seen by the Virtual Machine on its next reboot. Once it sees a change, it will apply it and automatically reboot. Changing the Network settings from within the Virtual Machine manually when using these properties will trigger them to revert to the OVF specified settings on the next reboot.
A bash script is provided that runs the packer scripts and appends the xml properties into the OVF file. When complete it uses ovftool to create a OVA file.
This script is compatible with the 3 following VMware based provisioners:
- local - VMWare workstation, fusion, or player
- esxi - Non-vCenter managed ESXi host
- vcenter - vCenter
To use the bash script:
./build.sh <provisioner>
The generated OVF / OVA can be found in the directory: __windows_server_2019
./build.sh esxi
When a Virtual machine is created using the OVF/OVA generated by this script, it will perform 2-3 reboots.
The first reboot performs the sysprep which regenerates the Windows SID and initializes the OpenSSH service. When this is complete, it will reboot again. While performing these operations, the VM will be using a DHCP acquired address.
If OVF properties were specified when the Virtual Machine was created, a 3rd reboot will happen after these settings are applied.
- Username: Administrator
- Password: PaSsWoRd@1234
Note: This password is intended for the initial connection by your provisioning tools. This ideally should be changed with those tools.
If you want to change this default password in the build scripts, you will need to modify the following files:
- autounattend.xml
- sysprep.xml
- esxi-provision.json
- local-provision.json
- vcenter-provision.json
The following examples demonstrate how to deploy the generated OVA using either Ansible or GOVC.
tasks/main.yml
---
- name: Get status of deployed Virtual Machines
local_action:
module: vmware_vm_info
hostname: "{{esxi_hostname}}"
username: "{{esxi_username}}"
password: "{{esxi_password}}"
validate_certs: false
vm_type: vm
register: vm_info
- name: Deploy Windows 2019 Server
when: "win_vm_name not in (vm_info.virtual_machines | map(attribute='guest_name'))"
local_action:
module: vmware_deploy_ovf
name: "{{win_vm_name}}"
hostname: "{{esxi_hostname}}"
username: "{{esxi_username}}"
password: "{{esxi_password}}"
validate_certs: false
disk_provisioning: "{{esxi_disk_provisioning}}"
ovf: "{{win_ova_path}}"
networks:
VM Network: "{{esxi_network}}"
datastore: "{{esxi_datastore}}"
fail_on_spec_warnings: true
allow_duplicates: false
power_on: false
inject_ovf_env: true
properties:
vami.hostname.windows_server_2019: "{{win_hostname}}"
vami.ip0.windows_server_2019: "{{win_ip_address}}"
vami.netmask0.windows_server_2019: "{{win_ip_prefix}}"
vami.gateway.windows_server_2019: "{{win_gateway}}"
vami.dns.windows_server_2019: "{{win_dns_servers}}"
vami.domain.windows_server_2019: "{{win_domain_name}}"
vami.searchpath.windows_server_2019: "{{win_domain_name}}"
- name: Resize Windows 2019 Server
when: "win_vm_name not in (vm_info.virtual_machines | map(attribute='guest_name'))"
local_action:
module: vmware_guest
name: "{{win_vm_name}}"
hostname: "{{esxi_hostname}}"
username: "{{esxi_username}}"
password: "{{esxi_password}}"
validate_certs: false
state: present
hardware:
memory_mb: "{{win_hw_memory}}"
num_cpus: "{{win_hw_cpus}}"
- name: Power On Windows 2019 Server
when: "win_vm_name not in (vm_info.virtual_machines | map(attribute='guest_name'))"
local_action:
module: vmware_guest
name: "{{win_vm_name}}"
hostname: "{{esxi_hostname}}"
username: "{{esxi_username}}"
password: "{{esxi_password}}"
validate_certs: false
state: poweredon
wait_for_ip_address: false
- name: "Waiting for Windows 2019 Server VM to finish configuration..."
local_action:
module: wait_for
host: "{{win_ip_address}}"
port: 22
state: started
delay: 5
sleep: 10
timeout: 600
vars/main.yml
# ESXi/vCenter API configuration
esxi_username: [email protected]
esxi_password: password1
esxi_hostname: vcenter.lab.local
esxi_datastore: "datastore1"
esxi_network: "VM Network"
esxi_disk_provisioning: thin
# win vm options
win_vm_name: windows2019
win_ova_path: /path/to/windows_server_2019.ova
win_hw_cpus: 2
win_hw_memory: 2048
# win platform options
win_admin_password: PaSsWoRd@1234
# win networking configuration
win_hostname: winsrv01
win_ip_address: 192.168.10.10
win_ip_prefix: "24"
win_gateway: 192.168.10.1
win_dns_servers: 8.8.8.8,8.8.4.4
win_domain_name: lab.local
Note: Requires govc, python, jq, and nc (netcat) to be available in path.
#!/usr/bin/env bash
set -e
GOVC_INSECURE=true
GOVC_URL=vcenter.lab.local
[email protected]
GOVC_PASSWORD=password1
GOVC_DATASTORE=datastore1
GOVC_NETWORK="VM Network"
GOVC_DATACENTER=ha-datacenter
VM_OVA=/path/to/windows_server_2019.ova
VM_NAME=windows2019
VM_HOSTNAME=winsrv01
VM_IPADDR=192.168.10.10
VM_NETMASK=255.255.255.0
VM_GATEWAY=192.168.10.1
VM_DNS=8.8.8.8,8.8.4.4
VM_DOMAIN=lab.local
VM_SEARCHPATH=lab.local
# create json spec
JSON_SPEC=$(govc import.spec ${VM_OVA} | python -m json.tool)
# {
# "DiskProvisioning": "flat",
# "IPAllocationPolicy": "dhcpPolicy",
# "IPProtocol": "IPv4",
# "PropertyMapping": [
# {
# "Key": "vami.hostname.windows_server_2019",
# "Value": ""
# },
# {
# "Key": "vami.ip0.windows_server_2019",
# "Value": ""
# },
# {
# "Key": "vami.netmask0.windows_server_2019",
# "Value": ""
# },
# {
# "Key": "vami.gateway.windows_server_2019",
# "Value": ""
# },
# {
# "Key": "vami.dns.windows_server_2019",
# "Value": ""
# },
# {
# "Key": "vami.domain.windows_server_2019",
# "Value": ""
# },
# {
# "Key": "vami.searchpath.windows_server_2019",
# "Value": ""
# }
# ],
# "NetworkMapping": [
# {
# "Name": "VM Network",
# "Network": ""
# }
# ],
# "MarkAsTemplate": false,
# "PowerOn": false,
# "InjectOvfEnv": false,
# "WaitForIP": false,
# "Name": null
# }
# update json spec
JSON_SPEC=$(jq '.DiskProvisioning = "thin"' <<<"${JSON_SPEC}")
JSON_SPEC=$(jq '.Name = "'"${VM_NAME}"'"' <<<"${JSON_SPEC}")
JSON_SPEC=$(jq '.InjectOvfEnv = true' <<<"${JSON_SPEC}")
# JSON_SPEC=$(jq '.WaitForIP = false' <<<"${JSON_SPEC}")
# JSON_SPEC=$(jq '.PowerOn = false' <<<"${JSON_SPEC}")
JSON_SPEC=$(jq '(.NetworkMapping[] | select(.Name == "VM Network") | .Network) |= "'"${GOVC_NETWORK}"'"' <<<"${JSON_SPEC}")
JSON_SPEC=$(jq '(.PropertyMapping[] | select(.Key == "vami.hostname.windows_server_2019") | .Value) |= "'"${VM_HOSTNAME}"'"' <<<"${JSON_SPEC}")
JSON_SPEC=$(jq '(.PropertyMapping[] | select(.Key == "vami.ip0.windows_server_2019") | .Value) |= "'"${VM_IPADDR}"'"' <<<"${JSON_SPEC}")
JSON_SPEC=$(jq '(.PropertyMapping[] | select(.Key == "vami.netmask0.windows_server_2019") | .Value) |= "'"${VM_NETMASK}"'"' <<<"${JSON_SPEC}")
JSON_SPEC=$(jq '(.PropertyMapping[] | select(.Key == "vami.gateway.windows_server_2019") | .Value) |= "'"${VM_GATEWAY}"'"' <<<"${JSON_SPEC}")
JSON_SPEC=$(jq '(.PropertyMapping[] | select(.Key == "vami.dns.windows_server_2019") | .Value) |= "'"${VM_DNS}"'"' <<<"${JSON_SPEC}")
JSON_SPEC=$(jq '(.PropertyMapping[] | select(.Key == "vami.domain.windows_server_2019") | .Value) |= "'"${VM_DOMAIN}"'"' <<<"${JSON_SPEC}")
JSON_SPEC=$(jq '(.PropertyMapping[] | select(.Key == "vami.searchpath.windows_server_2019") | .Value) |= "'"${VM_SEARCHPATH}"'"' <<<"${JSON_SPEC}")
# write json spec to temp location
JSON_SPEC_PATH="/tmp/${VM_NAME}_spec.json"
jq '.' <<<${JSON_SPEC} > ${JSON_SPEC_PATH}
# import ova with spec
govc import.ova -options=${JSON_SPEC_PATH} ${VM_OVA}
# power on vm
govc vm.power -on=true ${VM_NAME}
# wait for host to finish configuration
spin='-\|/'
i=0
until $(nc -4 -G 1 -z ${VM_IPADDR} 22 &> /dev/null); do
i=$(( (i 1) %4 ))
consolef.info "Waiting on ${VM_IPADDR}:22 to become reachable... ${spin:$i:1}"
sleep 1
done
# cleanup temp spec file
rm -rf ${JSON_SPEC_PATH}
Copyright (c) 2020
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.