feat: install logging, error trapping, PXE/ISO integration tests
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>
2026-03-26 22:26:33 +00:00
|
|
|
// Libvirt network for PXE boot testing.
|
|
|
|
|
// Unlike the regular test network, this one has NO DHCP —
|
|
|
|
|
// the bastion provides full DHCP + PXE on this network.
|
|
|
|
|
|
|
|
|
|
import { execSync, spawnSync } from "node:child_process";
|
|
|
|
|
import { writeFileSync, unlinkSync } from "node:fs";
|
|
|
|
|
import { log } from "./libvirt.js";
|
|
|
|
|
|
|
|
|
|
export const PXE_NETWORK_NAME = "lab-pxe-test";
|
|
|
|
|
export const PXE_BRIDGE = "virbr-pxe";
|
|
|
|
|
export const PXE_SUBNET = "192.168.251";
|
|
|
|
|
export const PXE_GATEWAY = `${PXE_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 ?? "" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No <dhcp> section — bastion dnsmasq provides full DHCP + PXE
|
|
|
|
|
const NETWORK_XML = `<network>
|
|
|
|
|
<name>${PXE_NETWORK_NAME}</name>
|
|
|
|
|
<forward mode='nat'/>
|
|
|
|
|
<bridge name='${PXE_BRIDGE}' stp='on' delay='0'/>
|
|
|
|
|
<ip address='${PXE_GATEWAY}' netmask='255.255.255.0'>
|
|
|
|
|
</ip>
|
|
|
|
|
</network>`;
|
|
|
|
|
|
|
|
|
|
/** Ensure the PXE test network exists and is active (no DHCP). */
|
|
|
|
|
export function ensurePxeNetwork(): void {
|
|
|
|
|
const result = virsh("net-info", PXE_NETWORK_NAME);
|
|
|
|
|
|
|
|
|
|
if (result.status === 0 && result.stdout.includes("Active: yes")) {
|
|
|
|
|
log(`Network ${PXE_NETWORK_NAME} already active`);
|
2026-03-28 20:24:14 +00:00
|
|
|
} else {
|
|
|
|
|
// Destroy existing if present but inactive
|
|
|
|
|
if (result.status === 0) {
|
|
|
|
|
virsh("net-destroy", PXE_NETWORK_NAME);
|
|
|
|
|
virsh("net-undefine", PXE_NETWORK_NAME);
|
|
|
|
|
}
|
feat: install logging, error trapping, PXE/ISO integration tests
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>
2026-03-26 22:26:33 +00:00
|
|
|
|
2026-03-28 20:24:14 +00:00
|
|
|
const xmlPath = "/tmp/lab-pxe-test-network.xml";
|
|
|
|
|
writeFileSync(xmlPath, NETWORK_XML);
|
|
|
|
|
|
|
|
|
|
log(`Creating PXE libvirt network: ${PXE_NETWORK_NAME} (${PXE_SUBNET}.0/24, no DHCP)`);
|
|
|
|
|
run(`virsh net-define "${xmlPath}"`);
|
|
|
|
|
run(`virsh net-start "${PXE_NETWORK_NAME}"`);
|
feat: install logging, error trapping, PXE/ISO integration tests
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>
2026-03-26 22:26:33 +00:00
|
|
|
|
2026-03-28 20:24:14 +00:00
|
|
|
try { unlinkSync(xmlPath); } catch { /* ignore */ }
|
feat: install logging, error trapping, PXE/ISO integration tests
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>
2026-03-26 22:26:33 +00:00
|
|
|
|
v2 partition schema: capability model, fixed sizing, and a proven rolling migration
The fleet's disk layout was decided by literal role-name comparisons at 17
sites across 8 files. That is why `labcontroller` got no rancher LV (it
matched neither "infra" nor "worker"), why labmaster carried an empty 728 GiB
Longhorn LV, and why a 62 GiB container image tipped that node into
DiskPressure and evicted bastion, labd, cockroach and openbao.
Three generators each hardcoded their own sizes and disagreed: the kickstart
said rancher=120G, ubuntu-autoinstall said 20G, asahi-firstboot said 20G under
a comment asserting the sizes matched install.ks.ts. They did not.
ROLE_REGISTRY already carried `k3s` and `parent` and nothing read them. This
makes capabilities authoritative, moves every size into one shared module, and
adds schema v2 (fixed 250G rancher / 300G longhorn, never --grow). Existing
machines stay v1 forever: schema is detected from the on-disk VG tag, so a v1
box stays v1 even when v2 is requested.
Proven on real VMs, not just unit tests: all four topologies installed and had
their layouts read back off the machines; markers in /home, /srv,
/var/lib/longhorn and /var/lib/rancher survived reinstall; and a 4-node cluster
formed, took real data, and had all three servers rolled one at a time through
evacuate / wipe / reinstall / rejoin with checksums verified before and after
each roll. Drill: 10/10, evidence in migration/drill-evidence/.
Production defects the drill found, none of which a unit test could reach:
- sshOpts() capped every operation at 30s while drainNode asked kubectl for
--timeout=15m, so the transport killed the drain after evicting most pods and
returned a transcript that read like success. `labctl app k3s evacuate` could
never have drained a real node.
- generateAgentConfig put node-role.kubernetes.io/worker in the kubelet's
node-label list, which NodeRestriction rejects, so no agent installed through
K3sModule could start. systemd reported only "did not take the steps required
by its unit configuration".
- toOpContext built K3sConfig by enumerating three keys and silently dropped
the rest, so the k3s version pin did nothing and --allow-single-replica was a
no-op that claimed to override a data-loss guard.
- The Longhorn disk annotation was mangled by shell quoting; with
createDefaultDiskLabeledNodes the node then gets no disk at all, joins, goes
Ready, and silently hosts no replica.
- evacuate drained before evicting, which blocks on Longhorn's PDBs and would
destroy a single-replica volume's only source.
- etcd member ids were parsed from JSON, losing uint64 precision and emitting
decimal where `member remove` needs hex -- at the moment a node is stopped
and about to be wiped.
- get.k3s.io installs latest, two minor versions ahead of the fleet, so a
reinstalled node would rejoin on a newer k3s than its peers and roll an
unplanned two-version control-plane upgrade. K3sConfig.k3sVersion now pins it.
- `labctl app k3s install --role x` mapped anything that was not "worker" to
"infra", so the runbook's own `--role k8s-combo` would have installed a
different role with different disk expectations and no warning.
Also: /var/lib/longhorn/replicas must never be deleted, only its contents, or
every replica placed on the node dies on a non-recursive mkdir -- for ever,
while the node reports Ready, Schedulable and healthy.
etcd is now in the kickstart's package set for k3s server roles, because
`k3s etcdctl` is not a command and a member that needs to recover its own
membership has no internet.
542 unit tests / 39 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-09 22:33:59 +01:00
|
|
|
// `virsh net-start` returns before the bridge is usable. The bastion's
|
|
|
|
|
// dnsmasq starts about a second later and binds to this device by name; if
|
|
|
|
|
// the interface is not up and addressed yet it fails with "error binding
|
|
|
|
|
// DHCP socket to device virbr-pxe", and because that failure is
|
|
|
|
|
// asynchronous the harness carries on with NO DHCP AT ALL -- every VM then
|
|
|
|
|
// sits in discovery until it times out, six minutes later, looking like a
|
|
|
|
|
// PXE problem rather than a startup-order one.
|
|
|
|
|
//
|
|
|
|
|
// Cheap to wait, expensive not to.
|
|
|
|
|
waitForBridge();
|
|
|
|
|
|
2026-03-28 20:24:14 +00:00
|
|
|
log(`Network ${PXE_NETWORK_NAME} created and active`);
|
|
|
|
|
}
|
feat: install logging, error trapping, PXE/ISO integration tests
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>
2026-03-26 22:26:33 +00:00
|
|
|
|
2026-03-28 20:24:14 +00:00
|
|
|
// Libvirt adds nftables reject rules for NAT networks that block host→VM SSH.
|
|
|
|
|
// Delete them now and after every VM reboot (libvirt recreates them).
|
|
|
|
|
deleteNftablesRejectRules();
|
|
|
|
|
}
|
feat: install logging, error trapping, PXE/ISO integration tests
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>
2026-03-26 22:26:33 +00:00
|
|
|
|
v2 partition schema: capability model, fixed sizing, and a proven rolling migration
The fleet's disk layout was decided by literal role-name comparisons at 17
sites across 8 files. That is why `labcontroller` got no rancher LV (it
matched neither "infra" nor "worker"), why labmaster carried an empty 728 GiB
Longhorn LV, and why a 62 GiB container image tipped that node into
DiskPressure and evicted bastion, labd, cockroach and openbao.
Three generators each hardcoded their own sizes and disagreed: the kickstart
said rancher=120G, ubuntu-autoinstall said 20G, asahi-firstboot said 20G under
a comment asserting the sizes matched install.ks.ts. They did not.
ROLE_REGISTRY already carried `k3s` and `parent` and nothing read them. This
makes capabilities authoritative, moves every size into one shared module, and
adds schema v2 (fixed 250G rancher / 300G longhorn, never --grow). Existing
machines stay v1 forever: schema is detected from the on-disk VG tag, so a v1
box stays v1 even when v2 is requested.
Proven on real VMs, not just unit tests: all four topologies installed and had
their layouts read back off the machines; markers in /home, /srv,
/var/lib/longhorn and /var/lib/rancher survived reinstall; and a 4-node cluster
formed, took real data, and had all three servers rolled one at a time through
evacuate / wipe / reinstall / rejoin with checksums verified before and after
each roll. Drill: 10/10, evidence in migration/drill-evidence/.
Production defects the drill found, none of which a unit test could reach:
- sshOpts() capped every operation at 30s while drainNode asked kubectl for
--timeout=15m, so the transport killed the drain after evicting most pods and
returned a transcript that read like success. `labctl app k3s evacuate` could
never have drained a real node.
- generateAgentConfig put node-role.kubernetes.io/worker in the kubelet's
node-label list, which NodeRestriction rejects, so no agent installed through
K3sModule could start. systemd reported only "did not take the steps required
by its unit configuration".
- toOpContext built K3sConfig by enumerating three keys and silently dropped
the rest, so the k3s version pin did nothing and --allow-single-replica was a
no-op that claimed to override a data-loss guard.
- The Longhorn disk annotation was mangled by shell quoting; with
createDefaultDiskLabeledNodes the node then gets no disk at all, joins, goes
Ready, and silently hosts no replica.
- evacuate drained before evicting, which blocks on Longhorn's PDBs and would
destroy a single-replica volume's only source.
- etcd member ids were parsed from JSON, losing uint64 precision and emitting
decimal where `member remove` needs hex -- at the moment a node is stopped
and about to be wiped.
- get.k3s.io installs latest, two minor versions ahead of the fleet, so a
reinstalled node would rejoin on a newer k3s than its peers and roll an
unplanned two-version control-plane upgrade. K3sConfig.k3sVersion now pins it.
- `labctl app k3s install --role x` mapped anything that was not "worker" to
"infra", so the runbook's own `--role k8s-combo` would have installed a
different role with different disk expectations and no warning.
Also: /var/lib/longhorn/replicas must never be deleted, only its contents, or
every replica placed on the node dies on a non-recursive mkdir -- for ever,
while the node reports Ready, Schedulable and healthy.
etcd is now in the kickstart's package set for k3s server roles, because
`k3s etcdctl` is not a command and a member that needs to recover its own
membership has no internet.
542 unit tests / 39 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-09 22:33:59 +01:00
|
|
|
/**
|
|
|
|
|
* Block until the bridge exists AND carries the gateway address.
|
|
|
|
|
*
|
|
|
|
|
* Checking only that the device exists is not enough: it appears before the
|
|
|
|
|
* address is assigned, and dnsmasq needs the address to bind.
|
|
|
|
|
*/
|
|
|
|
|
export function waitForBridge(timeoutMs = 30_000): void {
|
|
|
|
|
const start = Date.now();
|
|
|
|
|
let last = "";
|
|
|
|
|
while (Date.now() - start < timeoutMs) {
|
|
|
|
|
const r = spawnSync("ip", ["-br", "addr", "show", PXE_BRIDGE], { encoding: "utf-8", stdio: "pipe" });
|
|
|
|
|
last = (r.stdout ?? "").trim();
|
|
|
|
|
if (r.status === 0 && last.includes(`${PXE_GATEWAY}/`)) {
|
|
|
|
|
log(`Bridge ${PXE_BRIDGE} ready with ${PXE_GATEWAY} after ${Date.now() - start}ms`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Busy-wait briefly; this normally resolves in well under a second.
|
|
|
|
|
const until = Date.now() + 250;
|
|
|
|
|
while (Date.now() < until) { /* spin */ }
|
|
|
|
|
}
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Bridge ${PXE_BRIDGE} did not come up with ${PXE_GATEWAY} within ${timeoutMs}ms ` +
|
|
|
|
|
`(last: ${last || "device absent"}). dnsmasq cannot bind, so PXE would silently not work.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 20:24:14 +00:00
|
|
|
/** Delete libvirt's nftables reject rules for our bridge so host→VM traffic works.
|
|
|
|
|
* Must be called after every VM start/restart — libvirt recreates them. */
|
|
|
|
|
export function deleteNftablesRejectRules(): void {
|
|
|
|
|
// libvirt uses "ip libvirt_network" table (not "inet libvirt")
|
|
|
|
|
const tables = ["ip libvirt_network", "ip6 libvirt_network", "inet libvirt"];
|
|
|
|
|
for (const table of tables) {
|
|
|
|
|
try {
|
|
|
|
|
for (const chain of ["guest_input", "guest_output"]) {
|
|
|
|
|
const output = run(`nft -a list chain ${table} ${chain} 2>/dev/null || true`);
|
|
|
|
|
for (const line of output.split("\n")) {
|
|
|
|
|
if (line.includes(PXE_BRIDGE) && line.includes("reject")) {
|
|
|
|
|
const handleMatch = line.match(/# handle (\d+)/);
|
|
|
|
|
if (handleMatch) {
|
|
|
|
|
run(`nft delete rule ${table} ${chain} handle ${handleMatch[1]}`);
|
|
|
|
|
}
|
feat: install logging, error trapping, PXE/ISO integration tests
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>
2026-03-26 22:26:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-28 20:24:14 +00:00
|
|
|
} catch { /* table may not exist */ }
|
feat: install logging, error trapping, PXE/ISO integration tests
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>
2026-03-26 22:26:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Destroy the PXE test network. */
|
|
|
|
|
export function destroyPxeNetwork(): void {
|
|
|
|
|
log(`Destroying PXE network: ${PXE_NETWORK_NAME}`);
|
|
|
|
|
// nftables rules are cleaned up when the network is destroyed (libvirt removes them)
|
|
|
|
|
virsh("net-destroy", PXE_NETWORK_NAME);
|
|
|
|
|
virsh("net-undefine", PXE_NETWORK_NAME);
|
|
|
|
|
}
|