fix(bastion): refuse to serve an x86-only kernel to an arm64 client
The install guard catches an unsupported OS/architecture pair when the machine's architecture is already known, but a machine queued before it was ever discovered reaches dispatch with nothing having checked. Serving it the x86-only Ubuntu kernel is exactly the failure this work exists to fix, so stop with a legible reason on the console instead -- a machine handed a kernel it cannot execute fails later and far less clearly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nRFZXpKwUVE4SRSHw6GjF
This commit is contained in:
@@ -5,8 +5,8 @@
|
|||||||
// - unknown -> discovery mode (collect hardware, POST to bastion)
|
// - unknown -> discovery mode (collect hardware, POST to bastion)
|
||||||
|
|
||||||
import type { FastifyInstance } from "fastify";
|
import type { FastifyInstance } from "fastify";
|
||||||
import type { Arch, BastionConfig, BastionState } from "@lab/shared";
|
import type { Arch, BastionConfig, BastionState, OsId } from "@lab/shared";
|
||||||
import { normalizeArch, fedoraMirrorFor } from "@lab/shared";
|
import { normalizeArch, fedoraMirrorFor, osSupportsArch } from "@lab/shared";
|
||||||
import type { StateManager } from "../services/state.js";
|
import type { StateManager } from "../services/state.js";
|
||||||
import {
|
import {
|
||||||
renderDiscoverIpxe,
|
renderDiscoverIpxe,
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
renderDebugIpxe,
|
renderDebugIpxe,
|
||||||
renderPxeBootDebugIpxe,
|
renderPxeBootDebugIpxe,
|
||||||
renderLocalBootIpxe,
|
renderLocalBootIpxe,
|
||||||
|
renderUnsupportedIpxe,
|
||||||
} from "../templates/boot.ipxe.js";
|
} from "../templates/boot.ipxe.js";
|
||||||
import { renderUbuntuInstallIpxe } from "../templates/ubuntu-boot.ipxe.js";
|
import { renderUbuntuInstallIpxe } from "../templates/ubuntu-boot.ipxe.js";
|
||||||
import { renderDebugKickstart } from "../templates/debug.ks.js";
|
import { renderDebugKickstart } from "../templates/debug.ks.js";
|
||||||
@@ -215,6 +216,20 @@ echo "==============================="
|
|||||||
|
|
||||||
let script: string;
|
let script: string;
|
||||||
if (os.startsWith("ubuntu")) {
|
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({
|
script = renderUbuntuInstallIpxe({
|
||||||
mac,
|
mac,
|
||||||
hostname,
|
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.
|
* 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");
|
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 () => {
|
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.
|
// The Spark case: machine known to be aarch64, queued for rescue.
|
||||||
state.update((s) => {
|
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 { loadConfig } = await import("../../src/bastion/src/config.js");
|
||||||
const { generateDnsmasqConf, startDnsmasq, stopDnsmasq } = await import("../../src/bastion/src/services/dnsmasq.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 { generateDiscoverKickstart } = await import("../../src/bastion/src/services/kickstart-generator.js");
|
||||||
const { renderBootIpxe } = await import("../../src/bastion/src/templates/boot.ipxe.js");
|
const { renderBootIpxe, kernelPath, initrdPath } = await import("../../src/bastion/src/templates/boot.ipxe.js");
|
||||||
const { kernelPath, initrdPath } = await import("../../src/bastion/src/templates/boot.ipxe.js");
|
// Relative, not "@lab/shared": these tests run from the repo root against sources,
|
||||||
const { SUPPORTED_ARCHES, fedoraMirrorFor } = await import("@lab/shared");
|
// where the workspace package alias is not resolvable.
|
||||||
|
const { SUPPORTED_ARCHES, fedoraMirrorFor } = await import("../../src/shared/src/hardware/index.js");
|
||||||
|
|
||||||
const config = loadConfig({
|
const config = loadConfig({
|
||||||
bastionDir: testDir,
|
bastionDir: testDir,
|
||||||
|
|||||||
Reference in New Issue
Block a user