From 08f7bb17573502761bdde71241bf5d1a8881c5f2 Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 11 May 2026 18:34:00 +0200 Subject: [PATCH] feat(linux): add SSH/SCP transport module (_Transport.psm1) Exported functions: - Invoke-SshCommand run a command on a Linux guest via ssh.exe (BatchMode=yes) - Copy-SshItem scp a local path to a remote path (or vice versa) - Test-SshReady test TCP:22 + ssh echo round-trip Used by Wait-VMReady.ps1 -Transport SSH, Invoke-RemoteBuild.ps1 -GuestOS Linux, Get-BuildArtifacts.ps1 -GuestOS Linux. --- scripts/_Transport.psm1 | 181 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 scripts/_Transport.psm1 diff --git a/scripts/_Transport.psm1 b/scripts/_Transport.psm1 new file mode 100644 index 0000000..76ee0d5 --- /dev/null +++ b/scripts/_Transport.psm1 @@ -0,0 +1,181 @@ +#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