Files
hyperv-backup-scripts/Daily-SnapshotBackup.ps1

488 lines
17 KiB
PowerShell

# Daily VM Snapshot Backup Script - Checkpoint-based Incremental Backup
# Run Daily at 3am
# Location: Hyper-V Server
# Creates daily checkpoints and backs up only the differencing data
param(
[Parameter(ParameterSetName='SingleVM', Mandatory=$true)]
[string]$VMName,
[Parameter(ParameterSetName='AllVMs', Mandatory=$true)]
[switch]$AllVMs,
[Parameter(ParameterSetName='AllVMs')]
[string[]]$ExcludeVMs = @(),
[int]$MaxSnapshots = 7, # Keep 7 days of snapshots
[switch]$BackupSnapshotFiles, # Whether to also backup snapshot files to NAS
[string]$SnapshotPrefix = "Daily"
)
# Configuration - Modify these paths as needed
$NASBackupPath = "\\nas5362\Backups\Daily"
$LocalSnapshotBackupPath = "F:\Backups\Daily" # For snapshot file backups
$SevenZipPath = "C:\Program Files\7-Zip\7z.exe"
# Configure logging
$LogPath = "C:\Scripts\Logs\Daily_Snapshot_$(Get-Date -Format 'yyyyMM').log"
$null = New-Item -ItemType Directory -Path (Split-Path $LogPath -Parent) -Force -ErrorAction SilentlyContinue
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$LogEntry = "[$TimeStamp] [$Level] $Message"
Write-Host $LogEntry
Add-Content -Path $LogPath -Value $LogEntry
}
function Test-NetworkPath {
param([string]$Path)
try {
if (-not (Test-Path $Path -ErrorAction Stop)) {
Write-Log "Network path does not exist: $Path" "ERROR"
return $false
}
$TestFile = Join-Path $Path "test_write_$(Get-Date -Format 'yyyyMMddHHmmss').tmp"
try {
$null = New-Item -ItemType File -Path $TestFile -Force -ErrorAction Stop
Remove-Item -Path $TestFile -Force -ErrorAction SilentlyContinue
Write-Log "Network path is accessible and writable: $Path"
return $true
}
catch {
Write-Log "Network path exists but is not writable: $Path - $_" "ERROR"
return $false
}
}
catch {
Write-Log "Network path test failed: $Path - $_" "ERROR"
return $false
}
}
function Get-VMSnapshotFiles {
param([string]$VMName)
try {
$VM = Get-VM -Name $VMName
$CheckpointFiles = @()
# Get all checkpoints for this VM
$Checkpoints = Get-VMCheckpoint -VMName $VMName | Sort-Object CreationTime -Descending
foreach ($Checkpoint in $Checkpoints) {
# Get AVHDX files (differencing disks) for this checkpoint
$CheckpointVHDs = Get-VMCheckpoint -VMName $VMName -Name $Checkpoint.Name | Get-VMHardDiskDrive
foreach ($VHD in $CheckpointVHDs) {
if ($VHD.Path -match "\.avhdx$") {
$CheckpointFiles += [PSCustomObject]@{
CheckpointName = $Checkpoint.Name
CheckpointId = $Checkpoint.Id
CreationTime = $Checkpoint.CreationTime
FilePath = $VHD.Path
FileType = "AVHDX"
}
}
}
# Also get the checkpoint configuration files
$VMPath = Split-Path $VM.ConfigurationLocation -Parent
$CheckpointConfigPath = Join-Path $VMPath "Snapshots\$($Checkpoint.Id.Guid).vmrs"
if (Test-Path $CheckpointConfigPath) {
$CheckpointFiles += [PSCustomObject]@{
CheckpointName = $Checkpoint.Name
CheckpointId = $Checkpoint.Id
CreationTime = $Checkpoint.CreationTime
FilePath = $CheckpointConfigPath
FileType = "VMRS"
}
}
}
return $CheckpointFiles
}
catch {
Write-Log "Error getting snapshot files for $VMName : $_" "ERROR"
return @()
}
}
function Backup-SnapshotFiles {
param(
[string]$VMName,
[array]$SnapshotFiles,
[string]$BackupPath
)
try {
Write-Log "Backing up snapshot files for $VMName..."
# Create backup directory
$VMBackupPath = Join-Path $BackupPath $VMName
$DateBackupPath = Join-Path $VMBackupPath (Get-Date -Format 'yyyyMMdd')
$null = New-Item -ItemType Directory -Path $DateBackupPath -Force
$TotalSize = 0
$BackedUpFiles = 0
foreach ($File in $SnapshotFiles) {
try {
if (Test-Path $File.FilePath) {
$FileName = Split-Path $File.FilePath -Leaf
$BackupFilePath = Join-Path $DateBackupPath $FileName
# Only backup if file doesn't exist or is newer
$ShouldCopy = $true
if (Test-Path $BackupFilePath) {
$SourceDate = (Get-Item $File.FilePath).LastWriteTime
$BackupDate = (Get-Item $BackupFilePath).LastWriteTime
$ShouldCopy = $SourceDate -gt $BackupDate
}
if ($ShouldCopy) {
Copy-Item -Path $File.FilePath -Destination $BackupFilePath -Force
$FileSize = (Get-Item $File.FilePath).Length
$TotalSize += $FileSize
$BackedUpFiles++
$FileSizeMB = [math]::Round($FileSize / 1MB, 2)
Write-Log "Backed up $FileName ($FileSizeMB MB)"
}
}
}
catch {
Write-Log "Failed to backup $($File.FilePath): $_" "ERROR"
}
}
$TotalSizeGB = [math]::Round($TotalSize / 1GB, 2)
Write-Log "Snapshot file backup completed for $VMName. Files: $BackedUpFiles, Size: $TotalSizeGB GB"
return $true
}
catch {
Write-Log "Error backing up snapshot files for $VMName : $_" "ERROR"
return $false
}
}
function Cleanup-OldSnapshots {
param(
[string]$VMName,
[int]$MaxSnapshots,
[string]$SnapshotPrefix
)
try {
Write-Log "Cleaning up old snapshots for $VMName (keeping $MaxSnapshots)..."
# Get all snapshots with our prefix, sorted by creation time
$Snapshots = Get-VMCheckpoint -VMName $VMName |
Where-Object { $_.Name -like "$SnapshotPrefix*" } |
Sort-Object CreationTime -Descending
if ($Snapshots.Count -gt $MaxSnapshots) {
$ToDelete = $Snapshots | Select-Object -Skip $MaxSnapshots
foreach ($Snapshot in $ToDelete) {
Write-Log "Removing old snapshot: $($Snapshot.Name) (Created: $($Snapshot.CreationTime))"
Remove-VMCheckpoint -VMName $VMName -Name $Snapshot.Name -IncludeAllChildCheckpoints
}
Write-Log "Removed $($ToDelete.Count) old snapshots for $VMName"
}
else {
Write-Log "No cleanup needed for $VMName (has $($Snapshots.Count) snapshots)"
}
}
catch {
Write-Log "Error cleaning up snapshots for $VMName : $_" "ERROR"
}
}
function Cleanup-OldSnapshotBackups {
param(
[string]$BackupPath,
[string]$VMName,
[int]$MaxBackups
)
try {
$VMBackupPath = Join-Path $BackupPath $VMName
if (-not (Test-Path $VMBackupPath)) {
return
}
# Get backup folders (by date)
$BackupFolders = Get-ChildItem -Path $VMBackupPath -Directory |
Where-Object { $_.Name -match '^\d{8}$' } |
Sort-Object Name -Descending
if ($BackupFolders.Count -gt $MaxBackups) {
$ToDelete = $BackupFolders | Select-Object -Skip $MaxBackups
foreach ($Folder in $ToDelete) {
Write-Log "Removing old snapshot backup folder: $($Folder.FullName)"
Remove-Item -Path $Folder.FullName -Recurse -Force
}
Write-Log "Removed $($ToDelete.Count) old snapshot backup folders for $VMName"
}
}
catch {
Write-Log "Error cleaning up old snapshot backups for $VMName : $_" "ERROR"
}
}
function Create-DailySnapshot {
param(
[string]$VMName,
[int]$MaxSnapshots,
[string]$SnapshotPrefix
)
try {
Write-Log "=== Creating daily snapshot for VM: $VMName ==="
# Verify VM exists and is running
$VM = Get-VM -Name $VMName -ErrorAction Stop
Write-Log "VM found: $($VM.Name) - State: $($VM.State)"
if ($VM.State -ne 'Running') {
Write-Log "VM is not running, skipping snapshot creation" "WARNING"
return $false
}
# Create snapshot name with timestamp
$SnapshotName = "$SnapshotPrefix_$(Get-Date -Format 'yyyyMMdd_HHmm')"
Write-Log "Creating snapshot: $SnapshotName"
$StartTime = Get-Date
# Create the checkpoint
Checkpoint-VM -Name $VMName -SnapshotName $SnapshotName
$EndTime = Get-Date
$Duration = ($EndTime - $StartTime).TotalSeconds
Write-Log "Snapshot created successfully in $Duration seconds"
# Get the new snapshot info
$NewSnapshot = Get-VMCheckpoint -VMName $VMName -Name $SnapshotName
if ($NewSnapshot) {
Write-Log "Snapshot ID: $($NewSnapshot.Id)"
Write-Log "Snapshot Path: $($NewSnapshot.Path)"
# Get size of new differencing files
$SnapshotFiles = Get-VMSnapshotFiles -VMName $VMName | Where-Object { $_.CheckpointName -eq $SnapshotName }
$TotalSnapshotSize = 0
foreach ($File in $SnapshotFiles) {
if (Test-Path $File.FilePath) {
$FileSize = (Get-Item $File.FilePath).Length
$TotalSnapshotSize += $FileSize
}
}
$SnapshotSizeMB = [math]::Round($TotalSnapshotSize / 1MB, 2)
Write-Log "New snapshot differencing files size: $SnapshotSizeMB MB"
}
# Clean up old snapshots
Cleanup-OldSnapshots -VMName $VMName -MaxSnapshots $MaxSnapshots -SnapshotPrefix $SnapshotPrefix
Write-Log "=== Daily snapshot completed successfully for $VMName ==="
return $true
}
catch {
Write-Log "=== Snapshot creation failed for $VMName : $_ ===" "ERROR"
return $false
}
}
function Process-SingleVM {
param(
[string]$VMName,
[int]$MaxSnapshots,
[string]$SnapshotPrefix,
[bool]$BackupFiles
)
$Success = $false
try {
# Create daily snapshot
$SnapshotSuccess = Create-DailySnapshot -VMName $VMName -MaxSnapshots $MaxSnapshots -SnapshotPrefix $SnapshotPrefix
if ($SnapshotSuccess -and $BackupFiles) {
# Backup snapshot files to NAS/Local storage
$SnapshotFiles = Get-VMSnapshotFiles -VMName $VMName
if ($SnapshotFiles.Count -gt 0) {
# Backup to local storage
if (Test-Path $LocalSnapshotBackupPath) {
$LocalBackupSuccess = Backup-SnapshotFiles -VMName $VMName -SnapshotFiles $SnapshotFiles -BackupPath $LocalSnapshotBackupPath
}
# Backup to NAS if accessible
if (Test-NetworkPath $NASBackupPath) {
$NASBackupSuccess = Backup-SnapshotFiles -VMName $VMName -SnapshotFiles $SnapshotFiles -BackupPath $NASBackupPath
# Cleanup old NAS backups
Cleanup-OldSnapshotBackups -BackupPath $NASBackupPath -VMName $VMName -MaxBackups $MaxSnapshots
}
# Cleanup old local backups
if (Test-Path $LocalSnapshotBackupPath) {
Cleanup-OldSnapshotBackups -BackupPath $LocalSnapshotBackupPath -VMName $VMName -MaxBackups $MaxSnapshots
}
}
}
$Success = $SnapshotSuccess
}
catch {
Write-Log "Error processing VM $VMName : $_" "ERROR"
}
return $Success
}
# Main execution
try {
Write-Log "Starting daily snapshot backup process"
# Create directories if they don't exist
if ($BackupSnapshotFiles) {
$null = New-Item -ItemType Directory -Path $LocalSnapshotBackupPath -Force -ErrorAction SilentlyContinue
if (-not (Test-NetworkPath $NASBackupPath)) {
Write-Log "NAS path not accessible - will only backup locally" "WARNING"
}
}
# Determine which VMs to process
$VMsToProcess = @()
if ($AllVMs) {
Write-Log "Processing snapshots for all VMs (excluding specified VMs)"
$AllVirtualMachines = Get-VM | Where-Object { $_.Name -notin $ExcludeVMs }
$VMsToProcess = $AllVirtualMachines.Name
if ($ExcludeVMs.Count -gt 0) {
Write-Log "Excluded VMs: $($ExcludeVMs -join ', ')"
}
Write-Log "VMs to process: $($VMsToProcess -join ', ')"
}
else {
$VMsToProcess = @($VMName)
Write-Log "Processing single VM: $VMName"
}
if ($VMsToProcess.Count -eq 0) {
throw "No VMs found to process"
}
# Process each VM
$SuccessCount = 0
$FailureCount = 0
$Results = @()
foreach ($VM in $VMsToProcess) {
$StartTime = Get-Date
$Success = Process-SingleVM -VMName $VM -MaxSnapshots $MaxSnapshots -SnapshotPrefix $SnapshotPrefix -BackupFiles $BackupSnapshotFiles.IsPresent
$EndTime = Get-Date
$Duration = ($EndTime - $StartTime).TotalMinutes
$Results += [PSCustomObject]@{
VMName = $VM
Success = $Success
Duration = [math]::Round($Duration, 1)
}
if ($Success) {
$SuccessCount++
} else {
$FailureCount++
}
}
# Final summary
Write-Log "=== DAILY SNAPSHOT SUMMARY ==="
Write-Log "Total VMs processed: $($VMsToProcess.Count)"
Write-Log "Successful snapshots: $SuccessCount"
Write-Log "Failed snapshots: $FailureCount"
Write-Log "Snapshot file backup: $(if ($BackupSnapshotFiles) { 'ENABLED' } else { 'DISABLED' })"
foreach ($Result in $Results) {
$Status = if ($Result.Success) { "SUCCESS" } else { "FAILED" }
Write-Log "$($Result.VMName): $Status ($($Result.Duration) minutes)"
}
# Show current snapshot status for all VMs
Write-Log "=== CURRENT SNAPSHOT STATUS ==="
foreach ($VM in $VMsToProcess) {
try {
$Snapshots = Get-VMCheckpoint -VMName $VM | Where-Object { $_.Name -like "$SnapshotPrefix*" } | Sort-Object CreationTime -Descending
Write-Log "$VM : $($Snapshots.Count) $SnapshotPrefix snapshots"
# Show total size of snapshot files
$SnapshotFiles = Get-VMSnapshotFiles -VMName $VM
$TotalSize = 0
foreach ($File in $SnapshotFiles) {
if (Test-Path $File.FilePath) {
$TotalSize += (Get-Item $File.FilePath).Length
}
}
$TotalSizeGB = [math]::Round($TotalSize / 1GB, 2)
Write-Log "$VM : Total snapshot storage: $TotalSizeGB GB"
}
catch {
Write-Log "$VM : Could not get snapshot status" "WARNING"
}
}
if ($FailureCount -gt 0) {
Write-Log "Daily snapshot process completed with $FailureCount failure(s)" "WARNING"
exit 1
} else {
Write-Log "Daily snapshot process completed successfully" "SUCCESS"
}
}
catch {
Write-Log "Daily snapshot process failed: $_" "ERROR"
exit 1
}
# Example usage:
# Single VM with snapshots only:
# .\Daily-SnapshotBackup.ps1 -VMName "MyVM" -MaxSnapshots 7
#
# Single VM with snapshot file backup:
# .\Daily-SnapshotBackup.ps1 -VMName "MyVM" -MaxSnapshots 7 -BackupSnapshotFiles
#
# All VMs with snapshots only:
# .\Daily-SnapshotBackup.ps1 -AllVMs -MaxSnapshots 7
#
# All VMs with snapshot file backup:
# .\Daily-SnapshotBackup.ps1 -AllVMs -MaxSnapshots 7 -BackupSnapshotFiles
#
# All VMs except specific ones:
# .\Daily-SnapshotBackup.ps1 -AllVMs -ExcludeVMs @("TestVM", "TempVM") -MaxSnapshots 7 -BackupSnapshotFiles
# Daily snapshot workflow:
# 1. Create daily checkpoint/snapshot for each VM
# 2. Optionally backup the differencing files (.avhdx) and config files (.vmrs) to local/NAS storage
# 3. Automatically cleanup old snapshots (keeps specified number)
# 4. Cleanup old snapshot file backups
# 5. Report on current snapshot storage usage
#
# Benefits:
# - Near-instant snapshot creation (seconds, not minutes/hours)
# - Minimal storage overhead (only stores changes since last snapshot)
# - No VM downtime at all
# - Can restore to any point in the last N days quickly
# - Differencing files are much smaller than full backups