Managing Windows with Ansible
I have played with using SaltStack to manage Windows but did not follow through with using it properly. I recently installed a NUC10i7FNH and decided to revisit managing Windows, this time with Ansible, rather than doing most things on Windows manually.
I initially installed Windows 10 on the device, which was working well, but got upgraded to Windows 11 through Windows Update. After the upgrade, if I left it for a while when I came back it would either still have it’s solid power light (indicating it was on) or blinking (indicating it was in standby) but would not wake up in either case and would have to be hard powered off and back on to start again. In device manager I saw a couple of unknown devices, so I installed the Intel Driver & Support Assistant (Intel DSA) by hand and allowed it to update. The only driver it wanted to update was the Realtek audio driver, which removed one of the unknown devices. I did a little more digging and, having identified the unknown device as being an Intel one (Vendor ID 8086), I installed the Intel Chipset Device Software package - this resolved the other one. I found and followed Intel’s specific guidance for “NUC Can’t Wake From Standby Mode in Windows”. I also tried a list of other settings tweaks, including disabling PCI Express power management and double checking “fast startup” is disabled. Unfortunately the sleep issue persisted, my plan it to try a clean install of Windows 11 (as opposed to the Windows 10 upgraded to 11 route I initially took) and see if that helps - for now, I just disabled sleep entirely.
Setting up Windows
To use Ansible to manage Windows requires some setting up - specifically enabling Windows Remote Management (WinRM) which is disabled by default.
WinRM can be setup by running this command on the Windows system (it requires an Administrator command prompt):
winrm quickconfig
N.B. Ideally I’d like to set up https only (with winrm quickconfig -transport:https
) but this requires non-self-signed host certificates be pre-installed - so my plan is to get it working with http initially then push certificates (and switch the transport over) down the line.
After reading the Ansible documentation, I decided to setup Credential Security Support Provider protocol (CredSSP) for authentication. It appears to be more secure than plain NTLM and as I am running in a non-Domain environment, Kerberos is not currently an option.
To enable CredSSP and disable NTLM, I ran these commands in an Administrator PowerShell:
Enable-WSManCredSSP -Role Server -Force
# Disable NTLM
winrm set winrm/config/Service/Auth '@{Negotiate="false"}'
Setting up Ansible
The system where Ansible is running needs the pywinrm
Python package. I added it to my requirements.txt
for my playbooks but it can also be manually installed:
pip install pywinrm[credssp]
I also needed some galaxy collections, so I added them to requirements.yaml
:
---
collections:
#...
- ansible.windows
- chocolatey.chocolatey
...
and installed them with ansible-galaxy
:
ansible-galaxy collection install -r requirements.yaml
Once installed, I added a host to my inventory:
windows_host:
ansible_connection: winrm
ansible_user: windows_user
# winrm will automatically use http if the port is set to 5985. If
# set to anything else (including default 5986) will use https.
ansible_winrm_port: 5985
ansible_winrm_transport: credssp
# Don't use sudo to elevate on Windows
ansible_become_method: runas
# Default to become admin with the Ansible user - the Ansible user
# must be an administrator for WinRM to work (according to the docs)
# so this should always work for elevated rights.
ansible_become_user: ""
After adding the host to my inventory then used a plain ansible
(as opposed to ansible-playbook
) command to test it was working:
ansible -i inventory.yaml -k -m ansible.windows.win_command -a whoami.exe windows_host
Installing Microsoft Office with Ansible
Ansible’s documentation recommends using Chocolatey: “The win_chocolatey module is recommended since it has the most complete logic for checking to see if a package has already been installed and is up-to-date.”. Chocolatey describes itself as “The Package Manager for Windows”.
The first thing I wanted to install was Microsoft Office 2019 Professional Plus, so I did this as a test - the win_chocolatey
module will install Chocolatey if it is not already installed so no prerequisite work is required:
ansible -i inventory.yaml -k -m chocolatey.chocolatey.win_chocolatey -a 'name=office2019proplus state=present' windows_host
However, this failed with this error:
Attempt to get headers for https://download.microsoft.com/download/2/7/A/27AF1BE6-DD20-4CB4-B154-EBAB8A7D4A7E/officedeploymenttool_12325-20288.exe failed.
The remote file either doesn't exist, is unauthorized, or is forbidden for url 'https://download.microsoft.com/download/2/7/A/27AF1BE6-DD20-4CB4-B154-EBAB8A7D4A7E/officedeploymenttool_12325-20288.exe'. Exception calling "GetResponse" with "0" argument(s): "The remote server returned an error: (404) Not Found."
On investigating the Chocolatey package page, this appears to be due to Microsoft updating the download links. Fixes have been submitted by two community members weeks ago but the package has not been updated since 2020, so I assume it is abandoned. I found an alternative package, Microsoft Office Deployment which claimed to install many versions of Office, including 2019 Professional Plus:
ansible -i inventory.yaml -k -m chocolatey.chocolatey.win_chocolatey -a 'name=microsoft-office-deployment state=present package_params="/64bit /Product:ProPlusRetail"' windows_host
However that also failed due to the URL having changed - and there is an open pull request to update the list of installable packages.
Instead, I resorted to using the old (less than ideal in the wake of modern package management finally reaching Windows) method of download-then-run to get Office installed, in a new role specifically to install Office. I also started changing some core roles to support application to multiple platforms - I renamed my existing linux
role (that is applied to all:!dummy
) to common
. I added a file (in my roles directory) called roles/os-specific-include.yaml
:
---
- name: OS-specific tasks
include_tasks: "{{ ansible_facts.system }}.yaml"
...
I renamed roles/common/main.yaml
to roles/common/Linux.yaml
and symlinked roles/common/tasks/main.yaml
to ../../os-specific-include.yaml
to select tasks based on the OS.
Likewise, I modified os-lockdown
(also applied to all), firewall
and desktop
roles to work like this.
In desktop
I created office2019.yaml
(which is included by Win32NT.yaml
) to do the install if Word is not already present:
---
- name: Check if Word is installed (as a test to see if Office is installed)
ansible.windows.win_reg_stat:
path: HKCR:\Word.Application\CurVer
register: word_version
- name: Install Office
block:
- name: Make temporary file to download Office installer to
ansible.windows.win_tempfile:
state: directory
register: office_tempfile
- name: Download the Office deployment tool
ansible.windows.win_get_url:
url: https://download.microsoft.com/download/2/7/A/27AF1BE6-DD20-4CB4-B154-EBAB8A7D4A7E/officedeploymenttool_15928-20216.exe
dest: "{{ office_tempfile.path }}\\office-installer.exe"
- name: Extract the deployment tool
become: yes # Requires elevated privileges
ansible.windows.win_command:
argv:
- "{{ office_tempfile.path }}\\office-installer.exe"
- /log:extract.log
- /extract:{{ office_tempfile.path }}
- /quiet
args:
chdir: "{{ office_tempfile.path }}"
- name: Create deployment configuration file
ansible.windows.win_copy:
dest: "{{ office_tempfile.path }}\\config.xml"
content: |
<Configuration>
<Add OfficeClientEdition="64">
<Product ID="ProPlus2019Retail" >
<Language ID="MatchOS" />
<Display Level="None" AcceptEULA="TRUE" />
</Product>
</Add>
</Configuration>
- name: Download office
become: yes # Requires elevated privileges
ansible.windows.win_command:
argv:
- "{{ office_tempfile.path }}\\setup.exe"
- /download
- config.xml
args:
chdir: "{{ office_tempfile.path }}"
- name: Install office
become: yes # Requires elevated privileges
ansible.windows.win_command:
argv:
- "{{ office_tempfile.path }}\\setup.exe"
- /configure
- config.xml
args:
chdir: "{{ office_tempfile.path }}"
- name: Tidy up tempfile
ansible.windows.win_file:
path: "{{ office_tempfile.path }}"
state: absent
when: not word_version.exists
...
Enable long-path support
One headache I keep hitting my head against is Windows 260 character path-length limit. Long path support can be enabled, as of Windows 10 1607, with a registry setting:
---
- name: Enable long path support
ansible.windows.win_regedit:
data: 1
path: HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem
name: LongPathsEnabled
state: present
type: dword
...
Setting up OKI MC363dn Printer with Ansible
I did not get this to work, and as I had a pressing need to use the printer I installed it with the PCL6 driver by hand in the end.
I tried to use the OKI Driver Installer Generator to create an installer for the printer with the OKI PCL6 driver and the hostname for the printer embedded. I set the package to “show progress only” to make it non-interactive.
To distribute the package, I created a new “Software” share on my NAS and enabled ready-only anonymous http access. I also added a DNS alias of ‘software’ to the NAS (in case I want to change the distribution point later).
The problem was that, although Ansible ran the package no output was displayed. When run manually, the installer package showed an “extracting” window but nothing else afterwards - so I manually used thee base PCL6 driver package to set-up the printer by hand.
The tasks I was trying to use looked like this:
---
# Install the OKI MC363dn printer
# TODO: Also need to look at scanning function.
- name: Windows
block:
- name: Check if Printer driver already installed
ansible.windows.win_powershell:
script: Get-Printer | Where-Object { $_.Name -like "*MC363*" }
register: printer_check_output
changed_when: false # Always read-only operation
# Note this is not working - problem seems to be the InstallerPackage
# itself, Ansible is running it. When run manually, shows an
# "extracting" window but nothing else afterwards and printer is not
# added. Ended up manually installing it for now.
- name: Install printer
block:
- name: Install PCL6 driver
block:
- name: Make temporary file to download installer to
ansible.windows.win_tempfile:
state: directory
register: pcl6_tempfile
- ansible.builtin.debug: var=pcl6_tempfile
- name: Download the Driver
ansible.windows.win_get_url:
# Install package created with OKI install package creator:
# https://www.oki.com/uk/printing/download/DIG_020001_0_308218.exe
# URL for PCL6 Printer Driver from OKI drivers website (for use
# with above):
# https://www.oki.com/uk/printing/download/OKW3X05H1011_279871.exe
url: http://software/Software/printers/oki-mc363dn/InstallPackage.exe
dest: "{{ pcl6_tempfile.path }}\\InstallPackage.exe"
- name: Install Driver
become: yes # Requires elevated privileges
ansible.windows.win_command:
argv:
- "{{ pcl6_tempfile.path }}\\InstallPackage.exe"
args:
chdir: "{{ pcl6_tempfile.path }}"
- name: Tidy up tempfile
ansible.windows.win_file:
path: "{{ pcl6_tempfile.path }}\\InstallPackage.exe"
state: absent
when: printer_check_output.output | length == 0
when: ansible_facts.system == 'Win32NT'
- name: Unsupported platform
ansible.builtin.fail: msg="Unable to install the OKI MC363dn on {{ ansible_facts.system }} at present."
when: ansible_facts.system not in ['Win32NT']
...
Install desktop applications
To install a bunch of useful desktop applications, I just used the chocolaty module:
---
- name: Install useful packages
chocolatey.chocolatey.win_chocolatey:
name:
- procexp # Process Explorer
- 7zip # Archive utility
- nextcloud-client # Internal sync-and-share solution
- keepassxc # End-user Password Safe
- adobereader # Official PDF reader software
- MailViewer # Reader for .eml, .msg etc. email files
state: present
...
I still did my customisations by hand, like telling Windows to display the Nextcloud and KeePassXC icons in the system tray and my KeePassXC preferences.