Loading_
Loading_
Finds aged and oversized snapshots across vCenter, reports reclaimable capacity and consolidates with change-window awareness.
#Requires -Modules VMware.PowerCLI<#.SYNOPSIS Reports and optionally removes aged VMware snapshots. .DESCRIPTION Read-only by default. -Remove consolidates snapshots older than -MaxAgeDays, skipping VMs with a protected tag or an in-flight backup. .EXAMPLE .\Invoke-SnapshotReaper.ps1 -Server vcenter01.corp.local -MaxAgeDays 7 .EXAMPLE .\Invoke-SnapshotReaper.ps1 -Server vcenter01.corp.local -MaxAgeDays 7 -Remove -WhatIf .NOTES Author : AIInfraEngine Version: 3.0.1#>[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]param( [Parameter(Mandatory)] [string]$Server, [ValidateRange(1, 365)] [int]$MaxAgeDays = 7, [ValidateRange(1, 10240)] [int]$MaxSizeGB = 100, [switch]$Remove, [string]$ProtectedTag = 'snapshot-protected', [string]$ReportPath = ".\snapshot-report-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv") begin { $ErrorActionPreference = 'Stop' Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null Write-Verbose "Connecting to $Server" $connection = Connect-VIServer -Server $Server -ErrorAction Stop $cutoff = (Get-Date).AddDays(-$MaxAgeDays) $results = [System.Collections.Generic.List[object]]::new()} process { Write-Host "Scanning snapshots older than $MaxAgeDays day(s)..." -ForegroundColor Cyan $snapshots = Get-VM | Get-Snapshot | Where-Object { $_.Created -lt $cutoff -or $_.SizeGB -gt $MaxSizeGB } if (-not $snapshots) { Write-Host "No snapshots breach the policy." -ForegroundColor Green return } # Tag lookup once rather than per snapshot — this is the expensive call. $protectedVMs = @(Get-TagAssignment -Category $ProtectedTag -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Entity | Select-Object -ExpandProperty Name) foreach ($snap in $snapshots) { $vm = $snap.VM $ageDays = [math]::Round(((Get-Date) - $snap.Created).TotalDays, 1) $datastore = ($vm | Get-Datastore | Select-Object -First 1).Name $chain = @($vm | Get-Snapshot).Count $skipReason = $null if ($protectedVMs -contains $vm.Name) { $skipReason = 'Protected tag' } elseif ($vm.ExtensionData.Runtime.ConsolidationNeeded) { $skipReason = 'Consolidation already pending' } elseif ($snap.Name -match 'VEEAM|CONSOLIDATE_HELPER|_vcls') { $skipReason = 'Backup or system snapshot in flight' } $action = 'Reported' if ($Remove -and -not $skipReason) { if ($PSCmdlet.ShouldProcess("$($vm.Name) / $($snap.Name)", 'Remove snapshot')) { try { Remove-Snapshot -Snapshot $snap -RemoveChildren:$false -Confirm:$false $action = 'Removed' } catch { Write-Warning "Failed to remove $($snap.Name) on $($vm.Name): $($_.Exception.Message)" $action = "Error: $($_.Exception.Message)" } } else { $action = 'WhatIf' } } elseif ($skipReason) { $action = "Skipped: $skipReason" } $results.Add([pscustomobject]@{ VM = $vm.Name PowerState = $vm.PowerState Snapshot = $snap.Name Description = $snap.Description Created = $snap.Created AgeDays = $ageDays SizeGB = [math]::Round($snap.SizeGB, 2) ChainDepth = $chain Datastore = $datastore Action = $action }) }} end { if ($results.Count) { $results | Sort-Object SizeGB -Descending | Export-Csv -Path $ReportPath -NoTypeInformation $totalGB = [math]::Round(($results | Measure-Object SizeGB -Sum).Sum, 1) $byStore = $results | Group-Object Datastore | Sort-Object { ($_.Group | Measure-Object SizeGB -Sum).Sum } -Descending Write-Host "" Write-Host " Snapshots found : $($results.Count)" -ForegroundColor Cyan Write-Host " Reclaimable : $totalGB GB" -ForegroundColor Yellow Write-Host " Removed : $(($results | Where-Object Action -eq 'Removed').Count)" -ForegroundColor Green Write-Host "" Write-Host " Top datastores by reclaimable capacity:" -ForegroundColor Cyan foreach ($group in $byStore | Select-Object -First 5) { $gb = [math]::Round(($group.Group | Measure-Object SizeGB -Sum).Sum, 1) Write-Host (" {0,-28} {1,8} GB" -f $group.Name, $gb) } Write-Host "" Write-Host " Report : $ReportPath" -ForegroundColor Cyan Write-Host "" } Disconnect-VIServer -Server $connection -Confirm:$false}Snapshots are meant to be temporary. In practice they are created before a change, the change succeeds, and nobody removes them — until a datastore fills and takes production down with it.
The script inventories every snapshot with its age, size and chain depth, then reports the reclaimable capacity per datastore so you can prioritise. Removal is opt-in and respects a maintenance window.
It refuses to touch snapshots tagged as protected and skips VMs that are mid-backup, because consolidating during a backup is how you turn a housekeeping job into an incident.
| Name | Type | Required | Description |
|---|---|---|---|
Server | string | Required | vCenter FQDN. |
MaxAgeDays | int | Optional | Snapshot age threshold. Default 7. |
Remove | switch | Optional | Consolidate rather than report only. |
The platform turns any script into a governed automation — versioned, gated, audited and reversible.