mirror of
https://github.com/MAKS-IT-COM/uscheduler.git
synced 2026-02-14 06:37:18 +01:00
728 lines
26 KiB
PowerShell
728 lines
26 KiB
PowerShell
[CmdletBinding()]
|
|
param (
|
|
[switch]$Automated,
|
|
[string]$CurrentDateTimeUtc
|
|
)
|
|
|
|
#Requires -RunAsAdministrator
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Hyper-V Automated Backup Script
|
|
.DESCRIPTION
|
|
Production-ready Hyper-V backup solution with scheduling, checkpoints, and retention management.
|
|
.VERSION
|
|
1.0.2
|
|
.DATE
|
|
2026-01-28
|
|
.NOTES
|
|
- Requires Administrator privileges
|
|
- Requires Hyper-V PowerShell module (auto-installed if missing)
|
|
- Requires SchedulerTemplate.psm1 module
|
|
#>
|
|
|
|
# Script Version
|
|
$ScriptVersion = "1.0.2"
|
|
$ScriptDate = "2026-01-28"
|
|
|
|
try {
|
|
Import-Module "$PSScriptRoot\..\SchedulerTemplate.psm1" -Force -ErrorAction Stop
|
|
}
|
|
catch {
|
|
Write-Error "Failed to load SchedulerTemplate.psm1: $_"
|
|
exit 1
|
|
}
|
|
|
|
# Load Settings ============================================================
|
|
|
|
$settingsFile = Join-Path $PSScriptRoot "scriptsettings.json"
|
|
|
|
if (-not (Test-Path $settingsFile)) {
|
|
Write-Error "Settings file not found: $settingsFile"
|
|
exit 1
|
|
}
|
|
|
|
try {
|
|
$settings = Get-Content $settingsFile -Raw | ConvertFrom-Json
|
|
Write-Verbose "Loaded settings from $settingsFile"
|
|
}
|
|
catch {
|
|
Write-Error "Failed to load settings from $settingsFile : $_"
|
|
exit 1
|
|
}
|
|
|
|
# Process Settings =========================================================
|
|
|
|
# Validate required settings
|
|
$requiredSettings = @('backupRoot', 'tempExportRoot', 'retentionCount')
|
|
foreach ($setting in $requiredSettings) {
|
|
if (-not $settings.$setting) {
|
|
Write-Error "Required setting '$setting' is missing or empty in $settingsFile"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
$BackupRoot = $settings.backupRoot
|
|
$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 = @{
|
|
RunMonth = $settings.schedule.runMonth
|
|
RunWeekday = $settings.schedule.runWeekday
|
|
RunTime = $settings.schedule.runTime
|
|
MinIntervalMinutes = $settings.schedule.minIntervalMinutes
|
|
}
|
|
|
|
# Set hostname and backup path
|
|
$Hostname = ($env:COMPUTERNAME).ToLower()
|
|
$BackupPath = "$BackupRoot\Hyper-V\Backups\$Hostname"
|
|
|
|
# Validate and set temp directory
|
|
if (-not (Test-Path $TempExportRoot)) {
|
|
try {
|
|
$TempExportRoot = [System.IO.Path]::GetTempPath()
|
|
}
|
|
catch {
|
|
$TempExportRoot = $env:TEMP
|
|
}
|
|
}
|
|
|
|
# End Settings =============================================================
|
|
|
|
# Global variables
|
|
$script:BackupStats = @{
|
|
TotalVMs = 0
|
|
SuccessfulVMs = 0
|
|
FailedVMs = 0
|
|
SkippedVMs = 0
|
|
StartTime = Get-Date
|
|
EndTime = $null
|
|
FailureMessages = @()
|
|
}
|
|
|
|
# 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
|
|
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
|
|
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
Write-Log "Script must be run as Administrator!" -Level Error -Automated:$Automated
|
|
return $false
|
|
}
|
|
|
|
# Check Hyper-V module
|
|
if (-not (Test-HyperVModule -Automated:$Automated)) {
|
|
return $false
|
|
}
|
|
|
|
# Check Hyper-V service
|
|
$hvService = Get-Service -Name vmms -ErrorAction SilentlyContinue
|
|
if (-not $hvService -or $hvService.Status -ne 'Running') {
|
|
Write-Log "Hyper-V Virtual Machine Management service is not running!" -Level Error -Automated:$Automated
|
|
return $false
|
|
}
|
|
|
|
# Check temp directory
|
|
if (-not (Test-Path $TempExportRoot)) {
|
|
try {
|
|
New-Item -Path $TempExportRoot -ItemType Directory -Force | Out-Null
|
|
Write-Log "Created temp directory: $TempExportRoot" -Level Success -Automated:$Automated
|
|
}
|
|
catch {
|
|
Write-Log "Failed to create temp directory: $_" -Level Error -Automated:$Automated
|
|
return $false
|
|
}
|
|
}
|
|
|
|
Write-Log "All prerequisites passed" -Level Success -Automated:$Automated
|
|
return $true
|
|
}
|
|
|
|
function Connect-BackupShare {
|
|
param(
|
|
[string]$SharePath,
|
|
[switch]$Automated
|
|
)
|
|
|
|
# Check if path is UNC
|
|
if (-not $SharePath.StartsWith("\\")) {
|
|
Write-Log "Backup path is local, no authentication needed" -Level Info -Automated:$Automated
|
|
return $true
|
|
}
|
|
|
|
# Validate UNC path format
|
|
if (-not (Test-UNCPath -Path $SharePath)) {
|
|
Write-Log "Invalid UNC path format: $SharePath (expected \\server\share)" -Level Error -Automated:$Automated
|
|
return $false
|
|
}
|
|
|
|
Write-Log "Authenticating to UNC share: $SharePath" -Level Info -Automated:$Automated
|
|
|
|
# Validate credential environment variable name is configured
|
|
if (-not $CredentialEnvVar) {
|
|
Write-Log "credentialEnvVar is not configured in settings for UNC path authentication" -Level Error -Automated:$Automated
|
|
return $false
|
|
}
|
|
|
|
try {
|
|
# Retrieve credentials from environment variable
|
|
$creds = Get-CredentialFromEnvVar -EnvVarName $CredentialEnvVar -Automated:$Automated
|
|
|
|
if (-not $creds) {
|
|
return $false
|
|
}
|
|
|
|
# Check if already connected
|
|
$existingConnection = net use | Select-String $SharePath
|
|
if ($existingConnection) {
|
|
Write-Log "Already connected to $SharePath" -Level Info -Automated:$Automated
|
|
return $true
|
|
}
|
|
|
|
# Connect to share
|
|
$netUseResult = cmd /c "net use $SharePath $($creds.Password) /user:$($creds.Username) 2>&1"
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Log "Failed to connect to $SharePath : $netUseResult" -Level Error -Automated:$Automated
|
|
return $false
|
|
}
|
|
|
|
Write-Log "Successfully authenticated to $SharePath" -Level Success -Automated:$Automated
|
|
return $true
|
|
}
|
|
catch {
|
|
Write-Log "Error connecting to share: $_" -Level Error -Automated:$Automated
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Get-VMDiskSize {
|
|
param(
|
|
[string]$VMName,
|
|
[switch]$Automated
|
|
)
|
|
|
|
try {
|
|
$vm = Get-VM -Name $VMName -ErrorAction Stop
|
|
$vhds = $vm | Get-VMHardDiskDrive | Get-VHD -ErrorAction SilentlyContinue
|
|
|
|
if ($vhds) {
|
|
$totalSize = ($vhds | Measure-Object -Property FileSize -Sum).Sum
|
|
return $totalSize
|
|
}
|
|
|
|
return 0
|
|
}
|
|
catch {
|
|
Write-Log "Warning: Could not determine disk size for VM '$VMName': $_" -Level Warning -Automated:$Automated
|
|
return 0
|
|
}
|
|
}
|
|
|
|
function Get-PathFreeSpace {
|
|
param([string]$Path)
|
|
|
|
try {
|
|
$uri = [System.Uri]$Path
|
|
|
|
if ($uri.IsUnc) {
|
|
$server = $uri.Host
|
|
$share = $uri.Segments[1].TrimEnd('/')
|
|
|
|
# Query remote share info via WMI
|
|
$shareInfo = Get-WmiObject -Class Win32_LogicalDisk -ComputerName $server -ErrorAction Stop |
|
|
Where-Object { $_.DeviceID -or $_.ProviderName -like "*$share*" }
|
|
|
|
if ($shareInfo) {
|
|
return $shareInfo.FreeSpace
|
|
}
|
|
|
|
# Fallback: try to get info from mapped drive or direct query
|
|
$driveInfo = [System.IO.DriveInfo]::GetDrives() |
|
|
Where-Object { $_.DriveType -eq 'Network' -and $_.Name -and (Test-Path $Path) }
|
|
|
|
if ($driveInfo) {
|
|
return $driveInfo.AvailableFreeSpace
|
|
}
|
|
|
|
# Last resort: create a temp file and check available space
|
|
if (Test-Path $Path) {
|
|
$testFile = Join-Path $Path ".space_check_$(Get-Random)"
|
|
try {
|
|
[System.IO.File]::WriteAllText($testFile, "")
|
|
$drive = [System.IO.Path]::GetPathRoot((Resolve-Path $Path).Path)
|
|
$info = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Root -eq $drive }
|
|
if ($info) {
|
|
return $info.Free
|
|
}
|
|
}
|
|
finally {
|
|
if (Test-Path $testFile) {
|
|
Remove-Item $testFile -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
return $null
|
|
}
|
|
else {
|
|
# Local path - use PSDrive
|
|
$driveLetter = (Get-Item $Path -ErrorAction Stop).PSDrive.Name
|
|
$freeSpace = (Get-PSDrive $driveLetter -ErrorAction Stop).Free
|
|
return $freeSpace
|
|
}
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Backup-VM {
|
|
param(
|
|
[string]$VMName,
|
|
[string]$BackupFolder,
|
|
[string]$DateSuffix,
|
|
[switch]$Automated
|
|
)
|
|
|
|
Write-Log "=== Processing VM: $VMName ===" -Level Info -Automated:$Automated
|
|
|
|
try {
|
|
# Check if VM exists
|
|
$vm = Get-VM -Name $VMName -ErrorAction Stop
|
|
|
|
# Check VM state
|
|
$vmState = $vm.State
|
|
Write-Log "VM '$VMName' state: $vmState" -Level Info -Automated:$Automated
|
|
|
|
# Check if backup already exists
|
|
$vmBackupPath = Join-Path -Path $BackupFolder -ChildPath $VMName
|
|
if (Test-Path $vmBackupPath) {
|
|
Write-Log "Backup for VM '$VMName' already exists at '$vmBackupPath'. Skipping..." -Level Warning -Automated:$Automated
|
|
$script:BackupStats.SkippedVMs++
|
|
return $false
|
|
}
|
|
|
|
# Estimate required space and check temp drive
|
|
$vmDiskSize = Get-VMDiskSize -VMName $VMName -Automated:$Automated
|
|
if ($vmDiskSize -gt 0) {
|
|
$vmDiskSizeGB = [math]::Round($vmDiskSize / 1GB, 2)
|
|
Write-Log "VM '$VMName' estimated size: $vmDiskSizeGB GB" -Level Info -Automated:$Automated
|
|
|
|
# Check if enough temp space for export (need ~1.5x VM size)
|
|
$tempDrive = (Get-Item $TempExportRoot).PSDrive.Name
|
|
$freeSpace = (Get-PSDrive $tempDrive).Free
|
|
|
|
if ($freeSpace -lt ($vmDiskSize * 1.5)) {
|
|
Write-Log "Insufficient temp space for VM '$VMName' (need ~$([math]::Round($vmDiskSize * 1.5 / 1GB, 2)) GB, have $([math]::Round($freeSpace / 1GB, 2)) GB)" -Level Error -Automated:$Automated
|
|
$script:BackupStats.FailedVMs++
|
|
$script:BackupStats.FailureMessages += "Insufficient temp space for $VMName"
|
|
return $false
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
Write-Log "Export completed successfully" -Level Success -Automated:$Automated
|
|
|
|
# Get actual export size for destination space check
|
|
$exportSize = (Get-ChildItem -Path $tempExportPath -Recurse -File | Measure-Object -Property Length -Sum).Sum
|
|
if (-not $exportSize) { $exportSize = 0 }
|
|
$exportSizeGB = [math]::Round($exportSize / 1GB, 2)
|
|
Write-Log "Export size for VM '$VMName': $exportSizeGB GB" -Level Info -Automated:$Automated
|
|
|
|
# Check destination space before copying
|
|
$destFreeSpace = Get-PathFreeSpace -Path $BackupFolder
|
|
if ($null -ne $destFreeSpace) {
|
|
$requiredSpace = $exportSize * 1.1 # 10% buffer
|
|
if ($destFreeSpace -lt $requiredSpace) {
|
|
Write-Log "Insufficient space on destination for VM '$VMName' (need ~$([math]::Round($requiredSpace / 1GB, 2)) GB, have $([math]::Round($destFreeSpace / 1GB, 2)) GB)" -Level Error -Automated:$Automated
|
|
|
|
# Cleanup temp export
|
|
if (Test-Path $tempExportPath) {
|
|
Remove-Item -Path $tempExportPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
$script:BackupStats.FailedVMs++
|
|
$script:BackupStats.FailureMessages += "Insufficient destination space for $VMName"
|
|
return $false
|
|
}
|
|
Write-Log "Destination has $([math]::Round($destFreeSpace / 1GB, 2)) GB free space" -Level Info -Automated:$Automated
|
|
}
|
|
else {
|
|
Write-Log "Warning: Could not determine free space on destination, proceeding with copy" -Level Warning -Automated:$Automated
|
|
}
|
|
|
|
# Copy to NAS
|
|
Write-Log "Copying VM '$VMName' export to backup location: $vmBackupPath" -Level Info -Automated:$Automated
|
|
|
|
try {
|
|
if (-not (Test-Path $vmBackupPath)) {
|
|
New-Item -Path $vmBackupPath -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
Copy-Item -Path "$tempExportPath\*" -Destination $vmBackupPath -Recurse -Force -ErrorAction Stop
|
|
}
|
|
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
|
|
}
|
|
|
|
Write-Log "Copy to backup location completed successfully" -Level Success -Automated:$Automated
|
|
|
|
# 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
|
|
}
|
|
Write-Log "Temp cleanup completed" -Level Success -Automated:$Automated
|
|
}
|
|
catch {
|
|
Write-Log "Warning: Failed to cleanup temp export: $_" -Level Warning -Automated:$Automated
|
|
}
|
|
|
|
Write-Log "=== Backup completed successfully for VM: $VMName ===" -Level Success -Automated:$Automated
|
|
$script:BackupStats.SuccessfulVMs++
|
|
return $true
|
|
}
|
|
catch {
|
|
Write-Log "Unexpected error processing VM '$VMName': $_" -Level Error -Automated:$Automated
|
|
$script:BackupStats.FailedVMs++
|
|
$script:BackupStats.FailureMessages += "Unexpected error for $VMName : $_"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Remove-OldCheckpoints {
|
|
param(
|
|
[array]$VMs,
|
|
[int]$RetentionCount = 2,
|
|
[switch]$Automated
|
|
)
|
|
|
|
Write-Log "Starting checkpoint cleanup for all VMs (keeping $RetentionCount most recent)..." -Level Info -Automated:$Automated
|
|
|
|
$totalCheckpoints = 0
|
|
|
|
foreach ($vm in $VMs) {
|
|
$vmName = $vm.Name
|
|
|
|
try {
|
|
$checkpoints = Get-VMSnapshot -VMName $vmName -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -like "Backup-*" } |
|
|
Sort-Object CreationTime -Descending
|
|
|
|
if ($checkpoints -and $checkpoints.Count -gt $RetentionCount) {
|
|
$checkpointsToRemove = $checkpoints | Select-Object -Skip $RetentionCount
|
|
|
|
foreach ($checkpoint in $checkpointsToRemove) {
|
|
Write-Log "Removing checkpoint '$($checkpoint.Name)' from VM '$vmName'..." -Level Info -Automated:$Automated
|
|
|
|
try {
|
|
Remove-VMSnapshot -VMName $vmName -Name $checkpoint.Name -Confirm:$false -ErrorAction Stop
|
|
$totalCheckpoints++
|
|
}
|
|
catch {
|
|
Write-Log "Warning: Failed to remove checkpoint '$($checkpoint.Name)' from VM '$vmName': $_" -Level Warning -Automated:$Automated
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log "Warning: Error accessing checkpoints for VM '$vmName': $_" -Level Warning -Automated:$Automated
|
|
}
|
|
}
|
|
|
|
Write-Log "Checkpoint cleanup completed. Removed $totalCheckpoints checkpoint(s)" -Level Success -Automated:$Automated
|
|
}
|
|
|
|
function Remove-OldBackups {
|
|
param(
|
|
[string]$BackupPath,
|
|
[int]$RetentionCount,
|
|
[switch]$Automated
|
|
)
|
|
|
|
Write-Log "Starting old backup cleanup (retention: $RetentionCount most recent)..." -Level Info -Automated:$Automated
|
|
|
|
try {
|
|
if (-not (Test-Path $BackupPath)) {
|
|
Write-Log "Backup path does not exist, skipping cleanup" -Level Warning -Automated:$Automated
|
|
return
|
|
}
|
|
|
|
$backupDirs = Get-ChildItem -Path $BackupPath -Directory -ErrorAction Stop |
|
|
Sort-Object Name -Descending
|
|
|
|
if ($backupDirs.Count -le $RetentionCount) {
|
|
Write-Log "Only $($backupDirs.Count) backup(s) found, no cleanup needed" -Level Info -Automated:$Automated
|
|
return
|
|
}
|
|
|
|
$dirsToRemove = $backupDirs | Select-Object -Skip $RetentionCount
|
|
$removedCount = 0
|
|
|
|
foreach ($dir in $dirsToRemove) {
|
|
Write-Log "Removing old backup folder: $($dir.Name)" -Level Info -Automated:$Automated
|
|
|
|
try {
|
|
Remove-Item -Path $dir.FullName -Recurse -Force -ErrorAction Stop
|
|
$removedCount++
|
|
}
|
|
catch {
|
|
Write-Log "Warning: Failed to remove backup folder '$($dir.Name)': $_" -Level Warning -Automated:$Automated
|
|
}
|
|
}
|
|
|
|
Write-Log "Old backup cleanup completed. Removed $removedCount backup folder(s)" -Level Success -Automated:$Automated
|
|
}
|
|
catch {
|
|
Write-Log "Error during backup cleanup: $_" -Level Error -Automated:$Automated
|
|
}
|
|
}
|
|
|
|
function Write-BackupSummary {
|
|
param([switch]$Automated)
|
|
|
|
$script:BackupStats.EndTime = Get-Date
|
|
$duration = $script:BackupStats.EndTime - $script:BackupStats.StartTime
|
|
|
|
Write-Log "" -Level Info -Automated:$Automated
|
|
Write-Log "========================================" -Level Info -Automated:$Automated
|
|
Write-Log "BACKUP SUMMARY" -Level Info -Automated:$Automated
|
|
Write-Log "========================================" -Level Info -Automated:$Automated
|
|
Write-Log "Start Time : $($script:BackupStats.StartTime.ToString('yyyy-MM-dd HH:mm:ss'))" -Level Info -Automated:$Automated
|
|
Write-Log "End Time : $($script:BackupStats.EndTime.ToString('yyyy-MM-dd HH:mm:ss'))" -Level Info -Automated:$Automated
|
|
Write-Log "Duration : $($duration.Hours)h $($duration.Minutes)m $($duration.Seconds)s" -Level Info -Automated:$Automated
|
|
Write-Log "Total VMs : $($script:BackupStats.TotalVMs)" -Level Info -Automated:$Automated
|
|
Write-Log "Successful : $($script:BackupStats.SuccessfulVMs)" -Level Success -Automated:$Automated
|
|
Write-Log "Failed : $($script:BackupStats.FailedVMs)" -Level $(if ($script:BackupStats.FailedVMs -gt 0) { 'Error' } else { 'Info' }) -Automated:$Automated
|
|
Write-Log "Skipped : $($script:BackupStats.SkippedVMs)" -Level Info -Automated:$Automated
|
|
|
|
if ($script:BackupStats.FailureMessages.Count -gt 0) {
|
|
Write-Log "" -Level Info -Automated:$Automated
|
|
Write-Log "ERRORS:" -Level Error -Automated:$Automated
|
|
foreach ($failureMsg in $script:BackupStats.FailureMessages) {
|
|
Write-Log " - $failureMsg" -Level Error -Automated:$Automated
|
|
}
|
|
}
|
|
|
|
Write-Log "========================================" -Level Info -Automated:$Automated
|
|
}
|
|
|
|
# Main Business Logic ======================================================
|
|
|
|
function Start-BusinessLogic {
|
|
param([switch]$Automated)
|
|
|
|
Write-Log "========================================" -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 "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
|
|
if (-not (Test-Prerequisites -Automated:$Automated)) {
|
|
Write-Log "Prerequisites check failed. Aborting backup." -Level Error -Automated:$Automated
|
|
exit 1
|
|
}
|
|
|
|
# Connect to backup share
|
|
if (-not (Connect-BackupShare -SharePath $BackupRoot -Automated:$Automated)) {
|
|
Write-Log "Failed to connect to backup share. Aborting backup." -Level Error -Automated:$Automated
|
|
exit 1
|
|
}
|
|
|
|
# Create backup destination if it doesn't exist
|
|
if (-not (Test-Path $BackupPath)) {
|
|
Write-Log "Backup path does not exist. Creating: $BackupPath" -Level Info -Automated:$Automated
|
|
try {
|
|
New-Item -Path $BackupPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
|
Write-Log "Created backup path: $BackupPath" -Level Success -Automated:$Automated
|
|
}
|
|
catch {
|
|
Write-Log "Failed to create backup path '$BackupPath': $_" -Level Error -Automated:$Automated
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Get all VMs
|
|
try {
|
|
$vms = Get-VM -ErrorAction Stop
|
|
}
|
|
catch {
|
|
Write-Log "Failed to retrieve VMs: $_" -Level Error -Automated:$Automated
|
|
exit 1
|
|
}
|
|
|
|
if (-not $vms -or $vms.Count -eq 0) {
|
|
Write-Log "No VMs found on this host. Aborting backup." -Level Warning -Automated:$Automated
|
|
exit 0
|
|
}
|
|
|
|
$script:BackupStats.TotalVMs = $vms.Count
|
|
Write-Log "Found $($vms.Count) VM(s) on this host" -Level Info -Automated:$Automated
|
|
|
|
# Create backup folder with timestamp
|
|
$dateSuffix = Get-Date -Format "yyyyMMddHHmmss"
|
|
$backupFolder = Join-Path -Path $BackupPath -ChildPath $dateSuffix
|
|
|
|
if ($DryRun) {
|
|
Write-Log "DRY RUN: Would create backup folder: $backupFolder" -Level Warning -Automated:$Automated
|
|
}
|
|
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
|
|
foreach ($vm in $vms) {
|
|
$vmName = $vm.Name
|
|
|
|
if ($BlacklistedVMs -contains $vmName) {
|
|
Write-Log "Skipping blacklisted VM: $vmName" -Level Warning -Automated:$Automated
|
|
$script:BackupStats.SkippedVMs++
|
|
continue
|
|
}
|
|
|
|
Backup-VM -VMName $vmName -BackupFolder $backupFolder -DateSuffix $dateSuffix -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 (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
|
|
}
|
|
|
|
# Entry Point ==============================================================
|
|
|
|
if ($Automated) {
|
|
if (Get-Command Invoke-ScheduledExecution -ErrorAction SilentlyContinue) {
|
|
$params = @{
|
|
Config = $Config
|
|
Automated = $Automated
|
|
CurrentDateTimeUtc = $CurrentDateTimeUtc
|
|
ScriptBlock = { Start-BusinessLogic -Automated:$Automated }
|
|
}
|
|
Invoke-ScheduledExecution @params
|
|
}
|
|
else {
|
|
Write-Log "Invoke-ScheduledExecution not available. Execution aborted." -Level Error -Automated:$Automated
|
|
exit 1
|
|
}
|
|
}
|
|
else {
|
|
Write-Log "Manual execution started" -Level Info -Automated:$Automated
|
|
Start-BusinessLogic -Automated:$Automated
|
|
} |