I wanna write following orchestration that will be run regulary:
- Remove VM from load balancer
- Test if there are some active connections to a service on a VM.
- Invoke a powershell script that will install windows updates and do a OS reboot.
- Check status page of service.
- Return a VM to a pool.
I have problem with the pwsh script on Windows Server 2022. It declares that updates were installed but doesn't make restart and the list of updates from GUI / Settings / Updates is not empty.
The core calls
Get-WindowsUpdate -MicrosoftUpdate -AcceptAll |
Install-WindowsUpdate -AcceptAll -AutoReboot -Verbose 4
Should I use powershell 7?
The default is
PSVersion 5.1.20348.2849
Isn't there a standard solution?
The full script is
param(
[string]$Command
)
# --- Global paths & logging ---
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$ScriptPath = $MyInvocation.MyCommand.Path
Write-Host $ScriptPath
$LogFile = Join-Path $ScriptDir ("AutoUpdate_" + (Get-Date -Format "yyyy-MM") + ".log")
# Ensure log directory exists
New-Item -ItemType Directory -Path $ScriptDir -Force | Out-Null
function Write-Log {
param([Parameter(Mandatory)][string]$Message)
$ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$line = "[$ts] $Message"
Write-Host $line
Add-Content -Path $LogFile -Value $line
}
function Ensure-PSWindowsUpdate {
Write-Log "Checking PSWindowsUpdate module..."
try {
if (-not (Get-Module -ListAvailable -Name PSWindowsUpdate)) {
Write-Log "Installing NuGet provider and PSWindowsUpdate..."
Install-PackageProvider -Name NuGet -Force -ErrorAction Stop
# Try global (preferred when running as admin/SYSTEM)
try {
Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -ErrorAction Stop
} catch {
# Fallback to CurrentUser scope (may work for interactive runs)
Install-Module -Name PSWindowsUpdate -Force -Scope CurrentUser -ErrorAction Stop
}
}
Import-Module PSWindowsUpdate -ErrorAction Stop
Write-Log "PSWindowsUpdate is available."
return $true
} catch {
Write-Log "ERROR ensuring PSWindowsUpdate: $($_.Exception.Message)"
return $false
}
}
function Invoke-AutoUpdate {
<#
.SYNOPSIS
Searches, downloads and installs updates; reboots automatically if needed.
.NOTES
Run elevated. Best run as SYSTEM for reliability on servers.
#>
Write-Log "===== Starting Windows Update Process ====="
try {
if (-not (Ensure-PSWindowsUpdate)) {
throw "PSWindowsUpdate module unavailable."
}
Write-Log "Ensuring Windows Update service is running..."
Set-Service wuauserv -StartupType Manual
Start-Service wuauserv
Write-Log "Searching for and installing updates..."
# -AutoReboot triggers reboot when required after install
# Install-WindowsUpdate -MicrosoftUpdate -AcceptAll -AutoReboot -Install -Verbose 4>&1 |
Get-WindowsUpdate -MicrosoftUpdate -AcceptAll |
Install-WindowsUpdate -AcceptAll -AutoReboot -Verbose 4>&1 |
ForEach-Object {
if ($_ -is [System.Management.Automation.VerboseRecord]) {
Write-Log $_.Message
}
elseif ($_ -is [string]) {
Write-Log $_
}
}
Write-Log "Update workflow finished (system may reboot if required)."
} catch {
Write-Log "ERROR during update: $($_.Exception.Message)"
throw
} finally {
Write-Log "===== Windows Update Script Completed ====="
}
}
function Register-AutoUpdateTask {
<#
.SYNOPSIS
Registers a weekly scheduled task to run this script.
.PARAMETER RunAs
'System' (default), 'NetworkService', or 'LocalService'
.PARAMETER DayOfWeek
Day to run (default: Monday)
.PARAMETER Time
Time to run in HH:mm (24h) (default: 04:00)
.PARAMETER TaskName
Name of the scheduled task
#>
param(
[ValidateSet('System','NetworkService','LocalService')]
[string]$RunAs = 'System',
[ValidateSet('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday')]
[string]$DayOfWeek = 'Monday',
[ValidatePattern('^[0-2]\d:[0-5]\d$')]
[string]$Time = '04:00',
[string]$TaskName = 'Software602\WinUpdate'
)
# Map to principal identities
switch ($RunAs) {
'System' { $userId = 'SYSTEM' }
'NetworkService' { $userId = 'NT AUTHORITY\NetworkService' }
'LocalService' { $userId = 'NT AUTHORITY\LocalService' }
}
try {
# Build action to run this script
$escaped = $ScriptPath.Replace('"','`"')
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$escaped`" -Command Invoke-AutoUpdate"
# Trigger
$hour, $min = $Time.Split(':')
$at = Get-Date -Hour $hour -Minute $min -Second 0
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek $DayOfWeek -At $at
# Principal
$principal = New-ScheduledTaskPrincipal -UserId $userId -RunLevel Highest -LogonType ServiceAccount
# Settings (retry if missed, stop on battery constraints off)
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew
# Register
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
-Principal $principal -Settings $settings -Description "Installs Windows Updates and reboots if needed." -Force
$msg = "Scheduled task '$TaskName' registered to run as '$RunAs' every $DayOfWeek at $Time."
Write-Log $msg
Write-Host $msg
}
catch {
$err = $_.Exception.Message
$msg = "Failed to create scheduled task action: $err"
Write-Log $msg
Write-Warning $msg
}
}
switch ($Command) {
'Invoke-AutoUpdate' { Invoke-AutoUpdate }
'Register-AutoUpdate' { Register-AutoUpdateTask -RunAs System }
default { Write-Host "No valid -Command specified. Options: Invoke-AutoUpdate, Register-AutoUpdate" }
}