107 lines
2.6 KiB
TypeScript
107 lines
2.6 KiB
TypeScript
|
|
// 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;
|
||
|
|
}
|