win_updates: removed scheduled task to use become instead (#33118)

* win_updates: removed scheduled task to use become instead

* updated docs to remove scheduled task info

* fix issue with only installing last update in group
pull/33168/head
Jordan Borean 7 years ago committed by Matt Davis
parent ebd08d2a01
commit 0962a0d816

@ -1,422 +1,281 @@
#!powershell
# This file is part of Ansible
#
# Copyright 2015, Matt Davis <mdavis@rolpdog.com>
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# WANT_JSON
# POWERSHELL_COMMON
$ErrorActionPreference = "Stop"
$FormatEnumerationLimit = -1 # prevent out-string et al from truncating collection dumps
<# Most of the Windows Update Agent API will not run under a remote token,
which a remote WinRM session always has. win_updates uses the Task Scheduler
to run the bulk of the update functionality under a local token. Powershell's
Scheduled-Job capability provides a decent abstraction over the Task Scheduler
and handles marshaling Powershell args in and output/errors/etc back. The
module schedules a single job that executes all interactions with the Update
Agent API, then waits for completion. A significant amount of hassle is
involved to ensure that only one of these jobs is running at a time, and to
clean up the various error conditions that can occur. #>
# define the ScriptBlock that will be passed to Register-ScheduledJob
$job_body = {
Param(
[hashtable]$boundparms=@{},
[Object[]]$unboundargs=$()
)
Set-StrictMode -Version 2
$ErrorActionPreference = "Stop"
$DebugPreference = "Continue"
$FormatEnumerationLimit = -1 # prevent out-string et al from truncating collection dumps
# set this as a global for the Write-DebugLog function
$log_path = $boundparms['log_path']
Write-DebugLog "Scheduled job started with boundparms $($boundparms | out-string) and unboundargs $($unboundargs | out-string)"
# FUTURE: elevate this to module arg validation once we have it
Function MapCategoryNameToGuid {
Param([string] $category_name)
$category_guid = switch -exact ($category_name) {
# as documented by TechNet @ https://technet.microsoft.com/en-us/library/ff730937.aspx
"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 { throw "Unknown category_name $category_name, must be one of (Application,Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,UpdateRollups,Updates)" }
}
return $category_guid
}
Function DoWindowsUpdate {
Param(
[string[]]$category_names=@("CriticalUpdates","SecurityUpdates","UpdateRollups"),
[ValidateSet("installed", "searched")]
[string]$state="installed",
[bool]$_ansible_check_mode=$false
)
$is_check_mode = $($state -eq "searched") -or $_ansible_check_mode
$category_guids = $category_names | % { MapCategoryNameToGUID $_ }
$update_status = @{ changed = $false }
Write-DebugLog "Creating Windows Update session..."
$session = New-Object -ComObject Microsoft.Update.Session
Write-DebugLog "Create Windows Update searcher..."
$searcher = $session.CreateUpdateSearcher()
# 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)
# OR is only allowed at the top-level, so we have to repeat base criteria inside
# FUTURE: change this to client-side filtered?
$criteriabase = "IsInstalled = 0"
$criteria_list = $category_guids | % { "($criteriabase AND CategoryIDs contains '$_')" }
#Requires -Module Ansible.ModuleUtils.Legacy
$criteria = [string]::Join(" OR ", $criteria_list)
<# 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. #>
Write-DebugLog "Search criteria: $criteria"
#AnsibleRequires -Become
Write-DebugLog "Searching for updates to install in category IDs $category_guids..."
$searchresult = $searcher.Search($criteria)
$ErrorActionPreference = "Stop"
Write-DebugLog "Creating update collection..."
$updates_to_install = New-Object -ComObject Microsoft.Update.UpdateColl
$params = Parse-Args -arguments $args -supports_check_mode $true
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
Write-DebugLog "Found $($searchresult.Updates.Count) updates"
$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"
# TODO: blacklist and whitelist
$update_status.updates = @{ }
$result = @{
changed = $false
updates = @{}
}
# FUTURE: add further filtering options
foreach($update in $searchresult.Updates) {
if(-Not $update.EulaAccepted) {
Write-DebugLog "Accepting EULA for $($update.Identity.UpdateID)"
$update.AcceptEula()
}
if($update.IsHidden) {
Write-DebugLog "Skipping hidden update $($update.Title)"
continue
}
Write-DebugLog "Adding update $($update.Identity.UpdateID) - $($update.Title)"
$res = $updates_to_install.Add($update)
$update_status.updates[$update.Identity.UpdateID] = @{
title = $update.Title
# TODO: pluck the first KB out (since most have just one)?
kb = $update.KBArticleIDs
id = $update.Identity.UpdateID
installed = $false
}
}
Function Write-DebugLog($msg) {
$date_str = Get-Date -Format u
$msg = "$date_str $msg"
Write-DebugLog "Calculating pre-install reboot requirement..."
Write-Debug -Message $msg
if ($log_path -ne $null -and (-not $check_mode)) {
Add-Content -Path $log_path -Value $msg
}
}
# calculate this early for check mode, and to see if we should allow updates to continue
$sysinfo = New-Object -ComObject Microsoft.Update.SystemInfo
$update_status.reboot_required = $sysinfo.RebootRequired
$update_status.found_update_count = $updates_to_install.Count
$update_status.installed_update_count = 0
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
}
# bail out here for check mode
if($is_check_mode -eq $true) {
Write-DebugLog "Check mode; exiting..."
Write-DebugLog "Return value: $($update_status | out-string)"
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)"
}
if($updates_to_install.Count -gt 0) { $update_status.changed = $true }
return $update_status
}
return $system_info.RebootRequired
}
if($updates_to_install.Count -gt 0) {
if($update_status.reboot_required) {
throw "A reboot is required before more updates can be installed."
}
else {
Write-DebugLog "No reboot is pending..."
}
Write-DebugLog "Downloading updates..."
}
$category_guids = $category_names | ForEach-Object { Get-CategoryGuid -category_name $_ }
foreach($update in $updates_to_install) {
if($update.IsDownloaded) {
Write-DebugLog "Update $($update.Identity.UpdateID) already downloaded, skipping..."
continue
}
Write-DebugLog "Creating downloader object..."
$dl = $session.CreateUpdateDownloader()
Write-DebugLog "Creating download collection..."
$dl.Updates = New-Object -ComObject Microsoft.Update.UpdateColl
Write-DebugLog "Adding update $($update.Identity.UpdateID)"
$res = $dl.Updates.Add($update)
Write-DebugLog "Downloading update $($update.Identity.UpdateID)..."
$download_result = $dl.Download()
# FUTURE: configurable download retry
if($download_result.ResultCode -ne 2) { # OperationResultCode orcSucceeded
throw "Failed to download update $($update.Identity.UpdateID)"
}
}
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)"
}
if($updates_to_install.Count -lt 1 ) { return $update_status }
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)"
}
Write-DebugLog "Installing updates..."
# 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"
# install as a batch so the reboot manager will suppress intermediate reboots
Write-DebugLog "Creating installer object..."
$inst = $session.CreateUpdateInstaller()
Write-DebugLog "Creating install collection..."
$inst.Updates = New-Object -ComObject Microsoft.Update.UpdateColl
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 $updates_to_install) {
Write-DebugLog "Adding update $($update.Identity.UpdateID)"
$res = $inst.Updates.Add($update)
# FUTURE: add further filtering options (whitelist/blacklist)
foreach ($update in $search_result.Updates) {
if (-not $update.EulaAccepted) {
Write-DebugLog -msg "Accepting EULA for $($update.Identity.UpdateID)"
try {
$update.AcceptEula()
} catch {
Fail-Json -obj $result -message "Failed to accept EULA for update $($update.Identity.UpdateID) - $($update.Title)"
}
}
# FUTURE: use BeginInstall w/ progress reporting so we can at least log intermediate install results
Write-DebugLog "Installing updates..."
$install_result = $inst.Install()
$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_result = $install_result.GetUpdateResult($update_index)
$update_resultcode = $update_result.ResultCode
$update_hresult = $update_result.HResult
$update_index++
$update_dict = $update_status.updates[$update.Identity.UpdateID]
if($update_resultcode -eq 2) { # OperationResultCode orcSucceeded
$update_success_count++
$update_dict.installed = $true
Write-DebugLog "Update $($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 "Update $($update.Identity.UpdateID) failed resultcode $update_hresult hresult $update_hresult"
}
if ($update.IsHidden) {
Write-DebugLog -msg "Skipping hidden update $($update.Title)"
continue
}
}
Write-DebugLog -msg "Adding update $($update.Identity.UpdateID) - $($update.Title)"
$updates_to_install.Add($update) > $null
if($update_fail_count -gt 0) {
$update_status.failed = $true
$update_status.msg="Failed to install one or more updates"
}
else { $update_status.changed = $true }
$result.updates[$update.Identity.UpdateId] = @{
title = $update.Title
# TODO: pluck the first KB out (since most have just one)?
kb = $update.KBArticleIDs
id = $update.Identity.UpdateId
installed = $false
}
}
Write-DebugLog "Performing post-install reboot requirement check..."
Write-DebugLog -msg "Calculating pre-install reboot requirement..."
# recalculate reboot status after installs
$sysinfo = New-Object -ComObject Microsoft.Update.SystemInfo
$update_status.reboot_required = $sysinfo.RebootRequired
$update_status.installed_update_count = $update_success_count
$update_status.failed_update_count = $update_fail_count
# 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
Write-DebugLog "Return value: $($update_status | out-string)"
# 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)"
return $update_status
if ($updates_to_install.Count -gt 0 -and ($state -ne "searched")) {
$result.changed = $true
}
Exit-Json -obj $result
}
Try {
# job system adds a bunch of cruft to top-level dict, so we have to send a sub-dict
return @{ job_output = DoWindowsUpdate @boundparms }
}
Catch {
$excep = $_
Write-DebugLog "Fatal exception: $($excep.Exception.Message) at $($excep.ScriptStackTrace)"
return @{ job_output = @{ failed=$true;error=$excep.Exception.Message;location=$excep.ScriptStackTrace } }
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 DestroyScheduledJob {
Param([string] $job_name)
# find a scheduled job with the same name (should normally fail)
$schedjob = Get-ScheduledJob -Name $job_name -ErrorAction SilentlyContinue
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
}
# nuke it if it's there
If($schedjob -ne $null) {
Write-DebugLog "ScheduledJob $job_name exists, ensuring it's not running..."
# can't manage jobs across sessions, so we have to resort to the Task Scheduler script object to kill running jobs
$schedserv = New-Object -ComObject Schedule.Service
Write-DebugLog "Connecting to scheduler service..."
$schedserv.Connect()
Write-DebugLog "Getting running tasks named $job_name"
$running_tasks = @($schedserv.GetRunningTasks(0) | Where-Object { $_.Name -eq $job_name })
Write-DebugLog -msg "Creating downloader object..."
try {
$dl = $session.CreateUpdateDownloader()
} catch {
Fail-Json -obj $result -message "Failed to create downloader object: $($_.Exception.Message)"
}
Foreach($task_to_stop in $running_tasks) {
Write-DebugLog "Stopping running task $($task_to_stop.InstanceGuid)..."
$task_to_stop.Stop()
}
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)"
}
<# 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 :( #>
Write-DebugLog -msg "Adding update $update_number $($update.Identity.UpdateId)"
$dl.Updates.Add($update) > $null
Unregister-ScheduledJob -Name $job_name
}
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)"
}
$result.changed = $true
$update_index++
}
Function RunAsScheduledJob {
Param([scriptblock] $job_body, [string] $job_name, [scriptblock] $job_init, [Object[]] $job_arg_list=@())
DestroyScheduledJob -job_name $job_name
$rsj_args = @{
ScriptBlock = $job_body
Name = $job_name
ArgumentList = $job_arg_list
ErrorAction = "Stop"
ScheduledJobOption = @{ RunElevated=$True; StartIfOnBatteries=$True; StopIfGoingOnBatteries=$False }
}
if($job_init) { $rsj_args.InitializationScript = $job_init }
Write-DebugLog "Registering scheduled job with args $($rsj_args | Out-String -Width 300)"
$schedjob = Register-ScheduledJob @rsj_args
# RunAsTask isn't available in PS3- fall back to a 2s future trigger
if($schedjob | Get-Member -Name RunAsTask) {
Write-DebugLog "Starting scheduled job (PS4 method)"
$schedjob.RunAsTask()
}
else {
Write-DebugLog "Starting scheduled job (PS3 method)"
Add-JobTrigger -inputobject $schedjob -trigger $(New-JobTrigger -once -at $(Get-Date).AddSeconds(2))
}
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$job = $null
Write-DebugLog "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...
Throw "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 $schedjob.Name -ErrorAction SilentlyContinue
}
$sw = [System.Diagnostics.Stopwatch]::StartNew()
# 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 Keys -ErrorAction Ignore) -or -not $job.Output.Keys.Contains('job_output')) -and $sw.ElapsedMilliseconds -lt 15000) {
Write-DebugLog "Waiting for job output to populate..."
Start-Sleep -Milliseconds 500
}
# NB: fallthru on both timeout and success
$ret = @{
ErrorOutput = $job.Error
WarningOutput = $job.Warning
VerboseOutput = $job.Verbose
DebugOutput = $job.Debug
}
If ($job.Output -eq $null -or -not $job.Output.Keys.Contains('job_output')) {
$ret.Output = @{failed = $true; msg = "job output was lost"}
}
Else {
$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)"
}
return $ret
}
Write-DebugLog -msg "Installing updates..."
Function Log-Forensics {
Write-DebugLog "Arguments: $job_args | out-string"
Write-DebugLog "OS Version: $([environment]::OSVersion.Version | out-string)"
Write-DebugLog "Running as user: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
Write-DebugLog "Powershell version: $($PSVersionTable | out-string)"
# FUTURE: log auth method (kerb, password, etc)
# 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)"
}
# code shared between the scheduled job and the host script
$common_inject = {
# FUTURE: capture all to a list, dump on error
Function Write-DebugLog {
Param(
[string]$msg
)
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)"
}
$DebugPreference = "Continue"
$ErrorActionPreference = "Continue"
$date_str = Get-Date -Format u
$msg = "$date_str $msg"
foreach ($update in $updates_to_install) {
Write-DebugLog -msg "Adding update $($update.Identity.UpdateID)"
$installer.Updates.Add($update) > $null
}
Write-Debug $msg
# 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)"
}
if($log_path -ne $null) {
Add-Content $log_path $msg
}
$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"
}
}
# source the common code into the current scope so we can call it
. $common_inject
$job_args = Parse-Args $args $true
# set the log_path for the global log function we injected earlier
$log_path = $job_args['log_path']
Log-Forensics
Write-DebugLog "Starting scheduled job with args: $($job_args | Out-String -Width 300)"
if ($update_fail_count -gt 0) {
Fail-Json -obj $result -msg "Failed to install one or more updates"
}
# pass the common code as job_init so it'll be injected into the scheduled job script
$sjo = RunAsScheduledJob -job_init $common_inject -job_body $job_body -job_name ansible-win-updates -job_arg_list $job_args
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
Write-DebugLog "Scheduled job completed with output: $($sjo.Output | Out-String -Width 300)"
Write-DebugLog -msg "Return value:`r`n$(ConvertTo-Json -InputObject $result -Depth 99)"
Exit-Json $sjo.Output
Exit-Json $result

@ -72,25 +72,22 @@ notes:
- C(win_updates) does not manage reboots, but will signal when a reboot is required with the reboot_required return value.
- C(win_updates) can take a significant amount of time to complete (hours, in some cases).
Performance depends on many factors, including OS version, number of updates, system load, and update server load.
- C(win_updates) runs the module as a scheduled task, this task is set to start and continue to run even if the Windows host
swaps to battery power. This behaviour was changed from Ansible 2.4, before this the scheduled task would fail to start on
battery power.
'''
EXAMPLES = r'''
# Install all security, critical, and rollup updates
- win_updates:
- name: Install all security, critical, and rollup updates
win_updates:
category_names:
- SecurityUpdates
- CriticalUpdates
- UpdateRollups
# Install only security updates
- win_updates:
- name: Install only security updates
win_updates:
category_names: SecurityUpdates
# Search-only, return list of found updates (if any), log to c:\ansible_wu.txt
- win_updates:
- name: Search-only, return list of found updates (if any), log to c:\ansible_wu.txt
win_updates:
category_names: SecurityUpdates
state: searched
log_path: c:\ansible_wu.txt

@ -0,0 +1 @@
win_updates_dir: '{{win_output_dir}}\win_updates'

@ -0,0 +1,26 @@
---
- name: ensure test folder exists
win_file:
path: '{{win_updates_dir}}'
state: directory
- name: ensure WUA service is running
win_service:
name: wuauserv
state: started
start_mode: manual
- block:
- include_tasks: tests.yml
always:
- name: ensure test folder is deleted
win_file:
path: '{{win_updates_dir}}'
state: absent
- name: ensure WUA service is running
win_service:
name: wuauserv
state: started
start_mode: manual

@ -0,0 +1,104 @@
---
- name: expect failure when state is not a valid option
win_updates:
state: invalid
register: invalid_state
failed_when: "invalid_state.msg != 'Get-AnsibleParam: Argument state needs to be one of installed,searched but was invalid.'"
- name: expect failure with invalid category name
win_updates:
state: searched
category_names:
- Invalid
register: invalid_category_name
failed_when: invalid_category_name.msg != 'Unknown category_name Invalid, must be one of (Application,Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,UpdateRollups,Updates)'
- name: ensure log file not present before tests
win_file:
path: '{{win_updates_dir}}/update.log'
state: absent
- name: search for updates without log output
win_updates:
state: searched
category_names:
- CriticalUpdates
register: update_search_without_log
- name: get stat of update without log file
win_stat:
path: '{{win_updates_dir}}/update.log'
register: update_search_without_log_actual
- name: assert search for updates without log output
assert:
that:
- not update_search_without_log is changed
- update_search_without_log.reboot_required == False
- update_search_without_log.updates is defined
- update_search_without_log.installed_update_count is defined
- update_search_without_log.found_update_count is defined
- update_search_without_log_actual.stat.exists == False
- name: search for updates with log output (check)
win_updates:
state: searched
category_names:
- CriticalUpdates
log_path: '{{win_updates_dir}}/update.log'
register: update_search_with_log_check
check_mode: yes
- name: get stat of update log file (check)
win_stat:
path: '{{win_updates_dir}}/update.log'
register: update_search_with_log_check_actual
- name: assert search for updates with log output
assert:
that:
- not update_search_with_log_check is changed
- update_search_with_log_check.reboot_required == False
- update_search_with_log_check.updates is defined
- update_search_with_log_check.installed_update_count is defined
- update_search_with_log_check.found_update_count is defined
- update_search_with_log_check_actual.stat.exists == False
- name: search for updates with log output
win_updates:
state: searched
category_names:
- CriticalUpdates
log_path: '{{win_updates_dir}}/update.log'
register: update_search_with_log
- name: get stat of update log file
win_stat:
path: '{{win_updates_dir}}/update.log'
register: update_search_with_log_actual
- name: assert search for updates with log output
assert:
that:
- not update_search_with_log is changed
- update_search_with_log.reboot_required == False
- update_search_with_log.updates is defined
- update_search_with_log.installed_update_count is defined
- update_search_with_log.found_update_count is defined
- update_search_with_log_actual.stat.exists
- name: ensure WUA service is stopped for tests
win_service:
name: wuauserv
state: stopped
start_mode: disabled
- name: expect failed when running with stopped WUA service
win_updates:
state: searched
category_names:
- CriticalUpdates
register: update_service_stopped_failed
failed_when:
- "'Failed to search for updates with criteria' not in update_service_stopped_failed.msg"
- "'The service cannot be started' not in update_service_stopped_failed.msg"
Loading…
Cancel
Save