Files
local-ci-cd-system/scripts/_Transport.psm1
T
Simone 509d1fc284 feat(sprint3): Invoke-VmrunBounded, Get-GuestIPAddress, transport and concurrency hardening
H4: Invoke-VmrunBounded added to _Common.psm1 (Start-Process + Wait-Process + taskkill on
    timeout, returns {ExitCode;Output;TimedOut;ElapsedSeconds}). Wired in Invoke-CIJob.ps1
    for vmrun start (180s) and Remove-BuildVM.ps1 for stop/deleteVM (60s/90s).
H5: Cleanup-OrphanedBuildVMs.ps1 removes stale vm-start.lock (>30 min). ValidateRange
    lowered to 0 to allow emergency -MaxAgeHours 0. Invoke-RetentionPolicy.ps1 same.
H8: _Transport.psm1 SSH hardening: StrictHostKeyChecking=accept-new, F:\CI\State\known_hosts.
H9: Validate-DeployState.ps1 Linux branch added: checks ci-report-ip.service enabled,
    ci-report-ip.sh executable via SSH. New params: -GuestOS, -SshKeyPath, -SshUser.
H12: Get-GuestIPAddress extracted into _Common.psm1 (guestinfo.ci-ip primary, getGuestIPAddress
     fallback). Invoke-CIJob.ps1 Phase 3b replaced with single call. Measure-CIBenchmark.ps1
     updated to match.
Also: _Common.psm1 adds Write-CIRedactedCommand and ConvertTo-SafeShellSingleQuotedString.
     Pester suite updated: 30/30 pass.
2026-05-12 21:12:33 +02:00

202 lines
5.8 KiB
PowerShell

#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'
$ProgressPreference = 'SilentlyContinue' # suppress Write-Progress (Test-NetConnection, IWR, etc.)
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,
# Persistent known_hosts file for CI jobs.
# When non-empty: StrictHostKeyChecking=accept-new + this file.
# When empty (default): permissive mode (StrictHostKeyChecking=no, UserKnownHostsFile=NUL)
# — used during template provisioning when host key is not yet known.
[string] $KnownHostsFile = '',
# Capture + return stdout/stderr instead of printing to console.
[switch] $PassThru,
# Don't throw on non-zero exit — just return the output.
[switch] $AllowFail
)
if ($KnownHostsFile -ne '') {
$sshHostKeyOpts = @('-o', 'StrictHostKeyChecking=accept-new', '-o', "UserKnownHostsFile=$KnownHostsFile")
} else {
$sshHostKeyOpts = @('-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=NUL')
}
$sshArgs = @(
'-i', $KeyPath
) + $sshHostKeyOpts + @(
'-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,
# Persistent known_hosts file for CI jobs.
# When non-empty: StrictHostKeyChecking=accept-new + this file.
# When empty (default): permissive mode — used during template provisioning.
[string] $KnownHostsFile = ''
)
if ($Direction -eq 'ToGuest') {
$Destination = "$User@${IP}:$Destination"
}
elseif ($Direction -eq 'FromGuest') {
$Source = "$User@${IP}:$Source"
}
# Direction = 'Direct': use Source + Destination as-is
if ($KnownHostsFile -ne '') {
$scpHostKeyOpts = @('-o', 'StrictHostKeyChecking=accept-new', '-o', "UserKnownHostsFile=$KnownHostsFile")
} else {
$scpHostKeyOpts = @('-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=NUL')
}
$scpArgs = @(
'-i', $KeyPath
) + $scpHostKeyOpts + @(
'-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