v0.7.1: walk the ladder once ever — persistent learned modes, rule pins, same-boot iPXE recovery
Answers the operational question 'can a machine try all three boot binaries in one go?' The protocol can't carry three NBPs in one cycle (one boot file per DHCP round, the Secure-Boot refusal happens after handoff with no error report, and the broken-NIC case specifically needs the firmware itself to load builtin-driver iPXE — GRUB's network rides the same broken firmware stack). What we CAN do is make the walk a once-per-machine-ever event and give operators a way to skip it: - Learned driver modes persist (<work_dir>/driver_modes.json). A MAC that reaches the Shim rung, or confirms an iPXE handoff at Builtin, is pinned to disk: immune to the 30-min TTL, reloaded at startup. The file only carries exceptions — a healthy fleet never writes it. Corrupt file starts empty (standard crash-cache policy). - Boot rules gain an optional driver_mode pin (auto/firmware/builtin/ shim), consulted by the DHCP proxy BEFORE the escalation ladder: 'this OUI is a Secure Boot rack -> serve shim immediately' = zero failed cycles. Mode-only rules coexist with target rules (a pin doesn't shadow a later target match). Editor column on Hosts tab. - grub.cfg now tries to chainload all-drivers iPXE before showing the signed menu: with SB off the chainload succeeds and the client gets the full iPXE feature set back in the SAME boot (self-healing for mis-escalations, and the handoff then pins the working mode); with SB on, shim's verifier refuses it inline — no reboot — and the signed menu appears. DhcpProxyServer now takes the escalation table + rules store from main (persistence path comes from the configured work dir). Validation: clippy clean, fmt clean, 299 workspace tests green (+9: persistence round-trip across restart, Shim pin survives TTL, learned Builtin survives TTL, corrupt-file recovery, default-mode-never- persisted, rule-pin matching incl. unknown-mode tolerance and pin/target coexistence, GRUB chainload-before-menu ordering, API round-trip of the driver_mode field). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3a32d65fb7
commit
29040e8a5a
+17
-5
@@ -209,6 +209,13 @@
|
||||
['', 'any arch'], ['bios', 'BIOS'], ['uefi-x64', 'UEFI x64'],
|
||||
['uefi-ia32', 'UEFI IA32'], ['uefi-arm64', 'UEFI ARM64'],
|
||||
];
|
||||
// v0.7.1: optional first-boot binary pin. "Auto" lets the
|
||||
// escalation ladder learn per machine; pinning skips the learning
|
||||
// walk entirely (e.g. a rack known to run Secure Boot → shim).
|
||||
const modeChoices = [
|
||||
['', 'auto (learn)'], ['firmware', 'Firmware NIC'],
|
||||
['builtin', 'iPXE drivers'], ['shim', 'Secure Boot (shim)'],
|
||||
];
|
||||
const rules = (cfg.rules || []).map(r => Object.assign({}, r));
|
||||
const tbody = el('tbody', {});
|
||||
const msg = el('div', {class:'msg'});
|
||||
@@ -224,8 +231,8 @@
|
||||
const redraw = () => {
|
||||
tbody.innerHTML = '';
|
||||
if (!rules.length) {
|
||||
tbody.appendChild(el('tr', {}, el('td', {colspan:'6', class:'empty', style:'padding:14px'},
|
||||
'No rules. Add one to route whole groups of machines (an OUI, an architecture) to a target.')));
|
||||
tbody.appendChild(el('tr', {}, el('td', {colspan:'7', class:'empty', style:'padding:14px'},
|
||||
'No rules. Add one to route whole groups of machines (an OUI, an architecture) to a target — or to pin a boot binary (e.g. Secure Boot racks → shim, zero failed cycles).')));
|
||||
}
|
||||
rules.forEach((r, i) => {
|
||||
const macIn = el('input', {type:'text', spellcheck:'false', placeholder:'aa:bb:cc (prefix)',
|
||||
@@ -235,6 +242,9 @@
|
||||
el('option', Object.assign({value: v}, v === (r.arch || '') ? {selected:''} : {}), label)));
|
||||
const tgtSel = targetSelect(r.target || '');
|
||||
tgtSel.onchange = e => { r.target = e.target.value; };
|
||||
const modeSel = el('select', {onchange: e => { r.driver_mode = e.target.value; }},
|
||||
modeChoices.map(([v, label]) =>
|
||||
el('option', Object.assign({value: v}, v === (r.driver_mode || '') ? {selected:''} : {}), label)));
|
||||
const noteIn = el('input', {type:'text', placeholder:'note',
|
||||
value: r.note || '', oninput: e => { r.note = e.target.value; }});
|
||||
const enabled = el('input', {type:'checkbox', onchange: e => { r.enabled = e.target.checked; }});
|
||||
@@ -243,6 +253,7 @@
|
||||
el('td', {}, macIn),
|
||||
el('td', {}, archSel),
|
||||
el('td', {}, tgtSel),
|
||||
el('td', {}, modeSel),
|
||||
el('td', {}, noteIn),
|
||||
el('td', {style:'text-align:center'}, enabled),
|
||||
el('td', {style:'text-align:right'},
|
||||
@@ -257,8 +268,9 @@
|
||||
redraw();
|
||||
}}, '+ Add rule');
|
||||
const saveBtn = el('button', {onclick: async () => {
|
||||
const bad = rules.find(r => r.enabled !== false && !r.target);
|
||||
if (bad) { msg.textContent = 'Every enabled rule needs a target.'; msg.className = 'msg err'; return; }
|
||||
// A rule needs at least one effect: a target or a boot-binary pin.
|
||||
const bad = rules.find(r => r.enabled !== false && !r.target && !r.driver_mode);
|
||||
if (bad) { msg.textContent = 'Every enabled rule needs a target or a boot-binary pin.'; msg.className = 'msg err'; return; }
|
||||
const r = await putJSON('/api/boot-rules', {rules, webhook_url: webhookInput.value.trim()});
|
||||
if (r.ok) { msg.textContent = 'Saved.'; msg.className = 'msg ok'; }
|
||||
else { msg.textContent = 'Save failed: ' + await r.text(); msg.className = 'msg err'; }
|
||||
@@ -273,7 +285,7 @@
|
||||
el('table', {}, [
|
||||
el('thead', {}, el('tr', {}, [
|
||||
el('th',{},'MAC prefix'), el('th',{},'Arch'), el('th',{},'Target'),
|
||||
el('th',{},'Note'), el('th',{},'On'), el('th',{},''),
|
||||
el('th',{},'Boot binary'), el('th',{},'Note'), el('th',{},'On'), el('th',{},''),
|
||||
])),
|
||||
tbody,
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user