@ -1,61 +1,46 @@
#!powershell
#!powershell
# This file is part of Ansible
# This file is part of Ansible
#
# Copyright 2015, Matt Davis <mdavis@rolpdog.com>
# Copyright 2015, Matt Davis <mdavis@rolpdog.com>
#
# Copyright (c) 2017 Ansible Project
# Ansible is free software: you can redistribute it and/or modify
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# 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 "
# Requires -Module Ansible.ModuleUtils.Legacy
$FormatEnumerationLimit = -1 # prevent out-string et al from truncating collection dumps
<# Most of the Windows Update API will not run under a remote token, which a
<# Most of the Windows Update Agent API will not run under a remote token,
remote WinRM session always has . We set the below AnsibleRequires flag to
which a remote WinRM session always has . win_updates uses the Task Scheduler
require become being used when executing the module to bypass this restriction .
to run the bulk of the update function ality under a local token . Powershell ' s
This means we don ' t have to mess around with scheduled tasks . #>
Scheduled-Job capability provides a decent abstraction over the Task Scheduler
and handles marshaling Powershell args in and output / errors / etc back . The
#AnsibleRequires -Become
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 "
$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
$params = Parse-Args -arguments $args -supports_check_mode $true
$log_path = $boundparms [ 'log_path' ]
$check_mode = Get-AnsibleParam -obj $params -name " _ansible_check_mode " -type " bool " -default $false
Write-DebugLog " Scheduled job started with boundparms $( $boundparms | out-string ) and unboundargs $( $unboundargs | out-string ) "
$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
# FUTURE: elevate this to module arg validation once we have it
$result = @ {
Function MapCategoryNameToGuid {
changed = $false
Param ( [ string ] $category_name )
updates = @ { }
}
Function Write-DebugLog($msg ) {
$date_str = Get-Date -Format u
$msg = " $date_str $msg "
$category_guid = switch -exact ( $category_name ) {
Write-Debug -Message $msg
# as documented by TechNet @ https://technet.microsoft.com/en-us/library/ff730937.aspx
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 " }
" Application " { " 5C9376AB-8CE6-464A-B136-22113DD69801 " }
" Connectors " { " 434DE588-ED14-48F5-8EED-A15E09A991F6 " }
" Connectors " { " 434DE588-ED14-48F5-8EED-A15E09A991F6 " }
" CriticalUpdates " { " E6CF1350-C01B-414D-A61F-263D14D133B4 " }
" CriticalUpdates " { " E6CF1350-C01B-414D-A61F-263D14D133B4 " }
@ -68,355 +53,229 @@ $job_body = {
" 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 { throw " Unknown category_name $category_name , must be one of (Application,Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,UpdateRollups,Updates) " }
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
return $category_guid
}
}
Function DoWindowsUpdate {
Function Get-RebootStatus ( ) {
Param (
try {
[ string[] ] $category_names = @ ( " CriticalUpdates " , " SecurityUpdates " , " UpdateRollups " ) ,
$system_info = New-Object -ComObject Microsoft . Update . SystemInfo
[ ValidateSet ( " installed " , " searched " ) ]
} catch {
[ string ] $state = " installed " ,
Fail-Json -obj $result -message " Failed to create Microsoft.Update.SystemInfo COM object for reboot status: $( $_ . Exception . Message ) "
[ bool ] $_ansible_check_mode = $false
}
)
$is_check_mode = $ ( $state -eq " searched " ) -or $_ansible_check_mode
$category_guids = $category_names | % { MapCategoryNameToGUID $_ }
return $system_info . RebootRequired
}
$update_status = @ { changed = $false }
$category_guids = $category_names | ForEach-Object { Get-CategoryGuid -category_name $_ }
Write-DebugLog " Creating Windows Update session... "
Write-DebugLog -msg " Creating Windows Update session... "
try {
$session = New-Object -ComObject Microsoft . Update . Session
$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 " Create Windows Update searcher... "
Write-DebugLog -msg " Create Windows Update searcher... "
try {
$searcher = $session . CreateUpdateSearcher ( )
$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
# OR is only allowed at the top-level, so we have to repeat base criteria inside
# FUTURE: change this to client-side filtered?
# FUTURE: change this to client-side filtered?
$criteriabase = " IsInstalled = 0 "
$criteria_base = " IsInstalled = 0 "
$criteria_list = $category_guids | % { " ( $criteriabase AND CategoryIDs contains ' $_ ') " }
$criteria_list = $category_guids | ForEach-Object { " ( $criteria_base AND CategoryIds contains ' $_ ') " }
$criteria = [ string ] :: Join ( " OR " , $criteria_list )
$criteria = [ string ] :: Join ( " OR " , $criteria_list )
Write-DebugLog -msg " Search criteria: $criteria "
Write-DebugLog " Search criteria: $criteria "
Write-DebugLog -msg " Searching for updates to install in category Ids $category_guids ... "
try {
Write-DebugLog " Searching for updates to install in category IDs $category_guids ... "
$search_result = $searcher . Search ( $criteria )
$searchresult = $searcher . Search ( $criteria )
} catch {
Fail-Json -obj $result -message " Failed to search for updates with criteria ' $criteria ': $( $_ . Exception . Message ) "
Write-DebugLog " Creating update collection... "
}
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
$updates_to_install = New-Object -ComObject Microsoft . Update . UpdateColl
} catch {
Fail-Json -obj $result -message " Failed to create update collection object: $( $_ . Exception . Message ) "
}
Write-DebugLog " Found $( $searchresult . Updates . Count ) updates "
# FUTURE: add further filtering options (whitelist/blacklist)
foreach ( $update in $search_result . Updates ) {
$update_status . updates = @ { }
if ( -not $update . EulaAccepted ) {
Write-DebugLog -msg " Accepting EULA for $( $update . Identity . UpdateID ) "
# FUTURE: add further filtering options
try {
foreach ( $update in $searchresult . Updates ) {
if ( -Not $update . EulaAccepted ) {
Write-DebugLog " Accepting EULA for $( $update . Identity . UpdateID ) "
$update . AcceptEula ( )
$update . AcceptEula ( )
} catch {
Fail-Json -obj $result -message " Failed to accept EULA for update $( $update . Identity . UpdateID ) - $( $update . Title ) "
}
}
}
if ( $update . IsHidden ) {
if ( $update . IsHidden ) {
Write-DebugLo g " Skipping hidden update $( $update . Title ) "
Write-DebugLo g -ms g " Skipping hidden update $( $update . Title ) "
continue
continue
}
}
Write-DebugLo g " Adding update $( $update . Identity . UpdateID ) - $( $update . Title ) "
Write-DebugLo g -ms g " Adding update $( $update . Identity . UpdateID ) - $( $update . Title ) "
$res = $updates_to_install . Add ( $update )
$updates_to_install . Add ( $update ) > $null
$update_status . updates [ $update . Identity . UpdateID ] = @ {
$result . updates [ $update . Identity . UpdateId ] = @ {
title = $update . Title
title = $update . Title
# TODO: pluck the first KB out (since most have just one)?
# TODO: pluck the first KB out (since most have just one)?
kb = $update . KBArticleIDs
kb = $update . KBArticleIDs
id = $update . Identity . UpdateI D
id = $update . Identity . UpdateI d
installed = $false
installed = $false
}
}
}
}
Write-DebugLo g " Calculating pre-install reboot requirement... "
Write-DebugLo g -ms g " 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
$sysinfo = New-Object -ComObject Microsoft . Update . SystemInfo
$result . reboot_required = Get-RebootStatus
$update_status . reboot_required = $sysinfo . RebootRequired
$result . found_update_count = $updates_to_install . Count
$update_status . found_update_count = $updates_to_install . Count
$result . installed_update_count = 0
$update_status . installed_update_count = 0
# bail out here for check mode
# Early exit of check mode/state=searched as it cannot do more after this
if ( $is_check_mode -eq $true ) {
if ( $check_mode -or $state -eq " searched " ) {
Write-DebugLog " Check mode; exiting..."
Write-DebugLog -msg " Check mode: exiting..."
Write-DebugLog " Return value: $( $update_status | out-string ) "
Write-DebugLog -msg " Return value: `r `n $( ConvertTo-Json -InputObject $result -Depth 99 ) "
if ( $updates_to_install . Count -gt 0 ) { $update_status . changed = $true }
if ( $updates_to_install . Count -gt 0 -and ( $state -ne " searched " ) ) {
return $update_status
$result . changed = $true
}
Exit-Json -obj $result
}
}
if ( $updates_to_install . Count -gt 0 ) {
if ( $updates_to_install . Count -gt 0 ) {
if ( $update_status . reboot_required ) {
if ( $result . reboot_required ) {
throw " 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 "
}
}
else {
Write-DebugLog -msg " No reboot is pending... "
Write-DebugLog " No reboot is pending... "
} else {
}
# no updates to install exit here
Write-DebugLog " Downloading updates... "
Exit-Json -obj $result
}
}
Write-DebugLog -msg " Downloading updates... "
$update_index = 1
foreach ( $update in $updates_to_install ) {
foreach ( $update in $updates_to_install ) {
$update_number = " ( $update_index of $( $updates_to_install . Count ) ) "
if ( $update . IsDownloaded ) {
if ( $update . IsDownloaded ) {
Write-DebugLog " Update $( $update . Identity . UpdateID ) already downloaded, skipping... "
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateId ) already downloaded, skipping... "
$update_index + +
continue
continue
}
}
Write-DebugLog " Creating downloader object... "
Write-DebugLog -msg " Creating downloader object... "
try {
$dl = $session . CreateUpdateDownloader ( )
$dl = $session . CreateUpdateDownloader ( )
Write-DebugLog " Creating download collection... "
} catch {
Fail-Json -obj $result -message " Failed to create downloader object: $( $_ . Exception . Message ) "
}
Write-DebugLog -msg " Creating download collection... "
try {
$dl . Updates = New-Object -ComObject Microsoft . Update . UpdateColl
$dl . Updates = New-Object -ComObject Microsoft . Update . UpdateColl
Write-DebugLog " Adding update $( $update . Identity . UpdateID ) "
} catch {
$res = $dl . Updates . Add ( $update )
Fail-Json -obj $result -message " Failed to create download collection object: $( $_ . Exception . Message ) "
Write-DebugLog " Downloading update $( $update . Identity . UpdateID ) ... "
}
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 ( )
$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
# FUTURE: configurable download retry
if ( $download_result . ResultCode -ne 2 ) { # OperationResultCode orcSucceeded
if ( $download_result . ResultCode -ne 2 ) { # OperationResultCode orcSucceeded
throw " Failed to download update $( $update . Identity . UpdateID ) "
Fail-Json -obj $result -message " Failed to download update $update_number $( $update . Identity . UpdateId ) - $( $update . Title ) : Download Result $( $download_result . ResuleCode ) "
}
}
}
if ( $updates_to_install . Count -lt 1 ) { return $update_status }
$result . changed = $true
$update_index + +
}
Write-DebugLog " Installing updates... "
Write-DebugLo g -ms g " Installing updates... "
# install as a batch so the reboot manager will suppress intermediate reboots
# install as a batch so the reboot manager will suppress intermediate reboots
Write-DebugLog " Creating installer object... "
Write-DebugLog -msg " Creating installer object... "
$inst = $session . CreateUpdateInstaller ( )
try {
Write-DebugLog " Creating install collection... "
$installer = $session . CreateUpdateInstaller ( )
$inst . Updates = New-Object -ComObject Microsoft . Update . UpdateColl
} 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 ) {
foreach ( $update in $updates_to_install ) {
Write-DebugLog " Adding update $( $update . Identity . UpdateID ) "
Write-DebugLo g -ms g " Adding update $( $update . Identity . UpdateID ) "
$res = $inst . Updates . Add ( $update )
$inst aller . Updates . Add ( $update ) > $null
}
}
# FUTURE: use BeginInstall w/ progress reporting so we can at least log intermediate install results
# FUTURE: use BeginInstall w/ progress reporting so we can at least log intermediate install results
Write-DebugLog " Installing updates... "
try {
$install_result = $inst . Install ( )
$install_result = $installer . Install ( )
} catch {
Fail-Json -obj $result -message " Failed to install update from Update Collection: $( $_ . Exception . Message ) "
}
$update_success_count = 0
$update_success_count = 0
$update_fail_count = 0
$update_fail_count = 0
# WU result API requires us to index in to get the install results
# WU result API requires us to index in to get the install results
$update_index = 0
$update_index = 0
foreach ( $update in $updates_to_install ) {
foreach ( $update in $updates_to_install ) {
$update_number = " ( $( $update_index + 1 ) of $( $updates_to_install . Count ) ) "
try {
$update_result = $install_result . GetUpdateResult ( $update_index )
$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_resultcode = $update_result . ResultCode
$update_hresult = $update_result . HResult
$update_hresult = $update_result . HResult
$update_index + +
$update_index + +
$update_dict = $update_status . updates [ $update . Identity . UpdateID ]
$update_dict = $result . updates [ $update . Identity . UpdateID ]
if ( $update_resultcode -eq 2 ) { # OperationResultCode orcSucceeded
if ( $update_resultcode -eq 2 ) { # OperationResultCode orcSucceeded
$update_success_count + +
$update_success_count + +
$update_dict . installed = $true
$update_dict . installed = $true
Write-DebugLog " Update $( $update . Identity . UpdateID ) succeeded "
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateID ) succeeded "
}
} else {
else {
$update_fail_count + +
$update_fail_count + +
$update_dict . installed = $false
$update_dict . installed = $false
$update_dict . failed = $true
$update_dict . failed = $true
$update_dict . failure_hresult_code = $update_hresult
$update_dict . failure_hresult_code = $update_hresult
Write-DebugLog " Update $( $update . Identity . UpdateID ) failed resultcode $update_ h result hresult $update_hresult "
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateID ) failed , resultcode: $update_ resultcode , hresult: $update_hresult "
}
}
}
}
if ( $update_fail_count -gt 0 ) {
if ( $update_fail_count -gt 0 ) {
$update_status . failed = $true
Fail-Json -obj $result -msg " Failed to install one or more updates "
$update_status . msg = " Failed to install one or more updates "
}
else { $update_status . changed = $true }
Write-DebugLog " Performing post-install reboot requirement check... "
# 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
Write-DebugLog " Return value: $( $update_status | out-string ) "
return $update_status
}
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 } }
}
}
}
Function DestroyScheduledJob {
Write-DebugLog -msg " Performing post-install reboot requirement check... "
Param ( [ string ] $job_name )
$result . reboot_required = Get-RebootStatus
$result . installed_update_count = $update_success_count
# find a scheduled job with the same name (should normally fail)
$result . failed_update_count = $update_fail_count
$schedjob = Get-ScheduledJob -Name $job_name -ErrorAction SilentlyContinue
# 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 } )
Foreach ( $task_to_stop in $running_tasks ) {
Write-DebugLog " 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 $job_name
}
}
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
}
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)
}
# 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
)
$DebugPreference = " Continue "
$ErrorActionPreference = " Continue "
$date_str = Get-Date -Format u
$msg = " $date_str $msg "
Write-Debug $msg
if ( $log_path -ne $null ) {
Add-Content $log_path $msg
}
}
}
# 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 ) "
# 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 " 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. Outpu t
Exit-Json $result