Add daily snapshot backup script for checkpoint-based incremental backups
This commit is contained in:
488
Daily-SnapshotBackup.ps1
Normal file
488
Daily-SnapshotBackup.ps1
Normal file
@@ -0,0 +1,488 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user