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.
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user