Ansible is a very good configuration management tool and is arguably the most popular today. It is simple to use, powerful and it’s agentless.
Ansible can do a boatload of things, one of which is installing software onto a machine. Since Windows natively doesn’t have a package manager, many people opt to use Chocolatey with Ansible to simplify management of software installations. There is an Ansible module available here for Chocolatey for those whom are interested.
In this post, I will take a different approach to package management by creating a template that can be re-used with some slight tweaks and adjustments. It can be used to install, upgrade and uninstall applications on Windows machines using Ansible.
This template gives you the ability to manage packages without a 3rd party intermediary. With a little bit of patience and testing, you can create a package fairly quickly.
Approach
My preferred approach when using Ansible with software packages is to use Ansible Roles.
As written in the Ansible documentation: “Roles are ways of automatically loading certain vars_files, tasks, and handlers based on a known file structure”.
Example Project File Structure
install-windows-apps.yml
demoapp/
defaults/
main.yml
tasks/
main.yml
handlers/
main.yml
shared_files/
Get-ApplicationProductId.ps1
install-windows-apps.yml
Run this playbook to install or uninstall the demoapp role. Change demoapp_package_behavior to ‘uninstall’ if you want to uninstall the application.
By default the template is setup to install or upgrade automatically. An upgrade will be performed if the version installed on the system is less than the role’s version.
---
- hosts: windows
tasks:
- include_role:
name: demoapp
vars:
demoapp_package_behavior: 'install'
demoapp/defaults/main.yml
This file is for default variables that will be leveraged by tasks and handlers.
---
demoapp_package_behavior: install
demoapp_package_display_name_uninstall: "Demoapp"
demoapp_version: 1.3.443.2
demoapp/tasks/main.yml
This tasks file handles the packaging logic.
---
- name: Demoapp Playbook
block:
- name: Unsupported Package Behavior
fail:
msg:
- "The following package behavior is unsupported: {{ demoapp_package_behavior|lower }}"
- "Valid selections (install,uninstall)"
when: demoapp_package_behavior|lower != "install" and demoapp_package_behavior|lower != "uninstall"
- name: Check if Demoapp is Installed
win_service:
name: Demoapp
register: demoapp_svc
- name: Demoapp Service Info
debug:
var: demoapp_svc
when: demoapp_svc.exists == True
- name: Get Installed Demoapp Version
win_file_version:
path: "{{ demoapp_svc.path | regex_search('[a-z](.*)(.exe)(?i)') }}"
when: demoapp_svc.exists == True
register: demoapp_file_version
- name: Clean Install
debug:
msg:
- "Demoapp is not installed"
- "Installing Demoapp version {{ demoapp_version }}"
when: demoapp_svc.exists == False and demoapp_package_behavior|lower == "install"
changed_when: true
notify: "install demoapp"
- name: Upgrade
debug:
msg:
- "It appears that Demoapp version {{ demoapp_file_version.win_file_version.product_version }} is currently installed"
- "Upgrading to version {{ demoapp_version }}"
when: demoapp_svc.exists == True and demoapp_file_version.win_file_version.product_version is version(demoapp_version, operator='<') and demoapp_package_behavior|lower == "install"
changed_when: true
notify: "install demoapp"
- name: No Change
debug:
msg:
- "It appears that Demoapp version {{ demoapp_file_version.win_file_version.product_version }} is currently installed"
- "Nothing to do"
when: demoapp_svc.exists == True and demoapp_file_version.win_file_version.product_version is version(demoapp_version, operator='>=') and demoapp_package_behavior|lower == "install"
- name: Uninstall
debug:
msg:
- "Uninstall was requested"
- "Uninstalling Demoapp version {{ demoapp_file_version.win_file_version.product_version }}"
when: demoapp_svc.exists == True and demoapp_package_behavior|lower == "uninstall"
changed_when: true
notify: "uninstall demoapp"
- name: Uninstall Requested but Product not Found
debug:
msg:
- "Uninstall was requested"
- "Demoapp was not found on the system"
when: demoapp_svc.exists == False and demoapp_package_behavior|lower == "uninstall"
rescue:
- name: Clean Up Files
win_file:
path: C:\temp\Ansible
state: absent
ignore_errors: True
- name: Ansible Play Failed
fail:
msg: "The playbook has failed, please see previous errors."
demoapp/handlers/main.yml
This handlers file strictly just listens to instructions from the tasks file.
---
- name: Create Directory for Local Execution
win_file:
path: C:\temp\Ansible
state: directory
listen:
- "install demoapp"
- "uninstall demoapp"
- name: Download demoapp
win_get_url:
url: https://<source_path>/Windows/demoapp/v{{ demoapp_version }}/demoapp-Windows-{{ demoapp_version }}.zip
dest: C:\temp\Ansible
listen: "install demoapp"
- name: Unzip Files
win_unzip:
src: C:\temp\Ansible\demoapp-Windows-{{ demoapp_version }}.zip
dest: C:\temp\Ansible
listen: "install demoapp"
- name: Install demoapp
win_package:
path: C:\temp\Ansible\demoapp-Windows-{{ demoapp_version }}.msi
creates_path: C:\demoapp\demoapp.exe
creates_version: "{{ demoapp_version }}"
state: present
listen: "install demoapp"
- name: Copy get Product ID script
win_copy:
src: ../../shared_files/Get-ApplicationProductId.ps1
dest: C:\temp\Ansible\Get-ApplicationProductId.ps1
listen: "uninstall demoapp"
- name: Get Product ID for Uninstall (demoapp)
win_shell: C:\temp\Ansible\Get-ApplicationProductId.ps1 "{{ demoapp_package_display_name_uninstall }}"
register: product_id_demoapp
listen: "uninstall demoapp"
- name: Uninstall demoapp
win_package:
product_id: '{{ product_id_demoapp.stdout_lines[0] }}'
state: absent
listen: "uninstall demoapp"
- name: Clean Up Files
win_file:
path: C:\temp\Ansible
state: absent
ignore_errors: True
listen:
- "install demoapp"
- "uninstall demoapp"
shared_files/Get-ApplicationProductId.ps1
This is a PowerShell helper script leveraged by the Ansible role. When you pass the product Name (demoapp_package_display_name_uninstall) as seen under “Control Panel\All Control Panel Items\Programs and Features” to the script, it will return the Product ID which is used to uninstall the application.
<#
.SYNOPSIS
Gets the Product Id for an installed application.
.PARAMETER Name
Name of the installed product.
.DESCRIPTION
This script retrieves the Product Id for an installed application. The Product Id is returned for consumption by Ansible.
The Name parameter is the product name as seen in the "Control Panel" under "Programs and Features".
Name can also be retrieved using 'wmic product list' or from the registry.
.EXAMPLE
.\Get-ApplicationProductId.ps1 -Name "Application01"
#>
param(
[Parameter(Mandatory = $true)]
[String]$Name
)
Try {
$regPath = @("HKLM:Software\Microsoft\Windows\CurrentVersion\Uninstall\","HKLM:Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
foreach ($element in $regPath) {
Get-ChildItem $element -Recurse | ForEach-Object {
$oKey = (Get-ItemProperty -Path $_.PsPath)
If ($oKey -match $Name){
Write-Output $_.PSChildName
exit 0
}
}
}
Write-Output "$Name not found on the system, could not locate corresponding ProductId."
exit 1
}
Catch {
Write-Output "An error occurred while getting the ProductId for uninstall."
exit 2
}