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:

- 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:

  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)
    path: HKCR:\Word.Application\CurVer
  register: word_version
- name: Install Office
  - name: Make temporary file to download Office installer to
      state: directory
    register: office_tempfile
  - name: Download the Office deployment tool
      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
      - "{{ office_tempfile.path }}\\office-installer.exe"
      - /log:extract.log
      - /extract:{{ office_tempfile.path }}
      - /quiet
      chdir: "{{ office_tempfile.path }}"
  - name: Create deployment configuration file
      dest: "{{ office_tempfile.path }}\\config.xml"
      content: |
          <Add OfficeClientEdition="64"> 
            <Product ID="ProPlus2019Retail" > 
              <Language ID="MatchOS" />
              <Display Level="None" AcceptEULA="TRUE" />
  - name: Download office
    become: yes # Requires elevated privileges
      - "{{ office_tempfile.path }}\\setup.exe"
      - /download
      - config.xml
      chdir: "{{ office_tempfile.path }}"
  - name: Install office
    become: yes # Requires elevated privileges
      - "{{ office_tempfile.path }}\\setup.exe"
      - /configure
      - config.xml
      chdir: "{{ office_tempfile.path }}"
  - name: Tidy up tempfile
      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
    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
  - name: Check if Printer driver already installed
      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
    - name: Install PCL6 driver
      - name: Make temporary file to download installer to
          state: directory
        register: pcl6_tempfile
      - ansible.builtin.debug: var=pcl6_tempfile
      - name: Download the Driver
          # 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
          - "{{ pcl6_tempfile.path }}\\InstallPackage.exe"
          chdir: "{{ pcl6_tempfile.path }}"
      - name: Tidy up tempfile
          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
      - 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.