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)))