#Requires -Version 5.1 #Requires -RunAsAdministrator <# .SYNOPSIS Creates a timestamped backup of the CI template VM directory. .DESCRIPTION Run before any template refresh (toolchain update, KMS re-activation, snapshot rebuild). The act_runner service is stopped for the duration so no vmrun clone races the file copy, then restarted automatically. Restore procedure (after a bad refresh): Stop-Service act_runner Remove-Item 'F:\CI\Templates\WinBuild' -Recurse -Force Copy-Item 'F:\CI\Backups\Template_' 'F:\CI\Templates\WinBuild' -Recurse Start-Service act_runner .PARAMETER TemplatePath Directory containing the template VM files (.vmx, .vmdk, snapshots). Default: F:\CI\Templates\WinBuild .PARAMETER BackupBaseDir Directory under which timestamped backup folders are created. Default: F:\CI\Backups .PARAMETER KeepCount Number of most-recent backups to retain. Older ones are deleted. Default: 3 .PARAMETER SkipRunnerStop Skip stopping/restarting act_runner. Use when the service is already stopped or when called from a maintenance window where the runner is offline. .EXAMPLE # Standard pre-refresh backup (run elevated) .\Backup-CITemplate.ps1 # Dry run to preview what would happen .\Backup-CITemplate.ps1 -WhatIf # Keep 5 backups .\Backup-CITemplate.ps1 -KeepCount 5 #> [CmdletBinding(SupportsShouldProcess)] param( [string] $TemplatePath = 'F:\CI\Templates\WinBuild', [string] $BackupBaseDir = 'F:\CI\Backups', [ValidateRange(1, 20)] [int] $KeepCount = 3, [switch] $SkipRunnerStop ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if (-not (Test-Path $TemplatePath -PathType Container)) { throw "Template directory not found: $TemplatePath" } $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $backupDest = Join-Path $BackupBaseDir "Template_$timestamp" if (-not (Test-Path $BackupBaseDir)) { New-Item -ItemType Directory -Path $BackupBaseDir -Force | Out-Null } # ── Step 1: Stop runner ────────────────────────────────────────────────────── $runnerWasRunning = $false if (-not $SkipRunnerStop) { $svc = Get-Service -Name act_runner -ErrorAction SilentlyContinue if ($svc -and $svc.Status -eq 'Running') { if ($PSCmdlet.ShouldProcess('act_runner', 'Stop service before backup')) { Write-Host "[Backup-CITemplate] Stopping act_runner..." Stop-Service -Name act_runner -Force $runnerWasRunning = $true Write-Host "[Backup-CITemplate] act_runner stopped." } } } try { # ── Step 2: Copy template directory ─────────────────────────────────────── if ($PSCmdlet.ShouldProcess($TemplatePath, "Copy to $backupDest")) { Write-Host "[Backup-CITemplate] Copying $TemplatePath -> $backupDest ..." $sw = [System.Diagnostics.Stopwatch]::StartNew() Copy-Item -Path $TemplatePath -Destination $backupDest -Recurse -Force $sw.Stop() # Size sum without Measure-Object.Sum (Set-StrictMode safe) $backupFiles = Get-ChildItem $backupDest -Recurse -File -ErrorAction SilentlyContinue $totalBytes = [long]0 if ($backupFiles) { foreach ($f in $backupFiles) { $totalBytes += $f.Length } } $sizeMB = [math]::Round($totalBytes / 1MB, 0) Write-Host "[Backup-CITemplate] Backup complete: $backupDest ($sizeMB MB, $($sw.Elapsed.TotalSeconds.ToString('F0'))s)" } # ── Step 3: Prune old backups ────────────────────────────────────────────── $existing = Get-ChildItem $BackupBaseDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -like 'Template_????????_??????' } | Sort-Object LastWriteTime -Descending if ($existing.Count -gt $KeepCount) { $toRemove = $existing | Select-Object -Skip $KeepCount foreach ($old in $toRemove) { if ($PSCmdlet.ShouldProcess($old.FullName, 'Remove old backup')) { Write-Host "[Backup-CITemplate] Pruning old backup: $($old.Name)" Remove-Item $old.FullName -Recurse -Force } } Write-Host "[Backup-CITemplate] Retained $KeepCount backup(s)." } } finally { # ── Step 4: Restart runner ──────────────────────────────────────────────── if ($runnerWasRunning) { if ($PSCmdlet.ShouldProcess('act_runner', 'Start service after backup')) { Write-Host "[Backup-CITemplate] Restarting act_runner..." Start-Service -Name act_runner $newStatus = (Get-Service -Name act_runner).Status Write-Host "[Backup-CITemplate] act_runner status: $newStatus" } } }