fix: PXE boot debugging — bisect root cause, syslog logging, serial console #3

Merged
michal merged 31 commits from wip/ks-debugging into main 2026-03-29 00:50:05 +00:00
3 changed files with 73 additions and 50 deletions
Showing only changes of commit 937c01f5d9 - Show all commits

View File

@@ -16,6 +16,8 @@ export function registerServeCommand(program: Command): void {
.option("--arch <arch>", "Architecture", "x86_64") .option("--arch <arch>", "Architecture", "x86_64")
.option("--timezone <tz>", "Timezone", "Europe/London") .option("--timezone <tz>", "Timezone", "Europe/London")
.option("--locale <locale>", "Locale", "en_GB.UTF-8") .option("--locale <locale>", "Locale", "en_GB.UTF-8")
.option("--skip-dnsmasq", "Skip starting dnsmasq (for testing)")
.option("--skip-artifacts", "Skip downloading boot artifacts (for testing)")
.action(async (opts: { .action(async (opts: {
port: string; port: string;
dir: string; dir: string;
@@ -25,6 +27,8 @@ export function registerServeCommand(program: Command): void {
arch: string; arch: string;
timezone: string; timezone: string;
locale: string; locale: string;
skipDnsmasq?: boolean;
skipArtifacts?: boolean;
}) => { }) => {
await startBastion({ await startBastion({
httpPort: parseInt(opts.port, 10), httpPort: parseInt(opts.port, 10),
@@ -35,6 +39,8 @@ export function registerServeCommand(program: Command): void {
arch: opts.arch, arch: opts.arch,
timezone: opts.timezone, timezone: opts.timezone,
locale: opts.locale, locale: opts.locale,
skipDnsmasq: opts.skipDnsmasq,
skipArtifacts: opts.skipArtifacts,
}); });
}); });
} }

View File

@@ -11,6 +11,9 @@ export interface BastionConfig {
dhcpMode: "proxy" | "full"; dhcpMode: "proxy" | "full";
dhcpRangeStart: string; dhcpRangeStart: string;
dhcpRangeEnd: string; dhcpRangeEnd: string;
// Flags
skipDnsmasq?: boolean;
skipArtifacts?: boolean;
// Derived at runtime // Derived at runtime
iface: string; iface: string;
serverIp: string; serverIp: string;
@@ -59,6 +62,8 @@ export function loadConfig(overrides: Partial<BastionConfig> = {}): BastionConfi
gateway: overrides.gateway ?? "", gateway: overrides.gateway ?? "",
sshKeys: overrides.sshKeys ?? [], sshKeys: overrides.sshKeys ?? [],
adminUser: overrides.adminUser ?? "", adminUser: overrides.adminUser ?? "",
skipDnsmasq: overrides.skipDnsmasq,
skipArtifacts: overrides.skipArtifacts,
fedoraMirror, fedoraMirror,
tftpDir, tftpDir,
httpDir, httpDir,

View File

@@ -54,46 +54,50 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
mkdirSync(config.httpDir, { recursive: true }); mkdirSync(config.httpDir, { recursive: true });
// Prepare boot artifacts // Prepare boot artifacts
logger.info(`Preparing boot artifacts (Fedora ${config.fedoraVersion} ${config.arch})...`); if (!config.skipArtifacts) {
logger.info(`Preparing boot artifacts (Fedora ${config.fedoraVersion} ${config.arch})...`);
copyIfMissing(
"/usr/share/ipxe/undionly.kpxe",
`${config.tftpDir}/undionly.kpxe`,
"iPXE BIOS",
);
copyIfMissing(
"/usr/share/ipxe/ipxe-snponly-x86_64.efi",
`${config.tftpDir}/ipxe.efi`,
"iPXE UEFI x86_64",
);
try {
copyIfMissing( copyIfMissing(
"/usr/share/ipxe/arm64-efi/snponly.efi", "/usr/share/ipxe/undionly.kpxe",
`${config.tftpDir}/ipxe-arm64.efi`, `${config.tftpDir}/undionly.kpxe`,
"iPXE UEFI arm64", "iPXE BIOS",
); );
} catch { copyIfMissing(
logger.warn("arm64 iPXE not available -- skipping"); "/usr/share/ipxe/ipxe-snponly-x86_64.efi",
} `${config.tftpDir}/ipxe.efi`,
"iPXE UEFI x86_64",
download( );
`${config.fedoraMirror}/images/pxeboot/vmlinuz`, try {
`${config.httpDir}/vmlinuz`, copyIfMissing(
"Fedora kernel", "/usr/share/ipxe/arm64-efi/snponly.efi",
); `${config.tftpDir}/ipxe-arm64.efi`,
download( "iPXE UEFI arm64",
`${config.fedoraMirror}/images/pxeboot/initrd.img`, );
`${config.httpDir}/initrd.img`, } catch {
"Fedora initrd", logger.warn("arm64 iPXE not available -- skipping");
);
// Symlink iPXE binaries into HTTP dir for UEFI HTTP Boot
for (const name of ["ipxe.efi", "ipxe-arm64.efi"]) {
const src = `${config.tftpDir}/${name}`;
const dest = `${config.httpDir}/${name}`;
if (existsSync(src)) {
symlinkSafe(src, dest);
} }
download(
`${config.fedoraMirror}/images/pxeboot/vmlinuz`,
`${config.httpDir}/vmlinuz`,
"Fedora kernel",
);
download(
`${config.fedoraMirror}/images/pxeboot/initrd.img`,
`${config.httpDir}/initrd.img`,
"Fedora initrd",
);
// Symlink iPXE binaries into HTTP dir for UEFI HTTP Boot
for (const name of ["ipxe.efi", "ipxe-arm64.efi"]) {
const src = `${config.tftpDir}/${name}`;
const dest = `${config.httpDir}/${name}`;
if (existsSync(src)) {
symlinkSafe(src, dest);
}
}
} else {
logger.info("Skipping boot artifacts (--skip-artifacts)");
} }
// Write discovery kickstart // Write discovery kickstart
@@ -115,8 +119,25 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
await app.listen({ port: config.httpPort, host: "0.0.0.0" }); await app.listen({ port: config.httpPort, host: "0.0.0.0" });
logger.info(`HTTP server listening on :${config.httpPort}`); logger.info(`HTTP server listening on :${config.httpPort}`);
// Start dnsmasq // Start dnsmasq (unless skipped)
const dnsmasqProc = await startDnsmasq(config); if (!config.skipDnsmasq) {
const dnsmasqProc = startDnsmasq(config);
// Monitor dnsmasq
void dnsmasqProc.then(() => {
logger.error("dnsmasq exited unexpectedly");
logger.error("Check if another DHCP/TFTP service is running.");
process.exit(1);
}).catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err);
if (!message.includes("was killed")) {
logger.error(`dnsmasq error: ${message}`);
process.exit(1);
}
});
} else {
logger.info("Skipping dnsmasq (--skip-dnsmasq)");
}
// Print banner // Print banner
printBanner(config); printBanner(config);
@@ -124,7 +145,7 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
// Graceful shutdown // Graceful shutdown
const shutdown = async () => { const shutdown = async () => {
logger.info("Shutting down..."); logger.info("Shutting down...");
stopDnsmasq(); if (!config.skipDnsmasq) stopDnsmasq();
await app.close(); await app.close();
logger.info(`State preserved in ${config.stateFile}`); logger.info(`State preserved in ${config.stateFile}`);
process.exit(0); process.exit(0);
@@ -133,17 +154,8 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
process.on("SIGINT", () => void shutdown()); process.on("SIGINT", () => void shutdown());
process.on("SIGTERM", () => void shutdown()); process.on("SIGTERM", () => void shutdown());
// Wait for dnsmasq to exit // Keep process alive
try { await new Promise(() => {});
await dnsmasqProc;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (!message.includes("was killed")) {
logger.error(`dnsmasq exited unexpectedly: ${message}`);
logger.error("Check if another DHCP/TFTP service is running.");
process.exit(1);
}
}
} }
function printBanner(config: BastionConfig): void { function printBanner(config: BastionConfig): void {