ps/Modules/Cole.PowerShell.Developer/TODO/Invoke-WindowsUpdatesHeadless.ps1

210 lines
10 KiB
PowerShell
Raw Normal View History

2023-05-30 22:51:22 -07:00
function Invoke-WindowsUpdatesHeadless {
<#
.SYNOPSIS
Find and apply Windows Updates on remote computers
TODO: This function does not monitor to see when the instances are back up and ready again.
.PARAMETER ComputerName
One or more fully qualified computer names as a string array
.PARAMETER DoRestarts
Restart when completed
.PARAMETER DoUpdates
Do the actual update, don't just query for status
This is because this script is still being vetted for working state, we should eventually get rid of this and the DoRestarts flag
.PARAMETER Comment
Useful for when we introduce Invoke-UpdateAWSDrivers where a comment is required for the AWS call
This should typically be the Jira ticket number of the maintenance window
#>
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName, # = ('tea31697.fh.local', 'tea316155.fh.local', 'tea316208.fh.local', 'tea316229.fh.local', 'tea46658.fh.local'),
[switch]$DoUpdates,
[switch]$DoRestarts,
[string]$Comment
)
# Ensure all the computernames are in .fh.local
$ComputerName = $ComputerName | Foreach-Object { if (!$_.EndsWith('.fh.local')) { "$($_).fh.local" } else { $_ } }
Write-Host "Updating chrome on all remote machines"
$sbChrome = {
choco upgrade GoogleChrome -y -r
}
Invoke-Command -ComputerName $ComputerName -ScriptBlock $sbChrome
#region magic session things for PowerShell
# The COMObjects can not be created if you don't do this first
# I should probably have them get removed after the run, but meh, we will reuse it in the future
Write-Host "Ensuring VirtualAccount PSSessionConfiguration exists locally so we can run updates remotely"
if ($null -eq (Get-PSSessionConfiguration -Name 'VirtualAccount' -ErrorAction SilentlyContinue)) {
New-PSSessionConfigurationFile -RunAsVirtualAccount -Path .\VirtualAccount.pssc
# Note this will restart the WinRM service:
Register-PSSessionConfiguration -Name 'VirtualAccount' -Path .\VirtualAccount.pssc -Force
}
$ensureVirtualAccountSessionManagementExistsScriptBlock = {
Write-Host "Ensuring VirtualAccount PSSessionConfiguration exists so we can run updates remotely on $($env:COMPUTERNAME)"
if ($null -eq (Get-PSSessionConfiguration -Name 'VirtualAccount' -ErrorAction SilentlyContinue)) {
New-PSSessionConfigurationFile -RunAsVirtualAccount -Path .\VirtualAccount.pssc
# Note this will restart the WinRM service:
Register-PSSessionConfiguration -Name 'VirtualAccount' -Path .\VirtualAccount.pssc -Force
}
}
Invoke-Command -ComputerName $ComputerName -ScriptBlock $ensureVirtualAccountSessionManagementExistsScriptBlock
#endregion magic session things for PowerShell
# Take inventory, find out if we need to reboot before we continue
$rebootRequiredScriptBlock = {
param (
[string]$computerName
)
# This lets us invoke the COMObjects because they only let you run updates "locally"
$session = New-PSSession -ComputerName $computerName -ConfigurationName 'VirtualAccount'
$serverScript = {
# TODO: We could just move this to its own standalone function instead of it being deeply nested in callbacks
$UpdateCollection = New-Object -ComObject 'Microsoft.Update.UpdateColl' -Strict
$Searcher = New-Object -ComObject 'Microsoft.Update.Searcher' -Strict
$Session = New-Object -ComObject 'Microsoft.Update.Session' -Strict
$Installer = New-Object -ComObject 'Microsoft.Update.Installer' -Strict
$Searcher.Search("") | Out-Null
$totalHistoryCount = $Searcher.GetTotalHistoryCount()
$returnUpdates = @()
$Updates = @($Searcher.Search("IsHidden=0 and IsInstalled=0").Updates)
foreach ($update in $Updates) {
$UpdateCollection.Add($update) | Out-Null
$returnUpdates += New-Object -Type PSObject -Property @{
BundledUpdates = $update.BundledUpdates
Categories = $update.Categories
CveIDs = $update.CveIDs
Deadline = $update.Deadline
DeltaCompressedContentAvailable = $update.DeltaCompressedContentAvailable
DeltaCompressedContentPreferred = $update.DeltaCompressedContentPreferred
DeploymentAction = $update.DeploymentAction
Description = $update.Description
DownloadPriority = $update.DownloadPriority
EulaAccepted = $update.EulaAccepted
EulaText = $update.EulaText
HandlerID = $update.HandlerID
Identity = $update.Identity
InstallationBehavior = $update.InstallationBehavior
IsBeta = $update.IsBeta
IsDownloaded = $update.IsDownloaded
IsHidden = $update.IsHidden
IsInstalled = $update.IsInstalled
IsMandatory = $update.IsMandatory
IsPresent = $update.IsPresent
IsUninstallable = $update.IsUninstallable
KBArticleIDs = $update.KBArticleIDs
Languages = $update.Languages
LastDeploymentChangeTime = $update.LastDeploymentChangeTime
MoreInfoUrls = $update.MoreInfoUrls
MsrcSeverity = $update.MsrcSeverity
RebootRequired = $update.RebootRequired
RecommendedCpuSpeed = $update.RecommendedCpuSpeed
RecommendedHardDiskSpace = $update.RecommendedHardDiskSpace
RecommendedMemory = $update.RecommendedMemory
ReleaseNotes = $update.ReleaseNotes
SecurityBulletinIDs = $update.SecurityBulletinIDs
SupersededUpdateIDs = $update.SupersededUpdateIDs
SupportUrl = $update.SupportUrl
Title = $update.Title
Type = $update.Type
}
}
if ($UpdateCollection.Count -gt 0) {
$Downloader = $Session.CreateUpdateDownloader()
$Downloader.Updates = $UpdateCollection
$Downloader.Download() | Out-Null
}
$Installer.AllowSourcePrompts = $true
$installer.ForceQuiet = $true
$Installer.Updates = $UpdateCollection
$isRebootRequired = $installer.RebootRequiredBeforeInstallation
return $isRebootRequired, $returnUpdates, $totalHistoryCount
}
$isRebootRequired, $returnUpdates, $totalHistoryCount = Invoke-Command -Session $session -ScriptBlock $serverScript
Exit-PSSession
$retValue = @{ ComputerName = $computerName; IsRebootRequired = $isRebootRequired; Updates = $returnUpdates; TotalHistoryCount = $totalHistoryCount }
return $retValue
}
$doInstallsScriptBlock = {
param (
$computerName
)
# This lets us invoke the COMObjects because they only let you run updates "locally"
$session = New-PSSession -ComputerName $computerName -ConfigurationName 'VirtualAccount'
$serverScript = {
# TODO: We could just move this to its own standalone function instead of it being deeply nested in callbacks
$UpdateCollection = New-Object -ComObject Microsoft.Update.UpdateColl
$Searcher = New-Object -ComObject Microsoft.Update.Searcher
$Session = New-Object -ComObject Microsoft.Update.Session
$Installer = New-Object -ComObject Microsoft.Update.Installer
$Updates = @($Searcher.Search("IsHidden=0 and IsInstalled=0").Updates)
foreach ($update in $Updates) {
$UpdateCollection.Add($update) | Out-Null
}
if ($UpdateCollection.Count -gt 0) {
$Downloader = $Session.CreateUpdateDownloader()
$Downloader.Updates = $UpdateCollection
$Downloader.Download() | Out-Null
}
$Installer.AllowSourcePrompts = $true
$installer.ForceQuiet = $true
$Installer.Updates = $UpdateCollection
Write-Host "Beginning install"
$result = $Installer.Install()
if ($result.ResultCode -ne 2) {
Write-Warning "$($env:COMPUTERNAME) ResultCode was not 2, it was $($result.ResultCode)"
}
Write-Host "$($env:COMPUTERNAME) finished installing, time to reboot?"
}
Invoke-Command -Session $session -ScriptBlock $serverScript
Exit-PSSession
return @{ ComputerName = $computerName; InstallCompleted = $true }
}
$results = Invoke-Parallel -Script $rebootRequiredScriptBlock -Objects $ComputerNames -ReturnObjects
Write-Output $results
# TODO: Reboot before continuing, if the flag above was true
# We could automate this, but then we don't have any way to monitor for the servers to be back up and running ... yet
if ($results.IsRebootRequired) {
throw "reboots are required on one or more servers. Please reboot before continuing"
}
if ($DoUpdates) {
$results = Invoke-Parallel -Script $doInstallsScriptBlock -Objects $ComputerNames -ReturnObjects
Write-Output $results
}
if ($DoRestarts) {
# Restart the computer(s) and wait for Powershell to be able to run commands on it.
# Wait up to 10 minutes(600 seconds) and poll the computer(s) every 10 seconds
Restart-Computer -ComputerName $ComputerName -Wait -For PowerShell -Timeout 600 -Delay 10
}
# TODO: Add the ability to monitor for when reboots are done, then run Invoke-UpdateAWSDrivers -ComputerName $ComputerName
Write-Host "finished"
}