win_updates: add scheduled tasks back in for older hosts (#38708)

* win_updates: add scheduled tasks back in for older hosts

* Fixed up typo in category name error message

* Fixed up some minor issues after merge

* added changelog fragment

* Default to become but add override to use scheduled tasks

* Added basic unit tests for win_updates

* fix minor typos
This commit is contained in:
Jordan Borean 2018-05-24 06:21:01 +10:00 committed by Matt Davis
parent dff662fa0f
commit 457bccf540
6 changed files with 685 additions and 325 deletions

View file

@ -7,13 +7,6 @@
#Requires -Module Ansible.ModuleUtils.Legacy
<# 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. #>
#AnsibleRequires -Become
$ErrorActionPreference = "Stop"
$params = Parse-Args -arguments $args -supports_check_mode $true
@ -25,22 +18,6 @@ $state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "insta
$blacklist = Get-AnsibleParam -obj $params -name "blacklist" -type "list"
$whitelist = Get-AnsibleParam -obj $params -name "whitelist" -type "list"
$result = @{
changed = $false
updates = @{}
filtered_updates = @{}
}
Function Write-DebugLog($msg) {
$date_str = Get-Date -Format u
$msg = "$date_str $msg"
Write-Debug -Message $msg
if ($log_path -ne $null -and (-not $check_mode)) {
Add-Content -Path $log_path -Value $msg
}
}
Function Get-CategoryGuid($category_name) {
$guid = switch -exact ($category_name) {
"Application" {"5C9376AB-8CE6-464A-B136-22113DD69801"}
@ -55,270 +32,523 @@ Function Get-CategoryGuid($category_name) {
"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)" }
default { Fail-Json -message "Unknown category_name $category_name, must be one of (Application,Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,UpdateRollups,Updates)" }
}
return $guid
}
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)"
}
return $system_info.RebootRequired
}
$category_guids = $category_names | ForEach-Object { Get-CategoryGuid -category_name $_ }
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)"
}
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)"
}
# 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"
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)"
}
foreach ($update in $search_result.Updates) {
$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
$whitelist_match = $false
foreach ($whitelist_entry in $whitelist) {
if ($update_info.title -imatch $whitelist_entry) {
$whitelist_match = $true
break
}
foreach ($kb in $update_info.kb) {
if ("KB$kb" -imatch $whitelist_entry) {
$whitelist_match = $true
break
}
$common_functions = {
Function Write-DebugLog($msg) {
$date_str = Get-Date -Format u
$msg = "$date_str $msg"
Write-Debug -Message $msg
if ($log_path -ne $null -and (-not $check_mode)) {
Add-Content -Path $log_path -Value $msg
}
}
if ($whitelist.Length -gt 0 -and -not $whitelist_match) {
Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was not found in the whitelist"
$skipped = $true
}
}
foreach ($blacklist_entry in $blacklist) {
$kb_match = $false
foreach ($kb in $update_info.kb) {
if ("KB$kb" -imatch $blacklist_entry) {
$kb_match = $true
}
$update_script_block = {
Param(
[hashtable]$arguments
)
$ErrorActionPreference = "Stop"
$DebugPreference = "Continue"
Function Start-Updates {
Param(
$category_guids,
$log_path,
$state,
$blacklist,
$whitelist
)
$result = @{
changed = $false
updates = @{}
filtered_updates = @{}
}
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
}
if (-not $update.EulaAccepted) {
Write-DebugLog -msg "Accepting EULA for $($update_info.id)"
Write-DebugLog -msg "Creating Windows Update session..."
try {
$update.AcceptEula()
$session = New-Object -ComObject Microsoft.Update.Session
} catch {
Fail-Json -obj $result -message "Failed to accept EULA for update $($update_info.id) - $($update_info.title)"
$result.failed = $true
$result.msg = "Failed to create Microsoft.Update.Session COM object: $($_.Exception.Message)"
return $result
}
Write-DebugLog -msg "Create Windows Update searcher..."
try {
$searcher = $session.CreateUpdateSearcher()
} catch {
$result.failed = $true
$result.msg = "Failed to create Windows Update search from session: $($_.Exception.Message)"
return $result
}
# 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 {
$result.failed = $true
$result.msg = "Failed to search for updates with criteria '$criteria': $($_.Exception.Message)"
return $result
}
Write-DebugLog -msg "Found $($search_result.Updates.Count) updates"
Write-DebugLog -msg "Creating update collection..."
try {
$updates_to_install = New-Object -ComObject Microsoft.Update.UpdateColl
} catch {
$result.failed = $true
$result.msg = "Failed to create update collection object: $($_.Exception.Message)"
return $result
}
foreach ($update in $search_result.Updates) {
$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
$whitelist_match = $false
foreach ($whitelist_entry in $whitelist) {
if ($update_info.title -imatch $whitelist_entry) {
$whitelist_match = $true
break
}
foreach ($kb in $update_info.kb) {
if ("KB$kb" -imatch $whitelist_entry) {
$whitelist_match = $true
break
}
}
}
if ($whitelist.Length -gt 0 -and -not $whitelist_match) {
Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was not found in the whitelist"
$skipped = $true
}
foreach ($kb in $update_info.kb) {
if ("KB$kb" -imatch $blacklist_entry) {
$kb_match = $true
}
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
}
if (-not $update.EulaAccepted) {
Write-DebugLog -msg "Accepting EULA for $($update_info.id)"
try {
$update.AcceptEula()
} catch {
$result.failed = $true
$result.msg = "Failed to accept EULA for update $($update_info.id) - $($update_info.title)"
return $result
}
}
if ($update.IsHidden) {
Write-DebugLog -msg "Skipping hidden update $($update_info.title)"
continue
}
Write-DebugLog -msg "Adding update $($update_info.id) - $($update_info.title)"
$updates_to_install.Add($update) > $null
$result.updates[$update_info.id] = $update_info
}
Write-DebugLog -msg "Calculating pre-install reboot requirement..."
# calculate this early for check mode, and to see if we should allow updates to continue
$result.reboot_required = (New-Object -ComObject Microsoft.Update.SystemInfo).RebootRequired
$result.found_update_count = $updates_to_install.Count
$result.installed_update_count = 0
# 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)"
if ($updates_to_install.Count -gt 0 -and ($state -ne "searched")) {
$result.changed = $true
}
return $result
}
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"
$result.failed = $true
$result.msg = "A reboot is required before more updates can be installed"
return $result
}
Write-DebugLog -msg "No reboot is pending..."
} else {
# no updates to install exit here
return $result
}
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
}
Write-DebugLog -msg "Creating downloader object..."
try {
$dl = $session.CreateUpdateDownloader()
} catch {
$result.failed = $true
$result.msg = "Failed to create downloader object: $($_.Exception.Message)"
return $result
}
Write-DebugLog -msg "Creating download collection..."
try {
$dl.Updates = New-Object -ComObject Microsoft.Update.UpdateColl
} catch {
$result.failed = $true
$result.msg = "Failed to create download collection object: $($_.Exception.Message)"
return $result
}
Write-DebugLog -msg "Adding update $update_number $($update.Identity.UpdateId)"
$dl.Updates.Add($update) > $null
Write-DebugLog -msg "Downloading $update_number $($update.Identity.UpdateId)"
try {
$download_result = $dl.Download()
} catch {
$result.failed = $true
$result.msg = "Failed to download update $update_number $($update.Identity.UpdateId) - $($update.Title): $($_.Exception.Message)"
return $result
}
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
$result.failed = $true
$result.msg = "Failed to download update $update_number $($update.Identity.UpdateId) - $($update.Title): Download Result $($download_result.ResultCode)"
return $result
}
$result.changed = $true
$update_index++
}
Write-DebugLog -msg "Installing updates..."
# install as a batch so the reboot manager will suppress intermediate reboots
Write-DebugLog -msg "Creating installer object..."
try {
$installer = $session.CreateUpdateInstaller()
} catch {
$result.failed = $true
$result.msg = "Failed to create Update Installer object: $($_.Exception.Message)"
return $result
}
Write-DebugLog -msg "Creating install collection..."
try {
$installer.Updates = New-Object -ComObject Microsoft.Update.UpdateColl
} catch {
$result.failed = $true
$result.msg = "Failed to create Update Collection object: $($_.Exception.Message)"
return $result
}
foreach ($update in $updates_to_install) {
Write-DebugLog -msg "Adding update $($update.Identity.UpdateID)"
$installer.Updates.Add($update) > $null
}
# FUTURE: use BeginInstall w/ progress reporting so we can at least log intermediate install results
try {
$install_result = $installer.Install()
} catch {
$result.failed = $true
$result.msg = "Failed to install update from Update Collection: $($_.Exception.Message)"
return $result
}
$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 {
$result.failed = $true
$result.msg = "Failed to get update result for update $update_number $($update.Identity.UpdateID) - $($update.Title): $($_.Exception.Message)"
return $result
}
$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"
}
}
Write-DebugLog -msg "Performing post-install reboot requirement check..."
$result.reboot_required = (New-Object -ComObject Microsoft.Update.SystemInfo).RebootRequired
$result.installed_update_count = $update_success_count
$result.failed_update_count = $update_fail_count
if ($update_fail_count -gt 0) {
$result.failed = $true
$result.msg = "Failed to install one or more updates"
return $result
}
Write-DebugLog -msg "Return value:`r`n$(ConvertTo-Json -InputObject $result -Depth 99)"
}
$check_mode = $arguments.check_mode
try {
return @{
job_output = Start-Updates @arguments
}
} catch {
Write-DebugLog -msg "Fatal exception: $($_.Exception.Message) at $($_.ScriptStackTrace)"
return @{
job_output = @{
failed = $true
msg = $_.Exception.Message
location = $_.ScriptStackTrace
}
}
}
}
Function Start-Natively($common_functions, $script) {
$runspace_pool = [RunspaceFactory]::CreateRunspacePool()
$runspace_pool.Open()
try {
$ps_pipeline = [PowerShell]::Create()
$ps_pipeline.RunspacePool = $runspace_pool
# add the common script functions
$ps_pipeline.AddScript($common_functions) > $null
# add the update script block and required parameters
$ps_pipeline.AddStatement().AddScript($script) > $null
$ps_pipeline.AddParameter("arguments", @{
category_guids = $category_guids
log_path = $log_path
state = $state
blacklist = $blacklist
whitelist = $whitelist
check_mode = $check_mode
}) > $null
$output = $ps_pipeline.Invoke()
} finally {
$runspace_pool.Close()
}
$result = $output[0].job_output
if ($ps_pipeline.HadErrors) {
$result.failed = $true
# if the msg wasn't set, then add a generic error to at least tell the user something
if (-not ($result.ContainsKey("msg"))) {
$result.msg = "Unknown failure when executing native update script block"
$result.errors = $ps_pipeline.Streams.Error
}
}
if ($update.IsHidden) {
Write-DebugLog -msg "Skipping hidden update $($update_info.title)"
continue
}
Write-DebugLog -msg "Native job completed with output: $($result | Out-String -Width 300)"
Write-DebugLog -msg "Adding update $($update_info.id) - $($update_info.title)"
$updates_to_install.Add($update) > $null
$result.updates[$update_info.id] = $update_info
return ,$result
}
Write-DebugLog -msg "Calculating pre-install reboot requirement..."
Function Remove-ScheduledJob($name) {
$scheduled_job = Get-ScheduledJob -Name $name -ErrorAction SilentlyContinue
# 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
if ($scheduled_job -ne $null) {
Write-DebugLog -msg "Scheduled Job $name exists, ensuring it is not running..."
$scheduler = New-Object -ComObject Schedule.Service
Write-DebugLog -msg "Connecting to scheduler service..."
$scheduler.Connect()
Write-DebugLog -msg "Getting running tasks named $name"
$running_tasks = @($scheduler.GetRunningTasks(0) | Where-Object { $_.Name -eq $name })
# 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)"
foreach ($task_to_stop in $running_tasks) {
Write-DebugLog -msg "Stopping running task $($task_to_stop.InstanceGuid)..."
$task_to_stop.Stop()
}
if ($updates_to_install.Count -gt 0 -and ($state -ne "searched")) {
$result.changed = $true
<# FUTURE: add a global waithandle for this to release any other waiters. Wait-Job
and/or polling will block forever, since the killed job object in the parent
session doesn't know it's been killed :( #>
Unregister-ScheduledJob -Name $name
}
Exit-Json -obj $result
}
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"
}
Write-DebugLog -msg "No reboot is pending..."
} else {
# no updates to install exit here
Exit-Json -obj $result
}
Function Start-AsScheduledTask($common_functions, $script) {
$job_name = "ansible-win-updates"
Remove-ScheduledJob -name $job_name
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
$job_args = @{
ScriptBlock = $script
Name = $job_name
ArgumentList = @(
@{
category_guids = $category_guids
log_path = $log_path
state = $state
blacklist = $blacklist
whitelist = $whitelist
check_mode = $check_mode
}
)
ErrorAction = "Stop"
ScheduledJobOption = @{ RunElevated=$True; StartIfOnBatteries=$True; StopIfGoingOnBatteries=$False }
InitializationScript = $common_functions
}
Write-DebugLog -msg "Creating downloader object..."
try {
$dl = $session.CreateUpdateDownloader()
} catch {
Fail-Json -obj $result -message "Failed to create downloader object: $($_.Exception.Message)"
Write-DebugLog -msg "Registering scheduled job with args $($job_args | Out-String -Width 300)"
$scheduled_job = Register-ScheduledJob @job_args
# RunAsTask isn't available in PS3 - fall back to a 2s future trigger
if ($scheduled_job | Get-Member -Name RunAsTask) {
Write-DebugLog -msg "Starting scheduled job (PS4+ method)"
$scheduled_job.RunAsTask()
} else {
Write-DebugLog -msg "Starting scheduled job (PS3 method)"
Add-JobTrigger -InputObject $scheduled_job -trigger $(New-JobTrigger -Once -At $(Get-Date).AddSeconds(2))
}
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)"
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$job = $null
Write-DebugLog -msg "Waiting for job completion..."
# Wait-Job can fail for a few seconds until the scheduled task starts - poll for it...
while ($job -eq $null) {
Start-Sleep -Milliseconds 100
if ($sw.ElapsedMilliseconds -ge 30000) { # tasks scheduled right after boot on 2008R2 can take awhile to start...
Fail-Json -msg "Timed out waiting for scheduled task to start"
}
# FUTURE: configurable timeout so we don't block forever?
# FUTURE: add a global WaitHandle in case another instance kills our job, so we don't block forever
$job = Wait-Job -Name $scheduled_job.Name -ErrorAction SilentlyContinue
}
Write-DebugLog -msg "Adding update $update_number $($update.Identity.UpdateId)"
$dl.Updates.Add($update) > $null
$sw = [System.Diagnostics.Stopwatch]::StartNew()
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)"
# NB: output from scheduled jobs is delayed after completion (including the sub-objects after the primary Output object is available)
while (($job.Output -eq $null -or -not ($job.Output | Get-Member -Name Key -ErrorAction Ignore) -or -not $job.Output.Key.Contains("job_output")) -and $sw.ElapsedMilliseconds -lt 15000) {
Write-DebugLog -msg "Waiting for job output to populate..."
Start-Sleep -Milliseconds 500
}
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.ResultCode)"
# NB: fallthru on both timeout and success
$ret = @{
ErrorOutput = $job.Error
WarningOutput = $job.Warning
VerboseOutput = $job.Verbose
DebugOutput = $job.Debug
}
$result.changed = $true
$update_index++
}
Write-DebugLog -msg "Installing updates..."
# 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)"
}
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)"
}
foreach ($update in $updates_to_install) {
Write-DebugLog -msg "Adding update $($update.Identity.UpdateID)"
$installer.Updates.Add($update) > $null
}
# 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)"
}
$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"
if ($job.Output -eq $null -or -not $job.Output.Keys.Contains('job_output')) {
$ret.Output = @{failed = $true; msg = "job output was lost"}
} 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"
$ret.Output = $job.Output.job_output # sub-object returned, can only be accessed as a property for some reason
}
try { # this shouldn't be fatal, but can fail with both Powershell errors and COM Exceptions, hence the dual error-handling...
Unregister-ScheduledJob -Name $job_name -Force -ErrorAction Continue
} catch {
Write-DebugLog "Error unregistering job after execution: $($_.Exception.ToString()) $($_.ScriptStackTrace)"
}
Write-DebugLog -msg "Scheduled job completed with output: $($re.Output | Out-String -Width 300)"
return $ret.Output
}
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
# source the common code into the current scope so we can call it
. $common_functions
if ($update_fail_count -gt 0) {
Fail-Json -obj $result -msg "Failed to install one or more updates"
<# Most of the Windows Update Agent API will not run under a remote token,
which a remote WinRM session always has. Using become can bypass this
limitation but it is not always an option with older hosts. win_updates checks
if WUA is available in the current logon process and does either of the below;
* If become is used then it will run the windows update process natively
without any of the scheduled task hackery
* If become is not used then it will run the windows update process under
a scheduled job.
#>
try {
(New-Object -ComObject Microsoft.Update.Session).CreateUpdateInstaller().IsBusy > $null
$wua_available = $true
} catch {
$wua_available = $false
}
Write-DebugLog -msg "Return value:`r`n$(ConvertTo-Json -InputObject $result -Depth 99)"
Exit-Json $result
if ($wua_available) {
Write-DebugLog -msg "WUA is available in current logon process, running natively"
$result = Start-Natively -common_functions $common_functions -script $update_script_block
} else {
Write-DebugLog -msg "WUA is not avialable in current logon process, running with scheduled task"
$result = Start-AsScheduledTask -common_functions $common_functions -script $update_script_block
}
Exit-Json -obj $result

View file

@ -85,6 +85,18 @@ options:
I(category_names). It will not force the module to install an update
if it was not in the category specified.
version_added: '2.5'
use_scheduled_task:
description:
- Will not auto elevate the remote process with I(become) and use a
scheduled task instead.
- Set this to C(yes) when using this module with async on Server 2008,
2008 R2, or Windows 7, or on Server 2008 that is not authenticated
with basic or credssp.
- Can also be set to C(yes) on newer hosts where become does not work
due to further privilege restrictions from the OS defaults.
type: bool
default: 'no'
version_added: '2.6'
author:
- Matt Davis (@nitzmahone)
notes:
@ -100,16 +112,17 @@ notes:
'''
EXAMPLES = r'''
- name: Install all security, critical, and rollup updates
- name: Install all security, critical, and rollup updates without a scheduled task
win_updates:
category_names:
- SecurityUpdates
- CriticalUpdates
- UpdateRollups
- name: Install only security updates
- name: Install only security updates as a scheduled task for Server 2008
win_updates:
category_names: SecurityUpdates
use_scheduled_task: yes
- name: Search-only, return list of found updates (if any), log to C:\ansible_wu.txt
win_updates:
@ -139,37 +152,6 @@ EXAMPLES = r'''
blacklist:
- Windows Malicious Software Removal Tool for Windows
- \d{4}-\d{2} Cumulative Update for Windows Server 2016
# Note async works on Windows Server 2012 or newer - become must be explicitly set on the task for this to work
- name: Search for Windows updates asynchronously
win_updates:
category_names:
- SecurityUpdates
state: searched
async: 180
poll: 10
register: updates_to_install
become: yes
become_method: runas
become_user: SYSTEM
# Async can also be run in the background in a fire and forget fashion
- name: Search for Windows updates asynchronously (poll and forget)
win_updates:
category_names:
- SecurityUpdates
state: searched
async: 180
poll: 0
register: updates_to_install_async
- name: get status of Windows Update async job
async_status:
jid: '{{ updates_to_install_async.ansible_job_id }}'
register: updates_to_install_result
become: yes
become_method: runas
become_user: SYSTEM
'''
RETURN = r'''