From 728f657112a21f80d1273f117f35af20dac40aca Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Wed, 28 Jan 2026 19:32:28 +0100 Subject: [PATCH] (feature): hyper-v backup dryRun mode --- examples/HyperV-Backup/README.md | 28 ++++- examples/HyperV-Backup/hyper-v-backup.bat | 4 +- examples/HyperV-Backup/hyper-v-backup.ps1 | 117 ++++++++++++++++----- examples/HyperV-Backup/scriptsettings.json | 12 ++- 4 files changed, 127 insertions(+), 34 deletions(-) diff --git a/examples/HyperV-Backup/README.md b/examples/HyperV-Backup/README.md index b10daef..d460f35 100644 --- a/examples/HyperV-Backup/README.md +++ b/examples/HyperV-Backup/README.md @@ -1,7 +1,7 @@ # Hyper-V Backup Script -**Version:** 1.0.1 -**Last Updated:** 2026-01-26 +**Version:** 1.0.2 +**Last Updated:** 2026-01-28 ## 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) - ✅ **Space Validation** - Dynamic space checks for temp (per VM) and destination before copy - ✅ **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 - ✅ **Lock Files** - Prevents concurrent execution - ✅ **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 - PowerShell 5.1 or later - Administrator privileges -- Hyper-V PowerShell module +- Hyper-V PowerShell module (auto-installed if missing) ### Dependencies - `SchedulerTemplate.psm1` module (located in parent directory) @@ -65,7 +66,10 @@ HyperV-Backup/ "credentialEnvVar": "YOUR_ENV_VAR_NAME", "tempExportRoot": "D:\\Temp\\HyperVExport", "retentionCount": 3, - "excludeVMs": ["vm-to-exclude"] + "excludeVMs": ["vm-to-exclude"], + "options": { + "dryRun": false + } } ``` @@ -89,6 +93,8 @@ HyperV-Backup/ .\hyper-v-backup.bat # or .\hyper-v-backup.ps1 + + # Test with dry run first (set dryRun: true in scriptsettings.json) ``` ## Configuration Reference @@ -112,6 +118,12 @@ HyperV-Backup/ | `retentionCount` | number | Yes | Number of backup generations to keep (1-365) | | `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 | Property | Type | Description | @@ -346,6 +358,14 @@ Run with verbose output: - Performance improvement: Skip unnecessary space checks - 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 For issues or questions: diff --git a/examples/HyperV-Backup/hyper-v-backup.bat b/examples/HyperV-Backup/hyper-v-backup.bat index 1832a24..b99ce91 100644 --- a/examples/HyperV-Backup/hyper-v-backup.bat +++ b/examples/HyperV-Backup/hyper-v-backup.bat @@ -3,8 +3,8 @@ setlocal EnableDelayedExpansion REM ============================================================================ REM Hyper-V Backup Launcher -REM VERSION: 1.0.1 -REM DATE: 2026-01-26 +REM VERSION: 1.0.2 +REM DATE: 2026-01-28 REM DESCRIPTION: Batch file launcher for hyper-v-backup.ps1 with admin check REM ============================================================================ diff --git a/examples/HyperV-Backup/hyper-v-backup.ps1 b/examples/HyperV-Backup/hyper-v-backup.ps1 index 812a848..9a4a974 100644 --- a/examples/HyperV-Backup/hyper-v-backup.ps1 +++ b/examples/HyperV-Backup/hyper-v-backup.ps1 @@ -5,7 +5,6 @@ param ( ) #Requires -RunAsAdministrator -#Requires -Modules Hyper-V <# .SYNOPSIS @@ -13,18 +12,18 @@ param ( .DESCRIPTION Production-ready Hyper-V backup solution with scheduling, checkpoints, and retention management. .VERSION - 1.0.1 + 1.0.2 .DATE - 2026-01-26 + 2026-01-28 .NOTES - Requires Administrator privileges - - Requires Hyper-V PowerShell module + - Requires Hyper-V PowerShell module (auto-installed if missing) - Requires SchedulerTemplate.psm1 module #> # Script Version -$ScriptVersion = "1.0.1" -$ScriptDate = "2026-01-26" +$ScriptVersion = "1.0.2" +$ScriptDate = "2026-01-28" try { Import-Module "$PSScriptRoot\..\SchedulerTemplate.psm1" -Force -ErrorAction Stop @@ -68,6 +67,10 @@ $CredentialEnvVar = $settings.credentialEnvVar $TempExportRoot = $settings.tempExportRoot $RetentionCount = $settings.retentionCount $BlacklistedVMs = $settings.excludeVMs +$Options = $settings.options + +# Get DryRun from settings +$DryRun = $Options.dryRun # Schedule Configuration $Config = @{ @@ -106,9 +109,47 @@ $script:BackupStats = @{ # 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 { param([switch]$Automated) - + Write-Log "Checking prerequisites..." -Level Info -Automated:$Automated # Check if running as Administrator @@ -119,8 +160,7 @@ function Test-Prerequisites { } # Check Hyper-V module - if (-not (Get-Module -ListAvailable -Name Hyper-V)) { - Write-Log "Hyper-V PowerShell module is not installed!" -Level Error -Automated:$Automated + if (-not (Test-HyperVModule -Automated:$Automated)) { 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 } + # 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) $tempExportPath = Join-Path -Path $TempExportRoot -ChildPath "$VMName-$DateSuffix" Write-Log "Exporting VM '$VMName' to temp location: $tempExportPath" -Level Info -Automated:$Automated - + try { Export-VM -Name $VMName -Path $tempExportPath -ErrorAction Stop } catch { Write-Log "Failed to export VM '$VMName': $_" -Level Error -Automated:$Automated - + # Cleanup temp if export failed if (Test-Path $tempExportPath) { Remove-Item -Path $tempExportPath -Recurse -Force -ErrorAction SilentlyContinue } - + $script:BackupStats.FailedVMs++ $script:BackupStats.FailureMessages += "Export failed for $VMName" return $false @@ -394,12 +443,12 @@ function Backup-VM { } catch { Write-Log "Failed to copy VM '$VMName' to backup location: $_" -Level Error -Automated:$Automated - + # Cleanup partial backup if (Test-Path $vmBackupPath) { Remove-Item -Path $vmBackupPath -Recurse -Force -ErrorAction SilentlyContinue } - + $script:BackupStats.FailedVMs++ $script:BackupStats.FailureMessages += "Copy to NAS failed for $VMName" return $false @@ -409,7 +458,7 @@ function Backup-VM { # Cleanup temp export Write-Log "Cleaning up temp export for VM '$VMName'..." -Level Info -Automated:$Automated - + try { if (Test-Path $tempExportPath) { 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 "Script Version: $ScriptVersion ($ScriptDate)" -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 # Check prerequisites @@ -606,13 +658,18 @@ function Start-BusinessLogic { $dateSuffix = Get-Date -Format "yyyyMMddHHmmss" $backupFolder = Join-Path -Path $BackupPath -ChildPath $dateSuffix - try { - New-Item -Path $backupFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null - Write-Log "Created backup folder: $backupFolder" -Level Success -Automated:$Automated + if ($DryRun) { + Write-Log "DRY RUN: Would create backup folder: $backupFolder" -Level Warning -Automated:$Automated } - catch { - Write-Log "Failed to create backup folder '$backupFolder': $_" -Level Error -Automated:$Automated - exit 1 + else { + try { + 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 @@ -628,11 +685,21 @@ function Start-BusinessLogic { Backup-VM -VMName $vmName -BackupFolder $backupFolder -DateSuffix $dateSuffix -Automated:$Automated } - # Cleanup old checkpoints - Remove-OldCheckpoints -VMs $vms -Automated:$Automated + # Cleanup old checkpoints (skip in dry run mode) + if ($DryRun) { + Write-Log "DRY RUN: Skipping checkpoint cleanup" -Level Warning -Automated:$Automated + } + else { + Remove-OldCheckpoints -VMs $vms -Automated:$Automated + } - # Cleanup old backups - Remove-OldBackups -BackupPath $BackupPath -RetentionCount $RetentionCount -Automated:$Automated + # Cleanup old backups (skip in dry run mode) + 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 Write-BackupSummary -Automated:$Automated diff --git a/examples/HyperV-Backup/scriptsettings.json b/examples/HyperV-Backup/scriptsettings.json index 971bf14..249a611 100644 --- a/examples/HyperV-Backup/scriptsettings.json +++ b/examples/HyperV-Backup/scriptsettings.json @@ -2,8 +2,8 @@ "$schema": "https://json-schema.org/draft-07/schema", "title": "Hyper-V Backup Script Settings", "description": "Configuration file for hyper-v-backup.ps1 script", - "version": "1.0.1", - "lastModified": "2026-01-26", + "version": "1.0.2", + "lastModified": "2026-01-28", "schedule": { "runMonth": [], "runWeekday": ["Monday"], @@ -15,6 +15,9 @@ "tempExportRoot": "D:\\Temp\\HyperVExport", "retentionCount": 3, "excludeVMs": ["nassrv0002"], + "options": { + "dryRun": false + }, "_comments": { "version": "Configuration schema version", "lastModified": "Last modification date (YYYY-MM-DD)", @@ -28,6 +31,9 @@ "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).", "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" + } } }