1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

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

@ -0,0 +1,2 @@
bugfixes:
- win_updates - Added the ability to run on a scheduled task for older hosts so async starts working again - https://github.com/ansible/ansible/issues/38364

View file

@ -7,13 +7,6 @@
#Requires -Module Ansible.ModuleUtils.Legacy #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" $ErrorActionPreference = "Stop"
$params = Parse-Args -arguments $args -supports_check_mode $true $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" $blacklist = Get-AnsibleParam -obj $params -name "blacklist" -type "list"
$whitelist = Get-AnsibleParam -obj $params -name "whitelist" -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) { Function Get-CategoryGuid($category_name) {
$guid = switch -exact ($category_name) { $guid = switch -exact ($category_name) {
"Application" {"5C9376AB-8CE6-464A-B136-22113DD69801"} "Application" {"5C9376AB-8CE6-464A-B136-22113DD69801"}
@ -55,35 +32,63 @@ Function Get-CategoryGuid($category_name) {
"Tools" {"B4832BD8-E735-4761-8DAF-37F882276DAB"} "Tools" {"B4832BD8-E735-4761-8DAF-37F882276DAB"}
"UpdateRollups" {"28BC880E-0592-4CBF-8F95-C79B17911D5F"} "UpdateRollups" {"28BC880E-0592-4CBF-8F95-C79B17911D5F"}
"Updates" {"CD5FFD1E-E932-4E3A-BF74-18BF0B1BBD83"} "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 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 $_ } $category_guids = $category_names | ForEach-Object { Get-CategoryGuid -category_name $_ }
$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
}
}
}
$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 = @{}
}
Write-DebugLog -msg "Creating Windows Update session..." Write-DebugLog -msg "Creating Windows Update session..."
try { try {
$session = New-Object -ComObject Microsoft.Update.Session $session = New-Object -ComObject Microsoft.Update.Session
} catch { } catch {
Fail-Json -obj $result -message "Failed to create Microsoft.Update.Session COM object: $($_.Exception.Message)" $result.failed = $true
$result.msg = "Failed to create Microsoft.Update.Session COM object: $($_.Exception.Message)"
return $result
} }
Write-DebugLog -msg "Create Windows Update searcher..." Write-DebugLog -msg "Create Windows Update searcher..."
try { try {
$searcher = $session.CreateUpdateSearcher() $searcher = $session.CreateUpdateSearcher()
} catch { } catch {
Fail-Json -obj $result -message "Failed to create Windows Update search from session: $($_.Exception.Message)" $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 # OR is only allowed at the top-level, so we have to repeat base criteria inside
@ -97,7 +102,9 @@ Write-DebugLog -msg "Searching for updates to install in category Ids $category_
try { try {
$search_result = $searcher.Search($criteria) $search_result = $searcher.Search($criteria)
} catch { } catch {
Fail-Json -obj $result -message "Failed to search for updates with criteria '$criteria': $($_.Exception.Message)" $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 "Found $($search_result.Updates.Count) updates"
@ -105,7 +112,9 @@ Write-DebugLog -msg "Creating update collection..."
try { try {
$updates_to_install = New-Object -ComObject Microsoft.Update.UpdateColl $updates_to_install = New-Object -ComObject Microsoft.Update.UpdateColl
} catch { } catch {
Fail-Json -obj $result -message "Failed to create update collection object: $($_.Exception.Message)" $result.failed = $true
$result.msg = "Failed to create update collection object: $($_.Exception.Message)"
return $result
} }
foreach ($update in $search_result.Updates) { foreach ($update in $search_result.Updates) {
@ -137,6 +146,10 @@ foreach ($update in $search_result.Updates) {
$skipped = $true $skipped = $true
} }
foreach ($kb in $update_info.kb) {
if ("KB$kb" -imatch $blacklist_entry) {
$kb_match = $true
}
foreach ($blacklist_entry in $blacklist) { foreach ($blacklist_entry in $blacklist) {
$kb_match = $false $kb_match = $false
foreach ($kb in $update_info.kb) { foreach ($kb in $update_info.kb) {
@ -150,6 +163,7 @@ foreach ($update in $search_result.Updates) {
break break
} }
} }
}
if ($skipped) { if ($skipped) {
$result.filtered_updates[$update_info.id] = $update_info $result.filtered_updates[$update_info.id] = $update_info
continue continue
@ -161,7 +175,9 @@ foreach ($update in $search_result.Updates) {
try { try {
$update.AcceptEula() $update.AcceptEula()
} catch { } 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 accept EULA for update $($update_info.id) - $($update_info.title)"
return $result
} }
} }
@ -179,7 +195,7 @@ foreach ($update in $search_result.Updates) {
Write-DebugLog -msg "Calculating pre-install reboot requirement..." Write-DebugLog -msg "Calculating pre-install reboot requirement..."
# calculate this early for check mode, and to see if we should allow updates to continue # calculate this early for check mode, and to see if we should allow updates to continue
$result.reboot_required = Get-RebootStatus $result.reboot_required = (New-Object -ComObject Microsoft.Update.SystemInfo).RebootRequired
$result.found_update_count = $updates_to_install.Count $result.found_update_count = $updates_to_install.Count
$result.installed_update_count = 0 $result.installed_update_count = 0
@ -191,18 +207,20 @@ if ($check_mode -or $state -eq "searched") {
if ($updates_to_install.Count -gt 0 -and ($state -ne "searched")) { if ($updates_to_install.Count -gt 0 -and ($state -ne "searched")) {
$result.changed = $true $result.changed = $true
} }
Exit-Json -obj $result return $result
} }
if ($updates_to_install.Count -gt 0) { if ($updates_to_install.Count -gt 0) {
if ($result.reboot_required) { if ($result.reboot_required) {
Write-DebugLog -msg "FATAL: A reboot is required before more updates can be installed" 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" $result.failed = $true
$result.msg = "A reboot is required before more updates can be installed"
return $result
} }
Write-DebugLog -msg "No reboot is pending..." Write-DebugLog -msg "No reboot is pending..."
} else { } else {
# no updates to install exit here # no updates to install exit here
Exit-Json -obj $result return $result
} }
Write-DebugLog -msg "Downloading updates..." Write-DebugLog -msg "Downloading updates..."
@ -219,14 +237,18 @@ foreach ($update in $updates_to_install) {
try { try {
$dl = $session.CreateUpdateDownloader() $dl = $session.CreateUpdateDownloader()
} catch { } catch {
Fail-Json -obj $result -message "Failed to create downloader object: $($_.Exception.Message)" $result.failed = $true
$result.msg = "Failed to create downloader object: $($_.Exception.Message)"
return $result
} }
Write-DebugLog -msg "Creating download collection..." Write-DebugLog -msg "Creating download collection..."
try { try {
$dl.Updates = New-Object -ComObject Microsoft.Update.UpdateColl $dl.Updates = New-Object -ComObject Microsoft.Update.UpdateColl
} catch { } catch {
Fail-Json -obj $result -message "Failed to create download collection object: $($_.Exception.Message)" $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)" Write-DebugLog -msg "Adding update $update_number $($update.Identity.UpdateId)"
@ -236,13 +258,17 @@ foreach ($update in $updates_to_install) {
try { try {
$download_result = $dl.Download() $download_result = $dl.Download()
} catch { } catch {
Fail-Json -obj $result -message "Failed to download update $update_number $($update.Identity.UpdateId) - $($update.Title): $($_.Exception.Message)" $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)" Write-DebugLog -msg "Download result code for $update_number $($update.Identity.UpdateId) = $($download_result.ResultCode)"
# FUTURE: configurable download retry # FUTURE: configurable download retry
if ($download_result.ResultCode -ne 2) { # OperationResultCode orcSucceeded 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)" $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 $result.changed = $true
@ -256,14 +282,18 @@ Write-DebugLog -msg "Creating installer object..."
try { try {
$installer = $session.CreateUpdateInstaller() $installer = $session.CreateUpdateInstaller()
} catch { } catch {
Fail-Json -obj $result -message "Failed to create Update Installer object: $($_.Exception.Message)" $result.failed = $true
$result.msg = "Failed to create Update Installer object: $($_.Exception.Message)"
return $result
} }
Write-DebugLog -msg "Creating install collection..." Write-DebugLog -msg "Creating install collection..."
try { try {
$installer.Updates = New-Object -ComObject Microsoft.Update.UpdateColl $installer.Updates = New-Object -ComObject Microsoft.Update.UpdateColl
} catch { } catch {
Fail-Json -obj $result -message "Failed to create Update Collection object: $($_.Exception.Message)" $result.failed = $true
$result.msg = "Failed to create Update Collection object: $($_.Exception.Message)"
return $result
} }
foreach ($update in $updates_to_install) { foreach ($update in $updates_to_install) {
@ -275,7 +305,9 @@ foreach ($update in $updates_to_install) {
try { try {
$install_result = $installer.Install() $install_result = $installer.Install()
} catch { } catch {
Fail-Json -obj $result -message "Failed to install update from Update Collection: $($_.Exception.Message)" $result.failed = $true
$result.msg = "Failed to install update from Update Collection: $($_.Exception.Message)"
return $result
} }
$update_success_count = 0 $update_success_count = 0
@ -288,7 +320,9 @@ foreach ($update in $updates_to_install) {
try { try {
$update_result = $install_result.GetUpdateResult($update_index) $update_result = $install_result.GetUpdateResult($update_index)
} catch { } catch {
Fail-Json -obj $result -message "Failed to get update result for update $update_number $($update.Identity.UpdateID) - $($update.Title): $($_.Exception.Message)" $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_resultcode = $update_result.ResultCode
$update_hresult = $update_result.HResult $update_hresult = $update_result.HResult
@ -310,15 +344,211 @@ foreach ($update in $updates_to_install) {
} }
Write-DebugLog -msg "Performing post-install reboot requirement check..." Write-DebugLog -msg "Performing post-install reboot requirement check..."
$result.reboot_required = Get-RebootStatus $result.reboot_required = (New-Object -ComObject Microsoft.Update.SystemInfo).RebootRequired
$result.installed_update_count = $update_success_count $result.installed_update_count = $update_success_count
$result.failed_update_count = $update_fail_count $result.failed_update_count = $update_fail_count
if ($update_fail_count -gt 0) { if ($update_fail_count -gt 0) {
Fail-Json -obj $result -msg "Failed to install one or more updates" $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)" Write-DebugLog -msg "Return value:`r`n$(ConvertTo-Json -InputObject $result -Depth 99)"
}
Exit-Json $result $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
}
}
Write-DebugLog -msg "Native job completed with output: $($result | Out-String -Width 300)"
return ,$result
}
Function Remove-ScheduledJob($name) {
$scheduled_job = Get-ScheduledJob -Name $name -ErrorAction SilentlyContinue
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 })
foreach ($task_to_stop in $running_tasks) {
Write-DebugLog -msg "Stopping running task $($task_to_stop.InstanceGuid)..."
$task_to_stop.Stop()
}
<# 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
}
}
Function Start-AsScheduledTask($common_functions, $script) {
$job_name = "ansible-win-updates"
Remove-ScheduledJob -name $job_name
$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 "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))
}
$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
}
$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 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
}
# 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)"
}
Write-DebugLog -msg "Scheduled job completed with output: $($re.Output | Out-String -Width 300)"
return $ret.Output
}
# source the common code into the current scope so we can call it
. $common_functions
<# 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
}
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 I(category_names). It will not force the module to install an update
if it was not in the category specified. if it was not in the category specified.
version_added: '2.5' 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: author:
- Matt Davis (@nitzmahone) - Matt Davis (@nitzmahone)
notes: notes:
@ -100,16 +112,17 @@ notes:
''' '''
EXAMPLES = r''' EXAMPLES = r'''
- name: Install all security, critical, and rollup updates - name: Install all security, critical, and rollup updates without a scheduled task
win_updates: win_updates:
category_names: category_names:
- SecurityUpdates - SecurityUpdates
- CriticalUpdates - CriticalUpdates
- UpdateRollups - UpdateRollups
- name: Install only security updates - name: Install only security updates as a scheduled task for Server 2008
win_updates: win_updates:
category_names: SecurityUpdates category_names: SecurityUpdates
use_scheduled_task: yes
- name: Search-only, return list of found updates (if any), log to C:\ansible_wu.txt - name: Search-only, return list of found updates (if any), log to C:\ansible_wu.txt
win_updates: win_updates:
@ -139,37 +152,6 @@ EXAMPLES = r'''
blacklist: blacklist:
- Windows Malicious Software Removal Tool for Windows - Windows Malicious Software Removal Tool for Windows
- \d{4}-\d{2} Cumulative Update for Windows Server 2016 - \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''' RETURN = r'''

View file

@ -40,15 +40,17 @@ class ActionModule(ActionBase):
raise AnsibleError("Unknown category_name %s, must be one of " raise AnsibleError("Unknown category_name %s, must be one of "
"(%s)" % (name, ','.join(valid_categories))) "(%s)" % (name, ','.join(valid_categories)))
def _run_win_updates(self, module_args, task_vars): def _run_win_updates(self, module_args, task_vars, use_task):
display.vvv("win_updates: running win_updates module") display.vvv("win_updates: running win_updates module")
result = self._execute_module(module_name='win_updates', wrap_async = self._task.async_val
result = self._execute_module_with_become(module_name='win_updates',
module_args=module_args, module_args=module_args,
task_vars=task_vars, task_vars=task_vars,
wrap_async=self._task.async_val) wrap_async=wrap_async,
use_task=use_task)
return result return result
def _reboot_server(self, task_vars, reboot_timeout): def _reboot_server(self, task_vars, reboot_timeout, use_task):
display.vvv("win_updates: rebooting remote host after update install") display.vvv("win_updates: rebooting remote host after update install")
reboot_args = { reboot_args = {
'reboot_timeout': reboot_timeout 'reboot_timeout': reboot_timeout
@ -58,42 +60,35 @@ class ActionModule(ActionBase):
if reboot_result.get('failed', False): if reboot_result.get('failed', False):
raise AnsibleError(reboot_result['msg']) raise AnsibleError(reboot_result['msg'])
# only run this if the user has specified we can only use scheduled
# tasks, the win_shell command requires become and will be skipped if
# become isn't available to use
if use_task:
display.vvv("win_updates: skipping WUA is not busy check as "
"use_scheduled_task=True is set")
else:
display.vvv("win_updates: checking WUA is not busy with win_shell " display.vvv("win_updates: checking WUA is not busy with win_shell "
"command") "command")
# While this always returns False after a reboot it doesn't return a # While this always returns False after a reboot it doesn't return
# value until Windows is actually ready and finished installing updates # a value until Windows is actually ready and finished installing
# This needs to run with become as WUA doesn't work over WinRM # updates. This needs to run with become as WUA doesn't work over
# Ignore connection errors as another reboot can happen # WinRM, ignore connection errors as another reboot can happen
command = "(New-Object -ComObject Microsoft.Update.Session)." \ command = "(New-Object -ComObject Microsoft.Update.Session)." \
"CreateUpdateInstaller().IsBusy" "CreateUpdateInstaller().IsBusy"
shell_module_args = { shell_module_args = {
'_raw_params': command '_raw_params': command
} }
# run win_shell module with become and ignore any errors in case of
# a windows reboot during execution
orig_become = self._play_context.become
orig_become_method = self._play_context.become_method
orig_become_user = self._play_context.become_user
if orig_become is None or orig_become is False:
self._play_context.become = True
if orig_become_method != 'runas':
self._play_context.become_method = 'runas'
if orig_become_user is None or 'root':
self._play_context.become_user = 'SYSTEM'
try: try:
shell_result = self._execute_module(module_name='win_shell', shell_result = self._execute_module_with_become(
module_args=shell_module_args, module_name='win_shell', module_args=shell_module_args,
task_vars=task_vars) task_vars=task_vars, wrap_async=False, use_task=use_task
)
display.vvv("win_updates: shell wait results: %s" display.vvv("win_updates: shell wait results: %s"
% json.dumps(shell_result)) % json.dumps(shell_result))
except Exception as exc: except Exception as exc:
display.debug("win_updates: Fatal error when running shell " display.debug("win_updates: Fatal error when running shell "
"command, attempting to recover: %s" % to_text(exc)) "command, attempting to recover: %s" % to_text(exc))
finally:
self._play_context.become = orig_become
self._play_context.become_method = orig_become_method
self._play_context.become_user = orig_become_user
display.vvv("win_updates: ensure the connection is up and running") display.vvv("win_updates: ensure the connection is up and running")
# in case Windows needs to reboot again after the updates, we wait for # in case Windows needs to reboot again after the updates, we wait for
@ -129,6 +124,32 @@ class ActionModule(ActionBase):
dict_var.update(new) dict_var.update(new)
return dict_var return dict_var
def _execute_module_with_become(self, module_name, module_args, task_vars,
wrap_async, use_task):
orig_become = self._play_context.become
orig_become_method = self._play_context.become_method
orig_become_user = self._play_context.become_user\
if not use_task:
if orig_become is None or orig_become is False:
self._play_context.become = True
if orig_become_method != 'runas':
self._play_context.become_method = 'runas'
if orig_become_user is None or orig_become_user == 'root':
self._play_context.become_user = 'SYSTEM'
try:
module_res = self._execute_module(module_name=module_name,
module_args=module_args,
task_vars=task_vars,
wrap_async=wrap_async)
finally:
self._play_context.become = orig_become
self._play_context.become_method = orig_become_method
self._play_context.become_user = orig_become_user
return module_res
def run(self, tmp=None, task_vars=None): def run(self, tmp=None, task_vars=None):
self._supports_check_mode = True self._supports_check_mode = True
self._supports_async = True self._supports_async = True
@ -148,6 +169,8 @@ class ActionModule(ActionBase):
reboot = self._task.args.get('reboot', False) reboot = self._task.args.get('reboot', False)
reboot_timeout = self._task.args.get('reboot_timeout', reboot_timeout = self._task.args.get('reboot_timeout',
self.DEFAULT_REBOOT_TIMEOUT) self.DEFAULT_REBOOT_TIMEOUT)
use_task = boolean(self._task.args.get('use_scheduled_task', False),
strict=False)
# Validate the options # Validate the options
try: try:
@ -184,7 +207,7 @@ class ActionModule(ActionBase):
new_module_args = self._task.args.copy() new_module_args = self._task.args.copy()
new_module_args.pop('reboot', None) new_module_args.pop('reboot', None)
new_module_args.pop('reboot_timeout', None) new_module_args.pop('reboot_timeout', None)
result = self._run_win_updates(new_module_args, task_vars) result = self._run_win_updates(new_module_args, task_vars, use_task)
# if the module failed to run at all then changed won't be populated # if the module failed to run at all then changed won't be populated
# so we just return the result as is # so we just return the result as is
@ -232,7 +255,8 @@ class ActionModule(ActionBase):
"update install" "update install"
try: try:
changed = True changed = True
self._reboot_server(task_vars, reboot_timeout) self._reboot_server(task_vars, reboot_timeout,
use_task)
except AnsibleError as exc: except AnsibleError as exc:
result['failed'] = True result['failed'] = True
result['msg'] = "Failed to reboot remote host when " \ result['msg'] = "Failed to reboot remote host when " \
@ -242,7 +266,8 @@ class ActionModule(ActionBase):
result.pop('msg', None) result.pop('msg', None)
# rerun the win_updates module after the reboot is complete # rerun the win_updates module after the reboot is complete
result = self._run_win_updates(new_module_args, task_vars) result = self._run_win_updates(new_module_args, task_vars,
use_task)
if result.get('failed', False): if result.get('failed', False):
return result return result

View file

@ -63,12 +63,13 @@
- update_search_with_log_check.found_update_count is defined - update_search_with_log_check.found_update_count is defined
- update_search_with_log_check_actual.stat.exists == False - update_search_with_log_check_actual.stat.exists == False
- name: search for updates with log output - name: search for updates with log output and use scheduled task
win_updates: win_updates:
state: searched state: searched
category_names: category_names:
- CriticalUpdates - CriticalUpdates
log_path: '{{win_updates_dir}}/update.log' log_path: '{{win_updates_dir}}/update.log'
use_scheduled_task: yes
register: update_search_with_log register: update_search_with_log
- name: get stat of update log file - name: get stat of update log file

View file

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
# (c) 2018, Jordan Borean <jborean@redhat.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import pytest
from ansible.compat.tests.mock import patch, MagicMock, mock_open
from ansible.plugins.action.win_updates import ActionModule
from ansible.playbook.task import Task
class TestWinUpdatesActionPlugin(object):
INVALID_OPTIONS = (
(
{"category_names": ["fake category"]},
False,
"Unknown category_name fake category, must be one of (Application,"
"Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,"
"FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,"
"UpdateRollups,Updates)"
),
(
{"state": "invalid"},
False,
"state must be either installed or searched"
),
(
{"reboot": "nonsense"},
False,
"cannot parse reboot as a boolean: The value 'nonsense' is not a "
"valid boolean."
),
(
{"reboot_timeout": "string"},
False,
"reboot_timeout must be an integer"
),
(
{"reboot": True},
True,
"async is not supported for this task when reboot=yes"
)
)
# pylint bug: https://github.com/PyCQA/pylint/issues/511
# pylint: disable=undefined-variable
@pytest.mark.parametrize('task_args, async_val, expected',
((t, a, e) for t, a, e in INVALID_OPTIONS))
def test_invalid_options(self, task_args, async_val, expected):
task = MagicMock(Task)
task.args = task_args
task.async_val = async_val
connection = MagicMock()
play_context = MagicMock()
play_context.check_mode = False
plugin = ActionModule(task, connection, play_context, loader=None,
templar=None, shared_loader_obj=None)
res = plugin.run()
assert res['failed']
assert expected in res['msg']
BECOME_OPTIONS = (
(False, False, "sudo", "root", True, "runas", "SYSTEM"),
(False, True, "sudo", "root", True, "runas", "SYSTEM"),
(False, False, "runas", "root", True, "runas", "SYSTEM"),
(False, False, "sudo", "user", True, "runas", "user"),
(False, None, "sudo", None, True, "runas", "SYSTEM"),
# use scheduled task, we shouldn't change anything
(True, False, "sudo", None, False, "sudo", None),
(True, True, "runas", "SYSTEM", True, "runas", "SYSTEM"),
)
# pylint bug: https://github.com/PyCQA/pylint/issues/511
# pylint: disable=undefined-variable
@pytest.mark.parametrize('use_task, o_b, o_bmethod, o_buser, e_b, e_bmethod, e_buser',
((u, ob, obm, obu, eb, ebm, ebu)
for u, ob, obm, obu, eb, ebm, ebu in BECOME_OPTIONS))
def test_module_exec_with_become(self, use_task, o_b, o_bmethod, o_buser,
e_b, e_bmethod, e_buser):
def mock_execute_module(self, **kwargs):
pc = self._play_context
return {"become": pc.become, "become_method": pc.become_method,
"become_user": pc.become_user}
task = MagicMock(Task)
task.args = {}
connection = MagicMock()
connection.module_implementation_preferences = ('.ps1', '.exe', '')
play_context = MagicMock()
play_context.check_mode = False
play_context.become = o_b
play_context.become_method = o_bmethod
play_context.become_user = o_buser
plugin = ActionModule(task, connection, play_context, loader=None,
templar=None, shared_loader_obj=None)
with patch('ansible.plugins.action.ActionBase._execute_module',
new=mock_execute_module):
actual = plugin._execute_module_with_become('win_updates', {}, {},
True, use_task)
# always make sure we reset back to the defaults
assert play_context.become == o_b
assert play_context.become_method == o_bmethod
assert play_context.become_user == o_buser
# verify what was set when _execute_module was called
assert actual['become'] == e_b
assert actual['become_method'] == e_bmethod
assert actual['become_user'] == e_buser