v0.6.0: bootable-ISO polish + close the 0.5.x chapter
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) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
06695c3d77
commit
0b6d2e377b
Generated
+8
-8
@@ -2669,7 +2669,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe"
|
name = "openpxe"
|
||||||
version = "0.5.9"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2691,7 +2691,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-core"
|
name = "openpxe-core"
|
||||||
version = "0.5.9"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -2718,7 +2718,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-dhcp-proxy"
|
name = "openpxe-dhcp-proxy"
|
||||||
version = "0.5.9"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2732,7 +2732,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-http-api"
|
name = "openpxe-http-api"
|
||||||
version = "0.5.9"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2768,7 +2768,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-ipxe-assets"
|
name = "openpxe-ipxe-assets"
|
||||||
version = "0.5.9"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"openpxe-core",
|
"openpxe-core",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
@@ -2778,7 +2778,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-iso-store"
|
name = "openpxe-iso-store"
|
||||||
version = "0.5.9"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
@@ -2807,7 +2807,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-tftp"
|
name = "openpxe-tftp"
|
||||||
version = "0.5.9"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2821,7 +2821,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-webui"
|
name = "openpxe-webui"
|
||||||
version = "0.5.9"
|
version = "0.6.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "p256"
|
name = "p256"
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.5.9"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ pub enum BootKind {
|
|||||||
wimboot_url: String,
|
wimboot_url: String,
|
||||||
files: Vec<(String, String)>,
|
files: Vec<(String, String)>,
|
||||||
},
|
},
|
||||||
/// Last-resort: SAN-boot the ISO as an emulated CD. Only works for small
|
/// SAN-boot the raw ISO as an emulated CD (iPXE `sanboot`). The emulated
|
||||||
/// ISOs (<~1 GiB) and older distros. Kept for completeness, not the
|
/// CD is backed by on-demand HTTP range reads, so ISO size is *not* a
|
||||||
/// default.
|
/// 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 },
|
SanBootIso { iso_url: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// No Windows-install media and no Linux kernel/initrd. Decide
|
||||||
// lets the ISO at least appear in the menu.
|
// whether the ISO is bootable at all (v0.6.0):
|
||||||
vec![BootEntry {
|
// * `el_torito` — it carries a boot catalog, so iPXE sanboots
|
||||||
id: format!("{id}-sanboot"),
|
// the raw image as an emulated CD: BSDs, ESXi/VMvisor
|
||||||
title: format!("{title} (SAN boot — may fail for >1GiB ISOs)"),
|
// installers, firmware tools, custom spins. The emulated CD
|
||||||
kind: BootKind::SanBootIso {
|
// is backed by HTTP range reads, so ISO size is a non-issue
|
||||||
iso_url: format!("iso/{id}.iso"),
|
// (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}");
|
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 {
|
fn fake_meta(id: &str) -> IsoMeta {
|
||||||
IsoMeta {
|
IsoMeta {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
|
|||||||
@@ -365,7 +365,7 @@
|
|||||||
const settings = status.settings;
|
const settings = status.settings;
|
||||||
const problems = isos.map(i => ({i, b: bootability(i, settings)})).filter(x => !x.b.ok);
|
const problems = isos.map(i => ({i, b: bootability(i, settings)})).filter(x => !x.b.ok);
|
||||||
const problemsBlock = problems.length ? el('div', {class:'card'}, [
|
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'},
|
el('div', {class:'body'},
|
||||||
problems.map(({i, b}) => el('div', {class:'row-warn'},
|
problems.map(({i, b}) => el('div', {class:'row-warn'},
|
||||||
'⚠ ' + i.filename + ' — ' + b.reason)))
|
'⚠ ' + i.filename + ' — ' + b.reason)))
|
||||||
|
|||||||
Reference in New Issue
Block a user