(bugfix): hyper-v backup and file sync scripts improvements

This commit is contained in:
Maksym Sadovnychyy 2026-01-26 18:44:27 +01:00
parent 20c7419b75
commit de1add2172
11 changed files with 271 additions and 310 deletions

183
README.md
View File

@ -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 *
```

View File

@ -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:

View File

@ -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 ============================================================================

View File

@ -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

View File

@ -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"],

View File

@ -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:

View File

@ -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 ============================================================================

View File

@ -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
@ -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
# 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 space for $VMName"
return $false
}
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,6 +354,34 @@ 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
@ -382,10 +435,11 @@ 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
@ -394,10 +448,13 @@ function Remove-OldCheckpoints {
try {
$checkpoints = Get-VMSnapshot -VMName $vmName -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like "Backup-*" }
Where-Object { $_.Name -like "Backup-*" } |
Sort-Object CreationTime -Descending
if ($checkpoints) {
foreach ($checkpoint in $checkpoints) {
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 {
@ -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

View File

@ -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"
}
}

View File

@ -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
'@
}
}
}

View File

@ -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)