feat: debug --pxe-boot flag, boot installed system via PXE
Some checks failed
CI/CD / lint (pull_request) Failing after 10s
CI/CD / test (pull_request) Failing after 10s
CI/CD / typecheck (pull_request) Failing after 22s
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

Loads kernel+initrd from bastion HTTP server, mounts root from local
NVMe. Workaround for UEFI firmware bugs that make local disk boot
100x slower. One-time use, auto-clears after boot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-03-30 00:49:44 +01:00
parent 8da947a1c3
commit a4a4840930
9 changed files with 64 additions and 20 deletions

View File

@@ -270,6 +270,7 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
if (msg.type !== "command-debug") throw new Error("unexpected");
const mac = msg.mac.toLowerCase();
const sshd = msg.sshd ?? false;
const pxeBoot = msg.pxeBoot ?? false;
const currentState = state.load();
const hostname =
currentState.installed[mac]?.hostname ??
@@ -277,7 +278,7 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
currentState.discovered[mac]?.product ??
mac;
state.update((s) => {
s.debug[mac] = { hostname, queued_at: new Date().toISOString(), sshd };
s.debug[mac] = { hostname, queued_at: new Date().toISOString(), sshd, pxeBoot };
});
return { status: "ok", data: { mac, hostname } };
});

View File

@@ -191,10 +191,11 @@ export function registerApiRoutes(
// Queue debug/rescue mode for a machine
app.post<{
Body: { mac?: string; sshd?: boolean };
Body: { mac?: string; sshd?: boolean; pxeBoot?: boolean };
}>("/api/debug", async (request, reply) => {
const mac = (request.body?.mac ?? "").toLowerCase().replace(/-/g, ":");
const sshd = request.body?.sshd ?? false;
const pxeBoot = request.body?.pxeBoot ?? false;
if (mac === "") {
return reply.status(400).send({ error: "mac is required" });
}
@@ -208,7 +209,7 @@ export function registerApiRoutes(
mac;
state.update((s) => {
s.debug[mac] = { hostname, queued_at: new Date().toISOString(), sshd };
s.debug[mac] = { hostname, queued_at: new Date().toISOString(), sshd, pxeBoot };
});
logger.info(`DEBUG QUEUED: ${mac} -> ${hostname}`);

View File

@@ -11,6 +11,7 @@ import {
renderDiscoverIpxe,
renderInstallIpxe,
renderDebugIpxe,
renderPxeBootDebugIpxe,
renderLocalBootIpxe,
} from "../templates/boot.ipxe.js";
import { renderUbuntuInstallIpxe } from "../templates/ubuntu-boot.ipxe.js";
@@ -45,17 +46,27 @@ export function registerDispatchRoutes(
const debugEntry = currentState.debug[mac];
if (debugEntry) {
const hostname = debugEntry.hostname ?? "debug";
logger.info(`DEBUG BOOT: ${mac} -> ${hostname} (rescue mode)`);
state.update((s) => { delete s.debug[mac]; });
const script = renderDebugIpxe({
mac,
hostname,
serverIp: config.serverIp,
httpPort: config.httpPort,
fedoraMirror: config.fedoraMirror,
});
let script: string;
if (debugEntry.pxeBoot) {
logger.info(`PXE BOOT DEBUG: ${mac} -> ${hostname} (kernel+initrd from PXE, root from NVMe)`);
script = renderPxeBootDebugIpxe({
mac,
hostname,
serverIp: config.serverIp,
httpPort: config.httpPort,
});
} else {
logger.info(`DEBUG BOOT: ${mac} -> ${hostname} (rescue mode)`);
script = renderDebugIpxe({
mac,
hostname,
serverIp: config.serverIp,
httpPort: config.httpPort,
fedoraMirror: config.fedoraMirror,
});
}
return reply.type("text/plain").send(script);
}

View File

@@ -102,6 +102,34 @@ boot
`;
}
/**
* iPXE script for PXE-boot debug mode -- boots the installed system's root
* filesystem using the bastion's PXE kernel+initrd instead of local GRUB.
* Workaround for UEFI firmware bugs that make local disk boot slow.
*/
export function renderPxeBootDebugIpxe(params: {
mac: string;
hostname: string;
serverIp: string;
httpPort: number;
}): string {
return `#!ipxe
echo
echo =============================================
echo Lab PXE Bastion - PXE BOOT (debug)
echo Target: ${params.hostname}
echo MAC: ${params.mac}
echo Kernel+initrd from PXE, root from NVMe
echo =============================================
echo
kernel http://${params.serverIp}:${params.httpPort}/vmlinuz root=/dev/mapper/labvg-root ro rd.lvm.lv=labvg/root rd.lvm.lv=labvg/swap console=tty0 console=ttyS0,115200n8 modprobe.blacklist=amdgpu
initrd http://${params.serverIp}:${params.httpPort}/initrd.img
boot
`;
}
/**
* iPXE script for already-installed machines -- exits to boot from local disk.
*/

View File

@@ -94,8 +94,8 @@ export class LabdClient {
return this.request("POST", "/api/machines/install", { body: opts });
}
async debugMachine(mac: string, opts?: { sshd?: boolean }): Promise<{ status: string; data?: { mac: string; hostname: string }; error?: string }> {
return this.request("POST", "/api/machines/debug", { body: { mac, sshd: opts?.sshd } });
async debugMachine(mac: string, opts?: { sshd?: boolean; pxeBoot?: boolean }): Promise<{ status: string; data?: { mac: string; hostname: string }; error?: string }> {
return this.request("POST", "/api/machines/debug", { body: { mac, sshd: opts?.sshd, pxeBoot: opts?.pxeBoot } });
}
async forgetMachine(mac: string): Promise<{ status: string }> {

View File

@@ -49,8 +49,9 @@ export function registerDebugCommand(parent: Command): void {
.command("debug <target>")
.description("PXE boot into Fedora rescue mode for debugging (target: hostname, MAC, or IP)")
.option("--sshd", "Start SSH + nc listener automatically, report IP to bastion")
.option("--pxe-boot", "Boot installed system via PXE (kernel+initrd from network, root from NVMe)")
.showHelpAfterError(true)
.action(async (target: string, opts: { sshd?: boolean }) => {
.action(async (target: string, opts: { sshd?: boolean; pxeBoot?: boolean }) => {
const client = getLabdClient();
// Resolve target from labd aggregated state
@@ -74,7 +75,7 @@ export function registerDebugCommand(parent: Command): void {
console.log(`Queuing debug mode for ${hostname} (${mac})...`);
try {
const result = await client.debugMachine(mac, { sshd: opts.sshd === true });
const result = await client.debugMachine(mac, { sshd: opts.sshd === true, pxeBoot: opts.pxeBoot === true });
if (result.error) {
console.error(`Failed: ${result.error}`);
process.exit(1);

View File

@@ -174,10 +174,11 @@ export function registerBastionRoutes(app: FastifyInstance, db: DbClient): void
// Queue debug/rescue mode — route to correct bastion by MAC
app.post<{
Body: { mac?: string; sshd?: boolean };
Body: { mac?: string; sshd?: boolean; pxeBoot?: boolean };
}>("/api/machines/debug", async (request, reply) => {
const mac = (request.body?.mac ?? "").toLowerCase().replace(/-/g, ":");
const sshd = request.body?.sshd ?? false;
const pxeBoot = request.body?.pxeBoot ?? false;
if (!mac) {
return reply.code(400).send({ error: "mac is required" });
}
@@ -190,7 +191,7 @@ export function registerBastionRoutes(app: FastifyInstance, db: DbClient): void
}
if (all.length === 1) {
try {
const result = await sendCommand(all[0]!.bastionId, { type: "command-debug", mac, sshd });
const result = await sendCommand(all[0]!.bastionId, { type: "command-debug", mac, sshd, pxeBoot });
return reply.code(result.status === "ok" ? 200 : 500).send(result);
} catch (err) {
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
@@ -200,7 +201,7 @@ export function registerBastionRoutes(app: FastifyInstance, db: DbClient): void
}
try {
const result = await sendCommand(bastion.bastionId, { type: "command-debug", mac, sshd });
const result = await sendCommand(bastion.bastionId, { type: "command-debug", mac, sshd, pxeBoot });
return reply.code(result.status === "ok" ? 200 : 500).send(result);
} catch (err) {
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });

View File

@@ -111,7 +111,7 @@ export type LabdBastionMessage =
| { type: "command-install"; requestId: string; mac: string; hostname: string; disk?: string; role: string; os: string }
| { type: "command-forget"; requestId: string; mac: string }
| { type: "command-role-update"; requestId: string; mac: string; role: string }
| { type: "command-debug"; requestId: string; mac: string; sshd?: boolean }
| { type: "command-debug"; requestId: string; mac: string; sshd?: boolean; pxeBoot?: boolean }
| { type: "server-shutdown"; reconnectAfter: number };
export type BastionMessageType = BastionMessage["type"];

View File

@@ -102,6 +102,7 @@ export interface DebugConfig {
hostname: string;
queued_at: string;
sshd?: boolean;
pxeBoot?: boolean;
}
export interface BastionState {