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,56 @@
// Tests for LabdApiError.
import { describe, it, expect } from "vitest";
import { LabdApiError, isLabdApiError } from "../src/api/errors.js";
describe("LabdApiError", () => {
it("constructs with status code and message", () => {
const err = new LabdApiError(404, "Not found");
expect(err.statusCode).toBe(404);
expect(err.message).toBe("Not found");
expect(err.errorCode).toBe("NOT_FOUND");
});
it("fromResponse parses error body", () => {
const err = LabdApiError.fromResponse(400, {
error: "Invalid input",
detail: "hostname required",
});
expect(err.statusCode).toBe(400);
expect(err.message).toBe("Invalid input");
expect(err.detail).toBe("hostname required");
});
it("fromResponse handles non-object body", () => {
const err = LabdApiError.fromResponse(500, "plain text");
expect(err.statusCode).toBe(500);
expect(err.message).toBe("HTTP 500");
});
it("notConnected creates connection error", () => {
const err = LabdApiError.notConnected("https://localhost:8443");
expect(err.statusCode).toBe(0);
expect(err.errorCode).toBe("CONNECTION_ERROR");
expect(err.message).toContain("localhost:8443");
});
it("timeout creates timeout error", () => {
const err = LabdApiError.timeout(30000);
expect(err.message).toContain("30000ms");
});
});
describe("isLabdApiError", () => {
it("returns true for LabdApiError", () => {
expect(isLabdApiError(new LabdApiError(500, "err"))).toBe(true);
});
it("returns false for regular Error", () => {
expect(isLabdApiError(new Error("nope"))).toBe(false);
});
it("returns false for non-errors", () => {
expect(isLabdApiError(null)).toBe(false);
expect(isLabdApiError("string")).toBe(false);
});
});