~/2025/10/15/powershell-hyper-v-automate-vm-checkpoints-before-patch-tuesday.md
PowerShell: Hyper-V – Automate VM Checkpoints Before Patch Tuesday
--- author: Tom Lasswell date: read: 4 min in: [ps, scripts] tags: [powershell, hyper-v, cluster] ---
$ grep -n '^#' post.md
Patch Tuesday should be boring. The way I make it boring on Hyper-V is to checkpoint every running VM right before the update window opens, so a bad driver or a broken service after reboot is a two-minute Restore-VMSnapshot instead of a restore-from-backup ticket. This script takes the checkpoint, names it with a prefix and the date so it is easy to find, prunes the checkpoints its own earlier runs left behind, and works against a single host, a list of hosts or every node of a failover cluster. Microsoft publishes the monthly security release on the second Tuesday of each month (10:00 AM Pacific), so the script can also refuse to run outside that week.
Requirements
- Windows PowerShell 5.1 on a host or management machine with the Hyper-V PowerShell module (
Hyper-V-PowerShellfeature). AddRSAT-Clustering-PowerShellif you use-ClusterName. - Hyper-V Administrators (or local Administrators) on every target host.
- VMs at configuration version 6.2 or later for production checkpoints, and a guest whose Volume Shadow Copy Service (Windows) or file system freeze (Linux) works, since that is what makes the checkpoint data consistent.
- Free space on each VM's volume or CSV for the differencing disk (
.avhdx) to grow during the patch window, and again for the merge when the checkpoint is deleted.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
VMName | string | No | Names of VMs to checkpoint. Defaults to every running VM on the target hosts. |
ComputerName | string | No | Hyper-V hosts to target. Defaults to the local computer. Ignored when ClusterName is set. |
ClusterName | string | No | Failover cluster whose nodes (those that are Up) are all targeted. |
RetentionDays | int | No | Checkpoints created by this script and older than this many days are removed before new ones are taken. Defaults to 7. |
CheckpointPrefix | string | No | Prefix used to name and identify this script's checkpoints, so cleanup only ever touches its own. Defaults to PatchTuesday. |
RequirePatchWeek | switch | No | Exit without doing anything unless today falls in the seven days starting on this month's second Tuesday. |
The script also honours -WhatIf and -Verbose.
Usage
Checkpoint every running VM on the local host with the default seven-day retention, showing what it does.
.\New-PrePatchCheckpoint.ps1 -Verbose
Checkpoint three VMs on a cluster and keep the checkpoints for only two days.
.\New-PrePatchCheckpoint.ps1 -ClusterName 'hv-clu01' -VMName 'app-svr01', 'app-svr02', 'sql-svr01' -RetentionDays 2
The script returns one object per action, so the output doubles as a change record.
VM Host Action Checkpoint CheckpointType
-- ---- ------ ---------- --------------
app-svr01 HV-NODE01 Removed PatchTuesday-2025-09-09 Production
app-svr01 HV-NODE01 Created PatchTuesday-2025-10-15 Production
app-svr02 HV-NODE02 Created PatchTuesday-2025-10-15 Production
sql-svr01 HV-NODE02 Failed PatchTuesday-2025-10-15 ProductionOnly
Preview a cluster-wide run without changing anything.
.\New-PrePatchCheckpoint.ps1 -ClusterName 'hv-clu01' -WhatIf
Run it from Cluster-Aware Updating, so each node checkpoints its own VMs just before CAU drains and patches it. The pre-update script must be a .ps1 reachable from every node (a CSV or a highly available share), and PowerShell remoting must be enabled on each node.
$parameters = @{
ClusterName = 'hv-clu01'
CauPluginName = 'Microsoft.WindowsUpdatePlugin'
PreUpdateScript = 'C:\ClusterStorage\Volume1\Scripts\New-PrePatchCheckpoint.ps1'
MaxFailedNodes = 1
MaxRetriesPerNode = 3
RequireAllNodesOnline = $true
Force = $true
}
Invoke-CauRun @parameters
Script
<#
.SYNOPSIS
Creates a dated pre-patch checkpoint on running Hyper-V VMs and prunes the ones earlier runs created.
.DESCRIPTION
Finds the target VMs (every running VM on the local host, on a list of hosts, or on every Up node of a
failover cluster, optionally filtered by name), removes this script's own checkpoints older than the
retention window, then takes a checkpoint named <prefix>-<yyyy-MM-dd> using each VM's configured
checkpoint type. Returns one object per action. A failure on one VM is reported and the run continues,
so the script is safe to use as a Cluster-Aware Updating pre-update script.
.PARAMETER VMName
Names of VMs to checkpoint. Defaults to every running VM on the target hosts.
.PARAMETER ComputerName
Hyper-V hosts to target. Defaults to the local computer. Ignored when ClusterName is set.
.PARAMETER ClusterName
Failover cluster whose nodes (those that are Up) are all targeted.
.PARAMETER RetentionDays
Checkpoints created by this script and older than this many days are removed before new ones are taken.
.PARAMETER CheckpointPrefix
Prefix used to name and identify this script's checkpoints.
.PARAMETER RequirePatchWeek
Exit unless today falls in the seven days starting on this month's second Tuesday.
.EXAMPLE
.\New-PrePatchCheckpoint.ps1 -ClusterName 'hv-clu01' -VMName 'app-svr01', 'app-svr02' -RetentionDays 2
.NOTES
Author : Thomas Lasswell (https://www.techcolumnist.com)
Version : 1.0 (2025-10-15)
Requires: Hyper-V PowerShell module; FailoverClusters module for -ClusterName
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory = $false)]
[string[]]$VMName,
[Parameter(Mandatory = $false)]
[string[]]$ComputerName = $env:COMPUTERNAME,
[Parameter(Mandatory = $false)]
[string]$ClusterName,
[Parameter(Mandatory = $false)]
[ValidateRange(0, 365)]
[int]$RetentionDays = 7,
[Parameter(Mandatory = $false)]
[string]$CheckpointPrefix = 'PatchTuesday',
[switch]$RequirePatchWeek
)
function Get-PatchTuesday {
param (
[datetime]$Date
)
# Second Tuesday of the month: find the first Tuesday, then add a week.
$firstOfMonth = [datetime]::new($Date.Year, $Date.Month, 1)
$daysToTuesday = ([int][System.DayOfWeek]::Tuesday - [int]$firstOfMonth.DayOfWeek + 7) % 7
return $firstOfMonth.AddDays($daysToTuesday + 7)
}
$now = Get-Date
if ($RequirePatchWeek) {
$patchTuesday = Get-PatchTuesday -Date $now
if ($now.Date -lt $patchTuesday -or $now.Date -ge $patchTuesday.AddDays(7)) {
Write-Verbose "Today is outside the patch week that starts $($patchTuesday.ToString('yyyy-MM-dd')); nothing to do"
return
}
}
# Work out which hosts to query.
$targetHosts = if ($ClusterName) {
(Get-ClusterNode -Cluster $ClusterName | Where-Object { $_.State -eq 'Up' }).Name
} else {
$ComputerName
}
# Get-VM with no host falls back to the local computer, so stop rather than checkpoint the wrong machine.
if ($ClusterName -and -not $targetHosts) {
throw "No nodes of cluster $ClusterName are Up."
}
$allVMs = Get-VM -ComputerName $targetHosts
$targetVMs = if ($VMName) {
$allVMs | Where-Object { $VMName -contains $_.Name }
} else {
$allVMs | Where-Object { $_.State -eq 'Running' }
}
foreach ($name in $VMName) {
if ($targetVMs.Name -notcontains $name) {
Write-Warning "VM '$name' was not found on $($targetHosts -join ', ')"
}
}
$checkpointName = '{0}-{1:yyyy-MM-dd}' -f $CheckpointPrefix, $now
$cutoffDate = $now.AddDays(-$RetentionDays)
foreach ($vm in $targetVMs) {
if ("$($vm.CheckpointType)" -eq 'Disabled') {
Write-Warning "Checkpoints are disabled on $($vm.Name), skipping"
continue
}
if ("$($vm.CheckpointType)" -eq 'Standard') {
Write-Warning "$($vm.Name) uses standard checkpoints, which capture memory state and are not application consistent"
}
# Remove this script's own expired checkpoints before creating a new one.
$oldCheckpoints = Get-VMSnapshot -VM $vm |
Where-Object { $_.Name -like "$CheckpointPrefix-*" -and $_.CreationTime -lt $cutoffDate }
foreach ($old in $oldCheckpoints) {
if ($PSCmdlet.ShouldProcess("$($vm.Name) on $($vm.ComputerName)", "Remove checkpoint '$($old.Name)'")) {
Write-Verbose "Removing expired checkpoint '$($old.Name)' on $($vm.Name)"
$removeAction = 'Removed'
try {
Remove-VMSnapshot -VMSnapshot $old -ErrorAction Stop
} catch {
Write-Warning "Removing checkpoint '$($old.Name)' from $($vm.Name) failed: $($_.Exception.Message)"
$removeAction = 'Failed'
}
[PSCustomObject]@{
VM = $vm.Name
Host = $vm.ComputerName
Action = $removeAction
Checkpoint = $old.Name
CheckpointType = "$($vm.CheckpointType)"
}
}
}
$existing = Get-VMSnapshot -VM $vm -Name $checkpointName -ErrorAction SilentlyContinue
if ($existing) {
Write-Verbose "Checkpoint '$checkpointName' already exists on $($vm.Name), skipping"
continue
}
if ($PSCmdlet.ShouldProcess("$($vm.Name) on $($vm.ComputerName)", "Create checkpoint '$checkpointName'")) {
Write-Verbose "Creating checkpoint '$checkpointName' on $($vm.Name)"
$action = 'Created'
try {
Checkpoint-VM -VM $vm -SnapshotName $checkpointName -ErrorAction Stop
} catch {
Write-Warning "Checkpoint of $($vm.Name) failed: $($_.Exception.Message)"
$action = 'Failed'
}
[PSCustomObject]@{
VM = $vm.Name
Host = $vm.ComputerName
Action = $action
Checkpoint = $checkpointName
CheckpointType = "$($vm.CheckpointType)"
}
}
}
Notes
- Checkpoints are not backups. Microsoft's own FAQ answers "Should checkpoints be used as a substitute for backups?" with a flat no: a checkpoint is not the same as a VSS writer backup and is not recommended as a permanent data or system recovery solution. It lives on the same storage as the VM, so it protects you against a bad patch for the day or two it takes to confirm the update went well, and does nothing for storage failure or ransomware. Keep your real backup schedule regardless.
- The checkpoint type comes from the VM, not the script. Production (the default for new VMs) uses VSS in Windows guests or file system freeze in Linux guests and falls back to a standard checkpoint if that fails; ProductionOnly fails instead of falling back, which is why
sql-svr01showsFailedin the sample above. Change it withSet-VM -Name <vmname> -CheckpointType ProductionOnlyif you would rather have a failure than a memory-state checkpoint. Microsoft warns that standard checkpoints can cause consistency problems for anything that replicates between nodes, Active Directory being the example it gives. - Keep the retention short. Microsoft notes that the presence of a checkpoint reduces the VM's disk performance, and a differencing disk on a write-heavy SQL Server or Exchange VM grows for as long as it exists. I run
RetentionDaysat 1 or 2 on those hosts soRemove-VMSnapshotmerges the.avhdxback before the next cycle. A merge needs free space; if it fails with 0x80070070, free space or export the VM to a larger volume. Never delete.avhdxfiles by hand. - Rolling back a production checkpoint leaves the VM off (there is no memory state to resume), so plan for a guest boot:
Restore-VMSnapshot -VMName <vmname> -Name <checkpoint> -Confirm:$false, thenStart-VM -Name <vmname>. - Off VMs are skipped by default because nothing changes inside them until they start, so there is nothing for a patch to break. Name them in
-VMNameif you want a checkpoint anyway;Checkpoint-VMworks on an off VM. - As a CAU pre-update script, a script that fails stops that node from being updated. That is why the checkpoint and cleanup calls are wrapped in
try/catch: one VM whose VSS writer misbehaves, or whose old checkpoint won't merge, produces a warning and aFailedrow instead of blocking the whole Updating Run.Get-VMSnapshot,Checkpoint-VMandRemove-VMSnapshotare the real cmdlet names;Get-VMCheckpointand friends are aliases the Hyper-V module adds, so they are avoided here.
Source
- Using checkpoints (standard versus production, checkpoint types, merging)
- Checkpoints and Snapshots Overview (the "not a substitute for backups" FAQ, performance and merge space)
- Checkpoint-VM, Get-VMSnapshot and Remove-VMSnapshot cmdlet references
- Upgrade virtual machine version in Hyper-V (production checkpoints need configuration version 6.2)
- Update release cycle for Windows clients (monthly security release timing)
- Invoke-CauRun and Cluster-Aware Updating overview