bastion: progress callbacks report the queue mac, not the first NIC
Some checks failed
CI/CD / lint (push) Successful in 1m18s
CI/CD / typecheck (push) Successful in 1m3s
CI/CD / test (push) Successful in 2m4s
CI/CD / build (push) Successful in 2m27s
CI/CD / publish-rpm (push) Failing after 29s
CI/CD / publish-deb (push) Successful in 25s
Some checks failed
CI/CD / lint (push) Successful in 1m18s
CI/CD / typecheck (push) Successful in 1m3s
CI/CD / test (push) Successful in 2m4s
CI/CD / build (push) Successful in 2m27s
CI/CD / publish-rpm (push) Failing after 29s
CI/CD / publish-deb (push) Successful in 25s
The kickstart/autoinstall progress helpers detected their own MAC at runtime (first 'ether' line of ip link). On a multi-NIC board the first NIC need not be the PXE NIC the bastion keyed the install under, so the 'complete' callback arrived under an unknown mac and was a silent no-op: the install_queue entry survived, every reboot re-dispatched the installer, and the machine reinstalled in an endless loop. Observed on thebeast (2 onboard NICs) 2026-10-20; the single-NIC VM drill could not have caught it. The per-MAC /ks and /autoinstall routes already know the queue mac -- bake it into the generated script and keep runtime detection only as the fallback when no mac is known. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DceLLgGmyS7zTGLaLNjQBd
This commit is contained in:
@@ -30,6 +30,9 @@ export function registerKickstartRoutes(
|
|||||||
hostname: queueEntry?.hostname ?? "lab-node",
|
hostname: queueEntry?.hostname ?? "lab-node",
|
||||||
disk: queueEntry?.disk ?? "",
|
disk: queueEntry?.disk ?? "",
|
||||||
role: queueEntry?.role ?? "worker",
|
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?.schemaVersion ? { schemaVersion: queueEntry.schemaVersion } : {}),
|
||||||
...(queueEntry?.dataDisks ? { dataDisks: queueEntry.dataDisks } : {}),
|
...(queueEntry?.dataDisks ? { dataDisks: queueEntry.dataDisks } : {}),
|
||||||
});
|
});
|
||||||
@@ -197,6 +200,7 @@ esac
|
|||||||
hostname: queueEntry?.hostname ?? "lab-node",
|
hostname: queueEntry?.hostname ?? "lab-node",
|
||||||
disk: queueEntry?.disk ?? "",
|
disk: queueEntry?.disk ?? "",
|
||||||
role: queueEntry?.role ?? "worker",
|
role: queueEntry?.role ?? "worker",
|
||||||
|
...(mac !== "" ? { mac } : {}),
|
||||||
domain: config.domain,
|
domain: config.domain,
|
||||||
ubuntuVersion: config.ubuntuVersion,
|
ubuntuVersion: config.ubuntuVersion,
|
||||||
timezone: config.timezone,
|
timezone: config.timezone,
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export function generateInstallKickstart(
|
|||||||
hostname: string;
|
hostname: string;
|
||||||
disk: string;
|
disk: string;
|
||||||
role: Role;
|
role: Role;
|
||||||
|
/** PXE MAC the install is keyed under; baked into progress callbacks. */
|
||||||
|
mac?: string;
|
||||||
schemaVersion?: 1 | 2;
|
schemaVersion?: 1 | 2;
|
||||||
dataDisks?: import("@lab/shared").DataDisks;
|
dataDisks?: import("@lab/shared").DataDisks;
|
||||||
},
|
},
|
||||||
@@ -32,6 +34,7 @@ export function generateInstallKickstart(
|
|||||||
hostname: params.hostname,
|
hostname: params.hostname,
|
||||||
disk: params.disk,
|
disk: params.disk,
|
||||||
role: params.role,
|
role: params.role,
|
||||||
|
...(params.mac !== undefined && params.mac !== "" ? { mac: params.mac } : {}),
|
||||||
...(params.schemaVersion ? { schemaVersion: params.schemaVersion } : {}),
|
...(params.schemaVersion ? { schemaVersion: params.schemaVersion } : {}),
|
||||||
...(params.dataDisks ? { dataDisks: params.dataDisks } : {}),
|
...(params.dataDisks ? { dataDisks: params.dataDisks } : {}),
|
||||||
domain: config.domain,
|
domain: config.domain,
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ export interface InstallKickstartParams {
|
|||||||
hostname: string;
|
hostname: string;
|
||||||
disk: string;
|
disk: string;
|
||||||
role: Role;
|
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;
|
domain: string;
|
||||||
fedoraVersion: string;
|
fedoraVersion: string;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
@@ -44,6 +53,7 @@ export function renderInstallKickstart(params: InstallKickstartParams): string {
|
|||||||
hostname,
|
hostname,
|
||||||
disk,
|
disk,
|
||||||
role,
|
role,
|
||||||
|
mac,
|
||||||
domain,
|
domain,
|
||||||
fedoraVersion,
|
fedoraVersion,
|
||||||
timezone,
|
timezone,
|
||||||
@@ -55,6 +65,11 @@ export function renderInstallKickstart(params: InstallKickstartParams): string {
|
|||||||
adminUser,
|
adminUser,
|
||||||
} = params;
|
} = 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 fqdn = domain ? `${hostname}.${domain}` : hostname;
|
||||||
const vg = "labvg";
|
const vg = "labvg";
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
@@ -335,7 +350,7 @@ set -x
|
|||||||
# Progress callback helper
|
# Progress callback helper
|
||||||
bastion_progress() {
|
bastion_progress() {
|
||||||
local stage="$1" detail="\${2:-}"
|
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" \\
|
curl -sf -X POST "http://${serverIp}:${httpPort}/api/progress" \\
|
||||||
-H "Content-Type: application/json" \\
|
-H "Content-Type: application/json" \\
|
||||||
-d "{\\"mac\\":\\"$mac\\",\\"stage\\":\\"$stage\\",\\"detail\\":\\"$detail\\"}" 2>/dev/null || true
|
-d "{\\"mac\\":\\"$mac\\",\\"stage\\":\\"$stage\\",\\"detail\\":\\"$detail\\"}" 2>/dev/null || true
|
||||||
@@ -553,7 +568,7 @@ set -x
|
|||||||
# Progress callback helper
|
# Progress callback helper
|
||||||
bastion_progress() {
|
bastion_progress() {
|
||||||
local stage="$1" detail="\${2:-}"
|
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" \\
|
curl -sf -X POST "http://${serverIp}:${httpPort}/api/progress" \\
|
||||||
-H "Content-Type: application/json" \\
|
-H "Content-Type: application/json" \\
|
||||||
-d "{\\"mac\\":\\"$mac\\",\\"stage\\":\\"$stage\\",\\"detail\\":\\"$detail\\"}" 2>/dev/null || true
|
-d "{\\"mac\\":\\"$mac\\",\\"stage\\":\\"$stage\\",\\"detail\\":\\"$detail\\"}" 2>/dev/null || true
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export interface UbuntuAutoinstallParams {
|
|||||||
hostname: string;
|
hostname: string;
|
||||||
disk: string;
|
disk: string;
|
||||||
role: Role;
|
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. */
|
/** Fresh-install layout. Defaults to 1 so existing callers are unchanged. */
|
||||||
schemaVersion?: 1 | 2;
|
schemaVersion?: 1 | 2;
|
||||||
domain: string;
|
domain: string;
|
||||||
@@ -26,6 +28,7 @@ export function renderUbuntuAutoinstall(params: UbuntuAutoinstallParams): string
|
|||||||
hostname,
|
hostname,
|
||||||
disk,
|
disk,
|
||||||
role,
|
role,
|
||||||
|
mac,
|
||||||
domain,
|
domain,
|
||||||
timezone,
|
timezone,
|
||||||
serverIp,
|
serverIp,
|
||||||
@@ -34,6 +37,12 @@ export function renderUbuntuAutoinstall(params: UbuntuAutoinstallParams): string
|
|||||||
adminUser,
|
adminUser,
|
||||||
} = params;
|
} = 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 fqdn = domain ? `${hostname}.${domain}` : hostname;
|
||||||
const vg = "labvg";
|
const vg = "labvg";
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -175,7 +184,7 @@ export function renderUbuntuAutoinstall(params: UbuntuAutoinstallParams): string
|
|||||||
|
|
||||||
// Progress callback (complete)
|
// Progress callback (complete)
|
||||||
lateCommands.push(
|
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
|
// JSON.stringify, not `"${c}"`. JSON is a subset of YAML, so this produces a
|
||||||
|
|||||||
@@ -215,3 +215,21 @@ describe("renderInstallKickstart", () => {
|
|||||||
expect(ks).not.toContain("ttyS0");
|
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=""');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -182,3 +182,16 @@ describe("Ubuntu reinstall is destructive (known gap)", () => {
|
|||||||
expect(cfg.find((c) => c["type"] === "disk")?.["wipe"]).toBe("superblock-recursive");
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user