(feature): hyper-v backup dryRun mode

This commit is contained in:
Maksym Sadovnychyy 2026-01-28 19:32:28 +01:00
parent 5839f55999
commit 728f657112
4 changed files with 127 additions and 34 deletions

View File

@ -1,7 +1,7 @@
# Hyper-V Backup Script # Hyper-V Backup Script
**Version:** 1.0.1 **Version:** 1.0.2
**Last Updated:** 2026-01-26 **Last Updated:** 2026-01-28
## Overview ## Overview
@ -16,6 +16,7 @@ Production-ready automated backup solution for Hyper-V virtual machines with sch
- ✅ **Checkpoint Management** - Automatic cleanup of backup checkpoints (keeps last 2 for rollback) - ✅ **Checkpoint Management** - Automatic cleanup of backup checkpoints (keeps last 2 for rollback)
- ✅ **Space Validation** - Dynamic space checks for temp (per VM) and destination before copy - ✅ **Space Validation** - Dynamic space checks for temp (per VM) and destination before copy
- ✅ **VM Exclusion** - Exclude specific VMs from backup - ✅ **VM Exclusion** - Exclude specific VMs from backup
- ✅ **Dry Run Mode** - Test backup detection without actual export/copy operations
- ✅ **Detailed Logging** - Comprehensive logging with timestamps and severity levels - ✅ **Detailed Logging** - Comprehensive logging with timestamps and severity levels
- ✅ **Lock Files** - Prevents concurrent execution - ✅ **Lock Files** - Prevents concurrent execution
- ✅ **Error Handling** - Proper exit codes and error reporting - ✅ **Error Handling** - Proper exit codes and error reporting
@ -26,7 +27,7 @@ Production-ready automated backup solution for Hyper-V virtual machines with sch
- Windows Server with Hyper-V role installed - Windows Server with Hyper-V role installed
- PowerShell 5.1 or later - PowerShell 5.1 or later
- Administrator privileges - Administrator privileges
- Hyper-V PowerShell module - Hyper-V PowerShell module (auto-installed if missing)
### Dependencies ### Dependencies
- `SchedulerTemplate.psm1` module (located in parent directory) - `SchedulerTemplate.psm1` module (located in parent directory)
@ -65,7 +66,10 @@ HyperV-Backup/
"credentialEnvVar": "YOUR_ENV_VAR_NAME", "credentialEnvVar": "YOUR_ENV_VAR_NAME",
"tempExportRoot": "D:\\Temp\\HyperVExport", "tempExportRoot": "D:\\Temp\\HyperVExport",
"retentionCount": 3, "retentionCount": 3,
"excludeVMs": ["vm-to-exclude"] "excludeVMs": ["vm-to-exclude"],
"options": {
"dryRun": false
}
} }
``` ```
@ -89,6 +93,8 @@ HyperV-Backup/
.\hyper-v-backup.bat .\hyper-v-backup.bat
# or # or
.\hyper-v-backup.ps1 .\hyper-v-backup.ps1
# Test with dry run first (set dryRun: true in scriptsettings.json)
``` ```
## Configuration Reference ## Configuration Reference
@ -112,6 +118,12 @@ HyperV-Backup/
| `retentionCount` | number | Yes | Number of backup generations to keep (1-365) | | `retentionCount` | number | Yes | Number of backup generations to keep (1-365) |
| `excludeVMs` | array | No | VM names to exclude from backup | | `excludeVMs` | array | No | VM names to exclude from backup |
### Options
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `dryRun` | bool | `false` | Simulate backup without exporting or copying VMs |
### Version Tracking ### Version Tracking
| Property | Type | Description | | Property | Type | Description |
@ -346,6 +358,14 @@ Run with verbose output:
- Performance improvement: Skip unnecessary space checks - Performance improvement: Skip unnecessary space checks
- Refactored parameter splatting for Invoke-ScheduledExecution - Refactored parameter splatting for Invoke-ScheduledExecution
### 1.0.2 (2026-01-28)
- Added auto-installation of Hyper-V PowerShell module if missing
- Removed `#Requires -Modules Hyper-V` directive in favor of runtime installation
- Module installation uses `Enable-WindowsOptionalFeature` for the Microsoft-Hyper-V-Management-PowerShell feature
- Handles restart requirement notification if Windows feature installation requires reboot
- Added dry run mode (`options.dryRun`) to simulate backup without actual export/copy operations
- Restructured settings to use `options` object for consistency with other scripts
## Support ## Support
For issues or questions: For issues or questions:

View File

@ -3,8 +3,8 @@ setlocal EnableDelayedExpansion
REM ============================================================================ REM ============================================================================
REM Hyper-V Backup Launcher REM Hyper-V Backup Launcher
REM VERSION: 1.0.1 REM VERSION: 1.0.2
REM DATE: 2026-01-26 REM DATE: 2026-01-28
REM DESCRIPTION: Batch file launcher for hyper-v-backup.ps1 with admin check REM DESCRIPTION: Batch file launcher for hyper-v-backup.ps1 with admin check
REM ============================================================================ REM ============================================================================

View File

@ -5,7 +5,6 @@ param (
) )
#Requires -RunAsAdministrator #Requires -RunAsAdministrator
#Requires -Modules Hyper-V
<# <#
.SYNOPSIS .SYNOPSIS
@ -13,18 +12,18 @@ param (
.DESCRIPTION .DESCRIPTION
Production-ready Hyper-V backup solution with scheduling, checkpoints, and retention management. Production-ready Hyper-V backup solution with scheduling, checkpoints, and retention management.
.VERSION .VERSION
1.0.1 1.0.2
.DATE .DATE
2026-01-26 2026-01-28
.NOTES .NOTES
- Requires Administrator privileges - Requires Administrator privileges
- Requires Hyper-V PowerShell module - Requires Hyper-V PowerShell module (auto-installed if missing)
- Requires SchedulerTemplate.psm1 module - Requires SchedulerTemplate.psm1 module
#> #>
# Script Version # Script Version
$ScriptVersion = "1.0.1" $ScriptVersion = "1.0.2"
$ScriptDate = "2026-01-26" $ScriptDate = "2026-01-28"
try { try {
Import-Module "$PSScriptRoot\..\SchedulerTemplate.psm1" -Force -ErrorAction Stop Import-Module "$PSScriptRoot\..\SchedulerTemplate.psm1" -Force -ErrorAction Stop
@ -68,6 +67,10 @@ $CredentialEnvVar = $settings.credentialEnvVar
$TempExportRoot = $settings.tempExportRoot $TempExportRoot = $settings.tempExportRoot
$RetentionCount = $settings.retentionCount $RetentionCount = $settings.retentionCount
$BlacklistedVMs = $settings.excludeVMs $BlacklistedVMs = $settings.excludeVMs
$Options = $settings.options
# Get DryRun from settings
$DryRun = $Options.dryRun
# Schedule Configuration # Schedule Configuration
$Config = @{ $Config = @{
@ -106,9 +109,47 @@ $script:BackupStats = @{
# Helper Functions ========================================================= # Helper Functions =========================================================
function Test-HyperVModule {
param([switch]$Automated)
Write-Log "Checking Hyper-V PowerShell module..." -Level Info -Automated:$Automated
if (-not (Get-Module -ListAvailable -Name Hyper-V)) {
Write-Log "Hyper-V PowerShell module not found. Installing..." -Level Warning -Automated:$Automated
try {
# Install Hyper-V PowerShell module (Windows Feature)
$result = Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell -All -NoRestart -ErrorAction Stop
if ($result.RestartNeeded) {
Write-Log "Hyper-V PowerShell module installed but requires system restart" -Level Warning -Automated:$Automated
Write-Log "Please restart the system and run the script again" -Level Info -Automated:$Automated
return $false
}
Write-Log "Hyper-V PowerShell module installed successfully" -Level Success -Automated:$Automated
}
catch {
Write-Log "Failed to install Hyper-V PowerShell module: $_" -Level Error -Automated:$Automated
Write-Log "Please install manually: Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell -All" -Level Info -Automated:$Automated
return $false
}
}
try {
Import-Module Hyper-V -ErrorAction Stop
Write-Log "Hyper-V PowerShell module loaded" -Level Success -Automated:$Automated
return $true
}
catch {
Write-Log "Failed to import Hyper-V module: $_" -Level Error -Automated:$Automated
return $false
}
}
function Test-Prerequisites { function Test-Prerequisites {
param([switch]$Automated) param([switch]$Automated)
Write-Log "Checking prerequisites..." -Level Info -Automated:$Automated Write-Log "Checking prerequisites..." -Level Info -Automated:$Automated
# Check if running as Administrator # Check if running as Administrator
@ -119,8 +160,7 @@ function Test-Prerequisites {
} }
# Check Hyper-V module # Check Hyper-V module
if (-not (Get-Module -ListAvailable -Name Hyper-V)) { if (-not (Test-HyperVModule -Automated:$Automated)) {
Write-Log "Hyper-V PowerShell module is not installed!" -Level Error -Automated:$Automated
return $false return $false
} }
@ -332,21 +372,30 @@ function Backup-VM {
Write-Log "Temp drive ${tempDrive}: has $([math]::Round($freeSpace / 1GB, 2)) GB free" -Level Info -Automated:$Automated Write-Log "Temp drive ${tempDrive}: has $([math]::Round($freeSpace / 1GB, 2)) GB free" -Level Info -Automated:$Automated
} }
# Dry run mode - skip actual backup operations
if ($DryRun) {
Write-Log "DRY RUN: Would export VM '$VMName' to temp location" -Level Warning -Automated:$Automated
Write-Log "DRY RUN: Would copy export to backup location: $vmBackupPath" -Level Warning -Automated:$Automated
Write-Log "=== DRY RUN: Backup simulated for VM: $VMName ===" -Level Warning -Automated:$Automated
$script:BackupStats.SkippedVMs++
return $true
}
# Export VM to temp location (Export-VM creates its own checkpoint internally) # Export VM to temp location (Export-VM creates its own checkpoint internally)
$tempExportPath = Join-Path -Path $TempExportRoot -ChildPath "$VMName-$DateSuffix" $tempExportPath = Join-Path -Path $TempExportRoot -ChildPath "$VMName-$DateSuffix"
Write-Log "Exporting VM '$VMName' to temp location: $tempExportPath" -Level Info -Automated:$Automated Write-Log "Exporting VM '$VMName' to temp location: $tempExportPath" -Level Info -Automated:$Automated
try { try {
Export-VM -Name $VMName -Path $tempExportPath -ErrorAction Stop Export-VM -Name $VMName -Path $tempExportPath -ErrorAction Stop
} }
catch { catch {
Write-Log "Failed to export VM '$VMName': $_" -Level Error -Automated:$Automated Write-Log "Failed to export VM '$VMName': $_" -Level Error -Automated:$Automated
# Cleanup temp if export failed # Cleanup temp if export failed
if (Test-Path $tempExportPath) { if (Test-Path $tempExportPath) {
Remove-Item -Path $tempExportPath -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $tempExportPath -Recurse -Force -ErrorAction SilentlyContinue
} }
$script:BackupStats.FailedVMs++ $script:BackupStats.FailedVMs++
$script:BackupStats.FailureMessages += "Export failed for $VMName" $script:BackupStats.FailureMessages += "Export failed for $VMName"
return $false return $false
@ -394,12 +443,12 @@ function Backup-VM {
} }
catch { catch {
Write-Log "Failed to copy VM '$VMName' to backup location: $_" -Level Error -Automated:$Automated Write-Log "Failed to copy VM '$VMName' to backup location: $_" -Level Error -Automated:$Automated
# Cleanup partial backup # Cleanup partial backup
if (Test-Path $vmBackupPath) { if (Test-Path $vmBackupPath) {
Remove-Item -Path $vmBackupPath -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $vmBackupPath -Recurse -Force -ErrorAction SilentlyContinue
} }
$script:BackupStats.FailedVMs++ $script:BackupStats.FailedVMs++
$script:BackupStats.FailureMessages += "Copy to NAS failed for $VMName" $script:BackupStats.FailureMessages += "Copy to NAS failed for $VMName"
return $false return $false
@ -409,7 +458,7 @@ function Backup-VM {
# Cleanup temp export # Cleanup temp export
Write-Log "Cleaning up temp export for VM '$VMName'..." -Level Info -Automated:$Automated Write-Log "Cleaning up temp export for VM '$VMName'..." -Level Info -Automated:$Automated
try { try {
if (Test-Path $tempExportPath) { if (Test-Path $tempExportPath) {
Remove-Item -Path $tempExportPath -Recurse -Force -ErrorAction Stop Remove-Item -Path $tempExportPath -Recurse -Force -ErrorAction Stop
@ -558,6 +607,9 @@ function Start-BusinessLogic {
Write-Log "Hyper-V Backup Process Started" -Level Info -Automated:$Automated Write-Log "Hyper-V Backup Process Started" -Level Info -Automated:$Automated
Write-Log "Script Version: $ScriptVersion ($ScriptDate)" -Level Info -Automated:$Automated Write-Log "Script Version: $ScriptVersion ($ScriptDate)" -Level Info -Automated:$Automated
Write-Log "Host: $Hostname" -Level Info -Automated:$Automated Write-Log "Host: $Hostname" -Level Info -Automated:$Automated
if ($DryRun) {
Write-Log "DRY RUN MODE - No changes will be made" -Level Warning -Automated:$Automated
}
Write-Log "========================================" -Level Info -Automated:$Automated Write-Log "========================================" -Level Info -Automated:$Automated
# Check prerequisites # Check prerequisites
@ -606,13 +658,18 @@ function Start-BusinessLogic {
$dateSuffix = Get-Date -Format "yyyyMMddHHmmss" $dateSuffix = Get-Date -Format "yyyyMMddHHmmss"
$backupFolder = Join-Path -Path $BackupPath -ChildPath $dateSuffix $backupFolder = Join-Path -Path $BackupPath -ChildPath $dateSuffix
try { if ($DryRun) {
New-Item -Path $backupFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null Write-Log "DRY RUN: Would create backup folder: $backupFolder" -Level Warning -Automated:$Automated
Write-Log "Created backup folder: $backupFolder" -Level Success -Automated:$Automated
} }
catch { else {
Write-Log "Failed to create backup folder '$backupFolder': $_" -Level Error -Automated:$Automated try {
exit 1 New-Item -Path $backupFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-Log "Created backup folder: $backupFolder" -Level Success -Automated:$Automated
}
catch {
Write-Log "Failed to create backup folder '$backupFolder': $_" -Level Error -Automated:$Automated
exit 1
}
} }
# Process each VM # Process each VM
@ -628,11 +685,21 @@ function Start-BusinessLogic {
Backup-VM -VMName $vmName -BackupFolder $backupFolder -DateSuffix $dateSuffix -Automated:$Automated Backup-VM -VMName $vmName -BackupFolder $backupFolder -DateSuffix $dateSuffix -Automated:$Automated
} }
# Cleanup old checkpoints # Cleanup old checkpoints (skip in dry run mode)
Remove-OldCheckpoints -VMs $vms -Automated:$Automated if ($DryRun) {
Write-Log "DRY RUN: Skipping checkpoint cleanup" -Level Warning -Automated:$Automated
}
else {
Remove-OldCheckpoints -VMs $vms -Automated:$Automated
}
# Cleanup old backups # Cleanup old backups (skip in dry run mode)
Remove-OldBackups -BackupPath $BackupPath -RetentionCount $RetentionCount -Automated:$Automated if ($DryRun) {
Write-Log "DRY RUN: Skipping old backup cleanup" -Level Warning -Automated:$Automated
}
else {
Remove-OldBackups -BackupPath $BackupPath -RetentionCount $RetentionCount -Automated:$Automated
}
# Print summary # Print summary
Write-BackupSummary -Automated:$Automated Write-BackupSummary -Automated:$Automated

View File

@ -2,8 +2,8 @@
"$schema": "https://json-schema.org/draft-07/schema", "$schema": "https://json-schema.org/draft-07/schema",
"title": "Hyper-V Backup Script Settings", "title": "Hyper-V Backup Script Settings",
"description": "Configuration file for hyper-v-backup.ps1 script", "description": "Configuration file for hyper-v-backup.ps1 script",
"version": "1.0.1", "version": "1.0.2",
"lastModified": "2026-01-26", "lastModified": "2026-01-28",
"schedule": { "schedule": {
"runMonth": [], "runMonth": [],
"runWeekday": ["Monday"], "runWeekday": ["Monday"],
@ -15,6 +15,9 @@
"tempExportRoot": "D:\\Temp\\HyperVExport", "tempExportRoot": "D:\\Temp\\HyperVExport",
"retentionCount": 3, "retentionCount": 3,
"excludeVMs": ["nassrv0002"], "excludeVMs": ["nassrv0002"],
"options": {
"dryRun": false
},
"_comments": { "_comments": {
"version": "Configuration schema version", "version": "Configuration schema version",
"lastModified": "Last modification date (YYYY-MM-DD)", "lastModified": "Last modification date (YYYY-MM-DD)",
@ -28,6 +31,9 @@
"credentialEnvVar": "Name of Machine-level environment variable containing Base64-encoded 'username:password'", "credentialEnvVar": "Name of Machine-level environment variable containing Base64-encoded 'username:password'",
"tempExportRoot": "Local directory for temporary VM exports. Space is checked dynamically per VM (1.5x VM size).", "tempExportRoot": "Local directory for temporary VM exports. Space is checked dynamically per VM (1.5x VM size).",
"retentionCount": "Number of backup generations to keep (1-365). Older backups are automatically deleted.", "retentionCount": "Number of backup generations to keep (1-365). Older backups are automatically deleted.",
"excludeVMs": "Array of VM names to exclude from backup process" "excludeVMs": "Array of VM names to exclude from backup process",
"options": {
"dryRun": "Simulate backup without actually exporting or copying VMs"
}
} }
} }