#Requires -Version 5.1 <# .SYNOPSIS HOST-side script: delivers and runs Setup-TemplateVM.ps1 inside the template VM. .DESCRIPTION Automates the template VM provisioning from the host machine: 1. Verifies the VM is reachable via WinRM (must already be enabled in guest) 2. Copies Setup-TemplateVM.ps1 into the VM via WinRM 3. Runs Setup-TemplateVM.ps1 inside the VM with the given parameters 4. Handles the reboot-required case (exit 3010) and optionally re-runs PREREQUISITES (do these manually before running this script): a. VM NIC is set to VMnet8 (NAT) — the VM needs internet for downloads b. Windows Server is installed and booted c. WinRM is enabled inside the VM. From an elevated cmd/PS in the VM run: winrm quickconfig -q Enable-PSRemoting -Force -SkipNetworkProfileCheck d. You know the IP address the VM got from DHCP on VMnet8 (NAT) (check via: ipconfig inside the VM, or VMware → VM → Settings → Network Adapter → Advanced → MAC address, then check DHCP leases) AFTER THIS SCRIPT COMPLETES: 1. Shut down the VM 2. Take snapshot: VM → Snapshot → Take Snapshot → name: BaseClean (VM stays on VMnet8 NAT — internet access is required for builds) 3. Store credentials on host (use the same password chosen during provisioning): New-StoredCredential -Target 'BuildVMGuest' -UserName 'ci_build' ` -Password '' -Persist LocalMachine .PARAMETER VMIPAddress IP address of the template VM while it is on NAT (VMnet8). Example: 192.168.79.130 .PARAMETER AdminUsername Administrator username inside the VM. Default: Administrator .PARAMETER AdminPassword Administrator password inside the VM. If not supplied, prompts interactively. .PARAMETER BuildPassword Password to set for the ci_build user inside the VM. Default: CIBuild!ChangeMe2026 — CHANGE THIS before production use. .PARAMETER DotNetSdkVersion .NET SDK channel to install in the VM. Default: 10.0 (matches host SDK). .PARAMETER SkipWindowsUpdate Pass through to Setup-TemplateVM.ps1 to skip the Windows Update step. .EXAMPLE # Interactive (prompts for admin password): .\Prepare-TemplateSetup.ps1 -VMIPAddress 192.168.79.130 # With explicit credentials: .\Prepare-TemplateSetup.ps1 -VMIPAddress 192.168.79.130 ` -AdminPassword 'YourAdminPass' -BuildPassword 'StrongCIPass!2026' # Skip Windows Update (already applied): .\Prepare-TemplateSetup.ps1 -VMIPAddress 192.168.79.130 -SkipWindowsUpdate #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^(\d{1,3}\.){3}\d{1,3}$')] [string] $VMIPAddress, [string] $AdminUsername = 'Administrator', [string] $AdminPassword = '', [string] $BuildPassword = '', [string] $DotNetSdkVersion = '10.0', [switch] $SkipWindowsUpdate ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path # Prompt for BuildPassword if not supplied — never use a hardcoded default. if ($BuildPassword -eq '') { Write-Host "[Prepare] No -BuildPassword supplied. Enter password for the CI build account inside the VM:" $secPwd = Read-Host -Prompt " BuildPassword" -AsSecureString $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secPwd) $BuildPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } # ── Build PSCredential ──────────────────────────────────────────────────────── if ($AdminPassword -eq '') { Write-Host "[Prepare] Enter administrator credentials for the template VM ($VMIPAddress):" $credential = Get-Credential -UserName $AdminUsername -Message "Template VM administrator ($VMIPAddress)" } else { $secPwd = ConvertTo-SecureString $AdminPassword -AsPlainText -Force $credential = New-Object System.Management.Automation.PSCredential($AdminUsername, $secPwd) } $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck # ── Configure HOST-side WinRM client (required for Basic auth over HTTP) ────── # AllowUnencrypted is needed for HTTP/5985 Basic auth. We save the prior value # and restore it in the finally block so this script doesn't permanently change # the host security posture. Write-Host "[Prepare] Configuring host WinRM client for unencrypted Basic auth (will be restored on exit)..." $prevAllowUnencrypted = $null $prevTrustedHosts = $null try { $prevAllowUnencrypted = (Get-Item WSMan:\localhost\Client\AllowUnencrypted -ErrorAction Stop).Value $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force -ErrorAction Stop # Add VM IP to TrustedHosts if not already present if ($prevTrustedHosts -ne '*' -and $prevTrustedHosts -notlike "*$VMIPAddress*") { $newTrusted = if ($prevTrustedHosts -eq '') { $VMIPAddress } else { "$prevTrustedHosts,$VMIPAddress" } Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newTrusted -Force -ErrorAction Stop } Write-Host "[Prepare] Host WinRM client configured." } catch { Write-Warning "[Prepare] Could not configure host WinRM client settings (need elevation?)." Write-Warning "Run once in an elevated PowerShell on the HOST:" Write-Warning " Set-Item WSMan:\localhost\Client\AllowUnencrypted `$true -Force" Write-Warning " Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force" Write-Warning "Then re-run this script." exit 1 } Write-Host "[Prepare] Testing WinRM connectivity to $VMIPAddress..." try { Test-WSMan -ComputerName $VMIPAddress -Credential $credential ` -Authentication Basic -ErrorAction Stop | Out-Null Write-Host "[Prepare] WinRM reachable." } catch { Write-Error @" [Prepare] Cannot reach $VMIPAddress via WinRM. Ensure: - The VM is running and on VMnet8 (NAT) with internet access - WinRM is enabled inside the VM (run as Administrator in VM): winrm quickconfig -q Enable-PSRemoting -Force -SkipNetworkProfileCheck Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force - Windows Firewall allows port 5985 inbound - The IP is correct (check ipconfig inside the VM) Error: $_ "@ exit 1 } # ── Open session ────────────────────────────────────────────────────────────── Write-Host "[Prepare] Opening PSSession..." $session = New-PSSession ` -ComputerName $VMIPAddress ` -Credential $credential ` -SessionOption $sessionOptions ` -Authentication Basic ` -ErrorAction Stop try { # ── Copy Setup-TemplateVM.ps1 into the VM ───────────────────────────────── $setupScript = Join-Path $scriptDir 'Setup-TemplateVM.ps1' $guestScriptPath = 'C:\CI\Setup-TemplateVM.ps1' Write-Host "[Prepare] Ensuring C:\CI exists on guest..." Invoke-Command -Session $session -ScriptBlock { if (-not (Test-Path 'C:\CI')) { New-Item -ItemType Directory -Path 'C:\CI' -Force | Out-Null } } Write-Host "[Prepare] Copying Setup-TemplateVM.ps1 -> guest $guestScriptPath ..." Copy-Item -Path $setupScript -Destination $guestScriptPath -ToSession $session -Force # ── Build argument list for Setup-TemplateVM.ps1 ───────────────────────── $setupArgs = @{ BuildPassword = $BuildPassword DotNetSdkVersion = $DotNetSdkVersion } if ($SkipWindowsUpdate) { $setupArgs['SkipWindowsUpdate'] = $true } # ── Run Setup-TemplateVM.ps1 inside the VM ──────────────────────────────── Write-Host "[Prepare] Running Setup-TemplateVM.ps1 inside VM (this will take a while)..." Write-Host "[Prepare] Output from guest:" Write-Host "------------------------------------------------------------" $result = Invoke-Command -Session $session -ScriptBlock { param($scriptPath, $scriptArgs) Set-ExecutionPolicy Bypass -Scope Process -Force $exitCode = 0 try { & $scriptPath @scriptArgs $exitCode = $LASTEXITCODE if ($null -eq $exitCode) { $exitCode = 0 } } catch { Write-Error $_ $exitCode = 1 } return $exitCode } -ArgumentList $guestScriptPath, $setupArgs Write-Host "------------------------------------------------------------" # ── Handle reboot required (exit 3010) ─────────────────────────────────── if ($result -eq 3010) { Write-Host "" Write-Warning "*** REBOOT REQUIRED after Windows Update. ***" Write-Warning " The VM will reboot. Wait for it to come back up, then run:" Write-Warning " .\Prepare-TemplateSetup.ps1 -VMIPAddress $VMIPAddress -SkipWindowsUpdate" # Reboot the VM Write-Host "[Prepare] Sending reboot command to VM..." Invoke-Command -Session $session -ScriptBlock { Restart-Computer -Force } exit 3010 } if ($result -ne 0) { throw "Setup-TemplateVM.ps1 exited with code $result" } # ── Success ─────────────────────────────────────────────────────────────── Write-Host "" Write-Host "============================================================" -ForegroundColor Green Write-Host " Template VM provisioning complete!" -ForegroundColor Green Write-Host "============================================================" -ForegroundColor Green Write-Host "" Write-Host " NOW DO THESE STEPS (manual):" Write-Host "" Write-Host " 1. Shut down the VM: Start -> Shut down" Write-Host " (VM stays on VMnet8 NAT for internet access during builds)" Write-Host "" Write-Host " 2. Take snapshot:" Write-Host " VM -> Snapshot -> Take Snapshot" Write-Host " Name it EXACTLY: BaseClean" Write-Host "" Write-Host " 3. Store CI credentials on this HOST:" Write-Host " (run in elevated PowerShell with CredentialManager module)" Write-Host " New-StoredCredential -Target 'BuildVMGuest' ``" Write-Host " -UserName 'ci_build' -Password '' ``" Write-Host " -Persist LocalMachine" Write-Host "" Write-Host " 4. Verify runner online in Gitea: http://10.10.20.11:3100" Write-Host " Admin -> Runners -> local-windows-runner -> should show Online" Write-Host "" } finally { Remove-PSSession $session -ErrorAction SilentlyContinue # Restore host WinRM client settings to their original values if ($null -ne $prevAllowUnencrypted) { Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnencrypted -Force -ErrorAction SilentlyContinue } if ($null -ne $prevTrustedHosts) { Set-Item WSMan:\localhost\Client\TrustedHosts -Value $prevTrustedHosts -Force -ErrorAction SilentlyContinue } Write-Host "[Prepare] Host WinRM client settings restored." }