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

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:
Michal
2026-03-26 22:26:33 +00:00
parent ffc4a782d2
commit 46b017d77e
189 changed files with 16241 additions and 432 deletions

View File

@@ -0,0 +1,68 @@
// Libvirt network management for integration tests.
import { execSync, spawnSync } from "node:child_process";
import { writeFileSync, unlinkSync } from "node:fs";
import { log } from "./libvirt.js";
export const TEST_NETWORK_NAME = "lab-test";
export const TEST_NETWORK_BRIDGE = "virbr-lab";
export const TEST_NETWORK_SUBNET = "192.168.250";
export const TEST_NETWORK_GATEWAY = `${TEST_NETWORK_SUBNET}.1`;
const IS_ROOT = process.getuid?.() === 0;
function run(cmd: string): string {
const full = IS_ROOT ? cmd : `sudo ${cmd}`;
return execSync(full, { encoding: "utf-8", stdio: "pipe" });
}
function virsh(...args: string[]): { status: number; stdout: string } {
const cmd = IS_ROOT ? "virsh" : "sudo";
const finalArgs = IS_ROOT ? args : ["virsh", ...args];
const result = spawnSync(cmd, finalArgs, { encoding: "utf-8", stdio: "pipe" });
return { status: result.status ?? 1, stdout: result.stdout ?? "" };
}
const NETWORK_XML = `<network>
<name>${TEST_NETWORK_NAME}</name>
<forward mode='nat'/>
<bridge name='${TEST_NETWORK_BRIDGE}' stp='on' delay='0'/>
<ip address='${TEST_NETWORK_GATEWAY}' netmask='255.255.255.0'>
<dhcp>
<range start='${TEST_NETWORK_SUBNET}.100' end='${TEST_NETWORK_SUBNET}.200'/>
</dhcp>
</ip>
</network>`;
/** Ensure the test libvirt network exists and is active. */
export function ensureTestNetwork(): void {
const result = virsh("net-info", TEST_NETWORK_NAME);
if (result.status === 0 && result.stdout.includes("Active: yes")) {
log(`Network ${TEST_NETWORK_NAME} already active`);
return;
}
// Destroy existing if present but inactive
if (result.status === 0) {
virsh("net-destroy", TEST_NETWORK_NAME);
virsh("net-undefine", TEST_NETWORK_NAME);
}
const xmlPath = `/tmp/lab-test-network.xml`;
writeFileSync(xmlPath, NETWORK_XML);
log(`Creating libvirt network: ${TEST_NETWORK_NAME} (${TEST_NETWORK_SUBNET}.0/24)`);
run(`virsh net-define "${xmlPath}"`);
run(`virsh net-start "${TEST_NETWORK_NAME}"`);
try { unlinkSync(xmlPath); } catch { /* ignore */ }
log(`Network ${TEST_NETWORK_NAME} created and active`);
}
/** Destroy the test network. */
export function destroyTestNetwork(): void {
log(`Destroying network: ${TEST_NETWORK_NAME}`);
virsh("net-destroy", TEST_NETWORK_NAME);
virsh("net-undefine", TEST_NETWORK_NAME);
}