9fcc46afd6
Deploy-LinuxBuild2404.ps1 (host-side):
- Downloads Ubuntu 24.04 cloud VMDK (cached in F:\CI\ISO\)
- Generates cloud-init seed ISO (nocloud + guestinfo backup datasource)
- user-data: user+SSH key only no packages/runcmd, all provisioning
delegated to Prepare/Setup (removed -MinimalConfig, now always minimal)
- Assembles VMX, powers on VM, waits for cloud-init completion via SSH
- Removes seed ISO from VMX after first boot (ide1:0 disabled)
- Next-steps message uses correct -VMIPAddress parameter
Prepare-LinuxBuild2404.ps1 (host-side):
- Powers on template VM, waits for SSH readiness
- Copies Setup-LinuxBuild2404.sh into guest, runs it with sudo
- Validates setup result (all tools in PATH, dirs, sshd, swap off)
- Takes snapshot BaseClean-Linux (cold, VM shut down before snapshot)
- -NoMingw switch to skip mingw-w64 cross-compiler installation
Setup-LinuxBuild2404.sh (guest-side):
- apt update/upgrade, build-essential, gcc, g++, clang, cmake, ninja
- python3, pip3, venv; git, p7zip-full, curl, wget, ca-certificates
- Optional .NET SDK (--dotnet), mingw-w64 cross-compiler (--no-mingw to skip)
- /opt/ci/{build,output,scripts,cache} dirs (ci_build ownership)
- SSH hardening (PasswordAuthentication no, PubkeyAuthentication yes)
- Swap disable (swapoff -a + fstab), MAC-agnostic netplan for linked clones
- ci-report-ip systemd service (guestinfo.ci-ip reporter)
- apt auto-update disabled; full validation gate at end
437 lines
20 KiB
PowerShell
437 lines
20 KiB
PowerShell
#Requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
HOST-side script: delivers and runs Setup-LinuxBuild2404.sh inside the Linux template VM.
|
|
|
|
.DESCRIPTION
|
|
Automates provisioning of the Ubuntu 24.04 template VM from the host machine via SSH/SCP:
|
|
1. Pre-flight validation: ssh/scp in PATH, SSH key present, VMX exists (if supplied),
|
|
IPv4 address valid, TCP/22 reachable
|
|
2. Auto power-on + guest IP detection via vmrun getGuestIPAddress (when -VMXPath provided)
|
|
3. Add host key to known_hosts (idempotent, uses StrictHostKeyChecking=accept-new)
|
|
4. Copy Setup-LinuxBuild2404.sh to /tmp on guest via scp + validate file size
|
|
5. Execute Setup-LinuxBuild2404.sh via SSH (stdout/stderr streamed to host console)
|
|
6. Post-setup remote validation batch check (user, sudo, tools, CI dirs, swap, sshd)
|
|
7. Graceful shutdown + vmrun snapshot (when -TakeSnapshot or -VMXPath provided)
|
|
Final: print summary
|
|
|
|
Every validation step uses Assert-Step: prints [OK] on success, throws on failure.
|
|
The snapshot is only taken after this script exits 0.
|
|
|
|
PREREQUISITES (do these manually before running this script):
|
|
a. The VM was created by Deploy-LinuxBuild2404.ps1 and cloud-init has completed
|
|
b. The ci_build SSH public key ($SshKeyPath.pub) is already authorised in the guest
|
|
(cloud-init in Deploy-LinuxBuild2404.ps1 injects the public key automatically)
|
|
c. The VM NIC is on VMnet8 (NAT) with internet access
|
|
|
|
AFTER THIS SCRIPT COMPLETES (exit 0, all validations passed):
|
|
If -TakeSnapshot was NOT set and -VMXPath was NOT provided:
|
|
1. Shut down the VM manually
|
|
2. Take a snapshot named '$SnapshotName' in VMware Workstation
|
|
3. Use -SnapshotName '$SnapshotName' when calling New-BuildVM.ps1
|
|
|
|
.PARAMETER VMXPath
|
|
Full path to the template VM's .vmx file.
|
|
When provided:
|
|
- If the VM is powered off, the script starts it automatically via vmrun.
|
|
- The guest IP is auto-detected via vmrun getGuestIPAddress (requires open-vm-tools).
|
|
- At the end, the VM is shut down and a snapshot is taken automatically.
|
|
Either -VMXPath or -VMIPAddress (or both) must be supplied.
|
|
|
|
.PARAMETER VMIPAddress
|
|
IP address of the template VM on VMnet8 NAT.
|
|
Optional when -VMXPath is provided (IP is auto-detected via open-vm-tools).
|
|
Required when -VMXPath is omitted.
|
|
Example: 192.168.79.131
|
|
|
|
.PARAMETER CloudUser
|
|
SSH username in the guest VM. Default: ci_build.
|
|
|
|
.PARAMETER SshKeyPath
|
|
Path to the SSH private key on the host. Default: F:\CI\keys\ci_linux.
|
|
The corresponding public key is expected at $SshKeyPath.pub.
|
|
|
|
.PARAMETER SnapshotName
|
|
Name of the snapshot to create. Default: BaseClean-Linux.
|
|
Must match the snapshot name used in New-BuildVM.ps1.
|
|
|
|
.PARAMETER SkipUpdate
|
|
Pass --skip-update to Setup-LinuxBuild2404.sh (skips apt-get upgrade).
|
|
|
|
.PARAMETER InstallDotNet
|
|
Pass --dotnet to Setup-LinuxBuild2404.sh (installs .NET SDK).
|
|
|
|
.PARAMETER NoMingw
|
|
Pass --no-mingw to Setup-LinuxBuild2404.sh (skips MinGW cross-compiler).
|
|
MinGW is installed by default; use this only to skip it.
|
|
|
|
.PARAMETER TakeSnapshot
|
|
Shut down the VM and take a vmrun snapshot after provisioning completes.
|
|
Implied automatically when -VMXPath is provided (snapshot is always taken
|
|
when the script manages the VM lifecycle via vmrun).
|
|
|
|
.PARAMETER VMwareWorkstationDir
|
|
Installation directory of VMware Workstation on the host.
|
|
Default: C:\Program Files (x86)\VMware\VMware Workstation
|
|
|
|
.EXAMPLE
|
|
# Fully automated — VMX path only (power-on + IP detection + snapshot automatic):
|
|
.\Prepare-LinuxBuild2404.ps1 -VMXPath 'F:\CI\Templates\LinuxBuild2404\LinuxBuild2404.vmx'
|
|
|
|
# Explicit IP, skip update (VM already running, no snapshot):
|
|
.\Prepare-LinuxBuild2404.ps1 -VMIPAddress '192.168.79.131' -SkipUpdate
|
|
|
|
# VMX + .NET SDK + explicit snapshot:
|
|
.\Prepare-LinuxBuild2404.ps1 `
|
|
-VMXPath 'F:\CI\Templates\LinuxBuild2404\LinuxBuild2404.vmx' `
|
|
-InstallDotNet -TakeSnapshot
|
|
|
|
.NOTES
|
|
Requires:
|
|
- OpenSSH client on the host (ssh.exe + scp.exe in PATH)
|
|
- VMware Workstation with vmrun.exe (only needed when -VMXPath is supplied)
|
|
- open-vm-tools installed in the guest (for vmrun getGuestIPAddress)
|
|
- The SSH private key at $SshKeyPath with correct permissions
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
# Path to the VM .vmx — enables auto power-on + IP detection + auto snapshot.
|
|
# Either -VMXPath or -VMIPAddress (or both) must be supplied.
|
|
[string] $VMXPath = '',
|
|
|
|
# Guest IP on VMnet8 NAT. Optional when -VMXPath provided (auto-detected via open-vm-tools).
|
|
[string] $VMIPAddress = '',
|
|
|
|
[string] $CloudUser = 'ci_build',
|
|
|
|
[string] $SshKeyPath = 'F:\CI\keys\ci_linux',
|
|
|
|
# Snapshot name (case-sensitive — must match the name used in New-BuildVM.ps1)
|
|
[string] $SnapshotName = 'BaseClean-Linux',
|
|
|
|
[switch] $SkipUpdate,
|
|
|
|
[switch] $InstallDotNet,
|
|
|
|
[switch] $NoMingw,
|
|
|
|
# Shut down VM and take snapshot after provisioning (implied when -VMXPath is provided)
|
|
[switch] $TakeSnapshot,
|
|
|
|
[string] $VMwareWorkstationDir = 'C:\Program Files (x86)\VMware\VMware Workstation'
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# ── Locate vmrun.exe ──────────────────────────────────────────────────────────
|
|
$vmrunExe = Join-Path $VMwareWorkstationDir 'vmrun.exe'
|
|
if (-not (Test-Path $vmrunExe)) {
|
|
# Fallback: try the 64-bit install path or PATH
|
|
$alt64 = 'C:\Program Files\VMware\VMware Workstation\vmrun.exe'
|
|
if (Test-Path $alt64) {
|
|
$vmrunExe = $alt64
|
|
} else {
|
|
$vmrunInPath = Get-Command vmrun -ErrorAction SilentlyContinue
|
|
if ($vmrunInPath) {
|
|
$vmrunExe = $vmrunInPath.Source
|
|
} else {
|
|
throw "[Prepare] vmrun.exe not found at '$vmrunExe'. Install VMware Workstation or add it to PATH."
|
|
}
|
|
}
|
|
}
|
|
|
|
# ── Validate input: need VMXPath or VMIPAddress ───────────────────────────────
|
|
if ($VMXPath -eq '' -and $VMIPAddress -eq '') {
|
|
throw "[Prepare] Supply -VMXPath (auto power-on + IP detection) or -VMIPAddress (or both)."
|
|
}
|
|
|
|
# ── Auto power-on + IP detection (when VMXPath provided) ─────────────────────
|
|
if ($VMXPath -ne '') {
|
|
if (-not (Test-Path $VMXPath)) { throw "[Prepare] VMX not found: '$VMXPath'" }
|
|
|
|
$runningList = @(& $vmrunExe list 2>&1 |
|
|
Where-Object { $_ -match '\.vmx$' } |
|
|
ForEach-Object { $_.Trim() })
|
|
|
|
if ($runningList -notcontains $VMXPath) {
|
|
Write-Host "[Prepare] VM not running — starting: $VMXPath"
|
|
& $vmrunExe start $VMXPath nogui
|
|
if ($LASTEXITCODE -ne 0) { throw "[Prepare] vmrun start failed (exit $LASTEXITCODE)." }
|
|
Write-Host "[Prepare] VM started. Waiting for guest OS to initialize..."
|
|
} else {
|
|
Write-Host "[Prepare] VM already running: $VMXPath"
|
|
}
|
|
|
|
if ($VMIPAddress -eq '') {
|
|
Write-Host "[Prepare] Detecting guest IP via open-vm-tools (getGuestIPAddress -wait)..."
|
|
$detectedIP = (& $vmrunExe getGuestIPAddress $VMXPath -wait 2>&1).Trim()
|
|
if ($detectedIP -notmatch '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') {
|
|
throw "[Prepare] Could not get guest IP. Got: '$detectedIP'. Ensure open-vm-tools is running in the guest."
|
|
}
|
|
$VMIPAddress = $detectedIP
|
|
Write-Host "[Prepare] Guest IP: $VMIPAddress" -ForegroundColor Green
|
|
}
|
|
}
|
|
|
|
# ── Helper functions ──────────────────────────────────────────────────────────
|
|
|
|
function Assert-Step {
|
|
param(
|
|
[Parameter(Mandatory)] [string] $StepName,
|
|
[Parameter(Mandatory)] [string] $Description,
|
|
[Parameter(Mandatory)] [scriptblock] $Test
|
|
)
|
|
$ok = $false
|
|
try { $ok = [bool](& $Test) }
|
|
catch { throw "[Prepare/$StepName] Validation threw on '$Description': $_" }
|
|
if (-not $ok) { throw "[Prepare/$StepName] Validation failed: $Description" }
|
|
Write-Host " [OK] $Description" -ForegroundColor Green
|
|
}
|
|
|
|
function Invoke-SshCommand {
|
|
param(
|
|
[Parameter(Mandatory)] [string] $IP,
|
|
[Parameter(Mandatory)] [string] $User,
|
|
[Parameter(Mandatory)] [string] $KeyPath,
|
|
[Parameter(Mandatory)] [string] $Command,
|
|
[switch] $PassThru # return stdout instead of printing
|
|
)
|
|
$sshArgs = @(
|
|
'-i', $KeyPath,
|
|
'-o', 'StrictHostKeyChecking=no',
|
|
'-o', 'UserKnownHostsFile=NUL',
|
|
'-o', 'LogLevel=ERROR',
|
|
'-o', 'ConnectTimeout=10',
|
|
'-o', 'BatchMode=yes',
|
|
"$User@$IP",
|
|
$Command
|
|
)
|
|
if ($PassThru) {
|
|
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
|
|
$out = & ssh @sshArgs 2>&1
|
|
$sshRc = $LASTEXITCODE
|
|
$ErrorActionPreference = $savedEap
|
|
if ($sshRc -ne 0) { throw "[SSH] Command failed (exit $sshRc): $Command`nOutput: $out" }
|
|
return ($out | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] })
|
|
} else {
|
|
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
|
|
& ssh @sshArgs
|
|
$sshRc = $LASTEXITCODE
|
|
$ErrorActionPreference = $savedEap
|
|
if ($sshRc -ne 0) { throw "[SSH] Command failed (exit $sshRc): $Command" }
|
|
}
|
|
}
|
|
|
|
# ── Step 1 — Pre-flight validation ───────────────────────────────────────────
|
|
Write-Host "[Prepare] Step 1 — Pre-flight validation"
|
|
|
|
Assert-Step 'PreFlight' 'ssh.exe found in PATH' {
|
|
$null -ne (Get-Command ssh -ErrorAction SilentlyContinue)
|
|
}
|
|
Assert-Step 'PreFlight' 'scp.exe found in PATH' {
|
|
$null -ne (Get-Command scp -ErrorAction SilentlyContinue)
|
|
}
|
|
Assert-Step 'PreFlight' "SSH private key exists: $SshKeyPath" {
|
|
Test-Path $SshKeyPath -PathType Leaf
|
|
}
|
|
Assert-Step 'PreFlight' "SSH public key exists: $SshKeyPath.pub" {
|
|
Test-Path "$SshKeyPath.pub" -PathType Leaf
|
|
}
|
|
if ($VMXPath -ne '') {
|
|
Assert-Step 'PreFlight' "VMX file exists: $VMXPath" {
|
|
Test-Path $VMXPath -PathType Leaf
|
|
}
|
|
}
|
|
|
|
# ── Step 2 — Validate IP address ─────────────────────────────────────────────
|
|
Write-Host "[Prepare] Step 2 — Validate guest IP address"
|
|
Assert-Step 'IPValidation' "VMIPAddress '$VMIPAddress' matches IPv4 pattern" {
|
|
$VMIPAddress -match '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
|
|
}
|
|
Write-Host " Guest IP: $VMIPAddress"
|
|
|
|
# ── Step 3 — Wait for SSH port 22 ────────────────────────────────────────────
|
|
Write-Host "[Prepare] Step 3 — Waiting for SSH port 22 on $VMIPAddress (timeout 180 s)..."
|
|
$sshDeadline = (Get-Date).AddSeconds(180)
|
|
$sshReady = $false
|
|
while ((Get-Date) -lt $sshDeadline) {
|
|
if (Test-NetConnection -ComputerName $VMIPAddress -Port 22 `
|
|
-InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) {
|
|
$sshReady = $true
|
|
break
|
|
}
|
|
Start-Sleep -Seconds 10
|
|
Write-Host " ... waiting for SSH..."
|
|
}
|
|
Assert-Step 'SSHPort' "$VMIPAddress TCP/22 reachable" { $sshReady }
|
|
|
|
# ── Step 4 — Add host key to known_hosts ─────────────────────────────────────
|
|
Write-Host "[Prepare] Step 4 — Adding host key to known_hosts (StrictHostKeyChecking=accept-new)..."
|
|
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
|
|
& ssh -i $SshKeyPath -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL -o LogLevel=ERROR -o ConnectTimeout=10 `
|
|
-o BatchMode=yes "$CloudUser@$VMIPAddress" 'echo connected' 2>&1 | Out-Null
|
|
$ErrorActionPreference = $savedEap
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "[Prepare Step 4] SSH connectivity test failed (exit $LASTEXITCODE). Check key, user, and IP."
|
|
}
|
|
Write-Host " [OK] SSH connectivity confirmed, host key added."
|
|
|
|
# ── Step 5 — Copy Setup-LinuxBuild2404.sh into VM ────────────────────────────
|
|
Write-Host "[Prepare] Step 5 — Copying Setup-LinuxBuild2404.sh to guest..."
|
|
|
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
$setupScript = Join-Path $scriptDir 'Setup-LinuxBuild2404.sh'
|
|
$guestScript = '/tmp/Setup-LinuxBuild2404.sh'
|
|
|
|
if (-not (Test-Path $setupScript -PathType Leaf)) {
|
|
throw "[Prepare Step 5] Setup-LinuxBuild2404.sh not found alongside this script: $setupScript"
|
|
}
|
|
|
|
# Strip UTF-8 BOM and normalize CRLF→LF before copying.
|
|
# PS 5.1 / VS Code on Windows may save .sh files with BOM or CRLF; both break bash.
|
|
$shBytes = [System.IO.File]::ReadAllBytes($setupScript)
|
|
if ($shBytes.Length -ge 3 -and $shBytes[0] -eq 0xEF -and $shBytes[1] -eq 0xBB -and $shBytes[2] -eq 0xBF) {
|
|
Write-Host " [WARN] UTF-8 BOM detected in Setup-LinuxBuild2404.sh — stripping before copy"
|
|
$shBytes = $shBytes[3..($shBytes.Length - 1)]
|
|
}
|
|
$shText = [System.Text.Encoding]::UTF8.GetString($shBytes) -replace "`r`n", "`n" -replace "`r", "`n"
|
|
$tmpScript = Join-Path $env:TEMP 'Setup-LinuxBuild2404-ci.sh'
|
|
[System.IO.File]::WriteAllText($tmpScript, $shText, [System.Text.UTF8Encoding]::new($false))
|
|
$setupScript = $tmpScript
|
|
|
|
Write-Host " Source: $setupScript (BOM/CRLF-cleaned)"
|
|
Write-Host " Dest: $guestScript"
|
|
|
|
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
|
|
& scp -i $SshKeyPath -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL -o LogLevel=ERROR `
|
|
$setupScript "${CloudUser}@${VMIPAddress}:${guestScript}" 2>&1 | Out-Null
|
|
$scpRc = $LASTEXITCODE
|
|
$ErrorActionPreference = $savedEap
|
|
if ($scpRc -ne 0) { throw "[Prepare Step 5] scp failed (exit $scpRc)." }
|
|
|
|
# Validate: compare local size vs remote size (allow +-2 bytes for LF/CRLF variance)
|
|
$localSize = (Get-Item $setupScript).Length
|
|
$remoteSizeRaw = (Invoke-SshCommand -IP $VMIPAddress -User $CloudUser -KeyPath $SshKeyPath `
|
|
-Command "stat -c %s $guestScript" -PassThru).Trim()
|
|
$remoteSize = [int]$remoteSizeRaw
|
|
|
|
Assert-Step 'FileCopy' "guest script size matches local ($localSize bytes, remote $remoteSize bytes, tolerance 2)" {
|
|
[Math]::Abs($remoteSize - $localSize) -le 2
|
|
}
|
|
|
|
# ── Step 6 — Execute setup script ────────────────────────────────────────────
|
|
Write-Host "[Prepare] Step 6 — Running Setup-LinuxBuild2404.sh in guest (may take several minutes)..."
|
|
|
|
$setupFlags = [System.Collections.Generic.List[string]]::new()
|
|
if ($SkipUpdate) { $setupFlags.Add('--skip-update') }
|
|
if ($InstallDotNet) { $setupFlags.Add('--dotnet') }
|
|
if ($NoMingw) { $setupFlags.Add('--no-mingw') }
|
|
$flagsStr = $setupFlags -join ' '
|
|
$remoteCmd = "chmod +x $guestScript && sudo $guestScript $flagsStr".TrimEnd()
|
|
|
|
Write-Host " Command: $remoteCmd"
|
|
Write-Host "------------------------------------------------------------"
|
|
|
|
& ssh -i $SshKeyPath -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL -o ConnectTimeout=5 `
|
|
-o BatchMode=yes "$CloudUser@$VMIPAddress" $remoteCmd
|
|
$setupExitCode = $LASTEXITCODE
|
|
|
|
Write-Host "------------------------------------------------------------"
|
|
if ($setupExitCode -ne 0) {
|
|
throw "[Prepare Step 6] Setup-LinuxBuild2404.sh exited $setupExitCode — see output above."
|
|
}
|
|
Write-Host "[Prepare] Setup script completed (exit 0)." -ForegroundColor Green
|
|
|
|
# ── Step 7 — Post-setup remote validation ────────────────────────────────────
|
|
Write-Host "[Prepare] Step 7 — Running post-setup validation..."
|
|
|
|
$validationScript = @'
|
|
set -e
|
|
( id ci_build | grep -q sudo || sudo -l -U ci_build 2>/dev/null | grep -q NOPASSWD ) && echo "UserSudo:OK" || echo "UserSudo:FAIL"
|
|
sudo -n true 2>/dev/null && echo "SudoNoPass:OK" || echo "SudoNoPass:FAIL"
|
|
for t in gcc g++ clang cmake python3 git 7z; do command -v "$t" > /dev/null && echo "Tool:$t:OK" || echo "Tool:$t:FAIL"; done
|
|
test -d /opt/ci/build && echo "Dir:build:OK" || echo "Dir:build:FAIL"
|
|
test -d /opt/ci/output && echo "Dir:output:OK" || echo "Dir:output:FAIL"
|
|
test -d /opt/ci/scripts && echo "Dir:scripts:OK" || echo "Dir:scripts:FAIL"
|
|
swapon --show | grep -q . && echo "Swap:ACTIVE:FAIL" || echo "Swap:off:OK"
|
|
systemctl is-active ssh > /dev/null && echo "SSHD:active:OK" || echo "SSHD:inactive:FAIL"
|
|
grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config && echo "SSHPwdAuth:disabled:OK" || echo "SSHPwdAuth:enabled:FAIL"
|
|
systemctl is-enabled ci-report-ip.service > /dev/null 2>&1 && echo "CIReportIP:enabled:OK" || echo "CIReportIP:enabled:FAIL"
|
|
test -x /usr/local/bin/ci-report-ip.sh && echo "CIReportIPScript:exists:OK" || echo "CIReportIPScript:exists:FAIL"
|
|
'@
|
|
|
|
$results = Invoke-SshCommand -IP $VMIPAddress -User $CloudUser -KeyPath $SshKeyPath `
|
|
-Command $validationScript -PassThru
|
|
|
|
Write-Host "[Prepare] Validation results:"
|
|
$results | ForEach-Object { Write-Host " $_" }
|
|
|
|
$failures = @($results | Where-Object { $_ -match ':FAIL' })
|
|
if ($failures.Count -gt 0) {
|
|
throw "[Prepare Step 7] Post-setup validation failed:`n$($failures -join "`n")"
|
|
}
|
|
Write-Host "[Prepare] All post-setup checks passed." -ForegroundColor Green
|
|
|
|
# ── Step 8 — Shutdown + snapshot ─────────────────────────────────────────────
|
|
if ($TakeSnapshot -or $VMXPath -ne '') {
|
|
Write-Host "[Prepare] Step 8 — Sending graceful shutdown to guest..."
|
|
try {
|
|
Invoke-SshCommand -IP $VMIPAddress -User $CloudUser -KeyPath $SshKeyPath `
|
|
-Command 'sudo shutdown -h now' -PassThru | Out-Null
|
|
} catch {
|
|
Write-Warning "[Prepare] Shutdown command may have failed (VM may have already started shutting down): $_"
|
|
}
|
|
|
|
if ($VMXPath -ne '') {
|
|
Write-Host "[Prepare] Waiting for VM to power off (timeout 120 s)..."
|
|
$deadline = (Get-Date).AddSeconds(120)
|
|
$poweredOff = $false
|
|
while ((Get-Date) -lt $deadline) {
|
|
$listOut = & $vmrunExe list 2>&1 | Out-String
|
|
if ($listOut -notmatch [regex]::Escape($VMXPath)) {
|
|
$poweredOff = $true
|
|
break
|
|
}
|
|
Start-Sleep -Seconds 5
|
|
Write-Host " ... waiting for shutdown..."
|
|
}
|
|
if (-not $poweredOff) { throw "[Prepare Step 8] VM did not power off within 120 s." }
|
|
|
|
# Delete existing snapshot with the same name so vmrun can create a fresh one.
|
|
$existingSnaps = (& $vmrunExe listSnapshots $VMXPath 2>&1 | Out-String)
|
|
if ($existingSnaps -match [regex]::Escape($SnapshotName)) {
|
|
Write-Host "[Prepare] Snapshot '$SnapshotName' already exists — deleting before recreating..."
|
|
& $vmrunExe deleteSnapshot $VMXPath $SnapshotName
|
|
if ($LASTEXITCODE -ne 0) { throw "[Prepare Step 8] vmrun deleteSnapshot failed (exit $LASTEXITCODE)." }
|
|
Write-Host "[Prepare] Old snapshot deleted."
|
|
}
|
|
|
|
Write-Host "[Prepare] Taking snapshot '$SnapshotName'..."
|
|
& $vmrunExe snapshot $VMXPath $SnapshotName
|
|
if ($LASTEXITCODE -ne 0) { throw "[Prepare Step 8] vmrun snapshot failed (exit $LASTEXITCODE)." }
|
|
|
|
Assert-Step 'Snapshot' "snapshot '$SnapshotName' listed in vmrun listSnapshots" {
|
|
$snaps = (& $vmrunExe listSnapshots $VMXPath 2>&1 | Out-String)
|
|
$snaps -match [regex]::Escape($SnapshotName)
|
|
}
|
|
Write-Host "[Prepare] Snapshot '$SnapshotName' created." -ForegroundColor Green
|
|
} else {
|
|
Write-Host "[Prepare] -VMXPath not provided — snapshot skipped. Shut down and snapshot manually."
|
|
}
|
|
} else {
|
|
Write-Host "[Prepare] -TakeSnapshot not set — leaving VM running. Shut down and snapshot manually when ready."
|
|
}
|
|
|
|
# ── Final — Print summary ─────────────────────────────────────────────────────
|
|
Write-Host ""
|
|
Write-Host "[Prepare] Summary" -ForegroundColor Cyan
|
|
Write-Host " Guest IP: $VMIPAddress"
|
|
Write-Host " SSH key: $SshKeyPath"
|
|
if ($VMXPath -ne '') { Write-Host " VMX: $VMXPath" }
|
|
if ($TakeSnapshot -or $VMXPath -ne '') {
|
|
Write-Host " Snapshot: $SnapshotName — ready for New-BuildVM.ps1 -SnapshotName '$SnapshotName'"
|
|
} else {
|
|
Write-Host " Snapshot: not taken — run manually then use -SnapshotName '$SnapshotName'"
|
|
}
|