mirror of
https://github.com/MAKS-IT-COM/uscheduler.git
synced 2026-02-14 06:37:18 +01:00
(feature): hyper-v backup script example
This commit is contained in:
parent
aa5b446d8b
commit
f337212eee
@ -11,6 +11,7 @@ Designed for system administrators — and also for those who *feel like* system
|
||||
|
||||
- [MaksIT Unified Scheduler Service](#maksit-unified-scheduler-service)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Scripts Examples](#scripts-examples)
|
||||
- [Features at a Glance](#features-at-a-glance)
|
||||
- [Installation](#installation)
|
||||
- [Recommended (using bundled scripts)](#recommended-using-bundled-scripts)
|
||||
@ -30,6 +31,10 @@ Designed for system administrators — and also for those who *feel like* system
|
||||
- [Appendix](#appendix)
|
||||
- [SchedulerTemplate.psm1 (Full Source)](#schedulertemplatepsm1-full-source)
|
||||
|
||||
## Scripts Examples
|
||||
|
||||
- [Hyper-V Backup](./examples/HyperV-Backup/README.md) - Production-ready Hyper-V VM backup solution with scheduling and retention management
|
||||
|
||||
---
|
||||
|
||||
## Features at a Glance
|
||||
|
||||
355
examples/HyperV-Backup/README.md
Normal file
355
examples/HyperV-Backup/README.md
Normal file
@ -0,0 +1,355 @@
|
||||
# Hyper-V Backup Script
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Last Updated:** 2026-01-24
|
||||
|
||||
## Overview
|
||||
|
||||
Production-ready automated backup solution for Hyper-V virtual machines with scheduling, checkpoints, retention management, and remote storage support.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Automated VM Backup** - Exports all VMs on the host using Hyper-V checkpoints
|
||||
- ✅ **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
|
||||
- ✅ **VM Exclusion** - Exclude specific VMs from backup
|
||||
- ✅ **Detailed Logging** - Comprehensive logging with timestamps and severity levels
|
||||
- ✅ **Lock Files** - Prevents concurrent execution
|
||||
- ✅ **Error Handling** - Proper exit codes and error reporting
|
||||
|
||||
## Requirements
|
||||
|
||||
### System Requirements
|
||||
- Windows Server with Hyper-V role installed
|
||||
- PowerShell 5.1 or later
|
||||
- Administrator privileges
|
||||
- Hyper-V PowerShell module
|
||||
|
||||
### Dependencies
|
||||
- `SchedulerTemplate.psm1` module (located in parent directory)
|
||||
- `scriptsettings.json` configuration file
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
HyperV-Backup/
|
||||
├── hyper-v-backup.bat # Batch launcher with admin check
|
||||
├── hyper-v-backup.ps1 # Main PowerShell script
|
||||
├── scriptsettings.json # Configuration file
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Copy Files**
|
||||
```powershell
|
||||
# Copy the entire HyperV-Backup folder to your desired location
|
||||
# Ensure SchedulerTemplate.psm1 is in the parent directory
|
||||
```
|
||||
|
||||
2. **Configure Settings**
|
||||
|
||||
Edit `scriptsettings.json` with your environment settings:
|
||||
```json
|
||||
{
|
||||
"schedule": {
|
||||
"runMonth": [],
|
||||
"runWeekday": ["Monday"],
|
||||
"runTime": ["00:00"],
|
||||
"minIntervalMinutes": 10
|
||||
},
|
||||
"backupRoot": "\\\\your-nas\\backups",
|
||||
"credentialEnvVar": "YOUR_ENV_VAR_NAME",
|
||||
"tempExportRoot": "D:\\Temp\\HyperVExport",
|
||||
"retentionCount": 3,
|
||||
"minFreeSpaceGB": 100,
|
||||
"excludeVMs": ["vm-to-exclude"]
|
||||
}
|
||||
```
|
||||
|
||||
3. **Setup Credentials (for UNC paths)**
|
||||
|
||||
If backing up to a network share, create a Machine-level environment variable:
|
||||
```powershell
|
||||
# Create Base64-encoded credentials
|
||||
$username = "DOMAIN\user"
|
||||
$password = "your-password"
|
||||
$creds = "$username:$password"
|
||||
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($creds))
|
||||
|
||||
# Set Machine-level environment variable
|
||||
[System.Environment]::SetEnvironmentVariable("YOUR_ENV_VAR_NAME", $encoded, "Machine")
|
||||
```
|
||||
|
||||
4. **Test Manual Execution**
|
||||
```powershell
|
||||
# Run as Administrator
|
||||
.\hyper-v-backup.bat
|
||||
# or
|
||||
.\hyper-v-backup.ps1
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Schedule Settings
|
||||
|
||||
| Property | Type | Description | Example |
|
||||
|----------|------|-------------|---------|
|
||||
| `runMonth` | array | Month names to run. Empty = every month | `["January", "June", "December"]` or `[]` |
|
||||
| `runWeekday` | array | Weekday names to run. Empty = every day | `["Monday", "Friday"]` |
|
||||
| `runTime` | array | UTC times to run (HH:mm format) | `["00:00", "12:00"]` |
|
||||
| `minIntervalMinutes` | number | Minimum minutes between runs | `10` |
|
||||
|
||||
### Backup Settings
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
|----------|------|----------|-------------|
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `version` | string | Configuration schema version |
|
||||
| `lastModified` | string | Last modification date (YYYY-MM-DD) |
|
||||
|
||||
## Usage
|
||||
|
||||
### Manual Execution
|
||||
|
||||
**Using Batch File (Recommended):**
|
||||
```batch
|
||||
REM Right-click and select "Run as administrator"
|
||||
hyper-v-backup.bat
|
||||
```
|
||||
|
||||
**Using PowerShell:**
|
||||
```powershell
|
||||
# Run as Administrator
|
||||
.\hyper-v-backup.ps1
|
||||
|
||||
# With verbose output
|
||||
.\hyper-v-backup.ps1 -Verbose
|
||||
```
|
||||
|
||||
### Automated Execution
|
||||
|
||||
The script supports automated execution through the UScheduler service:
|
||||
|
||||
```powershell
|
||||
# Called by scheduler with -Automated flag
|
||||
.\hyper-v-backup.ps1 -Automated -CurrentDateTimeUtc "2026-01-24 00:00:00"
|
||||
```
|
||||
|
||||
When `-Automated` is specified:
|
||||
- Schedule is enforced (month, weekday, time)
|
||||
- Lock files prevent concurrent execution
|
||||
- Interval checking prevents duplicate runs
|
||||
- Logs are formatted for service logger (no timestamps)
|
||||
|
||||
## How It Works
|
||||
|
||||
### Backup Process Flow
|
||||
|
||||
1. **Initialization**
|
||||
- Load SchedulerTemplate.psm1 module
|
||||
- Load and validate scriptsettings.json
|
||||
- Validate required settings
|
||||
- Set up backup paths
|
||||
|
||||
2. **Pre-flight Checks**
|
||||
- Verify Administrator privileges
|
||||
- Check Hyper-V module availability
|
||||
- Verify Hyper-V service is running
|
||||
- Check temp directory and free space
|
||||
- Authenticate to backup share (if UNC)
|
||||
- Create backup destination directory
|
||||
|
||||
3. **Backup Execution**
|
||||
- Retrieve all VMs on the host
|
||||
- Filter excluded VMs
|
||||
- For each VM:
|
||||
- Create checkpoint with timestamp
|
||||
- Export VM to temp location
|
||||
- Copy to final backup location
|
||||
- Cleanup temp export
|
||||
|
||||
4. **Cleanup**
|
||||
- Remove all backup checkpoints
|
||||
- Delete old backup folders beyond retention count
|
||||
|
||||
5. **Summary**
|
||||
- Display backup statistics
|
||||
- Report success/failure counts
|
||||
- List any errors encountered
|
||||
|
||||
### Directory Structure Created
|
||||
|
||||
```
|
||||
\\backupRoot\Hyper-V\Backups\hostname\
|
||||
├── 20260124000000\ # Backup folder (timestamp)
|
||||
│ ├── VM-Name-1\
|
||||
│ ├── VM-Name-2\
|
||||
│ └── VM-Name-3\
|
||||
├── 20260117000000\ # Previous backup
|
||||
└── 20260110000000\ # Older backup (will be deleted if retentionCount=2)
|
||||
```
|
||||
|
||||
### Lock and State Files
|
||||
|
||||
When running in automated mode, the script creates:
|
||||
- `hyper-v-backup.lock` - Prevents concurrent execution
|
||||
- `hyper-v-backup.lastRun` - Tracks last execution time for interval control
|
||||
|
||||
## Logging
|
||||
|
||||
### Log Levels
|
||||
|
||||
| Level | Description | Color (Manual) |
|
||||
|-------|-------------|----------------|
|
||||
| `Info` | Informational messages | White |
|
||||
| `Success` | Successful operations | Green |
|
||||
| `Warning` | Non-critical issues | Yellow |
|
||||
| `Error` | Critical errors | Red |
|
||||
|
||||
### Log Format
|
||||
|
||||
**Manual Execution:**
|
||||
```
|
||||
[2026-01-24 00:00:00] [Info] Hyper-V Backup Process Started
|
||||
[2026-01-24 00:00:01] [Success] All prerequisites passed
|
||||
[2026-01-24 00:05:30] [Success] Backup completed successfully for VM: server01
|
||||
```
|
||||
|
||||
**Automated Execution:**
|
||||
```
|
||||
[Info] Hyper-V Backup Process Started
|
||||
[Success] All prerequisites passed
|
||||
[Success] Backup completed successfully for VM: server01
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| `0` | Success or no VMs found |
|
||||
| `1` | Error occurred (module load, config, prerequisites, backup failure) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**1. Module Not Found**
|
||||
```
|
||||
Error: Failed to load SchedulerTemplate.psm1
|
||||
```
|
||||
**Solution:** Ensure SchedulerTemplate.psm1 is in the parent directory (`../SchedulerTemplate.psm1`)
|
||||
|
||||
**2. UNC Path Authentication Failed**
|
||||
```
|
||||
Error: Failed to connect to \\server\share
|
||||
```
|
||||
**Solution:**
|
||||
- Verify `credentialEnvVar` is set in scriptsettings.json
|
||||
- Verify environment variable exists at Machine level
|
||||
- Verify credentials are Base64-encoded in format: `username:password`
|
||||
- Test with: `net use \\server\share` manually
|
||||
|
||||
**3. Insufficient Space**
|
||||
```
|
||||
Error: Insufficient free space on drive D:
|
||||
```
|
||||
**Solution:**
|
||||
- Free up space on temp drive
|
||||
- Reduce `minFreeSpaceGB` setting (not recommended)
|
||||
- Use different temp location
|
||||
|
||||
**4. Lock File Exists**
|
||||
```
|
||||
Guard: Lock file exists. Skipping.
|
||||
```
|
||||
**Solution:**
|
||||
- Another instance is running, or previous run didn't complete
|
||||
- Manually delete `.lock` file if stuck
|
||||
- Check for hung PowerShell processes
|
||||
|
||||
**5. Checkpoint Creation Failed**
|
||||
```
|
||||
Error: Failed to create checkpoint for VM
|
||||
```
|
||||
**Solution:**
|
||||
- Verify VM is in a valid state
|
||||
- Check Hyper-V event logs
|
||||
- Ensure sufficient disk space for checkpoints
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Run with verbose output:
|
||||
```powershell
|
||||
.\hyper-v-backup.ps1 -Verbose
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Test First** - Always test backups manually before scheduling
|
||||
2. **Monitor Space** - Ensure adequate space on both temp and backup locations
|
||||
3. **Verify Backups** - Periodically test restore from backups
|
||||
4. **Secure Credentials** - Use Machine-level environment variables, never store passwords in scripts
|
||||
5. **Schedule Wisely** - Run backups during low-usage periods
|
||||
6. **Review Logs** - Check backup summaries regularly
|
||||
7. **Update Retention** - Adjust `retentionCount` based on storage capacity
|
||||
8. **Exclude Carefully** - Only exclude VMs that don't need backup
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Credentials** are stored Base64-encoded in Machine-level environment variables (not encryption, just encoding)
|
||||
- Script requires **Administrator privileges**
|
||||
- **Network credentials** are passed to `net use` command
|
||||
- Consider using **dedicated backup account** with minimal required permissions
|
||||
- **Backup data** should be stored on secured network shares with appropriate ACLs
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Export time** depends on VM size and disk I/O performance
|
||||
- **Temp location** should be on fast local storage (SSD recommended)
|
||||
- **Network speed** affects copy time to remote shares
|
||||
- **Checkpoints** consume disk space temporarily
|
||||
- Multiple VMs are processed **sequentially** (not parallel)
|
||||
|
||||
## Version History
|
||||
|
||||
### 1.0.0 (2026-01-24)
|
||||
- Initial production release
|
||||
- Automated backup with scheduling
|
||||
- Checkpoint-based export
|
||||
- Retention management
|
||||
- UNC share support with credential management
|
||||
- Lock file and interval control
|
||||
- Comprehensive error handling and logging
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check the [Troubleshooting](#troubleshooting) section
|
||||
2. Review script logs for error details
|
||||
3. Verify all [Requirements](#requirements) are met
|
||||
4. Check Hyper-V event logs for VM-related issues
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](../../LICENSE.md) in the root directory.
|
||||
|
||||
## Related Files
|
||||
|
||||
- `../SchedulerTemplate.psm1` - Shared scheduling and logging module
|
||||
- `scriptsettings.json` - Configuration file
|
||||
- `hyper-v-backup.bat` - Batch launcher
|
||||
- `hyper-v-backup.ps1` - Main script
|
||||
74
examples/HyperV-Backup/hyper-v-backup.bat
Normal file
74
examples/HyperV-Backup/hyper-v-backup.bat
Normal file
@ -0,0 +1,74 @@
|
||||
@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
REM ============================================================================
|
||||
REM Hyper-V Backup Launcher
|
||||
REM VERSION: 1.0.0
|
||||
REM DATE: 2026-01-24
|
||||
REM DESCRIPTION: Batch file launcher for hyper-v-backup.ps1 with admin check
|
||||
REM ============================================================================
|
||||
|
||||
echo.
|
||||
echo ============================================
|
||||
echo Hyper-V Automated Backup Launcher
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
REM Check for Administrator privileges
|
||||
net session >nul 2>&1
|
||||
if %errorLevel% NEQ 0 (
|
||||
echo [ERROR] This script must be run as Administrator!
|
||||
echo.
|
||||
echo Please right-click and select "Run as administrator"
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [OK] Running with Administrator privileges
|
||||
echo.
|
||||
|
||||
REM Get script directory
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "PS_SCRIPT=%SCRIPT_DIR%hyper-v-backup.ps1"
|
||||
|
||||
REM Check if PowerShell script exists
|
||||
if not exist "%PS_SCRIPT%" (
|
||||
echo [ERROR] PowerShell script not found: %PS_SCRIPT%
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [OK] Found PowerShell script: %PS_SCRIPT%
|
||||
echo.
|
||||
echo ============================================
|
||||
echo Starting backup process...
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
REM Execute PowerShell script
|
||||
REM Note: Logging is handled by UScheduler service
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%PS_SCRIPT%"
|
||||
|
||||
REM Capture exit code
|
||||
set "EXIT_CODE=%ERRORLEVEL%"
|
||||
|
||||
echo.
|
||||
echo ============================================
|
||||
echo Backup process completed
|
||||
echo Exit Code: %EXIT_CODE%
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
if %EXIT_CODE% EQU 0 (
|
||||
echo [SUCCESS] Backup completed successfully
|
||||
) else (
|
||||
echo [ERROR] Backup completed with errors
|
||||
)
|
||||
|
||||
echo.
|
||||
pause
|
||||
|
||||
endlocal
|
||||
exit /b %EXIT_CODE%
|
||||
602
examples/HyperV-Backup/hyper-v-backup.ps1
Normal file
602
examples/HyperV-Backup/hyper-v-backup.ps1
Normal file
@ -0,0 +1,602 @@
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[switch]$Automated,
|
||||
[string]$CurrentDateTimeUtc
|
||||
)
|
||||
|
||||
#Requires -RunAsAdministrator
|
||||
#Requires -Modules Hyper-V
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Hyper-V Automated Backup Script
|
||||
.DESCRIPTION
|
||||
Production-ready Hyper-V backup solution with scheduling, checkpoints, and retention management.
|
||||
.VERSION
|
||||
1.0.0
|
||||
.DATE
|
||||
2026-01-24
|
||||
.NOTES
|
||||
- Requires Administrator privileges
|
||||
- Requires Hyper-V PowerShell module
|
||||
- Requires SchedulerTemplate.psm1 module
|
||||
#>
|
||||
|
||||
# Script Version
|
||||
$ScriptVersion = "1.0.0"
|
||||
$ScriptDate = "2026-01-24"
|
||||
|
||||
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
|
||||
$MinFreeSpaceGB = $settings.minFreeSpaceGB
|
||||
$BlacklistedVMs = $settings.excludeVMs
|
||||
|
||||
# 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-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 (Get-Module -ListAvailable -Name Hyper-V)) {
|
||||
Write-Log "Hyper-V PowerShell module is not installed!" -Level Error -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
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 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
|
||||
$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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
$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
|
||||
|
||||
# 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,
|
||||
[switch]$Automated
|
||||
)
|
||||
|
||||
Write-Log "Starting checkpoint cleanup for all VMs..." -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) {
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
Remove-OldCheckpoints -VMs $vms -Automated:$Automated
|
||||
|
||||
# Cleanup old backups
|
||||
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) {
|
||||
Invoke-ScheduledExecution `
|
||||
-Config $Config `
|
||||
-Automated:$Automated `
|
||||
-CurrentDateTimeUtc $CurrentDateTimeUtc `
|
||||
-ScriptBlock { Start-BusinessLogic -Automated:$Automated }
|
||||
}
|
||||
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
|
||||
}
|
||||
35
examples/HyperV-Backup/scriptsettings.json
Normal file
35
examples/HyperV-Backup/scriptsettings.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"$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",
|
||||
"schedule": {
|
||||
"runMonth": [],
|
||||
"runWeekday": ["Monday"],
|
||||
"runTime": ["00:00"],
|
||||
"minIntervalMinutes": 10
|
||||
},
|
||||
"backupRoot": "\\\\nassrv0001.corp.maks-it.com\\data-1",
|
||||
"credentialEnvVar": "nassrv0001",
|
||||
"tempExportRoot": "D:\\Temp\\HyperVExport",
|
||||
"retentionCount": 3,
|
||||
"minFreeSpaceGB": 100,
|
||||
"excludeVMs": ["nassrv0002"],
|
||||
"_comments": {
|
||||
"version": "Configuration schema version",
|
||||
"lastModified": "Last modification date (YYYY-MM-DD)",
|
||||
"schedule": {
|
||||
"runMonth": "Array of month names (e.g. 'January', 'June', 'December') to run backup. Empty array = every month.",
|
||||
"runWeekday": "Array of weekday names (e.g. 'Monday', 'Friday') to run backup. Empty array = every day.",
|
||||
"runTime": "Array of UTC times in HH:mm format when backup should run.",
|
||||
"minIntervalMinutes": "Minimum minutes between backup runs to prevent duplicate executions."
|
||||
},
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
249
examples/SchedulerTemplate.psm1
Normal file
249
examples/SchedulerTemplate.psm1
Normal file
@ -0,0 +1,249 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
SchedulerTemplate.psm1 - Scheduling + Lock + Interval + Callback Runner
|
||||
.DESCRIPTION
|
||||
Reusable PowerShell module for scheduled script execution with lock files,
|
||||
interval control, and credential management.
|
||||
.VERSION
|
||||
1.0.0
|
||||
.DATE
|
||||
2026-01-24
|
||||
.NOTES
|
||||
- Provides Write-Log function with timestamp and level support
|
||||
- Provides Get-CredentialFromEnvVar for secure credential retrieval
|
||||
- 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"
|
||||
|
||||
# Module load confirmation
|
||||
Write-Verbose "SchedulerTemplate.psm1 v$ModuleVersion loaded ($ModuleDate)"
|
||||
|
||||
function Write-Log {
|
||||
param(
|
||||
[string]$Message,
|
||||
[ValidateSet('Info', 'Success', 'Warning', 'Error')]
|
||||
[string]$Level = 'Info',
|
||||
[switch]$Automated
|
||||
)
|
||||
|
||||
$colors = @{
|
||||
'Info' = 'White'
|
||||
'Success' = 'Green'
|
||||
'Warning' = 'Yellow'
|
||||
'Error' = 'Red'
|
||||
}
|
||||
|
||||
if ($Automated) {
|
||||
# Service logger adds timestamps, so only include level and message
|
||||
Write-Output "[$Level] $Message"
|
||||
}
|
||||
else {
|
||||
# Manual execution: include timestamp for better tracking
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $colors[$Level]
|
||||
}
|
||||
}
|
||||
|
||||
function Get-CredentialFromEnvVar {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$EnvVarName,
|
||||
[switch]$Automated
|
||||
)
|
||||
|
||||
try {
|
||||
# Retrieve environment variable from Machine level
|
||||
$envVar = [System.Environment]::GetEnvironmentVariable($EnvVarName, "Machine")
|
||||
|
||||
if (-not $envVar) {
|
||||
Write-Log "Environment variable '$EnvVarName' not found at Machine level!" -Level Error -Automated:$Automated
|
||||
return $null
|
||||
}
|
||||
|
||||
# Decode Base64
|
||||
try {
|
||||
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($envVar))
|
||||
}
|
||||
catch {
|
||||
Write-Log "Failed to decode '$EnvVarName' as Base64. Expected format: Base64('username:password')" -Level Error -Automated:$Automated
|
||||
return $null
|
||||
}
|
||||
|
||||
# Split credentials
|
||||
$creds = $decoded -split ':', 2
|
||||
if ($creds.Count -ne 2) {
|
||||
Write-Log "Invalid credential format in '$EnvVarName'. Expected 'username:password'" -Level Error -Automated:$Automated
|
||||
return $null
|
||||
}
|
||||
|
||||
return @{
|
||||
Username = $creds[0]
|
||||
Password = $creds[1]
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log "Error retrieving credentials from '$EnvVarName': $_" -Level Error -Automated:$Automated
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
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 -Level Error
|
||||
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 -Level Success
|
||||
Write-Log "Current UTC Time: $now" -Automated:$Automated -Level Success
|
||||
|
||||
if (-not (Test-Schedule -DateTime $now -RunMonth $Config.RunMonth -RunWeekday $Config.RunWeekday -RunTime $Config.RunTime)) {
|
||||
Write-Log "Execution skipped due to schedule." -Automated:$Automated -Level Warning
|
||||
$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 -Level Warning
|
||||
$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 -Level Error
|
||||
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 -Level Error
|
||||
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 -Level Info
|
||||
}
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Main unified executor (callback-based)
|
||||
# ======================================================================
|
||||
function Invoke-ScheduledExecution {
|
||||
param(
|
||||
[scriptblock]$ScriptBlock,
|
||||
[hashtable]$Config,
|
||||
[switch]$Automated,
|
||||
[string]$CurrentDateTimeUtc
|
||||
)
|
||||
|
||||
# Get the calling script's path (not the module path)
|
||||
$scriptPath = (Get-PSCallStack)[1].ScriptName
|
||||
if (-not $scriptPath) {
|
||||
Write-Log "Unable to determine calling script path" -Level Error -Automated:$Automated
|
||||
return
|
||||
}
|
||||
|
||||
$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 -Level Warning
|
||||
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 * -Variable ModuleVersion, ModuleDate
|
||||
Loading…
Reference in New Issue
Block a user