#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"