feat: install logging, error trapping, PXE/ISO integration tests
Some checks failed
CI/CD / lint (pull_request) Failing after 13s
CI/CD / test (pull_request) Failing after 10s
CI/CD / typecheck (pull_request) Failing after 36s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
Some checks failed
CI/CD / lint (pull_request) Failing after 13s
CI/CD / test (pull_request) Failing after 10s
CI/CD / typecheck (pull_request) Failing after 36s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
Kickstart installs on real hardware failed silently — no error reporting, only 3 progress callbacks, zero log streaming. This overhaul makes every install fully observable. Kickstart improvements: - Error trapping in %pre and %post (trap ERR sends failure details to bastion) - 12+ granular progress stages (was 3): SSH, hostname, k3s prep, EFI boot, metadata - Background log streamer: tails %post output and batch-sends to /api/log - bastion_log() function for explicit log lines from kickstart scripts Bastion API: - POST /api/log — receives raw log lines from kickstart (single or batch) - InstallLogBuffer — per-MAC ring buffer (2000 lines) + file persistence - GET /api/logs/:mac — now returns log_lines + log_total alongside stages - SSE /api/logs/:mac/follow — uses named events (event: stage vs event: log) - Progress events forwarded to labd via bastion-progress WebSocket message - Post-provision k3s logs routed through progressBus (was console-only) dnsmasq fixes found during VM testing: - HTTP Boot filename: ipxe-real.efi → ipxe.efi (leftover from old 2-stage approach) - pxe-service directives: only in proxy mode (breaks OVMF PXE in full mode) - PXEClient vendor class echo for UEFI firmware compatibility Integration tests: - PXE boot test: blank UEFI VM → dnsmasq → HTTP Boot → iPXE → bastion → install - ISO boot test: blank VM boots from bastion-generated ISO → same flow - Shared helpers: pxe-network (no DHCP, nftables fix), pxe-vm (UEFI + ISO boot) - test-provision.sh: runs both PXE + ISO tests with prerequisite checks - 250GB sparse QCOW2 disk (LVM layout needs ~204GB) 201 unit tests passing (11 new). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
106
bastion/tests/integration/helpers/ssh.ts
Normal file
106
bastion/tests/integration/helpers/ssh.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
// SSH execution helper for integration tests.
|
||||
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { log } from "./libvirt.js";
|
||||
|
||||
export interface SshResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
/** Execute a command via SSH and return the result. */
|
||||
export function sshExec(
|
||||
ip: string,
|
||||
user: string,
|
||||
command: string,
|
||||
options?: { keyPath?: string; timeout?: number },
|
||||
): SshResult {
|
||||
const args = [
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-o", "BatchMode=yes",
|
||||
...(options?.keyPath ? ["-i", options.keyPath] : []),
|
||||
`${user}@${ip}`,
|
||||
command,
|
||||
];
|
||||
|
||||
const result = spawnSync("ssh", args, {
|
||||
stdio: "pipe",
|
||||
timeout: options?.timeout ?? 120_000,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
return {
|
||||
exitCode: result.status ?? 1,
|
||||
stdout: result.stdout ?? "",
|
||||
stderr: result.stderr ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
/** Execute a command via SSH with live streaming output. */
|
||||
export async function sshExecStreaming(
|
||||
ip: string,
|
||||
user: string,
|
||||
command: string,
|
||||
onLine: (line: string) => void,
|
||||
options?: { keyPath?: string; timeout?: number },
|
||||
): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-o", "BatchMode=yes",
|
||||
...(options?.keyPath ? ["-i", options.keyPath] : []),
|
||||
`${user}@${ip}`,
|
||||
command,
|
||||
];
|
||||
|
||||
const proc = spawn("ssh", args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
timeout: options?.timeout ?? 600_000,
|
||||
});
|
||||
|
||||
let buffer = "";
|
||||
proc.stdout.on("data", (data: Buffer) => {
|
||||
buffer += data.toString();
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
onLine(line);
|
||||
}
|
||||
});
|
||||
|
||||
proc.stderr.on("data", (data: Buffer) => {
|
||||
const text = data.toString().trim();
|
||||
if (text) onLine(`[stderr] ${text}`);
|
||||
});
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (buffer.trim()) onLine(buffer.trim());
|
||||
resolve(code ?? 1);
|
||||
});
|
||||
|
||||
proc.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Run a command on the VM via SSH and log it with a prefix. */
|
||||
export async function sshRun(
|
||||
ip: string,
|
||||
user: string,
|
||||
command: string,
|
||||
label: string,
|
||||
options?: { keyPath?: string; timeout?: number },
|
||||
): Promise<number> {
|
||||
log(`${label}: ${command.slice(0, 80)}${command.length > 80 ? "..." : ""}`);
|
||||
const code = await sshExecStreaming(ip, user, command, (line) => {
|
||||
console.log(` ${line}`);
|
||||
}, options);
|
||||
if (code !== 0) {
|
||||
log(`${label}: FAILED (exit ${code})`);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
Reference in New Issue
Block a user