diff --git a/bastion/src/bastion/src/routes/kickstart.ts b/bastion/src/bastion/src/routes/kickstart.ts index a9ae2b0..543bd65 100644 --- a/bastion/src/bastion/src/routes/kickstart.ts +++ b/bastion/src/bastion/src/routes/kickstart.ts @@ -30,6 +30,9 @@ export function registerKickstartRoutes( hostname: queueEntry?.hostname ?? "lab-node", disk: queueEntry?.disk ?? "", role: queueEntry?.role ?? "worker", + // Progress callbacks must report the mac this install is QUEUED under, + // not whatever NIC enumerates first on the installed system. + ...(mac !== "" ? { mac } : {}), ...(queueEntry?.schemaVersion ? { schemaVersion: queueEntry.schemaVersion } : {}), ...(queueEntry?.dataDisks ? { dataDisks: queueEntry.dataDisks } : {}), }); @@ -197,6 +200,7 @@ esac hostname: queueEntry?.hostname ?? "lab-node", disk: queueEntry?.disk ?? "", role: queueEntry?.role ?? "worker", + ...(mac !== "" ? { mac } : {}), domain: config.domain, ubuntuVersion: config.ubuntuVersion, timezone: config.timezone, diff --git a/bastion/src/bastion/src/services/kickstart-generator.ts b/bastion/src/bastion/src/services/kickstart-generator.ts index 9ce58d1..dd67c79 100644 --- a/bastion/src/bastion/src/services/kickstart-generator.ts +++ b/bastion/src/bastion/src/services/kickstart-generator.ts @@ -24,6 +24,8 @@ export function generateInstallKickstart( hostname: string; disk: string; role: Role; + /** PXE MAC the install is keyed under; baked into progress callbacks. */ + mac?: string; schemaVersion?: 1 | 2; dataDisks?: import("@lab/shared").DataDisks; }, @@ -32,6 +34,7 @@ export function generateInstallKickstart( hostname: params.hostname, disk: params.disk, role: params.role, + ...(params.mac !== undefined && params.mac !== "" ? { mac: params.mac } : {}), ...(params.schemaVersion ? { schemaVersion: params.schemaVersion } : {}), ...(params.dataDisks ? { dataDisks: params.dataDisks } : {}), domain: config.domain, diff --git a/bastion/src/bastion/src/templates/install.ks.ts b/bastion/src/bastion/src/templates/install.ks.ts index 69b26e3..b5cc2f2 100644 --- a/bastion/src/bastion/src/templates/install.ks.ts +++ b/bastion/src/bastion/src/templates/install.ks.ts @@ -12,6 +12,15 @@ export interface InstallKickstartParams { hostname: string; disk: string; role: Role; + /** + * The PXE MAC the bastion keys this install under (install_queue / + * installed). When set, progress callbacks report THIS mac verbatim. + * Runtime detection (`ip link ... exit` = first NIC) is only a fallback: + * on a multi-NIC board the first NIC need not be the PXE NIC, and a + * completion reported under the wrong mac is a silent no-op — the queue + * entry survives and the machine reinstalls forever (thebeast 2026-10-20). + */ + mac?: string; domain: string; fedoraVersion: string; timezone: string; @@ -44,6 +53,7 @@ export function renderInstallKickstart(params: InstallKickstartParams): string { hostname, disk, role, + mac, domain, fedoraVersion, timezone, @@ -55,6 +65,11 @@ export function renderInstallKickstart(params: InstallKickstartParams): string { adminUser, } = params; + // Shell line defining `mac` inside the bastion_progress helpers. + const macLine = mac !== undefined && mac !== "" + ? `local mac="${mac}"` + : `local mac=$(ip link show | awk '/ether/ && !/00:00:00:00/ {print $2; exit}')`; + const fqdn = domain ? `${hostname}.${domain}` : hostname; const vg = "labvg"; const now = new Date().toISOString(); @@ -335,7 +350,7 @@ set -x # Progress callback helper bastion_progress() { local stage="$1" detail="\${2:-}" - local mac=$(ip link show | awk '/ether/ && !/00:00:00:00/ {print $2; exit}') + ${macLine} curl -sf -X POST "http://${serverIp}:${httpPort}/api/progress" \\ -H "Content-Type: application/json" \\ -d "{\\"mac\\":\\"$mac\\",\\"stage\\":\\"$stage\\",\\"detail\\":\\"$detail\\"}" 2>/dev/null || true @@ -553,7 +568,7 @@ set -x # Progress callback helper bastion_progress() { local stage="$1" detail="\${2:-}" - local mac=$(ip link show | awk '/ether/ && !/00:00:00:00/ {print $2; exit}') + ${macLine} curl -sf -X POST "http://${serverIp}:${httpPort}/api/progress" \\ -H "Content-Type: application/json" \\ -d "{\\"mac\\":\\"$mac\\",\\"stage\\":\\"$stage\\",\\"detail\\":\\"$detail\\"}" 2>/dev/null || true diff --git a/bastion/src/bastion/src/templates/ubuntu-autoinstall.ts b/bastion/src/bastion/src/templates/ubuntu-autoinstall.ts index 088e143..6272990 100644 --- a/bastion/src/bastion/src/templates/ubuntu-autoinstall.ts +++ b/bastion/src/bastion/src/templates/ubuntu-autoinstall.ts @@ -9,6 +9,8 @@ export interface UbuntuAutoinstallParams { hostname: string; disk: string; role: Role; + /** PXE MAC the install is keyed under; baked into the progress callback. */ + mac?: string; /** Fresh-install layout. Defaults to 1 so existing callers are unchanged. */ schemaVersion?: 1 | 2; domain: string; @@ -26,6 +28,7 @@ export function renderUbuntuAutoinstall(params: UbuntuAutoinstallParams): string hostname, disk, role, + mac, domain, timezone, serverIp, @@ -34,6 +37,12 @@ export function renderUbuntuAutoinstall(params: UbuntuAutoinstallParams): string adminUser, } = params; + // MAC reported by the completion callback: the queue key when known; + // first-NIC runtime detection only as fallback (wrong on multi-NIC boards). + const reportMac = mac !== undefined && mac !== "" + ? mac + : `$(ip link show | awk "/ether/ && !/00:00:00:00/ {print \\$2; exit}")`; + const fqdn = domain ? `${hostname}.${domain}` : hostname; const vg = "labvg"; // --------------------------------------------------------------------------- @@ -175,7 +184,7 @@ export function renderUbuntuAutoinstall(params: UbuntuAutoinstallParams): string // Progress callback (complete) lateCommands.push( - `curtin in-target -- bash -c 'IP_ADDR=$(ip -4 addr show | awk "/inet / && !/127.0.0/ {split(\\$2,a,\\"/\\"); print a[1]; exit}"); curl -sf -X POST "http://${serverIp}:${httpPort}/api/progress" -H "Content-Type: application/json" -d "{\\"mac\\":\\"$(ip link show | awk "/ether/ && !/00:00:00:00/ {print \\$2; exit}")\\",\\"stage\\":\\"complete\\",\\"detail\\":\\"ready at $IP_ADDR\\"}" || true'`, + `curtin in-target -- bash -c 'IP_ADDR=$(ip -4 addr show | awk "/inet / && !/127.0.0/ {split(\\$2,a,\\"/\\"); print a[1]; exit}"); curl -sf -X POST "http://${serverIp}:${httpPort}/api/progress" -H "Content-Type: application/json" -d "{\\"mac\\":\\"${reportMac}\\",\\"stage\\":\\"complete\\",\\"detail\\":\\"ready at $IP_ADDR\\"}" || true'`, ); // JSON.stringify, not `"${c}"`. JSON is a subset of YAML, so this produces a diff --git a/bastion/src/bastion/tests/kickstart.test.ts b/bastion/src/bastion/tests/kickstart.test.ts index 5a5dada..50e4ed9 100644 --- a/bastion/src/bastion/tests/kickstart.test.ts +++ b/bastion/src/bastion/tests/kickstart.test.ts @@ -215,3 +215,21 @@ describe("renderInstallKickstart", () => { expect(ks).not.toContain("ttyS0"); }); }); + +describe("progress callback mac", () => { + it("bakes the queue mac into every bastion_progress helper when given", () => { + const ks = renderInstallKickstart(baseParams({ mac: "34:97:f6:32:76:e9" })); + const baked = ks.match(/local mac="34:97:f6:32:76:e9"/g) ?? []; + expect(baked.length).toBeGreaterThanOrEqual(2); // %pre and %post helpers + // No runtime first-NIC detection left: on multi-NIC boards the first NIC + // need not be the PXE NIC, and a completion under the wrong mac is a + // silent no-op that reinstalls the machine forever. + expect(ks).not.toContain("local mac=$(ip link show"); + }); + + it("falls back to runtime detection when no mac is known", () => { + const ks = renderInstallKickstart(baseParams()); + expect(ks).toContain("local mac=$(ip link show"); + expect(ks).not.toContain('local mac=""'); + }); +}); diff --git a/bastion/src/bastion/tests/ubuntu-autoinstall.test.ts b/bastion/src/bastion/tests/ubuntu-autoinstall.test.ts index d870962..65151a9 100644 --- a/bastion/src/bastion/tests/ubuntu-autoinstall.test.ts +++ b/bastion/src/bastion/tests/ubuntu-autoinstall.test.ts @@ -182,3 +182,16 @@ describe("Ubuntu reinstall is destructive (known gap)", () => { expect(cfg.find((c) => c["type"] === "disk")?.["wipe"]).toBe("superblock-recursive"); }); }); + +describe("progress callback mac", () => { + it("bakes the queue mac into the completion callback when given", () => { + const doc = renderUbuntuAutoinstall({ ...base, role: "worker", mac: "aa:bb:cc:dd:ee:ff" }); + expect(doc).toContain("aa:bb:cc:dd:ee:ff"); + expect(doc).not.toContain("ip link show"); + }); + + it("falls back to runtime detection when no mac is known", () => { + const doc = renderUbuntuAutoinstall({ ...base, role: "worker" }); + expect(doc).toContain("ip link show"); + }); +});