Autopatching Windows Templates with ansible
The first thing I have been working on is the autopatching of our golden images that we use for our workflows where we create new virtual machines.
So I thought that I should go through that process now in detail. In this post, we will cover the Nutanix Windows Workflow in detail, and the key differences for the VMware workflow.
Nutanix Windows workflow
Here is a breakdown of the general workflow for Windows Templates on Nutanix:
I will cover the most important steps in detail below from the different plays.

I will cover the most important steps in detail below from the different plays
Play 1 - Start and clone
First step: Gather facts about template to patch
### Set template name and product key based on version ###
- name: "Set template name for Windows 2022"
ansible.builtin.set_fact:
template_vm_name: "tmpl-win22-ansible"
when: template_os_version == "2022"
- name: "Set template name for Windows 2025"
ansible.builtin.set_fact:
template_vm_name: "tmpl-win25-ansible"
when: template_os_version == "2025"
### Get template info ###
- name: "Nutanix: Get template VM info"
nutanix.ncp.ntnx_vms_info:
filter:
vm_name: "{{ template_vm_name }}"
kind: vm
register: template_infoWe set the template VM to update based on extra_var 2025 or 2022, and gather facts about it using the nutanix.ncp.ntnx_vms_info Ansible module.
Second step: Prepare Unattend file
After we know what VM to update and everything, we prepare some files needed.
### Prepare temporary unattend.xml file ###
- name: "Nutanix: Prepare temporary unattend file"
ansible.builtin.tempfile:
state: file
suffix: unattend-{{ temp_vm_name }}.xml
register: unattend_xml
### Create Unattend file with WinRM HTTPS for staging network ###
- name: "Nutanix: Create Unattended file with WinRM HTTPS (staging)"
ansible.builtin.template:
src: "unattend_addons.xml"
dest: "{{ unattend_xml.path }}"We use ansible.builtin.tempfile and template modules. The template for unattend.xml has these important steps:
<AutoLogon>
<Password>
<Value>{{ local_pw.password }}</Value>
</Password>
<Enabled>true</Enabled>
<LogonCount>1</LogonCount>
<Username>Administrator</Username>
</AutoLogon>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Enable-PSRemoting -Force"</CommandLine>
<Description>Enable WinRM</Description>
<Order>1</Order>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "New-SelfSignedCertificate -DnsName {{ new_vm_name }} -CertStoreLocation Cert:\LocalMachine\My | Select-Object -ExpandProperty Thumbprint | Out-File C:\cert-thumbprint.txt -Encoding ASCII"</CommandLine>
<Description>Create Certificate</Description>
<Order>2</Order>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "$thumb = (Get-Content C:\cert-thumbprint.txt).Trim(); New-WSManInstance -ResourceURI winrm/config/Listener -SelectorSet @{Address='*';Transport='HTTPS'} -ValueSet @{Hostname='{{ new_vm_name }}';CertificateThumbprint=$thumb}"</CommandLine>
<Description>Create HTTPS Listener</Description>
<Order>3</Order>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "New-NetFirewallRule -DisplayName Temp-WINRM-HTTPS -Name Temp-WINRM-HTTPS-In-TCP -Direction Inbound -LocalPort 5986 -Protocol TCP -Action Allow -Profile Any"</CommandLine>
<Description>Open WinRM HTTPS Firewall</Description>
<Order>4</Order>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "New-NetFirewallRule -DisplayName Temp-ICMP-Echo -Name Temp-ICMP-Echo-In -Direction Inbound -Protocol ICMPv4 -IcmpType 8 -Action Allow -Profile Any"</CommandLine>
<Description>Allow ICMP Echo (Ping)</Description>
<Order>5</Order>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<CommandLine>slmgr.vbs //b /ato</CommandLine>
<Description>Activate Windows</Description>
<Order>6</Order>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<CommandLine>shutdown /r /t 10</CommandLine>
<Description>Reboot after 10 seconds</Description>
<Order>7</Order>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
</FirstLogonCommands>We autologon to the VM and prepare the VM for WinRM actions to be able to patch things.
Third step: Wait for Network connection and WINRM to be ready.
After the files are prepared, we clone the VM to patch and wait for WINRM to become stable.
### Clone template WITH sysprep to staging network ###
- name: "Nutanix: Clone template to temporary VM with sysprep"
nutanix.ncp.ntnx_vms_clone:
src_vm_uuid: "{{ template_uuid }}"
vcpus: 8
cores_per_vcpu: 1
memory_gb: 24
name: "{{ temp_vm_name }}"
networks:
- is_connected: true
subnet:
uuid: "910a545f-16b7-4537-a32f-344878143adb"
guest_customization:
type: "sysprep"
script_path: "{{ unattend_xml.path }}"
timezone: Europe/Stockholm
register: temp_vm_clone
### Monitor clone creation ###
- name: "Nutanix: Monitor clone creation status"
ansible.builtin.assert:
that:
- temp_vm_clone.response is defined
- temp_vm_clone.response.status.state == 'COMPLETE'
### Power on the VM ###
- name: "Nutanix: Power on temp VM"
nutanix.ncp.ntnx_vms:
state: power_on
vm_uuid: "{{ temp_vm_clone.vm_uuid }}"
### Fetch VM IP address from Nutanix (wait for valid IP, not link-local) ###
- name: "Nutanix Windows: Get VM IP address (with retry)"
nutanix.ncp.ntnx_vms_info:
vm_uuid: "{{ temp_vm_clone.vm_uuid }}"
register: vm_info
until:
- vm_info.response.status.resources.nic_list is defined
- vm_info.response.status.resources.nic_list | length > 0
- vm_info.response.status.resources.nic_list[0].ip_endpoint_list is defined
- vm_info.response.status.resources.nic_list[0].ip_endpoint_list | length > 0
- (vm_info.response.status.resources.nic_list[0].ip_endpoint_list | map(attribute='ip') | reject('match', '^169\\.254\\..*') | list | length) > 0
retries: 15
delay: 20
- name: "Nutanix: Set Temp VM IP Fact"
ansible.builtin.set_fact:
temp_vm_ip: "{{ temp_vm_ip_address }}"
### Wait for WinRM to be ready ###
- name: "Nutanix: Wait for WinRM HTTPS port to be available"
ansible.builtin.wait_for:
host: "{{ temp_vm_ip }}"
port: 5986
state: started
timeout: 600
delay: 30So here we wait for the VM to boot, sysprep to configure the VM, and then we extract the VM IP details using the nutanix.ncp.ntnx_vms_info module.
Lastly, but not least, we add the temp VM to our inventory.
### Add temp VM to in-memory inventory for patching ###
- name: "Add temp VM to inventory for Windows Updates"
ansible.builtin.add_host:
name: "{{ temp_vm_ip }}"
groups: windows_temp_patch
ansible_host: "{{ temp_vm_ip }}"
ansible_user: "Administrator"
ansible_password: "{{ local_pw.password }}"
ansible_connection: winrm
ansible_winrm_transport: ntlm
ansible_winrm_server_cert_validation: ignore
ansible_port: 5986Play 2 - Patching
In the second play we connect to the Temp VM and use ansible built in tools to patch the VM:
First step: Wait for WINRM to become stable
### Wait for WinRM port to be truly open (delegate to localhost) ###
- name: "Wait for WinRM port to be available after Play 1"
ansible.builtin.wait_for:
host: "{{ ansible_host }}"
port: 5986
state: started
timeout: 900
delay: 10
delegate_to: localhost
### Wait for Windows to stabilize after sysprep - with retry on reboot ###
- name: "Windows: Wait for stable WinRM (handle unexpected reboots)"
block:
- name: "Windows: Test WinRM connection (wait for VM boot)"
ansible.windows.win_ping:
register: winrm_ready
retries: 60
delay: 5
until: winrm_ready is succeeded
ignore_unreachable: yes
- name: "Windows: Verify WinRM stability (10 pings in a row)"
ansible.windows.win_ping:
loop: "{{ range(1, 11) | list }}"
loop_control:
pause: 3
label: "Ping {{ item }}/10" Here we assert that the WINRM connection works and is stable using the ansible.windows.win_ping module.
Second step: Patching
Once the WINRM connection to the Temp VM is stable, we install the latest VirtIO drivers using the following method:
### Install VirtIO drivers before Windows Updates ###
- name: "Windows: Copy VirtIO drivers MSI to VM"
ansible.windows.win_copy:
src: files/virtio/virtio-win-latest-x64.msi
dest: C:\Windows\Temp\virtio-win-latest-x64.msi
- name: "Windows: Install VirtIO drivers (may cause reboot)"
block:
- name: "Windows: Run VirtIO installation"
ansible.windows.win_package:
path: C:\Windows\Temp\virtio-win-latest-x64.msi
state: present
arguments: /quiet /norestart
register: virtio_install
ignore_unreachable: yes
async: 900
poll: 30
- name: "Windows: Check if VirtIO installation completed"
ansible.builtin.fail:
msg: "VirtIO installation failed or timed out"
when: virtio_install is failed or virtio_install is unreachable
rescue:
- name: "Windows: VirtIO may have triggered reboot - waiting for VM"
ansible.builtin.debug:
msg: "VirtIO installation may have rebooted VM - waiting for it to come back..."
- name: "Windows: Wait for WinRM port after VirtIO reboot"
ansible.builtin.wait_for:
host: "{{ ansible_host }}"
port: 5986
state: started
timeout: 900
delay: 10
delegate_to: localhost
- name: "Windows: Re-establish WinRM after VirtIO reboot"
ansible.windows.win_ping:
register: winrm_after_virtio
retries: 40
delay: 30
until: winrm_after_virtio is succeeded
ignore_unreachable: yes
- name: "Windows: Set VirtIO install as changed after recovery"
ansible.builtin.set_fact:
virtio_install:
changed: true
reboot_required: falseWe copy the VirtIO .msi to the temp VM and install it using ansible.windows.win_package, verify, reboot if needed and then wait for WINRM to become stable again. Then on to Windows Updates:
### Install Windows Updates with automatic retry on failure ###
- name: "Windows: Install all updates (loop until none remain)"
block:
- name: "Windows: Search and install all updates"
ansible.windows.win_updates:
category_names:
- SecurityUpdates
- CriticalUpdates
- UpdateRollups
- Updates
state: installed
reboot: true
reboot_timeout: 3600
register: update_result
until: update_result.found_update_count == 0
retries: 10
delay: 30
rescue:
- name: "Windows: Update failed - Reset Windows Update components"
ansible.builtin.debug:
msg: "Windows Update failed, resetting components and retrying..."
- name: "Windows: Reset Windows Update on failure"
ansible.windows.win_shell: |
Write-Output "ERROR detected - Resetting Windows Update components..."
Stop-Service -Name wuauserv, cryptSvc, bits, msiserver -Force -ErrorAction SilentlyContinue
if (Test-Path "C:\Windows\SoftwareDistribution.old2") {
Remove-Item -Path "C:\Windows\SoftwareDistribution.old2" -Recurse -Force -ErrorAction SilentlyContinue
}
if (Test-Path "C:\Windows\SoftwareDistribution.old") {
Rename-Item -Path "C:\Windows\SoftwareDistribution.old" -NewName "SoftwareDistribution.old2" -ErrorAction SilentlyContinue
}
Rename-Item -Path "C:\Windows\SoftwareDistribution" -NewName "SoftwareDistribution.old" -ErrorAction SilentlyContinue
Start-Service -Name wuauserv, cryptSvc, bits, msiserver -ErrorAction SilentlyContinue
Start-Sleep -Seconds 10
Write-Output "Reset complete - retrying updates"
register: wu_reset_rescue
- name: "Windows: Retry updates after reset"
ansible.windows.win_updates:
category_names:
- SecurityUpdates
- CriticalUpdates
- UpdateRollups
- Updates
state: installed
reboot: true
reboot_timeout: 3600
register: update_result
until: update_result.found_update_count == 0
retries: 10
delay: 30
### Clean up temp files ###
- name: "Windows: Cleanup temporary files"
ansible.windows.win_shell: |
Write-Output "Cleaning temporary files..."
Stop-Service -Name wuauserv -Force
Remove-Item -Path C:\Windows\SoftwareDistribution\Download\* -Recurse -Force -ErrorAction SilentlyContinue
Start-Service -Name wuauserv
Remove-Item -Path $env:TEMP\* -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path C:\Windows\Temp\* -Recurse -Force -ErrorAction SilentlyContinue
wevtutil cl Application
wevtutil cl System
Write-Output "Cleanup completed"
register: cleanup_resultWe use a rescue logic if there are some problems with the installation so that Ansible can try again if needed, then we clean up WinUpdate temp files using the ansible.windows.win_shell module.
Third step: Prepare for cleanup
After we're done with the patching, we prepare for cleanup. Here we had to think this through. We faced a problem: we would like to clean up the VM from all the WinRM things that we set during sysprep—we do not want WinRM enabled going into production. And we cannot clean WINRM settings using WINRM, then we will cut the branch that we're sitting on and lose control of the VM, and we could not trigger sysprep.
So we solved that with a two-step fix. First, we create a self-deleting scheduled task using win_shell:
### Schedule sysprep to run on next boot ###
- name: "Windows: Schedule sysprep to run at next startup (self-deleting)"
ansible.windows.win_shell: |
Write-Output "Creating scheduled task for sysprep..."
# Create wrapper script that runs at next boot
$wrapperContent = "Unregister-ScheduledTask -TaskName 'GDM-AutoSysprep' -Confirm:`$false`n"
$wrapperContent += "Start-Process -FilePath 'C:\Windows\System32\Sysprep\sysprep.exe' -ArgumentList '/generalize /oobe /shutdown /quiet' -Wait"
# Save wrapper-script
$wrapperContent | Out-File -FilePath "C:\Windows\Temp\gdm-sysprep-once.ps1" -Encoding UTF8 -Force
# Create action that runs the wrapper-scriptet
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File C:\Windows\Temp\gdm-sysprep-once.ps1"
# Create trigger at next boot
$trigger = New-ScheduledTaskTrigger -AtStartup
# Set rights
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
# Settings
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
# Register scheduled task
Register-ScheduledTask -TaskName "GDM-AutoSysprep" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
Write-Output "Sysprep scheduled to run ONCE at next boot (self-deleting)"
register: schedule_sysprepOnce we have scheduled the sysprep action as a scheduled task, we run our cleanup script and reboot:
### Cleanup artifacts and reboot (fire-and-forget) ###
- name: "Windows: Remove artifacts and trigger reboot (fire-and-forget)"
ansible.windows.win_shell: |
Write-Output "=== Starting cleanup and reboot ==="
# Log to file
$logFile = "C:\Windows\Temp\gdm-cleanup.log"
"Cleanup started: $(Get-Date)" | Out-File -FilePath $logFile -Append
# 1. Remove Firewall Rules
Remove-NetFirewallRule -Name "Temp-WINRM-HTTPS-In-TCP" -ErrorAction SilentlyContinue
Remove-NetFirewallRule -Name "Temp-ICMP-Echo-In" -ErrorAction SilentlyContinue
"Firewall rules removed" | Out-File -FilePath $logFile -Append
# 3. Remove Temp Certificate
if (Test-Path C:\cert-thumbprint.txt) {
$thumbprint = (Get-Content C:\cert-thumbprint.txt).Trim()
$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Thumbprint -eq $thumbprint}
if ($cert) {
$cert | Remove-Item -Force
}
Remove-Item C:\cert-thumbprint.txt -Force
"Certificate removed" | Out-File -FilePath $logFile -Append
}
# 4. Remove WinRM HTTPS Listener
$httpsListeners = Get-WSManInstance -ResourceURI winrm/config/Listener -Enumerate | Where-Object {$_.Transport -eq "HTTPS"}
if ($httpsListeners) {
$httpsListeners | ForEach-Object {
Remove-WSManInstance -ResourceURI winrm/config/Listener -SelectorSet @{Address=$_.Address;Transport=$_.Transport}
}
"HTTPS listeners removed" | Out-File -FilePath $logFile -Append
}
# 5. Remove WinRM HTTP Listener
$httpListeners = Get-WSManInstance -ResourceURI winrm/config/Listener -Enumerate | Where-Object {$_.Transport -eq "HTTP"}
if ($httpListeners) {
$httpListeners | ForEach-Object {
Remove-WSManInstance -ResourceURI winrm/config/Listener -SelectorSet @{Address=$_.Address;Transport=$_.Transport}
}
"HTTP listeners removed" | Out-File -FilePath $logFile -Append
}
"All cleanup completed: $(Get-Date)" | Out-File -FilePath $logFile -Append
"Triggering reboot - sysprep will run automatically on startup" | Out-File -FilePath $logFile -Append
Write-Output "Cleanup completed - rebooting in 10 seconds..."
# 6. Reboot the VM to run sysprep as scheduled task
shutdown /r /t 10 /c "Cleanup completed - rebooting to run sysprep"
async: 60
poll: 0
ignore_errors: trueWe run this async and do not assert anything, it's a fire-and-forget action. Once that is done, we move on to Play 3:
Play 3 - Cleanup
Since we cloned the old template for patching, we need to make sure everything was smooth and then we need to replace the old template. This is what's done in the third Play.
First, we assert that sysprep is done and has shut down the VM:
### Wait for VM to be powered off after sysprep ###
- name: "Nutanix: Wait for temp VM to be powered off after sysprep"
nutanix.ncp.ntnx_vms_info:
vm_uuid: "{{ temp_vm_clone.vm_uuid }}"
register: power_state
retries: 40
delay: 30
until: power_state.response.status.resources.power_state == "OFF"For some reason, Nutanix now leaves the Sysprep CDROM attached to the VM, so we remove all the VM's CDROM units:
### Remove CD-ROM with unattend.xml before making template ###
- name: "Nutanix: Get current VM configuration with CD-ROM info"
nutanix.ncp.ntnx_vms_info:
vm_uuid: "{{ temp_vm_clone.vm_uuid }}"
register: vm_config
- name: "Nutanix: Extract CD-ROM external ID from disk list"
ansible.builtin.set_fact:
cdrom_list: "{{ vm_config.response.status.resources.disk_list | selectattr('device_properties.device_type', 'equalto', 'CDROM') | list }}"
- name: "Nutanix: Remove CD-ROM from VM (unmount unattend.xml)"
nutanix.ncp.ntnx_vms:
state: present
vm_uuid: "{{ temp_vm_clone.vm_uuid }}"
disks:
- state: absent
uuid: "{{ cdrom_list[0].uuid }}"
register: cdrom_removed
when: cdrom_list | length > 0Then we change the name of the temp VM to take over the name of the old unpatched template:
### Get old template info BEFORE renaming (needed for CI number extraction) ###
- name: "Nutanix: Get old template info"
nutanix.ncp.ntnx_vms_info:
filter:
vm_name: "{{ template_vm_name }}"
kind: vm
register: old_template_info
### Rename old template ###
- name: "Nutanix: Rename old template with timestamp"
nutanix.ncp.ntnx_vms:
state: present
vm_uuid: "{{ old_template_info.response.entities[0].metadata.uuid }}"
name: "{{ template_vm_name }}-old-{{ playbook_timestamp }}"
desc: "Old template - replaced {{ playbook_date }}"
### Rename temp VM to template name ###
- name: "Nutanix: Rename patched VM to template name"
nutanix.ncp.ntnx_vms:
state: present
vm_uuid: "{{ temp_vm_clone.vm_uuid }}"
name: "{{ template_vm_name }}"
desc: "Patched template - {{ playbook_date }}"And if all those steps are successful, we delete the old template:
### Delete old template after successful update ###
- name: "Nutanix: Delete old template (now replaced)"
nutanix.ncp.ntnx_vms:
state: absent
vm_uuid: "{{ old_template_info.response.entities[0].metadata.uuid }}"
when:
- old_template_info.response.entities is defined
- old_template_info.response.entities | length > 0And we're done with the Windows Workflow on Nutanix.
VMware Windows workflow
We also have a VMware environment and we have created the same workflow to patch the different Golden images to work for that platform as well. The overall workflow follows the same path, but has some differences.
I will not go through the VMware workflow in detail, but I will go through the steps that differ from the Nutanix Playbook.
First, here is the general workflow for VMware. As you can see, it's pretty similar.

Play 1 - Start and clone
Here it differs a bit from the Nutanix workflow. In the Nutanix workflow, the image or template VM is sysprepped at rest. In VMware, they do not want the VM to be sysprepped when using the Guest Customization module.
So here, we just power on the template, wait for DHCP, update VMware tools, then we create all the WINRM config through VMware tools.
### Power on template VM ###
- name: "VMware: Power on template VM"
vmware.vmware.vm_powerstate:
datacenter: "GDM"
name: "{{ template_vm_name }}"
state: "powered-on"
### Wait for VM to get DHCP IP in staging network (xx.xx.xx.20-30) ###
- name: "VMware: Wait for VM to get DHCP IP in staging network"
vmware.vmware_rest.vcenter_vm_guest_identity_info:
vm: "{{ template_id }}"
register: temp_vm_ip_info
retries: 30
delay: 30
until: >
(temp_vm_ip_info.value.ip_address is defined)
and
(temp_vm_ip_info.value.ip_address | ansible.utils.ipaddr('xx.xx.xx.0/24'))
and
(temp_vm_ip_info.value.ip_address.split('.')[3] | int >= 20)
and
(temp_vm_ip_info.value.ip_address.split('.')[3] | int <= 30)
and
(temp_vm_ip_info.value.host_name is defined)
and
(temp_vm_ip_info.value.host_name | lower is match('tmpl-win.*'))
Update of VMware tools:
### Upgrade VMware Tools to latest version ###
- name: "VMware: Check current VMware Tools status"
community.vmware.vmware_guest_tools_info:
datacenter: "GDM"
name: "{{ template_vm_name }}"
register: vm_tools_info
- name: "VMware: Display VMware Tools status"
ansible.builtin.debug:
msg:
- "VMware Tools Status: {{ vm_tools_info.vmtools_info.vm_tools_running_status }}"
- "VMware Tools Version: {{ vm_tools_info.vmtools_info.vm_tools_version }}"
- "VMware Tools Version Status: {{ vm_tools_info.vmtools_info.vm_tools_version_status }}"
- name: "VMware: Upgrade VMware Tools if needed"
community.vmware.vmware_guest_tools_upgrade:
datacenter: "GDM"
name: "{{ template_vm_name }}"
when: vm_tools_info.vmtools_info.vm_tools_version_status != "guestToolsCurrent"
register: tools_upgrade
- name: "VMware: Wait for VMware Tools upgrade to complete"
community.vmware.vmware_guest_tools_wait:
datacenter: "GDM"
name: "{{ template_vm_name }}"
timeout: 600
when: tools_upgrade is changed
- name: "VMware: Wait for VMware Tools to stabilize after Tools upgrade"
ansible.builtin.pause:
seconds: 30
when: tools_upgrade is changed
- name: "VMware: Verify VMware Tools communication after upgrade"
vmware.vmware_rest.vcenter_vm_guest_identity_info:
vm: "{{ template_id }}"
register: tools_verify_info
retries: 30
delay: 10
failed_when: false
until: >
tools_verify_info.value.ip_address is defined
and tools_verify_info.value.ip_address != None
and tools_verify_info.value.ip_address == temp_vm_ip_info.value.ip_address
when: tools_upgrade is changedThen we configure WINRM through VMware tools the same way we did in the Nutanix workflow using unattend.xml file.
### Configure WinRM HTTPS via VMware Tools ###
- name: "VMware: Enable PSRemoting via VMware Tools"
community.vmware.vmware_vm_shell:
datacenter: "GDM"
vm_id: "{{ template_vm_name }}"
vm_username: "Administrator"
vm_password: "{{ template_admin_password }}"
vm_shell: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
vm_shell_args: '-ExecutionPolicy Bypass -Command "Enable-PSRemoting -Force"'
wait_for_process: true
timeout: 300
- name: "VMware: Check if WinRM HTTPS listener already exists"
community.vmware.vmware_vm_shell:
datacenter: "GDM"
vm_id: "{{ template_vm_name }}"
vm_username: "Administrator"
vm_password: "{{ template_admin_password }}"
vm_shell: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
vm_shell_args: '-ExecutionPolicy Bypass -Command "$listeners = Get-WSManInstance -ResourceURI winrm/config/Listener -Enumerate | Where-Object {$_.Transport -eq ''HTTPS''}; if ($listeners) { exit 0 } else { exit 1 }"'
wait_for_process: true
timeout: 300
register: https_listener_check
failed_when: false
- name: "VMware: Create self-signed certificate via VMware Tools"
community.vmware.vmware_vm_shell:
datacenter: "GDM"
vm_id: "{{ template_vm_name }}"
vm_username: "Administrator"
vm_password: "{{ template_admin_password }}"
vm_shell: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
vm_shell_args: '-ExecutionPolicy Bypass -Command "New-SelfSignedCertificate -DnsName {{ template_vm_name }} -CertStoreLocation Cert:\LocalMachine\My | Select-Object -ExpandProperty Thumbprint | Out-File C:\cert-thumbprint.txt -Encoding ASCII"'
wait_for_process: true
timeout: 300
when: https_listener_check.exit_code != 0
- name: "VMware: Create HTTPS listener via VMware Tools"
community.vmware.vmware_vm_shell:
datacenter: "GDM"
vm_id: "{{ template_vm_name }}"
vm_username: "Administrator"
vm_password: "{{ template_admin_password }}"
vm_shell: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
vm_shell_args: '-ExecutionPolicy Bypass -Command "$thumb = (Get-Content C:\cert-thumbprint.txt).Trim(); New-WSManInstance -ResourceURI winrm/config/Listener -SelectorSet @{Address=''*'';Transport=''HTTPS''} -ValueSet @{Hostname=''{{ template_vm_name }}'';CertificateThumbprint=$thumb}"'
wait_for_process: true
timeout: 300
when: https_listener_check.exit_code != 0
- name: "VMware: Open firewall for WinRM HTTPS via VMware Tools"
community.vmware.vmware_vm_shell:
datacenter: "GDM"
vm_id: "{{ template_vm_name }}"
vm_username: "Administrator"
vm_password: "{{ template_admin_password }}"
vm_shell: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
vm_shell_args: '-ExecutionPolicy Bypass -Command "if (-not (Get-NetFirewallRule -Name Temp-WINRM-HTTPS-In-TCP -ErrorAction SilentlyContinue)) { New-NetFirewallRule -DisplayName Temp-WINRM-HTTPS -Name Temp-WINRM-HTTPS-In-TCP -Direction Inbound -LocalPort 5986 -Protocol TCP -Action Allow -Profile Any }"'
wait_for_process: true
timeout: 300
- name: "VMware: Open firewall for ICMP Echo via VMware Tools"
community.vmware.vmware_vm_shell:
datacenter: "GDM"
vm_id: "{{ template_vm_name }}"
vm_username: "Administrator"
vm_password: "{{ template_admin_password }}"
vm_shell: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
vm_shell_args: '-ExecutionPolicy Bypass -Command "if (-not (Get-NetFirewallRule -Name Temp-ICMP-Echo-In -ErrorAction SilentlyContinue)) { New-NetFirewallRule -DisplayName Temp-ICMP-Echo -Name Temp-ICMP-Echo-In -Direction Inbound -Protocol ICMPv4 -IcmpType 8 -Action Allow -Profile Any }"'
wait_for_process: true
timeout: 300
### Wait for WinRM port to be reachable ###
- name: "VMware: Wait for WinRM HTTPS port to respond"
ansible.builtin.wait_for:
host: "{{ temp_vm_ip_info.value.ip_address }}"
port: 5986
state: started
timeout: 600
delay: 30
Play 2 - Patching
Play 2 in the workflow is exactly the same as the Nutanix workflow. But the difference is that we do not Sysprep the image afterwards. We just reboot after each WinUpdate cycle.
Play 3 - Cleanup
Here there are some differences. We initiate the Cleanup of the WINRM settings in this play:
### Cleanup WinRM configuration via VMware Tools ###
- name: "VMware: Remove WinRM artifacts via VMware Tools"
community.vmware.vmware_vm_shell:
datacenter: "GDM"
vm_id: "{{ template_vm_name }}"
vm_username: "Administrator"
vm_password: "{{ template_admin_password }}"
vm_shell: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
vm_shell_args: |
-ExecutionPolicy Bypass -Command "
Write-Output 'Starting cleanup...'
# Remove firewall rules
Remove-NetFirewallRule -Name 'Temp-WINRM-HTTPS-In-TCP' -ErrorAction SilentlyContinue
Remove-NetFirewallRule -Name 'Temp-ICMP-Echo-In' -ErrorAction SilentlyContinue
# Remove certificate
if (Test-Path C:\cert-thumbprint.txt) {
$thumbprint = (Get-Content C:\cert-thumbprint.txt).Trim()
$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Thumbprint -eq $thumbprint}
if ($cert) { $cert | Remove-Item -Force }
Remove-Item C:\cert-thumbprint.txt -Force
}
# Remove WinRM HTTPS Listener
$httpsListeners = Get-WSManInstance -ResourceURI winrm/config/Listener -Enumerate | Where-Object {$_.Transport -eq 'HTTPS'}
if ($httpsListeners) {
$httpsListeners | ForEach-Object {
Remove-WSManInstance -ResourceURI winrm/config/Listener -SelectorSet @{Address=$_.Address;Transport=$_.Transport}
}
}
# Remove WinRM HTTP Listener
$httpListeners = Get-WSManInstance -ResourceURI winrm/config/Listener -Enumerate | Where-Object {$_.Transport -eq 'HTTP'}
if ($httpListeners) {
$httpListeners | ForEach-Object {
Remove-WSManInstance -ResourceURI winrm/config/Listener -SelectorSet @{Address=$_.Address;Transport=$_.Transport}
}
}
# Disable PSRemoting
Disable-PSRemoting -Force
Write-Output 'Cleanup completed'
"
wait_for_process: true
timeout: 600
register: cleanup_result
### Graceful shutdown via VMware Tools ###
- name: "VMware: Shutdown VM gracefully"
vmware.vmware.vm_powerstate:
datacenter: "GDM"
name: "{{ template_vm_name }}"
state: "shutdown-guest"
ignore_errors: true
### Wait for VM to be powered off ###
- name: "VMware: Wait for VM to be powered off"
vmware.vmware_rest.vcenter_vm_power_info:
vm: "{{ template_id }}"
register: power_state
retries: 40
delay: 15
until: power_state.value.state == "POWERED_OFF"We use the same commands but through the community.vmware.vmware_vm_shell module to remove all the WINRM configuration, and then shut down the VM.
Summary
So there you have it. An automated way to patch our templates to make sure that they are production ready when we're using our other workflows to autocreate Virtual Machines.
Hope you enjoyed this post. Thanks for reading!
To read the next post in the series, you can find it here:
