210 lines
10 KiB
PowerShell
210 lines
10 KiB
PowerShell
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"
|
|
} |