2014-09-29 23:27:29 +00:00
#!powershell
# This file is part of Ansible
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
# Copyright 2015, Matt Davis <mdavis@rolpdog.com>
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
# Requires -Module Ansible.ModuleUtils.Legacy
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
<# Most of the Windows Update API will not run under a remote token, which a
remote WinRM session always has . We set the below AnsibleRequires flag to
require become being used when executing the module to bypass this restriction .
This means we don ' t have to mess around with scheduled tasks . #>
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
#AnsibleRequires -Become
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
$ErrorActionPreference = " Stop "
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
$params = Parse-Args -arguments $args -supports_check_mode $true
$check_mode = Get-AnsibleParam -obj $params -name " _ansible_check_mode " -type " bool " -default $false
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
$category_names = Get-AnsibleParam -obj $params -name " category_names " -type " list " -default @ ( " CriticalUpdates " , " SecurityUpdates " , " UpdateRollups " )
$log_path = Get-AnsibleParam -obj $params -name " log_path " -type " path "
$state = Get-AnsibleParam -obj $params -name " state " -type " str " -default " installed " -validateset " installed " , " searched "
2018-01-19 00:29:46 +00:00
$blacklist = Get-AnsibleParam -obj $params -name " blacklist " -type " list "
$whitelist = Get-AnsibleParam -obj $params -name " whitelist " -type " list "
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
$result = @ {
changed = $false
updates = @ { }
2018-01-19 00:29:46 +00:00
filter ed_updates = @ { }
2017-11-21 21:49:38 +00:00
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Function Write-DebugLog($msg ) {
$date_str = Get-Date -Format u
$msg = " $date_str $msg "
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-Debug -Message $msg
if ( $log_path -ne $null -and ( -not $check_mode ) ) {
Add-Content -Path $log_path -Value $msg
}
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Function Get-CategoryGuid($category_name ) {
$guid = switch -exact ( $category_name ) {
" Application " { " 5C9376AB-8CE6-464A-B136-22113DD69801 " }
" Connectors " { " 434DE588-ED14-48F5-8EED-A15E09A991F6 " }
" CriticalUpdates " { " E6CF1350-C01B-414D-A61F-263D14D133B4 " }
" DefinitionUpdates " { " E0789628-CE08-4437-BE74-2495B842F43B " }
" DeveloperKits " { " E140075D-8433-45C3-AD87-E72345B36078 " }
" FeaturePacks " { " B54E7D24-7ADD-428F-8B75-90A396FA584F " }
" Guidance " { " 9511D615-35B2-47BB-927F-F73D8E9260BB " }
" SecurityUpdates " { " 0FA1201D-4330-4FA8-8AE9-B877473B6441 " }
" ServicePacks " { " 68C5B0A3-D1A6-4553-AE49-01D3A7827828 " }
" Tools " { " B4832BD8-E735-4761-8DAF-37F882276DAB " }
" UpdateRollups " { " 28BC880E-0592-4CBF-8F95-C79B17911D5F " }
" Updates " { " CD5FFD1E-E932-4E3A-BF74-18BF0B1BBD83 " }
default { Fail-Json -obj $result -message " Unknown category_name $category_name , must be one of (Application,Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,UpdateRollups,Updates) " }
}
return $guid
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Function Get-RebootStatus ( ) {
try {
$system_info = New-Object -ComObject Microsoft . Update . SystemInfo
} catch {
Fail-Json -obj $result -message " Failed to create Microsoft.Update.SystemInfo COM object for reboot status: $( $_ . Exception . Message ) "
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
return $system_info . RebootRequired
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
$category_guids = $category_names | ForEach-Object { Get-CategoryGuid -category_name $_ }
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Creating Windows Update session... "
try {
$session = New-Object -ComObject Microsoft . Update . Session
} catch {
Fail-Json -obj $result -message " Failed to create Microsoft.Update.Session COM object: $( $_ . Exception . Message ) "
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Create Windows Update searcher... "
try {
$searcher = $session . CreateUpdateSearcher ( )
} catch {
Fail-Json -obj $result -message " Failed to create Windows Update search from session: $( $_ . Exception . Message ) "
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
# OR is only allowed at the top-level, so we have to repeat base criteria inside
# FUTURE: change this to client-side filtered?
$criteria_base = " IsInstalled = 0 "
$criteria_list = $category_guids | ForEach-Object { " ( $criteria_base AND CategoryIds contains ' $_ ') " }
$criteria = [ string ] :: Join ( " OR " , $criteria_list )
Write-DebugLog -msg " Search criteria: $criteria "
Write-DebugLog -msg " Searching for updates to install in category Ids $category_guids ... "
try {
$search_result = $searcher . Search ( $criteria )
} catch {
Fail-Json -obj $result -message " Failed to search for updates with criteria ' $criteria ': $( $_ . Exception . Message ) "
}
Write-DebugLog -msg " Found $( $search_result . Updates . Count ) updates "
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Creating update collection... "
try {
$updates_to_install = New-Object -ComObject Microsoft . Update . UpdateColl
} catch {
Fail-Json -obj $result -message " Failed to create update collection object: $( $_ . Exception . Message ) "
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
foreach ( $update in $search_result . Updates ) {
2018-01-19 00:29:46 +00:00
$update_info = @ {
title = $update . Title
# TODO: pluck the first KB out (since most have just one)?
kb = $update . KBArticleIDs
id = $update . Identity . UpdateId
installed = $false
}
# validate update again blacklist/whitelist
$skipped = $false
foreach ( $whitelist_entry in $whitelist ) {
$kb_match = $false
foreach ( $kb in $update_info . kb ) {
if ( " KB $kb " -imatch $whitelist_entry ) {
$kb_match = $true
}
}
if ( -not ( $kb_match -or $update_info . title -imatch $whitelist_entry ) ) {
Write-DebugLog -msg " Skipping update $( $update_info . id ) - $( $update_info . title ) as it was not found in the whitelist "
$skipped = $true
break
}
}
foreach ( $blacklist_entry in $blacklist ) {
$kb_match = $false
foreach ( $kb in $update_info . kb ) {
if ( " KB $kb " -imatch $blacklist_entry ) {
$kb_match = $true
}
}
if ( $kb_match -or $update_info . title -imatch $blacklist_entry ) {
Write-DebugLog -msg " Skipping update $( $update_info . id ) - $( $update_info . title ) as it was found in the blacklist "
$skipped = $true
break
}
}
if ( $skipped ) {
$result . filtered_updates [ $update_info . id ] = $update_info
continue
}
2017-11-21 21:49:38 +00:00
if ( -not $update . EulaAccepted ) {
2018-01-19 00:29:46 +00:00
Write-DebugLog -msg " Accepting EULA for $( $update_info . id ) "
2017-11-21 21:49:38 +00:00
try {
$update . AcceptEula ( )
} catch {
2018-01-19 00:29:46 +00:00
Fail-Json -obj $result -message " Failed to accept EULA for update $( $update_info . id ) - $( $update_info . title ) "
2015-08-21 16:49:36 +00:00
}
2017-11-21 21:49:38 +00:00
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
if ( $update . IsHidden ) {
2018-01-19 00:29:46 +00:00
Write-DebugLog -msg " Skipping hidden update $( $update_info . title ) "
2017-11-21 21:49:38 +00:00
continue
}
2015-08-21 16:49:36 +00:00
2018-01-19 00:29:46 +00:00
Write-DebugLog -msg " Adding update $( $update_info . id ) - $( $update_info . title ) "
2017-11-21 21:49:38 +00:00
$updates_to_install . Add ( $update ) > $null
2015-08-21 16:49:36 +00:00
2018-01-19 00:29:46 +00:00
$result . updates [ $update_info . id ] = $update_info
2017-11-21 21:49:38 +00:00
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Calculating pre-install reboot requirement... "
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
# calculate this early for check mode, and to see if we should allow updates to continue
$result . reboot_required = Get-RebootStatus
$result . found_update_count = $updates_to_install . Count
$result . installed_update_count = 0
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
# Early exit of check mode/state=searched as it cannot do more after this
if ( $check_mode -or $state -eq " searched " ) {
Write-DebugLog -msg " Check mode: exiting... "
Write-DebugLog -msg " Return value: `r `n $( ConvertTo-Json -InputObject $result -Depth 99 ) "
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
if ( $updates_to_install . Count -gt 0 -and ( $state -ne " searched " ) ) {
$result . changed = $true
2015-08-21 16:49:36 +00:00
}
2017-11-21 21:49:38 +00:00
Exit-Json -obj $result
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
if ( $updates_to_install . Count -gt 0 ) {
if ( $result . reboot_required ) {
Write-DebugLog -msg " FATAL: A reboot is required before more updates can be installed "
Fail-Json -obj $result -message " A reboot is required before more updates can be installed "
2015-08-21 16:49:36 +00:00
}
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " No reboot is pending... "
} else {
# no updates to install exit here
Exit-Json -obj $result
2014-09-29 23:27:29 +00:00
}
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Downloading updates... "
$update_index = 1
foreach ( $update in $updates_to_install ) {
$update_number = " ( $update_index of $( $updates_to_install . Count ) ) "
if ( $update . IsDownloaded ) {
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateId ) already downloaded, skipping... "
$update_index + +
continue
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Creating downloader object... "
try {
$dl = $session . CreateUpdateDownloader ( )
} catch {
Fail-Json -obj $result -message " Failed to create downloader object: $( $_ . Exception . Message ) "
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Creating download collection... "
try {
$dl . Updates = New-Object -ComObject Microsoft . Update . UpdateColl
} catch {
Fail-Json -obj $result -message " Failed to create download collection object: $( $_ . Exception . Message ) "
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Adding update $update_number $( $update . Identity . UpdateId ) "
$dl . Updates . Add ( $update ) > $null
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Downloading $update_number $( $update . Identity . UpdateId ) "
try {
$download_result = $dl . Download ( )
} catch {
Fail-Json -obj $result -message " Failed to download update $update_number $( $update . Identity . UpdateId ) - $( $update . Title ) : $( $_ . Exception . Message ) "
}
Write-DebugLog -msg " Download result code for $update_number $( $update . Identity . UpdateId ) = $( $download_result . ResultCode ) "
# FUTURE: configurable download retry
if ( $download_result . ResultCode -ne 2 ) { # OperationResultCode orcSucceeded
Fail-Json -obj $result -message " Failed to download update $update_number $( $update . Identity . UpdateId ) - $( $update . Title ) : Download Result $( $download_result . ResuleCode ) "
}
2014-09-29 23:27:29 +00:00
2017-11-21 21:49:38 +00:00
$result . changed = $true
$update_index + +
2014-09-29 23:27:29 +00:00
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Installing updates... "
2014-09-29 23:27:29 +00:00
2017-11-21 21:49:38 +00:00
# install as a batch so the reboot manager will suppress intermediate reboots
Write-DebugLog -msg " Creating installer object... "
try {
$installer = $session . CreateUpdateInstaller ( )
} catch {
Fail-Json -obj $result -message " Failed to create Update Installer object: $( $_ . Exception . Message ) "
2014-09-29 23:27:29 +00:00
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Creating install collection... "
try {
$installer . Updates = New-Object -ComObject Microsoft . Update . UpdateColl
} catch {
Fail-Json -obj $result -message " Failed to create Update Collection object: $( $_ . Exception . Message ) "
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
foreach ( $update in $updates_to_install ) {
Write-DebugLog -msg " Adding update $( $update . Identity . UpdateID ) "
$installer . Updates . Add ( $update ) > $null
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
# FUTURE: use BeginInstall w/ progress reporting so we can at least log intermediate install results
try {
$install_result = $installer . Install ( )
} catch {
Fail-Json -obj $result -message " Failed to install update from Update Collection: $( $_ . Exception . Message ) "
}
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
$update_success_count = 0
$update_fail_count = 0
# WU result API requires us to index in to get the install results
$update_index = 0
foreach ( $update in $updates_to_install ) {
$update_number = " ( $( $update_index + 1 ) of $( $updates_to_install . Count ) ) "
try {
$update_result = $install_result . GetUpdateResult ( $update_index )
} catch {
Fail-Json -obj $result -message " Failed to get update result for update $update_number $( $update . Identity . UpdateID ) - $( $update . Title ) : $( $_ . Exception . Message ) "
}
$update_resultcode = $update_result . ResultCode
$update_hresult = $update_result . HResult
$update_index + +
$update_dict = $result . updates [ $update . Identity . UpdateID ]
if ( $update_resultcode -eq 2 ) { # OperationResultCode orcSucceeded
$update_success_count + +
$update_dict . installed = $true
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateID ) succeeded "
} else {
$update_fail_count + +
$update_dict . installed = $false
$update_dict . failed = $true
$update_dict . failure_hresult_code = $update_hresult
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateID ) failed, resultcode: $update_resultcode , hresult: $update_hresult "
2015-08-21 16:49:36 +00:00
}
2014-09-29 23:27:29 +00:00
}
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Performing post-install reboot requirement check... "
$result . reboot_required = Get-RebootStatus
$result . installed_update_count = $update_success_count
$result . failed_update_count = $update_fail_count
2015-08-21 16:49:36 +00:00
2018-01-11 04:41:52 +00:00
if ( $update_fail_count -gt 0 ) {
Fail-Json -obj $result -msg " Failed to install one or more updates "
}
2017-11-21 21:49:38 +00:00
Write-DebugLog -msg " Return value: `r `n $( ConvertTo-Json -InputObject $result -Depth 99 ) "
2015-08-21 16:49:36 +00:00
2017-11-21 21:49:38 +00:00
Exit-Json $result
2018-01-19 00:29:46 +00:00