Files
local-ci-cd-system/scripts/Wait-VMReady.ps1
T
Simone 9de86ac289 feat(sprint4): operational hardening, diagnostics, VMDK SHA256, runbook
Task 4.1: Invoke-CIJob.ps1 pre-clone disk gate (needs 10 GB free), JobId/ExtraGuestEnv
          pattern validation, redacted command logger (already included via _Common.psm1).
Task 4.2: Get-GuestDiagnostics helper invoked from catch block (best-effort, no throw).
Task 4.3: UseSharedCache wired through composite action and orchestrator.
Task 4.4: SSH known_hosts in F:\CI\State\known_hosts for CI jobs; accept-new for templates.
Task 4.5: Deploy-LinuxBuild2404.ps1 verifies Ubuntu VMDK SHA256 before import.
Task 4.6: RUNBOOK.md consolidated template-refresh section: stop runner, backup, boot,
          KMS reactivation, Prepare-*, shutdown, snapshot, Validate-DeployState, smoke,
          update config, retain prior snapshot 7 days, rollback procedure.
Validate-SetupState.ps1: minor lint fixes and docblock updates.
Wait-VMReady.ps1: Get-BuildArtifacts.ps1: hardening alignment with Sprint 4 objectives.
2026-05-12 21:12:51 +02:00

251 lines
9.9 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Waits until a build VM is fully ready to accept WinRM connections.
.DESCRIPTION
Performs a three-phase readiness check in a polling loop:
1. vmrun getState returns "running"
2. Network reachability (ICMP ping for WinRM; TCP port 22 check for SSH)
3. Transport-specific login: WinRM port 5986 (Windows) or SSH echo (Linux)
Throws if the VM does not become ready within the timeout period.
Transport modes:
WinRM (default) — existing Windows VM path: ping + WinRM port 5986.
SSH — Linux VM path: port 22 check + ssh echo test.
.PARAMETER VMPath
Full path to the clone VM's .vmx file.
.PARAMETER IPAddress
IP address of the VM on the build network (e.g. 192.168.11.101).
.PARAMETER TimeoutSeconds
Maximum seconds to wait for the VM to become ready.
Default: 300 (5 minutes)
.PARAMETER PollIntervalSeconds
Seconds between readiness checks.
Default: 5
.PARAMETER VmrunPath
Full path to vmrun.exe.
Default: C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe
.PARAMETER Transport
Transport protocol for readiness checks.
WinRM (default): checks ICMP ping + TCP 5986 (Windows VMs).
SSH: checks TCP 22 + ssh echo (Linux VMs).
.PARAMETER Credential
Optional PSCredential for Windows VMs. When supplied, Wait-VMReady performs
a real Invoke-Command { 'ready' } probe after TCP 5986 succeeds, confirming
that WinRM authentication (not just port) is working. Useful to detect the
window where the WinRM listener is up but the LocalSystem session is not yet
accepting logins. When omitted the TCP check alone is used (existing behaviour).
.PARAMETER SshKeyPath
Path to the SSH private key for key-based auth when Transport=SSH.
Default: F:\CI\keys\ci_linux
.PARAMETER SshUser
SSH username when Transport=SSH.
Default: ci_build
.EXAMPLE
.\Wait-VMReady.ps1 `
-VMPath "F:\CI\BuildVMs\Clone_run-42_20260508_143022\Clone_run-42_20260508_143022.vmx" `
-IPAddress "192.168.11.101"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $VMPath,
[Parameter(Mandatory)]
[ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')]
[string] $IPAddress,
[ValidateRange(3, 600)]
[int] $TimeoutSeconds = 300,
[ValidateRange(1, 30)]
[int] $PollIntervalSeconds = 5,
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe',
# Skip ICMP ping phase (phase 2). Useful when the guest firewall blocks ICMP
# but WinRM (TCP 5986) is open. Phase 1 (vmrun state) and phase 3 (WinRM)
# are still checked.
[switch] $SkipPing,
# Transport protocol for phase 2/3 checks.
# WinRM (default) = Windows VM; SSH = Linux VM.
[ValidateSet('WinRM', 'SSH')]
[string] $Transport = 'WinRM',
# SSH private key path (used when Transport=SSH).
[string] $SshKeyPath = 'F:\CI\keys\ci_linux',
# SSH username (used when Transport=SSH).
[string] $SshUser = 'ci_build',
# Optional credential for WinRM authentication probe (Transport=WinRM).
# When supplied, performs Invoke-Command { 'ready' } after TCP 5986 succeeds.
[PSCredential] $Credential = $null
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue' # suppress Write-Progress (Test-NetConnection, IWR, etc.)
if (-not (Test-Path $VmrunPath -PathType Leaf)) {
throw "vmrun.exe not found at: $VmrunPath"
}
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$attempt = 0
$phase = 'vmrun-state' # tracks current phase for informative output
$lastPhase = ''
Write-Host "[Wait-VMReady] Waiting for VM ready (timeout: ${TimeoutSeconds}s, IP: $IPAddress)..."
while ((Get-Date) -lt $deadline) {
$attempt++
# ── Phase 1: VM must be running ─────────────────────────────────────
# vmrun has no getState. We previously used `getGuestIPAddress`, but that
# requires VMware Tools to be up and responding inside the guest. In
# headless boots Tools can take 30-60s to come online, during which the
# call exits non-zero even though the VM is fully powered on. That caused
# Phase 1 to appear "stuck" until a GUI was opened manually.
# `vmrun list` reports actual power state without needing Tools.
$vmxNorm = (Resolve-Path -LiteralPath $VMPath -ErrorAction SilentlyContinue)
if ($null -ne $vmxNorm) { $vmxNorm = $vmxNorm.Path } else { $vmxNorm = $VMPath }
$listOut = & $VmrunPath list 2>&1
$isRunning = $false
foreach ($line in $listOut) {
if ([string]::Equals($line.Trim(), $vmxNorm, [System.StringComparison]::OrdinalIgnoreCase)) {
$isRunning = $true
break
}
}
if (-not $isRunning) {
if ($phase -ne 'vmrun-state') {
$phase = 'vmrun-state'
}
if ($lastPhase -ne $phase) {
Write-Host "[Wait-VMReady] Phase 1/3: waiting for VM to enter running state..."
$lastPhase = $phase
}
Start-Sleep -Seconds $PollIntervalSeconds
continue
}
# ── Phase 2: Network check ──────────────────────────────────────────
if ($Transport -eq 'SSH') {
$port22Open = Test-NetConnection -ComputerName $IPAddress -Port 22 `
-InformationLevel Quiet -WarningAction SilentlyContinue `
-ErrorAction SilentlyContinue
if (-not $port22Open) {
$phase = 'ssh-port'
if ($lastPhase -ne $phase) {
Write-Host "[Wait-VMReady] Phase 2/3: VM running, waiting for SSH port 22 at $IPAddress..."
$lastPhase = $phase
}
Start-Sleep -Seconds $PollIntervalSeconds
continue
}
}
elseif (-not $SkipPing) {
$pingOk = Test-Connection -ComputerName $IPAddress -Count 1 -Quiet -ErrorAction SilentlyContinue
if (-not $pingOk) {
$phase = 'ping'
if ($lastPhase -ne $phase) {
Write-Host "[Wait-VMReady] Phase 2/3: VM running, waiting for network (ping $IPAddress)..."
$lastPhase = $phase
}
Start-Sleep -Seconds $PollIntervalSeconds
continue
}
}
# ── Phase 3: Transport-specific login check ────────────────────────
if ($Transport -eq 'SSH') {
$sshArgs = @(
'-i', $SshKeyPath,
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=NUL',
'-o', 'ConnectTimeout=5',
'-o', 'BatchMode=yes',
"$SshUser@$IPAddress",
'echo ready'
)
# ssh writes diagnostic warnings (e.g. "Permanently added ... to known hosts")
# to stderr. Under $ErrorActionPreference='Stop' (set at script top),
# those stderr lines captured via 2>&1 are promoted to terminating errors
# and abort the script. Wrap the call to demote stderr to non-terminating.
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
$sshOut = & ssh @sshArgs 2>&1
$sshExit = $LASTEXITCODE
$ErrorActionPreference = $savedEap
if ($sshExit -eq 0 -and ($sshOut -join '') -like '*ready*') {
Write-Host "[Wait-VMReady] VM ready after ~$([int]($attempt * $PollIntervalSeconds))s (attempt $attempt)."
return
}
else {
$phase = 'ssh-echo'
if ($lastPhase -ne $phase) {
Write-Host "[Wait-VMReady] Phase 3/3: SSH port open, waiting for login ($SshUser@$IPAddress)..."
$lastPhase = $phase
}
Start-Sleep -Seconds $PollIntervalSeconds
continue
}
}
else {
# ── WinRM port 5986 accepting TCP connections ─────────────────────
# Test-WSMan has no -SessionOption in PS 5.1 and fails cert validation for
# self-signed certs. TCP open on 5986 = HTTPS listener up = VM ready.
$winrmUp = Test-NetConnection -ComputerName $IPAddress -Port 5986 `
-InformationLevel Quiet -WarningAction SilentlyContinue `
-ErrorAction SilentlyContinue
if ($winrmUp) {
# Optional auth probe — ensures WinRM login is functional, not just TCP open
if ($null -ne $Credential) {
try {
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$null = Invoke-Command -ComputerName $IPAddress -Credential $Credential `
-Authentication Basic -UseSSL -Port 5986 -SessionOption $so `
-ScriptBlock { 'ready' } -ErrorAction Stop
Write-Host "[Wait-VMReady] WinRM auth confirmed for $IPAddress."
} catch {
$phase = 'winrm-auth'
if ($lastPhase -ne $phase) {
Write-Host "[Wait-VMReady] Phase 3/3: WinRM port open, waiting for auth at $IPAddress..."
$lastPhase = $phase
}
Start-Sleep -Seconds $PollIntervalSeconds
continue
}
}
Write-Host "[Wait-VMReady] VM ready after ~$([int]($attempt * $PollIntervalSeconds))s (attempt $attempt)."
return
}
else {
$phase = 'winrm'
if ($lastPhase -ne $phase) {
Write-Host "[Wait-VMReady] Phase 3/3: ping OK, waiting for WinRM on port 5986 (HTTPS)..."
$lastPhase = $phase
}
Start-Sleep -Seconds $PollIntervalSeconds
continue
}
}
}
throw "[Wait-VMReady] Timed out after ${TimeoutSeconds}s waiting for VM at $IPAddress to become ready. Last phase: $phase"