#Requires -Version 5.1 <# .SYNOPSIS SSH/SCP transport helpers for Linux build VMs. .DESCRIPTION SSH-only transport layer for the Linux branch of the CI/CD system. The existing WinRM transport (used by Windows VMs) is NOT modified. Exported functions: Invoke-SshCommand — run a command in a Linux guest via ssh.exe Copy-SshItem — copy files to/from a Linux guest via scp.exe Test-SshReady — poll until SSH is ready (port 22 open + login succeeds) Requirements: - ssh.exe and scp.exe in PATH (default on Windows 11 / Server 2022) - SSH private key at the path provided - Guest user must be set up for key-based auth (no passphrase) #> Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Invoke-SshCommand { <# .SYNOPSIS Run a command in a Linux guest via ssh.exe #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $IP, [string] $User = 'ci_build', [string] $KeyPath = 'F:\CI\keys\ci_linux', [Parameter(Mandatory)] [string] $Command, [int] $ConnectTimeout = 10, # Capture + return stdout/stderr instead of printing to console. [switch] $PassThru, # Don't throw on non-zero exit — just return the output. [switch] $AllowFail ) $sshArgs = @( '-i', $KeyPath, '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=NUL', '-o', "ConnectTimeout=$ConnectTimeout", '-o', 'BatchMode=yes', "$User@$IP", $Command ) if ($PassThru) { $output = & ssh @sshArgs 2>&1 $exit = $LASTEXITCODE } else { & ssh @sshArgs $exit = $LASTEXITCODE $output = @() } if ($exit -ne 0 -and -not $AllowFail) { throw "[SSH] Command failed (exit $exit): $Command" } return [pscustomobject]@{ ExitCode = [int]$exit Output = [string[]]$output } } function Copy-SshItem { <# .SYNOPSIS Copy files to/from Linux guest via scp.exe #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Source, [Parameter(Mandatory)] [string] $Destination, # Guest IP — used to build user@host prefix when Direction is set. [string] $IP = '', [string] $User = 'ci_build', [Parameter(Mandatory)] [string] $KeyPath, [ValidateSet('ToGuest', 'FromGuest', 'Direct')] [string] $Direction = 'Direct', # Pass -r to scp (recursive copy). [switch] $Recurse, [int] $ConnectTimeout = 10 ) if ($Direction -eq 'ToGuest') { $Destination = "$User@${IP}:$Destination" } elseif ($Direction -eq 'FromGuest') { $Source = "$User@${IP}:$Source" } # Direction = 'Direct': use Source + Destination as-is $scpArgs = @( '-i', $KeyPath, '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=NUL', '-o', "ConnectTimeout=$ConnectTimeout" ) if ($Recurse) { $scpArgs += '-r' } $scpArgs += @($Source, $Destination) & scp @scpArgs if ($LASTEXITCODE -ne 0) { throw "[SCP] Copy failed (exit $LASTEXITCODE): $Source -> $Destination" } } function Test-SshReady { <# .SYNOPSIS Poll until SSH port 22 is open and a login command succeeds. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $IP, [string] $User = 'ci_build', [string] $KeyPath = 'F:\CI\keys\ci_linux', [int] $TimeoutSec = 300, [int] $PollSec = 10 ) $deadline = (Get-Date).AddSeconds($TimeoutSec) # Phase 1: wait for port 22 to be open $portOpen = $false while (-not $portOpen -and (Get-Date) -lt $deadline) { $portOpen = (Test-NetConnection -ComputerName $IP -Port 22 ` -InformationLevel Quiet -WarningAction SilentlyContinue ` -ErrorAction SilentlyContinue) if (-not $portOpen) { Start-Sleep -Seconds $PollSec } } if (-not $portOpen) { throw "[Test-SshReady] Timed out after ${TimeoutSec}s waiting for SSH at $IP" } # Phase 2: try ssh echo — success when output contains 'ready' while ((Get-Date) -lt $deadline) { $result = Invoke-SshCommand -IP $IP -User $User -KeyPath $KeyPath ` -Command 'echo ready' -AllowFail -PassThru if ($result.ExitCode -eq 0 -and ($result.Output -join '') -like '*ready*') { return $true } Start-Sleep -Seconds $PollSec } throw "[Test-SshReady] Timed out after ${TimeoutSec}s waiting for SSH at $IP" } Export-ModuleMember -Function Invoke-SshCommand, Copy-SshItem, Test-SshReady