From 5df0fd597221150a6cc09035537a460abd283e19 Mon Sep 17 00:00:00 2001 From: Miles Ward Date: Fri, 5 Jun 2026 13:02:30 -0400 Subject: [PATCH] v0.6.0: bootable-ISO polish + close the 0.5.x chapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on v0.5.9's El Torito detection to make the boot menu honest and clean, and confirms generic El Torito ISOs (ESXi/VMvisor installers, BSDs, firmware tools) boot via iPXE sanboot with no special-casing: - generate_boot_entries: an Unknown-family ISO now produces a sanboot entry only when it's actually bootable — it carries an El Torito catalog, OR it's a remote-share ISO we couldn't introspect (rev 0, assumed bootable). A locally-introspected ISO with no boot catalog (a data/appliance image like a VMware vCenter Server Appliance bundle) yields NO entry, so it stays out of the iPXE menu instead of offering a pick that always fails. ESXi installers (Unknown family + El Torito) surface under the installer menu and sanboot the raw ISO — backed by HTTP range reads, so size is moot. - Dropped the stale "(SAN boot — may fail for >1GiB ISOs)" disclaimer and refreshed the SanBootIso doc: sanboot is the primary path for Windows and any El Torito image, and HTTP range reads remove the size limit. - WebUI: renamed the dashboard panel "Images that won't boot with current settings" -> "Non-bootable images" (there's no setting that would make a data/appliance ISO boot). - Tests: el_torito catalog detection + boot-entry generation across the ESXi / VCSA / remote-share cases. Full v0.5.0->v0.5.9 compatibility sweep: clippy clean; entire workspace test suite green (core 96, http-api 31+68, iso-store 61, dhcp 1, tftp 6, bin 2); app.js syntax-checked. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 16 +++---- Cargo.toml | 2 +- crates/iso-store/src/entry.rs | 8 ++-- crates/iso-store/src/store.rs | 79 +++++++++++++++++++++++++++++++---- crates/webui/src/app.js | 2 +- 5 files changed, 85 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1dbeb6..45e34a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2669,7 +2669,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openpxe" -version = "0.5.9" +version = "0.6.0" dependencies = [ "anyhow", "axum", @@ -2691,7 +2691,7 @@ dependencies = [ [[package]] name = "openpxe-core" -version = "0.5.9" +version = "0.6.0" dependencies = [ "anyhow", "base64", @@ -2718,7 +2718,7 @@ dependencies = [ [[package]] name = "openpxe-dhcp-proxy" -version = "0.5.9" +version = "0.6.0" dependencies = [ "anyhow", "bytes", @@ -2732,7 +2732,7 @@ dependencies = [ [[package]] name = "openpxe-http-api" -version = "0.5.9" +version = "0.6.0" dependencies = [ "anyhow", "axum", @@ -2768,7 +2768,7 @@ dependencies = [ [[package]] name = "openpxe-ipxe-assets" -version = "0.5.9" +version = "0.6.0" dependencies = [ "openpxe-core", "rust-embed", @@ -2778,7 +2778,7 @@ dependencies = [ [[package]] name = "openpxe-iso-store" -version = "0.5.9" +version = "0.6.0" dependencies = [ "anyhow", "bcrypt", @@ -2807,7 +2807,7 @@ dependencies = [ [[package]] name = "openpxe-tftp" -version = "0.5.9" +version = "0.6.0" dependencies = [ "anyhow", "bytes", @@ -2821,7 +2821,7 @@ dependencies = [ [[package]] name = "openpxe-webui" -version = "0.5.9" +version = "0.6.0" [[package]] name = "p256" diff --git a/Cargo.toml b/Cargo.toml index e968fe2..7fcc93d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ ] [workspace.package] -version = "0.5.9" +version = "0.6.0" edition = "2021" rust-version = "1.95" license = "MIT OR Apache-2.0" diff --git a/crates/iso-store/src/entry.rs b/crates/iso-store/src/entry.rs index 0f832af..b098420 100644 --- a/crates/iso-store/src/entry.rs +++ b/crates/iso-store/src/entry.rs @@ -25,9 +25,11 @@ pub enum BootKind { wimboot_url: String, files: Vec<(String, String)>, }, - /// Last-resort: SAN-boot the ISO as an emulated CD. Only works for small - /// ISOs (<~1 GiB) and older distros. Kept for completeness, not the - /// default. + /// SAN-boot the raw ISO as an emulated CD (iPXE `sanboot`). The emulated + /// CD is backed by on-demand HTTP range reads, so ISO size is *not* a + /// constraint — this is the primary path for Windows (v0.5.8) and for any + /// El Torito-bootable image we don't special-case: ESXi/VMvisor + /// installers, BSDs, firmware/diagnostic tools, custom spins (v0.6.0). SanBootIso { iso_url: String }, } diff --git a/crates/iso-store/src/store.rs b/crates/iso-store/src/store.rs index de8fcd6..7afe7cd 100644 --- a/crates/iso-store/src/store.rs +++ b/crates/iso-store/src/store.rs @@ -639,15 +639,33 @@ fn generate_boot_entries(id: &str, filename: &str, r: &IntrospectionReport) -> V }] } _ => { - // Last-resort SAN boot. Won't work for large modern ISOs, but - // lets the ISO at least appear in the menu. - vec![BootEntry { - id: format!("{id}-sanboot"), - title: format!("{title} (SAN boot — may fail for >1GiB ISOs)"), - kind: BootKind::SanBootIso { - iso_url: format!("iso/{id}.iso"), - }, - }] + // No Windows-install media and no Linux kernel/initrd. Decide + // whether the ISO is bootable at all (v0.6.0): + // * `el_torito` — it carries a boot catalog, so iPXE sanboots + // the raw image as an emulated CD: BSDs, ESXi/VMvisor + // installers, firmware tools, custom spins. The emulated CD + // is backed by HTTP range reads, so ISO size is a non-issue + // (this is the same path Windows uses since v0.5.8) — hence + // no more "may fail for >1GiB ISOs" disclaimer. + // * `introspect_rev == 0` — a remote-share ISO we couldn't + // introspect (SMB/NFS/SFTP listings don't seek into the ISO). + // Offer sanboot optimistically rather than hide a + // likely-bootable installer. + // Otherwise it's a local image we *did* introspect and found to + // carry no boot catalog — a data/appliance ISO (e.g. a VMware + // vCenter Server Appliance bundle). It genuinely cannot boot, so + // we expose no menu entry; the dashboard flags it instead. + if r.el_torito || r.introspect_rev == 0 { + vec![BootEntry { + id: format!("{id}-sanboot"), + title, + kind: BootKind::SanBootIso { + iso_url: format!("iso/{id}.iso"), + }, + }] + } else { + Vec::new() + } } } } @@ -721,6 +739,49 @@ mod tests { assert!(!s.contains(" --- "), "stray ---: {s}"); } + #[test] + fn boot_entries_respect_el_torito_and_source() { + use crate::introspect::INTROSPECT_REV; + + // ESXi / VMvisor installer shape: bootable (carries an El Torito + // catalog) but not classifiable as Windows or Linux. Must yield a + // single sanboot entry so it's selectable + boots via emulated CD. + let esxi = IntrospectionReport { + family: DistroFamily::Unknown, + volume_label: Some("ESXI-7.0U3".into()), + el_torito: true, + introspect_rev: INTROSPECT_REV, + ..Default::default() + }; + let e = generate_boot_entries("esxi", "VMware-VMvisor-Installer-7.0U3n.iso", &esxi); + assert_eq!(e.len(), 1, "ESXi should get exactly one boot entry"); + assert!(matches!(e[0].kind, BootKind::SanBootIso { .. })); + // Clean title — no stale ">1GiB may fail" disclaimer. + assert!(!e[0].title.contains("may fail"), "title: {}", e[0].title); + + // VCSA / data-appliance shape: locally introspected (rev set), no + // boot catalog, not Windows/Linux. Genuinely unbootable → no entry, + // so it stays out of the iPXE menu (the dashboard flags it instead). + let vcsa = IntrospectionReport { + family: DistroFamily::Unknown, + el_torito: false, + introspect_rev: INTROSPECT_REV, + ..Default::default() + }; + assert!( + generate_boot_entries("vcsa", "VMware-VCSA-all-8.0.iso", &vcsa).is_empty(), + "data/appliance ISO must produce no boot entry" + ); + + // Remote-share ISO: never introspected (rev 0, no random access over + // SMB/NFS/SFTP). Assume bootable and offer sanboot rather than hide a + // likely-bootable installer. + let remote = IntrospectionReport::default(); + let r = generate_boot_entries("remote", "unknown-remote.iso", &remote); + assert_eq!(r.len(), 1, "remote (uninspected) ISO keeps a sanboot entry"); + assert!(matches!(r[0].kind, BootKind::SanBootIso { .. })); + } + fn fake_meta(id: &str) -> IsoMeta { IsoMeta { id: id.into(), diff --git a/crates/webui/src/app.js b/crates/webui/src/app.js index 1a981d9..667b92e 100644 --- a/crates/webui/src/app.js +++ b/crates/webui/src/app.js @@ -365,7 +365,7 @@ const settings = status.settings; const problems = isos.map(i => ({i, b: bootability(i, settings)})).filter(x => !x.b.ok); const problemsBlock = problems.length ? el('div', {class:'card'}, [ - el('header', {}, [el('h2', {}, 'Images that won\'t boot with current settings')]), + el('header', {}, [el('h2', {}, 'Non-bootable images')]), el('div', {class:'body'}, problems.map(({i, b}) => el('div', {class:'row-warn'}, '⚠ ' + i.filename + ' — ' + b.reason)))