509d1fc284
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.
202 lines
5.8 KiB
PowerShell
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
|