Installing Updates on GNU/Linux using Ansible
1. März 2024A Backup Strategy through the Power of Ansible and Restic
30. April 2024Patching Windows Servers is key to keeping them secure and running smoothly. Doing this by hand can be a lot of work and sometimes mistakes happen. That is where Ansible comes in. It is a tool that helps automate this task, making it faster, reducing mistakes, and keeping everything consistent. In this small blog post we will show you how Ansible can make patching Windows Servers easier. It’s aimed at those who manage servers and want to make their lives a bit easier with automation.
That being said it took a while to create a playbook which works for us. In Windows there are many hidden functions and nobody talks about them! It can be, the following may not work correctly for you.
The Ansible Playbook
In our automation environment we use roles to make the playbook reusable by different functions. Here we took out the most important functions to form a simple playbook for demonstration purposes.
FYI: We are planning to provide a public access to our Git later, where you can fetch those files.
hosts.ini
We won’t explain the connection between Ansible and Windows. We are using SSH public key authentification also in the Windows world, since OpenSSH comes with Windows Server 2019/2022, even though it is still marked as experimental by the Ansible project. Perhaps we will explore this in more detail later.
[local]
localhost ansible_connection=local
[win22]
win ansible_host=172.16.88.88
[win22:vars]
ansible_connection=ssh
ansible_user=ansible
ansible_shell_type=powershell
ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
ansible_become_method=runas
ansible_become_user=SYSTEM
win-updates.yaml
The file, along with the accompanying comments, should be self-explanatory. We will dive into the crucial aspects of the file in the following section.
---
# Tasks to patch a Windows Server
- name: Windows update playbook
hosts: win22
gather_facts: false
become: true
# Create a reject_list of patches we don't want
vars:
reject_list: ["KB5034439", "KB1111111"]
tasks:
# Ensure the logging directory does exist, if not create it
- name: Proof the logging path does exist
ansible.windows.win_file:
path: C:\Automation\logs\update
state: directory
- name: Revoke 30d update block
ansible.windows.win_shell: |
$pause = (Get-Date).AddDays(0)
$pause = $pause.ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" )
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings' -Name 'PauseUpdatesExpiryTime' -Value $pause
- name: "Run Update Session Orchestrator check to clear GUI cached results"
ansible.windows.win_shell: |
Stop-Service -Name BITS,WaaSMedicSvc,WUAUSERV,CryptSvc -Force
Remove-Item -Path C:\Windows\System32\catroot2 -Recurse -Force
Remove-Item -Path C:\Windows\SoftwareDistribution -Recurse -Force
Start-Service -Name BITS,WaaSMedicSvc,WUAUSERV,CryptSvc
UsoClient RefreshSettings
UsoClient ScanInstallWait
# Determine the current time, to be used with the log-file later
- name: Get current date and time
ansible.builtin.shell:
cmd: date '+%Y%m%d'
executable: /bin/bash
register: current_time
delegate_to: localhost
changed_when: false
# Set the log-file name
- name: Set log-file name
ansible.builtin.set_fact:
log_file_name: "win-updates-log-{{ current_time.stdout | trim }}.txt"
# First search for pending patches of all categories
- name: Check for available updates
ansible.windows.win_updates:
category_names: '*'
reject_list: "{{ reject_list }}"
state: searched
server_selection: windows_update
log_path: "C:\\Automation\\logs\\update\\{{ log_file_name }}"
register: update_result
# Print the result in human readable format
- name: update check results
debug:
var: update_result
# Apply pending patches of all categories
- name: Apply pending updates
ansible.windows.win_updates:
category_names: '*'
reject_list: "{{ reject_list }}"
state: installed
server_selection: windows_update
reboot: true
reboot_timeout: 1800
log_path: "C:\\Automation\\logs\\update\\{{ log_file_name }}"
register: apply_result
retries: 2
delay: 60
until: apply_result is succeeded
ignore_errors: yes
# Print the result in human readable format
- name: Output update application results
debug:
var: apply_result
- name: Set update block to 30d
ansible.windows.win_shell: |
$pause = (Get-Date).AddDays(30)
$pause = $pause.ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" )
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings' -Name 'PauseUpdatesExpiryTime' -Value $pause
Execute the playbook
Executing the playbook is straightforward: you can run it using the ansible-playbook
command, or through automation platforms like Ansible AWX or Ansible Semaphore, among others.
➜ ansible-playbook -i hosts.ini win-updates.yaml
Further Explanations
In this section, we will dive little bit deeper into the special sections of the previously mentioned playbook file.
Pause the Internal Update Function
Windows Servers have their own update procedure. By default the policy allows to download the patches, but it disallows the installation of them. To prevent an uncontrolled patching of the systems to patch we can set a day block. The following statements will instruct Windows to not touch the systems before the day pause is over. Of course, you can do it also by modifing the server policy, but this is often not wanted. Thus we can set the update pause for 30 days.
- name: Set update block to 30d
ansible.windows.win_shell: |
$pause = (Get-Date).AddDays(30)
$pause = $pause.ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" )
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings' -Name 'PauseUpdatesExpiryTime' -Value $pause
As we trained our Ansible Server to patch the Windows systems ones in a month the patching pause is revoked again.
- name: Revoke 30d update block
ansible.windows.win_shell: |
$pause = (Get-Date).AddDays(0)
$pause = $pause.ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" )
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings' -Name 'PauseUpdatesExpiryTime' -Value $pause
Clear Collected Update Information and Reset the GUI Cache
We had many problems with the Windows systems. Often we experienced update loops, which means the patch is appearing again and again independent of whether the patch is already installed of not. Or the system cannot find any patches, even though they are available.
- name: "Run Update Session Orchestrator check to clear GUI cached results"
ansible.windows.win_shell: |
Stop-Service -Name BITS,WaaSMedicSvc,WUAUSERV,CryptSvc -Force
Remove-Item -Path C:\Windows\System32\catroot2 -Recurse -Force
Remove-Item -Path C:\Windows\SoftwareDistribution -Recurse -Force
Start-Service -Name BITS,WaaSMedicSvc,WUAUSERV,CryptSvc
UsoClient RefreshSettings
UsoClient ScanInstallWait
Apply Pending Patches Across All Categories
We aim to highlight four key parameters that are central to the functionality of the win_updates mechanism: reject_list
, category_names
, retries
, and the reboot
parameters.
The reject_list parameter plays a crucial role in our Ansible role management. It allows you to specify which patches should be ignored by the win_updates module, enabling more refined control over the patching process.
The category_names parameter is designed to provide visibility into every patch of each category. This flexibility means you can target specific categories like Critical Updates, Definition Updates, or Security Updates, according to your needs. For more detailed information, it is advisable to refer to the Ansible documentation.
The retries parameter is instrumental in managing failures during patch installation. Should the initial attempt fail, it permits two additional tries, with a delay
of 60 seconds between attempts, before considering the task failed. This approach enhances the resilience of the patching process, ensuring greater success rates in applying updates.
# Create a reject_list of patches we don't want
vars:
reject_list: ["KB5034439", "KB1111111"]
# Apply pending patches of all categories
- name: Apply pending updates
ansible.windows.win_updates:
category_names: '*'
reject_list: "{{ reject_list }}"
state: installed
server_selection: windows_update
reboot: true
reboot_timeout: 1800
log_path: "C:\\Automation\\logs\\update\\{{ log_file_name }}"
register: apply_result
retries: 2
delay: 60
until: apply_result is succeeded
ignore_errors: yes
The most important thing at the end!
The reboot parameter is by default set to false. By setting it to true, you delegate full control of the patching process to Ansible, enabling the win_updates module to reboot the target server autonomously. After the reboot, patching resumes automatically. It is possible for the server to undergo multiple reboots before the Ansible process concludes.
Initially, we experimented with Windows patching using custom scripts to manage the reboots. However, we found that entrusting Ansible with full system control lead to the best results.
Final Thoughts
For us, Ansible is good management tool when it comes to automation, offering crucial modules that extend control to Windows systems as well. We encountered numerous challenges in achieving reliable operation with the win_updates module. Indeed, while there is room for improvement on Ansible’s part, it is important to recognize that many hidden functions within the Windows OS complicate matters for a Free Software project like Ansible. These complexities can create big challenges, highlighting the difficult balance between using Free Software tools to manage proprietary systems.