feat(bastion): first-class aarch64 support in the network-PXE path #17
@@ -5,8 +5,8 @@
|
||||
// - unknown -> discovery mode (collect hardware, POST to bastion)
|
||||
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import type { Arch, BastionConfig, BastionState } from "@lab/shared";
|
||||
import { normalizeArch, fedoraMirrorFor } from "@lab/shared";
|
||||
import type { Arch, BastionConfig, BastionState, OsId } from "@lab/shared";
|
||||
import { normalizeArch, fedoraMirrorFor, osSupportsArch } from "@lab/shared";
|
||||
import type { StateManager } from "../services/state.js";
|
||||
import {
|
||||
renderDiscoverIpxe,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
renderDebugIpxe,
|
||||
renderPxeBootDebugIpxe,
|
||||
renderLocalBootIpxe,
|
||||
renderUnsupportedIpxe,
|
||||
} from "../templates/boot.ipxe.js";
|
||||
import { renderUbuntuInstallIpxe } from "../templates/ubuntu-boot.ipxe.js";
|
||||
import { renderDebugKickstart } from "../templates/debug.ks.js";
|
||||
@@ -215,6 +216,20 @@ echo "==============================="
|
||||
|
||||
let script: string;
|
||||
if (os.startsWith("ubuntu")) {
|
||||
// Last line of defence. The install guard refuses this combination when the
|
||||
// machine's architecture is already known, but a machine queued before it was
|
||||
// discovered can reach here. Serving the x86-only Ubuntu kernel to an arm64
|
||||
// client is precisely the bug this work exists to fix, so stop instead.
|
||||
if (!osSupportsArch(os as OsId, arch)) {
|
||||
logger.error(`INSTALL BLOCKED: ${mac} -> ${hostname} -- ${os} has no ${arch} artifacts`);
|
||||
script = renderUnsupportedIpxe({
|
||||
hostname,
|
||||
mac,
|
||||
reason: `${os} publishes no ${arch} netboot artifacts`,
|
||||
action: `labctl provision install ${mac} ${hostname} --os fedora-43`,
|
||||
});
|
||||
return reply.type("text/plain").send(script);
|
||||
}
|
||||
script = renderUbuntuInstallIpxe({
|
||||
mac,
|
||||
hostname,
|
||||
|
||||
@@ -212,6 +212,34 @@ boot
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* iPXE script for a request we refuse to serve.
|
||||
*
|
||||
* Better a machine that stops with a legible reason on its console than one handed a
|
||||
* kernel it cannot execute, which fails much later and much less clearly.
|
||||
*/
|
||||
export function renderUnsupportedIpxe(params: {
|
||||
mac: string;
|
||||
hostname: string;
|
||||
reason: string;
|
||||
action?: string;
|
||||
}): string {
|
||||
return `#!ipxe
|
||||
|
||||
echo
|
||||
echo =============================================
|
||||
echo Lab PXE Bastion - CANNOT BOOT THIS MACHINE
|
||||
echo Target: ${params.hostname}
|
||||
echo MAC: ${params.mac}
|
||||
echo
|
||||
echo ${params.reason}
|
||||
${params.action !== undefined ? `echo\necho Try: ${params.action}\n` : ""}echo =============================================
|
||||
echo
|
||||
sleep 10
|
||||
exit 1
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* iPXE script for already-installed machines -- exits to boot from local disk.
|
||||
*/
|
||||
|
||||
@@ -141,6 +141,36 @@ describe("aarch64 dispatch", () => {
|
||||
expect(res.body).not.toContain("nomodeset");
|
||||
});
|
||||
|
||||
it("refuses to serve the x86-only Ubuntu kernel to an arm64 client", async () => {
|
||||
// A machine queued for Ubuntu before it was discovered as aarch64 reaches dispatch
|
||||
// with no guard having run. Serving it /ubuntu-vmlinuz is the original bug.
|
||||
state.update((s) => {
|
||||
s.install_queue[mac] = {
|
||||
hostname: "arm-node", disk: "", role: "worker",
|
||||
os: "ubuntu-26.04", queued_at: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
const res = await app.inject({ method: "GET", url: `/dispatch?mac=${mac}&arch=arm64` });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toContain("CANNOT BOOT THIS MACHINE");
|
||||
expect(res.body).toContain("no aarch64 netboot artifacts");
|
||||
expect(res.body).not.toContain("ubuntu-vmlinuz");
|
||||
});
|
||||
|
||||
it("still serves Ubuntu to an x86_64 client", async () => {
|
||||
state.update((s) => {
|
||||
s.install_queue[mac] = {
|
||||
hostname: "x86-node", disk: "", role: "worker",
|
||||
os: "ubuntu-26.04", queued_at: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
const res = await app.inject({ method: "GET", url: `/dispatch?mac=${mac}&arch=x86_64` });
|
||||
expect(res.body).toContain("ubuntu-vmlinuz");
|
||||
expect(res.body).not.toContain("CANNOT BOOT");
|
||||
});
|
||||
|
||||
it("serves a rescue kernel for the recorded architecture, not the requester's", async () => {
|
||||
// The Spark case: machine known to be aarch64, queued for rescue.
|
||||
state.update((s) => {
|
||||
|
||||
@@ -170,9 +170,10 @@ async function startHarness(vmName: string, httpPort: number, pubKey: string): P
|
||||
const { loadConfig } = await import("../../src/bastion/src/config.js");
|
||||
const { generateDnsmasqConf, startDnsmasq, stopDnsmasq } = await import("../../src/bastion/src/services/dnsmasq.js");
|
||||
const { generateDiscoverKickstart } = await import("../../src/bastion/src/services/kickstart-generator.js");
|
||||
const { renderBootIpxe } = await import("../../src/bastion/src/templates/boot.ipxe.js");
|
||||
const { kernelPath, initrdPath } = await import("../../src/bastion/src/templates/boot.ipxe.js");
|
||||
const { SUPPORTED_ARCHES, fedoraMirrorFor } = await import("@lab/shared");
|
||||
const { renderBootIpxe, kernelPath, initrdPath } = await import("../../src/bastion/src/templates/boot.ipxe.js");
|
||||
// Relative, not "@lab/shared": these tests run from the repo root against sources,
|
||||
// where the workspace package alias is not resolvable.
|
||||
const { SUPPORTED_ARCHES, fedoraMirrorFor } = await import("../../src/shared/src/hardware/index.js");
|
||||
|
||||
const config = loadConfig({
|
||||
bastionDir: testDir,
|
||||
|
||||
Reference in New Issue
Block a user