diff --git a/README.md b/README.md index 686109b..1e33d17 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,11 @@ Designed for system administrators — and also for those who *feel like* system - [Logging](#logging) - [Contact](#contact) - [License](#license) -- [Appendix](#appendix) - - [SchedulerTemplate.psm1 (Full Source)](#schedulertemplatepsm1-full-source) ## Scripts Examples - +- [Scheduler Template Module](./examples/SchedulerTemplate.psm1) - [Hyper-V Backup](./examples/HyperV-Backup/README.md) - Production-ready Hyper-V VM backup solution with scheduling and retention management -- [File-Sync](./examples//File-Sync/README.md) - [FreeFileSync](https://freefilesync.org/) batch job execution +- [File-Sync](./examples/File-Sync/README.md) - [FreeFileSync](https://freefilesync.org/) batch job execution --- @@ -235,180 +233,3 @@ Maksym Sadovnychyy – MAKS-IT maksym.sadovnychyy@gmail.com --- - -# Appendix - -## SchedulerTemplate.psm1 (Full Source) - -```powershell -# ====================================================================== -# SchedulerTemplate.psm1 - Scheduling + Lock + Interval + Callback Runner -# ====================================================================== - -function Write-Log { - param( - [string]$Message, - [switch]$Automated, - [string]$Color = 'White' - ) - - if ($Automated) { - Write-Output $Message - } - else { - Write-Host $Message -ForegroundColor $Color - } -} - -function Get-CurrentUtcDateTime { - param([string]$ExternalDateTime, [switch]$Automated) - - if ($ExternalDateTime) { - try { - return [datetime]::Parse($ExternalDateTime).ToUniversalTime() - } - catch { - try { - return [datetime]::ParseExact($ExternalDateTime, 'dd/MM/yyyy HH:mm:ss', $null).ToUniversalTime() - } - catch { - Write-Log "Failed to parse CurrentDateTimeUtc ('$ExternalDateTime'). Using system time (UTC)." -Automated:$Automated -Color 'Red' - return (Get-Date).ToUniversalTime() - } - } - } - return (Get-Date).ToUniversalTime() -} - -function Test-ScheduleMonth { param([datetime]$DateTime, [array]$Months) - $name = $DateTime.ToString('MMMM') - return ($Months.Count -eq 0) -or ($Months -contains $name) -} -function Test-ScheduleWeekday { param([datetime]$DateTime, [array]$Weekdays) - $name = $DateTime.DayOfWeek.ToString() - return ($Weekdays.Count -eq 0) -or ($Weekdays -contains $name) -} -function Test-ScheduleTime { param([datetime]$DateTime, [array]$Times) - $t = $DateTime.ToString('HH:mm') - return ($Times.Count -eq 0) -or ($Times -contains $t) -} - -function Test-Schedule { - param( - [datetime]$DateTime, - [array]$RunMonth, - [array]$RunWeekday, - [array]$RunTime - ) - - return (Test-ScheduleMonth -DateTime $DateTime -Months $RunMonth) -and - (Test-ScheduleWeekday -DateTime $DateTime -Weekdays $RunWeekday) -and - (Test-ScheduleTime -DateTime $DateTime -Times $RunTime) -} - -function Test-Interval { - param([datetime]$LastRun,[datetime]$Now,[int]$MinIntervalMinutes) - return $Now -ge $LastRun.AddMinutes($MinIntervalMinutes) -} - -function Test-ScheduledExecution { - param( - [switch]$Automated, - [string]$CurrentDateTimeUtc, - [hashtable]$Config, - [string]$LastRunFilePath - ) - - $now = Get-CurrentUtcDateTime -ExternalDateTime $CurrentDateTimeUtc -Automated:$Automated - $shouldRun = $true - - if ($Automated) { - Write-Log "Automated: $Automated" -Automated:$Automated -Color 'Green' - Write-Log "Current UTC Time: $now" -Automated:$Automated -Color 'Green' - - if (-not (Test-Schedule -DateTime $now -RunMonth $Config.RunMonth -RunWeekday $Config.RunWeekday -RunTime $Config.RunTime)) { - Write-Log "Execution skipped due to schedule." -Automated:$Automated -Color 'Yellow' - $shouldRun = $false - } - } - - if ($shouldRun -and $LastRunFilePath -and (Test-Path $LastRunFilePath)) { - $lastRun = Get-Content $LastRunFilePath | Select-Object -First 1 - if ($lastRun) { - [datetime]$lr = $lastRun - if (-not (Test-Interval -LastRun $lr -Now $now -MinIntervalMinutes $Config.MinIntervalMinutes)) { - Write-Log "Last run at $lr. Interval not reached." -Automated:$Automated -Color 'Yellow' - $shouldRun = $false - } - } - } - - return @{ - ShouldExecute = $shouldRun - Now = $now - } -} - -function New-LockGuard { - param([string]$LockFile,[switch]$Automated) - - if (Test-Path $LockFile) { - Write-Log "Guard: Lock file exists ($LockFile). Skipping." -Automated:$Automated -Color 'Red' - return $false - } - try { - New-Item -Path $LockFile -ItemType File -Force | Out-Null - return $true - } - catch { - Write-Log "Guard: Cannot create lock file ($LockFile)." -Automated:$Automated -Color 'Red' - return $false - } -} - -function Remove-LockGuard { - param([string]$LockFile,[switch]$Automated) - if (Test-Path $LockFile) { - Remove-Item $LockFile -Force - Write-Log "Lock removed: $LockFile" -Automated:$Automated -Color 'Cyan' - } -} - -# ====================================================================== -# Main unified executor (callback-based) -# ====================================================================== -function Invoke-ScheduledExecution { - param( - [scriptblock]$ScriptBlock, - [hashtable]$Config, - [switch]$Automated, - [string]$CurrentDateTimeUtc - ) - - $scriptPath = $MyInvocation.ScriptName - $lastRunFile = [IO.Path]::ChangeExtension($scriptPath, ".lastRun") - $lockFile = [IO.Path]::ChangeExtension($scriptPath, ".lock") - - # Check schedule - $schedule = Test-ScheduledExecution -Automated:$Automated -CurrentDateTimeUtc $CurrentDateTimeUtc -Config $Config -LastRunFilePath $lastRunFile - if (-not $schedule.ShouldExecute) { - Write-Log "Execution skipped." -Automated:$Automated -Color 'Yellow' - return - } - - # Lock - if (-not (New-LockGuard -LockFile $lockFile -Automated:$Automated)) { - return - } - - try { - $schedule.Now.ToString("o") | Set-Content $lastRunFile - & $ScriptBlock - } - finally { - Remove-LockGuard -LockFile $lockFile -Automated:$Automated - } -} - -Export-ModuleMember -Function * -Alias * -``` diff --git a/examples/File-Sync/README.md b/examples/File-Sync/README.md index e88f779..e3b5dda 100644 --- a/examples/File-Sync/README.md +++ b/examples/File-Sync/README.md @@ -1,7 +1,7 @@ # File Sync Script -**Version:** 1.0.0 -**Last Updated:** 2026-01-24 +**Version:** 1.0.1 +**Last Updated:** 2026-01-26 ## Overview @@ -361,6 +361,11 @@ Run with verbose output: - Dynamic batch file path updates - Prerequisite validation +### 1.0.1 (2026-01-26) +- Improve UNC path validation for network share connections +- Code formatting improvements for better readability +- Refactored parameter splatting for Invoke-ScheduledExecution + ## Support For issues or questions: diff --git a/examples/File-Sync/file-sync.bat b/examples/File-Sync/file-sync.bat index 4890fb4..0d8d022 100644 --- a/examples/File-Sync/file-sync.bat +++ b/examples/File-Sync/file-sync.bat @@ -3,8 +3,8 @@ setlocal EnableDelayedExpansion REM ============================================================================ REM File Sync Launcher -REM VERSION: 1.0.0 -REM DATE: 2026-01-24 +REM VERSION: 1.0.1 +REM DATE: 2026-01-26 REM DESCRIPTION: Batch file launcher for file-sync.ps1 with admin check REM ============================================================================ diff --git a/examples/File-Sync/file-sync.ps1 b/examples/File-Sync/file-sync.ps1 index a117c55..1936d20 100644 --- a/examples/File-Sync/file-sync.ps1 +++ b/examples/File-Sync/file-sync.ps1 @@ -12,9 +12,9 @@ param ( .DESCRIPTION Production-ready file synchronization solution with scheduling and secure credential management. .VERSION - 1.0.0 + 1.0.1 .DATE - 2026-01-24 + 2026-01-26 .NOTES - Requires FreeFileSync installed - Requires SchedulerTemplate.psm1 module @@ -22,8 +22,8 @@ param ( #> # Script Version -$ScriptVersion = "1.0.0" -$ScriptDate = "2026-01-24" +$ScriptVersion = "1.0.1" +$ScriptDate = "2026-01-26" try { Import-Module "$PSScriptRoot\..\SchedulerTemplate.psm1" -Force -ErrorAction Stop @@ -135,6 +135,12 @@ function Connect-NasShare { 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 NAS share: $SharePath" -Level Info -Automated:$Automated # Validate credential environment variable name is configured @@ -211,14 +217,14 @@ function Start-FreeFileSyncProcess { try { # Use ProcessStartInfo to ensure that no window is shown $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.FileName = $FreeFileSyncExe - $psi.Arguments = "`"$FfsBatchFile`"" - $psi.WorkingDirectory = [System.IO.Path]::GetDirectoryName($FreeFileSyncExe) - $psi.UseShellExecute = $false - $psi.CreateNoWindow = $true - $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden + $psi.FileName = $FreeFileSyncExe + $psi.Arguments = "`"$FfsBatchFile`"" + $psi.WorkingDirectory = [System.IO.Path]::GetDirectoryName($FreeFileSyncExe) + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true + $psi.RedirectStandardError = $true $proc = [System.Diagnostics.Process]::Start($psi) $global:FFS_Process = $proc @@ -356,11 +362,13 @@ function Start-BusinessLogic { if ($Automated) { if (Get-Command Invoke-ScheduledExecution -ErrorAction SilentlyContinue) { - Invoke-ScheduledExecution ` - -Config $Config ` - -Automated:$Automated ` - -CurrentDateTimeUtc $CurrentDateTimeUtc ` - -ScriptBlock { Start-BusinessLogic -Automated:$Automated } + $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 diff --git a/examples/File-Sync/scriptsettings.json b/examples/File-Sync/scriptsettings.json index 07d1cd5..771742f 100644 --- a/examples/File-Sync/scriptsettings.json +++ b/examples/File-Sync/scriptsettings.json @@ -2,8 +2,8 @@ "$schema": "https://json-schema.org/draft-07/schema", "title": "File Sync Script Settings", "description": "Configuration file for file-sync.ps1 script using FreeFileSync", - "version": "1.0.0", - "lastModified": "2026-01-24", + "version": "1.0.1", + "lastModified": "2026-01-26", "schedule": { "runMonth": [], "runWeekday": ["Monday"], diff --git a/examples/HyperV-Backup/README.md b/examples/HyperV-Backup/README.md index c73a1c8..b10daef 100644 --- a/examples/HyperV-Backup/README.md +++ b/examples/HyperV-Backup/README.md @@ -1,7 +1,7 @@ # Hyper-V Backup Script -**Version:** 1.0.0 -**Last Updated:** 2026-01-24 +**Version:** 1.0.1 +**Last Updated:** 2026-01-26 ## Overview @@ -9,12 +9,12 @@ Production-ready automated backup solution for Hyper-V virtual machines with sch ## Features -- ✅ **Automated VM Backup** - Exports all VMs on the host using Hyper-V checkpoints +- ✅ **Automated VM Backup** - Exports all VMs on the host (Export-VM handles checkpoints internally) - ✅ **Flexible Scheduling** - Schedule backups by month, weekday, and time with interval control - ✅ **Remote Storage Support** - Backup to UNC shares with secure credential management - ✅ **Retention Management** - Automatically cleanup old backups based on retention count -- ✅ **Checkpoint Management** - Automatic cleanup of backup checkpoints -- ✅ **Space Validation** - Pre-flight checks for available disk space +- ✅ **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 - ✅ **Detailed Logging** - Comprehensive logging with timestamps and severity levels - ✅ **Lock Files** - Prevents concurrent execution @@ -65,7 +65,6 @@ HyperV-Backup/ "credentialEnvVar": "YOUR_ENV_VAR_NAME", "tempExportRoot": "D:\\Temp\\HyperVExport", "retentionCount": 3, - "minFreeSpaceGB": 100, "excludeVMs": ["vm-to-exclude"] } ``` @@ -109,9 +108,8 @@ HyperV-Backup/ |----------|------|----------|-------------| | `backupRoot` | string | Yes | UNC or local path for backups. Hostname is appended automatically. | | `credentialEnvVar` | string | No* | Name of Machine-level environment variable with credentials (*Required for UNC paths) | -| `tempExportRoot` | string | Yes | Local directory for temporary VM exports | +| `tempExportRoot` | string | Yes | Local directory for temporary VM exports. Space checked dynamically per VM (1.5x VM size). | | `retentionCount` | number | Yes | Number of backup generations to keep (1-365) | -| `minFreeSpaceGB` | number | No | Minimum required free space in GB (0 = disable check) | | `excludeVMs` | array | No | VM names to exclude from backup | ### Version Tracking @@ -177,13 +175,14 @@ When `-Automated` is specified: - Retrieve all VMs on the host - Filter excluded VMs - For each VM: - - Create checkpoint with timestamp - - Export VM to temp location + - Check temp space (requires 1.5x VM size) + - Export VM to temp location (Export-VM handles checkpoints internally) + - Check destination space before copy - Copy to final backup location - Cleanup temp export 4. **Cleanup** - - Remove all backup checkpoints + - Remove old backup checkpoints (keeps last 2 for rollback) - Delete old backup folders beyond retention count 5. **Summary** @@ -265,12 +264,13 @@ Error: Failed to connect to \\server\share **3. Insufficient Space** ``` -Error: Insufficient free space on drive D: +Error: Insufficient temp space for VM 'xxx' (need ~150 GB, have 100 GB) +Error: Insufficient space on destination for VM 'xxx' ``` **Solution:** -- Free up space on temp drive -- Reduce `minFreeSpaceGB` setting (not recommended) -- Use different temp location +- Free up space on temp drive or destination +- Use different temp location with more space +- Check destination share quota/capacity **4. Lock File Exists** ``` @@ -281,14 +281,15 @@ Guard: Lock file exists. Skipping. - Manually delete `.lock` file if stuck - Check for hung PowerShell processes -**5. Checkpoint Creation Failed** +**5. Export Failed** ``` -Error: Failed to create checkpoint for VM +Error: Failed to export VM 'xxx' ``` **Solution:** - Verify VM is in a valid state - Check Hyper-V event logs -- Ensure sufficient disk space for checkpoints +- Ensure sufficient disk space for export +- Verify no other export/checkpoint operations in progress ### Debug Mode @@ -329,12 +330,22 @@ Run with verbose output: ### 1.0.0 (2026-01-24) - Initial production release - Automated backup with scheduling -- Checkpoint-based export +- Export-VM based backup (handles checkpoints internally) - Retention management - UNC share support with credential management - Lock file and interval control - Comprehensive error handling and logging +### 1.0.1 (2026-01-26) +- Improved disk space checking: Dynamic per-VM validation (1.5x VM size) for temp and destination +- Removed static `minFreeSpaceGB` setting in favor of smart per-VM space checks +- Enhanced checkpoint retention: Keep last 2 backup checkpoints for rollback capability +- Removed manual checkpoint creation (Export-VM handles checkpoints internally) +- Improved UNC path validation +- Better error messages for space-related failures +- Performance improvement: Skip unnecessary space checks +- Refactored parameter splatting for Invoke-ScheduledExecution + ## Support For issues or questions: diff --git a/examples/HyperV-Backup/hyper-v-backup.bat b/examples/HyperV-Backup/hyper-v-backup.bat index f5bdad9..1832a24 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.0 -REM DATE: 2026-01-24 +REM VERSION: 1.0.1 +REM DATE: 2026-01-26 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 ca79375..812a848 100644 --- a/examples/HyperV-Backup/hyper-v-backup.ps1 +++ b/examples/HyperV-Backup/hyper-v-backup.ps1 @@ -13,9 +13,9 @@ param ( .DESCRIPTION Production-ready Hyper-V backup solution with scheduling, checkpoints, and retention management. .VERSION - 1.0.0 + 1.0.1 .DATE - 2026-01-24 + 2026-01-26 .NOTES - Requires Administrator privileges - Requires Hyper-V PowerShell module @@ -23,8 +23,8 @@ param ( #> # Script Version -$ScriptVersion = "1.0.0" -$ScriptDate = "2026-01-24" +$ScriptVersion = "1.0.1" +$ScriptDate = "2026-01-26" try { Import-Module "$PSScriptRoot\..\SchedulerTemplate.psm1" -Force -ErrorAction Stop @@ -67,7 +67,6 @@ $BackupRoot = $settings.backupRoot $CredentialEnvVar = $settings.credentialEnvVar $TempExportRoot = $settings.tempExportRoot $RetentionCount = $settings.retentionCount -$MinFreeSpaceGB = $settings.minFreeSpaceGB $BlacklistedVMs = $settings.excludeVMs # Schedule Configuration @@ -144,19 +143,6 @@ function Test-Prerequisites { } } - # Check free space on temp drive - if ($MinFreeSpaceGB -gt 0) { - $tempDrive = (Get-Item $TempExportRoot).PSDrive.Name - $freeSpace = (Get-PSDrive $tempDrive).Free / 1GB - - if ($freeSpace -lt $MinFreeSpaceGB) { - Write-Log "Insufficient free space on drive ${tempDrive}: ($([math]::Round($freeSpace, 2)) GB available, $MinFreeSpaceGB GB required)" -Level Error -Automated:$Automated - return $false - } - - Write-Log "Free space on drive ${tempDrive}: $([math]::Round($freeSpace, 2)) GB" -Level Info -Automated:$Automated - } - Write-Log "All prerequisites passed" -Level Success -Automated:$Automated return $true } @@ -173,6 +159,12 @@ function Connect-BackupShare { 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 @@ -222,12 +214,12 @@ function Get-VMDiskSize { 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 { @@ -236,6 +228,64 @@ function Get-VMDiskSize { } } +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, @@ -262,52 +312,27 @@ function Backup-VM { return $false } - # Estimate required space + # 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 - if ($MinFreeSpaceGB -gt 0) { - $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 space for $VMName" - return $false - } + + # 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 } - # Create checkpoint - Write-Log "Creating checkpoint for VM '$VMName'..." -Level Info -Automated:$Automated - $checkpointName = "Backup-$DateSuffix" - - try { - Checkpoint-VM -Name $VMName -SnapshotName $checkpointName -ErrorAction Stop - } - catch { - Write-Log "Failed to create checkpoint for VM '$VMName': $_" -Level Error -Automated:$Automated - $script:BackupStats.FailedVMs++ - $script:BackupStats.FailureMessages += "Checkpoint failed for $VMName" - return $false - } - - # Verify checkpoint - $checkpoint = Get-VMSnapshot -VMName $VMName -Name $checkpointName -ErrorAction SilentlyContinue - if (-not $checkpoint) { - Write-Log "Checkpoint verification failed for VM '$VMName'" -Level Error -Automated:$Automated - $script:BackupStats.FailedVMs++ - $script:BackupStats.FailureMessages += "Checkpoint verification failed for $VMName" - return $false - } - - Write-Log "Checkpoint created successfully: $checkpointName" -Level Success -Automated:$Automated - - # Export VM to temp location + # 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 @@ -329,14 +354,42 @@ function Backup-VM { 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 { @@ -382,24 +435,28 @@ function Backup-VM { function Remove-OldCheckpoints { param( [array]$VMs, + [int]$RetentionCount = 2, [switch]$Automated ) - Write-Log "Starting checkpoint cleanup for all VMs..." -Level Info -Automated:$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-*" } - if ($checkpoints) { - foreach ($checkpoint in $checkpoints) { + 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++ @@ -414,7 +471,7 @@ function Remove-OldCheckpoints { 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 } @@ -585,11 +642,13 @@ function Start-BusinessLogic { if ($Automated) { if (Get-Command Invoke-ScheduledExecution -ErrorAction SilentlyContinue) { - Invoke-ScheduledExecution ` - -Config $Config ` - -Automated:$Automated ` - -CurrentDateTimeUtc $CurrentDateTimeUtc ` - -ScriptBlock { Start-BusinessLogic -Automated:$Automated } + $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 diff --git a/examples/HyperV-Backup/scriptsettings.json b/examples/HyperV-Backup/scriptsettings.json index 2738eaf..971bf14 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.0", - "lastModified": "2026-01-24", + "version": "1.0.1", + "lastModified": "2026-01-26", "schedule": { "runMonth": [], "runWeekday": ["Monday"], @@ -14,7 +14,6 @@ "credentialEnvVar": "nassrv0001", "tempExportRoot": "D:\\Temp\\HyperVExport", "retentionCount": 3, - "minFreeSpaceGB": 100, "excludeVMs": ["nassrv0002"], "_comments": { "version": "Configuration schema version", @@ -27,9 +26,8 @@ }, "backupRoot": "UNC path or local path to backup root directory. Hostname will be appended automatically.", "credentialEnvVar": "Name of Machine-level environment variable containing Base64-encoded 'username:password'", - "tempExportRoot": "Local directory for temporary VM exports. Must have sufficient free space.", + "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.", - "minFreeSpaceGB": "Minimum required free space in GB before starting backup. Set to 0 to disable check.", "excludeVMs": "Array of VM names to exclude from backup process" } } diff --git a/examples/SchedulerTemplate.psd1 b/examples/SchedulerTemplate.psd1 new file mode 100644 index 0000000..b52e640 --- /dev/null +++ b/examples/SchedulerTemplate.psd1 @@ -0,0 +1,46 @@ +@{ + RootModule = 'SchedulerTemplate.psm1' + ModuleVersion = '1.0.1' + GUID = 'a3b2c1d0-e4f5-6a7b-8c9d-0e1f2a3b4c5d' + Author = 'MaksIT' + CompanyName = 'MaksIT' + Copyright = '(c) 2026 MaksIT. All rights reserved.' + Description = 'Reusable PowerShell module for scheduled script execution with lock files, interval control, and credential management.' + PowerShellVersion = '5.1' + FunctionsToExport = @( + 'Write-Log', + 'Get-CredentialFromEnvVar', + 'Test-UNCPath', + 'Get-CurrentUtcDateTime', + 'Test-ScheduleMonth', + 'Test-ScheduleWeekday', + 'Test-ScheduleTime', + 'Test-Schedule', + 'Test-Interval', + 'Test-ScheduledExecution', + 'New-LockGuard', + 'Remove-LockGuard', + 'Invoke-ScheduledExecution' + ) + CmdletsToExport = @() + VariablesToExport = @('ModuleVersion', 'ModuleDate') + AliasesToExport = @() + PrivateData = @{ + PSData = @{ + Tags = @('Scheduler', 'Automation', 'Lock', 'Logging', 'Credentials') + LicenseUri = '' + ProjectUri = 'https://github.com/MaksIT/uscheduler' + ReleaseNotes = @' +## 1.0.1 (2026-01-26) +- Improved UNC path validation (Test-UNCPath function) +- Enhanced credential management +- Comprehensive logging with timestamp support +- Scheduled execution with lock files and interval control +- Schedule validation (month, weekday, time) +- Write-Log function with severity levels and color support +- Get-CredentialFromEnvVar for secure Base64-encoded credential retrieval +- Invoke-ScheduledExecution for automated scheduled task management +'@ + } + } +} diff --git a/examples/SchedulerTemplate.psm1 b/examples/SchedulerTemplate.psm1 index b24e853..a1f43a5 100644 --- a/examples/SchedulerTemplate.psm1 +++ b/examples/SchedulerTemplate.psm1 @@ -5,18 +5,19 @@ Reusable PowerShell module for scheduled script execution with lock files, interval control, and credential management. .VERSION - 1.0.0 + 1.0.1 .DATE - 2026-01-24 + 2026-01-26 .NOTES - Provides Write-Log function with timestamp and level support - Provides Get-CredentialFromEnvVar for secure credential retrieval + - Provides Test-UNCPath for UNC path validation - Provides Invoke-ScheduledExecution for scheduled task management #> # Module Version (exported for external scripts to check version) -$script:ModuleVersion = "1.0.0" -$script:ModuleDate = "2026-01-24" +$script:ModuleVersion = "1.0.1" +$script:ModuleDate = "2026-01-26" # Module load confirmation Write-Verbose "SchedulerTemplate.psm1 v$ModuleVersion loaded ($ModuleDate)" @@ -90,6 +91,18 @@ function Get-CredentialFromEnvVar { } } +function Test-UNCPath { + param([string]$Path) + + try { + $uri = [System.Uri]$Path + return $uri.IsUnc + } + catch { + return $false + } +} + function Get-CurrentUtcDateTime { param([string]$ExternalDateTime, [switch]$Automated)