PSWinUpdate.psm1
Set-StrictMode -Version Latest function Get-RemotingParameter { [OutputType([hashtable])] [CmdletBinding()] param ( [Parameter()] [string]$ComputerName, [Parameter()] [pscredential]$Credential ) $params = @{ ComputerName = $ComputerName } if ($Credential) { $params.Credential = $Credential } $params } function Remove-ScheduledTask { <# .SYNOPSIS This function looks for a scheduled task on a remote system and, once found, removes it. .EXAMPLE PS> Remove-ScheduledTask -ComputerName FOO -Name Task1 .PARAMETER ComputerName A mandatory string parameter representing a FQDN of a remote computer. .PARAMETER Name A mandatory string parameter representing the name of the scheduled task. Scheduled tasks can be retrieved by using the Get-ScheduledTask cmdlet. .PARAMETER Credential Specifies a user account that has permission to perform this action. The default is the current user. Type a user name, such as 'User01' or 'Domain01\User01', or enter a variable that contains a PSCredential object, such as one generated by the Get-Credential cmdlet. When you type a user name, you will be prompted for a password. #> [OutputType([void])] [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$ComputerName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter()] [ValidateNotNullOrEmpty()] [pscredential]$Credential ) begin { $ErrorActionPreference = 'Stop' } process { try { $icmParams = Get-RemotingParameter -ComputerName $ComputerName -Credential $Credential $icmParams.ArgumentList = $Name $icmParams.ErrorAction = 'Ignore' $sb = { $taskName = "\$($args[0])" if (schtasks /query /TN $taskName) { schtasks /delete /TN $taskName /F } } if ($PSCmdlet.ShouldProcess("Remove scheduled task [$($Name)] from [$($ComputerName)]", '----------------------')) { Invoke-Command @icmParams -ScriptBlock $sb } } catch { throw $_.Exception.Message } } } function Wait-ScheduledTask { <# .SYNOPSIS This function looks for a scheduled task on a remote system and, once found, checks to see if it's running. If so, it will wait until the task has completed and return control. .EXAMPLE PS> Wait-ScheduledTask -ComputerName FOO -Name Task1 -Timeout 120 .PARAMETER ComputerName A mandatory string parameter representing a FQDN of a remote computer. .PARAMETER Name A mandatory string parameter representing the name of the scheduled task. Scheduled tasks can be retrieved by using the Get-ScheduledTask cmdlet. .PARAMETER Timeout A optional integer parameter representing how long to wait for the scheduled task to complete. By default, it will wait 60 seconds. .PARAMETER Credential Specifies a user account that has permission to perform this action. The default is the current user. Type a user name, such as 'User01' or 'Domain01\User01', or enter a variable that contains a PSCredential object, such as one generated by the Get-Credential cmdlet. When you type a user name, you will be prompted for a password. #> [OutputType([void])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateScript({Test-IsValidFqdn $_})] [string]$ComputerName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter()] [ValidateNotNullOrEmpty()] [int]$Timeout = 300, ## seconds [Parameter()] [ValidateNotNullOrEmpty()] [pscredential]$Credential ) begin { $ErrorActionPreference = 'Stop' } process { try { $sessParams = Get-RemotingParameter -ComputerName $ComputerName -Credential $Credential $session = New-PSSession @sessParams $scriptBlock = { $taskName = "\$($args[0])" $VerbosePreference = 'Continue' $timer = [Diagnostics.Stopwatch]::StartNew() while (((schtasks /query /TN $taskName /FO CSV /v | ConvertFrom-Csv).Status -ne 'Ready') -and ($timer.Elapsed.TotalSeconds -lt $args[1])) { Write-Verbose -Message "Waiting on scheduled task [$taskName]..." Start-Sleep -Seconds 3 } $timer.Stop() Write-Verbose -Message "We waited [$($timer.Elapsed.TotalSeconds)] seconds on the task [$taskName]" } Invoke-Command -Session $session -ScriptBlock $scriptBlock -ArgumentList $Name, $Timeout } catch { throw $_.Exception.Message } finally { if (Test-Path Variable:\session) { $session | Remove-PSSession } } } } function Get-WindowsUpdate { <# .SYNOPSIS This function retrieves a list of Microsoft updates based on a number of different criteria for a remote computer. It will retrieve these updates over a PowerShell remoting session. It uses the update source set at the time of query. If it's set to WSUS, it will only return updates that are advertised to the computer by WSUS. .EXAMPLE PS> Get-WindowsUpdate -ComputerName FOO .PARAMETER ComputerName A mandatory string parameter representing the FQDN of a computer. This is only mandatory is Session is not used. .PARAMETER Credential A optoional pscredential parameter representing an alternate credential to connect to the remote computer. .PARAMETER Session A mandatory PSSession parameter representing a PowerShell remoting session created with New-PSSession. This is only mandatory if ComputerName is not used. .PARAMETER Installed A optional boolean parameter set to either $true or $false depending on if you'd like to filter the resulting updates on this criteria. .PARAMETER Hidden A optional boolean parameter set to either $true or $false depending on if you'd like to filter the resulting updates on this criteria. .PARAMETER Assigned A optional boolean parameter set to either $true or $false depending on if you'd like to filter the resulting updates on this criteria. .PARAMETER RebootRequired A optional boolean parameter set to either $true or $false depending on if you'd like to filter the resulting updates on this criteria. #> [OutputType([System.Management.Automation.PSObject])] [CmdletBinding()] param ( [Parameter(Mandatory, ParameterSetName = 'ByComputerName')] [ValidateNotNullOrEmpty()] [string]$ComputerName, [Parameter(ParameterSetName = 'ByComputerName')] [ValidateNotNullOrEmpty()] [pscredential]$Credential, [Parameter(Mandatory, ParameterSetName = 'BySession')] [ValidateNotNullOrEmpty()] [System.Management.Automation.Runspaces.PSSession]$Session, [Parameter()] [ValidateNotNullOrEmpty()] [ValidateSet('True', 'False')] [string]$Installed = 'False', [Parameter()] [ValidateNotNullOrEmpty()] [ValidateSet('True', 'False')] [string]$Hidden, [Parameter()] [ValidateNotNullOrEmpty()] [ValidateSet('True', 'False')] [string]$Assigned, [Parameter()] [ValidateNotNullOrEmpty()] [ValidateSet('True', 'False')] [string]$RebootRequired ) begin { $ErrorActionPreference = 'Stop' if (-not $Session) { $sessParams = Get-RemotingParameter -ComputerName $ComputerName -Credential $Credential $Session = New-PSSession @sessParams } } process { try { $criteriaParams = @{} ## Had to set these to string values because if they're boolean they will have a $false value even if ## they aren't set. I needed to check for a $null value.ided @('Installed', 'Hidden', 'Assigned', 'RebootRequired').where({ (Get-Variable -Name $_).Value }).foreach({ $criteriaParams[$_] = if ((Get-Variable -Name $_).Value -eq 'True') { $true } else { $false } }) $query = NewUpdateCriteriaQuery @criteriaParams Write-Verbose -Message "Using the update criteria query: [$($Query)]..." SearchWindowsUpdate -Session $Session -Query $query } catch { throw $_.Exception.Message } finally { ## Only clean up the session if it was generated from within this function. This is because updates ## are stored in a variable to be used again by other functions, if necessary. if (($PSCmdlet.ParameterSetName -eq 'ByComputerName') -and (Test-Path Variable:\session)) { $session | Remove-PSSession } } } } function Install-WindowsUpdate { <# .SYNOPSIS This function retrieves all updates that are targeted at a remote computer, download and installs any that it finds. Depending on how the remote computer's update source is set, it will either read WSUS or Microsoft Update for a compliancy report. Once found, it will download each update, install them and then read output to detect if a reboot is required or not. .EXAMPLE PS> Install-WindowsUpdate -ComputerName FOO.domain.local .EXAMPLE PS> Install-WindowsUpdate -ComputerName FOO.domain.local,FOO2.domain.local .EXAMPLE PS> Install-WindowsUpdate -ComputerName FOO.domain.local,FOO2.domain.local -ForceReboot .PARAMETER ComputerName A mandatory string parameter representing one or more computer FQDNs. .PARAMETER Credential A optional pscredential parameter representing an alternate credential to connect to the remote computer. .PARAMETER ForceReboot An optional switch parameter to set if any updates on any computer targeted needs a reboot following update install. By default, computers are NOT rebooted automatically. Use this switch to force a reboot. .PARAMETER AsJob A optional switch parameter to set when activity needs to be sent to a background job. By default, this function waits for each computer to finish. However, if this parameter is used, it will start the process on each computer and immediately return a background job object to then monitor yourself with Get-Job. #> [OutputType([void])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]]$ComputerName, [Parameter()] [ValidateNotNullOrEmpty()] [pscredential]$Credential, [Parameter()] [ValidateNotNullOrEmpty()] [switch]$ForceReboot, [Parameter()] [ValidateNotNullOrEmpty()] [switch]$AsJob ) begin { $ErrorActionPreference = 'Stop' $scheduledTaskName = 'Windows Update Install' } process { try { @($ComputerName).foreach({ Write-Verbose -Message "Starting Windows update on [$($_)]" $installProcess = { param($ComputerName, $TaskName, $Credential, $ForceReboot) $ErrorActionPreference = 'Stop' try { if (-not (Get-WindowsUpdate -ComputerName $ComputerName)) { Write-Verbose -Message 'No updates needed to install. Skipping computer...' } else { $sessParams = @{ ComputerName = $ComputerName } if ($Credential) { $sessParams.Credential = $Credential } $session = New-PSSession @sessParams $scriptBlock = { $updateSession = New-Object -ComObject 'Microsoft.Update.Session'; $objSearcher = $updateSession.CreateUpdateSearcher(); if ($updates = ($objSearcher.Search('IsInstalled=0'))) { $updates = $updates.Updates; $downloader = $updateSession.CreateUpdateDownloader(); $downloader.Updates = $updates; $downloadResult = $downloader.Download(); if ($downloadResult.ResultCode -ne 2) { exit $downloadResult.ResultCode; } $installer = New-Object -ComObject Microsoft.Update.Installer; $installer.Updates = $updates; $installResult = $installer.Install(); if ($installResult.RebootRequired) { exit 7; } else { $installResult.ResultCode } } else { exit 6; } } $taskParams = @{ Session = $session Name = $TaskName Scriptblock = $scriptBlock } if ($Credential) { $taskParams.Credential = $args[2] } Write-Verbose -Message 'Creating scheduled task...' New-WindowsUpdateScheduledTask @taskParams Write-Verbose -Message "Starting scheduled task [$($TaskName)]..." $icmParams = @{ Session = $session ScriptBlock = { schtasks /run /TN "\$($args[0])" /I } ArgumentList = $TaskName } Invoke-Command @icmParams ## This could take awhile depending on the number of updates Wait-ScheduledTask -Name $TaskName -ComputerName $ComputerName -Timeout 2400 $installResult = Get-WindowsUpdateInstallResult -Session $session if ($installResult -eq 'NoUpdatesNeeded') { Write-Verbose -Message "No updates to install" } elseif ($installResult -eq 'RebootRequired') { if ($ForceReboot) { Restart-Computer -ComputerName $ComputerName -Force -Wait; } else { Write-Warning "Reboot required but -ForceReboot was not used." } } else { throw "Updates failed. Reason: [$($installResult)]" } } } catch { Write-Error -Message $_.Exception.Message } finally { Remove-ScheduledTask -ComputerName $ComputerName -Name $TaskName } } $blockArgs = $_, $scheduledTaskName, $Credential, $ForceReboot.IsPresent if ($AsJob.IsPresent) { $jobParams = @{ ScriptBlock = $installProcess Name = "$_ - Windows Update Install" ArgumentList = $blockArgs InitializationScript = { Import-Module -Name 'GHI.Library.WindowsUpdate' } } Start-Job @jobParams } else { Invoke-Command -ScriptBlock $installProcess -ArgumentList $blockArgs } }) } catch { throw $_.Exception.Message } finally { if (-not $AsJob.IsPresent) { # Remove any sessions created. This is done when processes aren't invoked under a PS job Write-Verbose -Message 'Finding any lingering PS sessions on computers...' @(Get-PSSession -ComputerName $ComputerName).foreach({ Write-Verbose -Message "Removing PS session from [$($_)]..." Remove-PSSession -Session $_ }) } } } } function Get-WindowsUpdateInstallResult { [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory)] [System.Management.Automation.Runspaces.PSSession]$Session, [Parameter()] [ValidateNotNullOrEmpty()] [string]$ScheduledTaskName = 'Windows Update Install' ) $sb = { if ($result = schtasks /query /TN "\$($args[0])" /FO CSV /v | ConvertFrom-Csv) { $result.'Last Result' } } $resultCode = Invoke-Command -Session $Session -ScriptBlock $sb -ArgumentList $ScheduledTaskName switch -exact ($resultCode) { 0 { 'NotStarted' } 1 { 'InProgress' } 2 { 'Installed' } 3 { 'InstalledWithErrors' } 4 { 'Failed' } 5 { 'Aborted' } 6 { 'NoUpdatesNeeded' } 7 { 'RebootRequired' } default { "Unknown result code [$($_)]" } } } function NewUpdateCriteriaQuery { [OutputType([string])] [CmdletBinding()] param ( [Parameter()] [bool]$Installed, [Parameter()] [bool]$Hidden, [Parameter()] [bool]$Assigned, [Parameter()] [bool]$RebootRequired ) $conversion = @{ Installed = 'IsInstalled' Hidden = 'IsHidden' Assigned = 'IsAssigned' RebootRequired = 'RebootRequired' } $queryElements = @() $PSBoundParameters.GetEnumerator().where({ $_.Key -in $conversion.Keys }).foreach({ $queryElements += '{0}={1}' -f $conversion[$_.Key], [int]$_.Value }) $queryElements -join ' and ' } function SearchWindowsUpdate { [OutputType()] [CmdletBinding()] param ( [Parameter()] [string]$Query, [Parameter()] [System.Management.Automation.Runspaces.PSSession]$Session ) $scriptBlock = { $objSession = New-Object -ComObject 'Microsoft.Update.Session' $objSearcher = $objSession.CreateUpdateSearcher() if ($updates = ($objSearcher.Search($args[0]))) { $updates = $updates.Updates ## Save the updates needed to the file system for other functions to pick them up to download/install later. $updates | Export-CliXml -Path "$env:TEMP\Updates.xml" $updates } } Invoke-Command -Session $Session -ScriptBlock $scriptBlock -ArgumentList $Query } function New-WindowsUpdateScheduledTask { [OutputType([void])] [CmdletBinding()] param ( [Parameter(Mandatory)] [System.Management.Automation.Runspaces.PSSession]$Session, [Parameter(Mandatory)] [string]$Name, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [scriptblock]$Scriptblock, [Parameter()] [ValidateNotNullOrEmpty()] [pscredential]$Credential ) $createStartSb = { $taskName = $args[0] $taskArgs = $args[1] -replace '"', '\"' $taskUser = $args[2] $tempScript = "$env:TEMP\WUUpdateScript.ps1" Set-Content -Path $tempScript -Value $taskArgs schtasks /create /SC ONSTART /TN $taskName /TR "powershell.exe -NonInteractive -NoProfile -File $tempScript" /F /RU $taskUser /RL HIGHEST } $command = $Scriptblock.ToString() $icmParams = @{ Session = $Session ScriptBlock = $createStartSb ArgumentList = $Name, $command } if ($PSBoundParameters.ContainsKey('Credential')) { $icmParams.ArgumentList += $Credential.UserName } else { $icmParams.ArgumentList += 'SYSTEM' } Write-Verbose -Message "Running code via powershell.exe: [$($command)]" Invoke-Command @icmParams } function Wait-WindowsUpdate { <# .SYNOPSIS This function looks for any currently running background jobs that were created by Install-WindowsUpdate and continually waits for all of them to finish before returning control to the console. .EXAMPLE PS> Wait-WindowsUpdate .PARAMETER Timeout An optional integer parameter representing the amount of seconds to wait for the job to finish. #> [OutputType([void])] [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [int]$Timeout = 300 ) process { try { if ($updateJobs = (Get-Job -Name '*Windows Update Install*').where({ $_.State -eq 'Running'})) { $timer = Start-Timer while ((Microsoft.PowerShell.Core\Get-Job -Id $updateJobs.Id | Where-Object { $_.State -eq 'Running' }) -and ($timer.Elapsed.TotalSeconds -lt $Timeout)) { Write-Verbose -Message "Waiting for all Windows Update install background jobs to complete..." Start-Sleep -Seconds 3 } Stop-Timer -Timer $timer } } catch { throw $_.Exception.Message } } } |