v0.3.2: OpenPXE naming cleanup and beta hardening
- remove remaining PXEForge/Gate/anvil wording from code, docs, UI, and deployment examples - fix Queue tab view wiring and rename queue-facing terminal/API copy - harden raw ISO Range handling, iPXE fallback lines, and WinPE SMB reconnect behavior - bump workspace and deployment examples to 0.3.2
This commit is contained in:
@@ -23,8 +23,8 @@
|
||||
//! There is intentionally no UI path to upload a custom `.ipxe` script.
|
||||
|
||||
use openpxe_core::{Settings, TimeoutAction};
|
||||
use openpxe_iso_store::{BootEntry, BootKind, IsoMeta};
|
||||
use openpxe_iso_store::introspect::DistroFamily;
|
||||
use openpxe_iso_store::{BootEntry, BootKind, IsoMeta};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
/// Top-level OpenPXE boot menu. Serialized identically for BIOS and UEFI
|
||||
@@ -49,9 +49,15 @@ pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> Str
|
||||
let _ = writeln!(s, "set cls ${{esc:string}}[2J");
|
||||
let _ = writeln!(s, ":menu");
|
||||
let _ = writeln!(s, "menu OpenPXE - network boot menu");
|
||||
let _ = writeln!(s, "item --gap -- ------------------------- Default -------------------------");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"item --gap -- ------------------------- Default -------------------------"
|
||||
);
|
||||
let _ = writeln!(s, "item local Boot from Local HDD");
|
||||
let _ = writeln!(s, "item --gap -- ----------------------- Installers -----------------------");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"item --gap -- ----------------------- Installers -----------------------"
|
||||
);
|
||||
if has_family(isos, is_linux_family) {
|
||||
let _ = writeln!(s, "item linux Linux Installers >");
|
||||
} else {
|
||||
@@ -64,9 +70,15 @@ pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> Str
|
||||
} else {
|
||||
let _ = writeln!(s, "item --gap -- (Windows support disabled in Settings)");
|
||||
}
|
||||
let _ = writeln!(s, "item --gap -- -------------------------- Tools --------------------------");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"item --gap -- -------------------------- Tools --------------------------"
|
||||
);
|
||||
let _ = writeln!(s, "item tools Tools >");
|
||||
let _ = writeln!(s, "item --gap -- ---------------------- Queued Deployment ---------------------");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"item --gap -- ---------------------- Queued Deployment ---------------------"
|
||||
);
|
||||
let _ = writeln!(s, "item queue Queued Deployment (join queue)");
|
||||
let _ = writeln!(s, "item --gap");
|
||||
let _ = writeln!(s, "item --key x exit Exit iPXE");
|
||||
@@ -74,18 +86,39 @@ pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> Str
|
||||
if matches!(settings.timeout_action, TimeoutAction::Stay) {
|
||||
let _ = writeln!(s, "choose --default {default_item} target || goto menu");
|
||||
} else {
|
||||
let _ = writeln!(s, "choose --default {default_item} --timeout {timeout_ms} target || goto menu");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"choose --default {default_item} --timeout {timeout_ms} target || goto menu"
|
||||
);
|
||||
}
|
||||
|
||||
// iPXE's `||` is strict about what follows. Each test uses `goto menu`
|
||||
// as the fallthrough target so the parser never sees a bare `||` with
|
||||
// trailing whitespace — some iPXE builds reject that.
|
||||
let _ = writeln!(s, "iseq ${{target}} local && chain {base}/boot/_local.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} linux && chain {base}/boot/_linux_menu.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} windows && chain {base}/boot/_windows_menu.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} tools && chain {base}/boot/_tools_menu.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} queue && chain {base}/boot/_queue.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} exit && exit || goto menu");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} local && chain {base}/boot/_local.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} linux && chain {base}/boot/_linux_menu.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} windows && chain {base}/boot/_windows_menu.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} tools && chain {base}/boot/_tools_menu.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} queue && chain {base}/boot/_queue.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} exit && exit || goto menu"
|
||||
);
|
||||
let _ = writeln!(s, "goto menu");
|
||||
s
|
||||
}
|
||||
@@ -95,27 +128,41 @@ pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> Str
|
||||
#[must_use]
|
||||
pub fn render_family_menu(isos: &[IsoMeta], base_url: &str, is_windows: bool) -> String {
|
||||
let base = base_url.trim_end_matches('/');
|
||||
let title = if is_windows { "Windows Installers" } else { "Linux Installers" };
|
||||
let title = if is_windows {
|
||||
"Windows Installers"
|
||||
} else {
|
||||
"Linux Installers"
|
||||
};
|
||||
let label = if is_windows { "windows" } else { "linux" };
|
||||
let mut s = String::new();
|
||||
let _ = writeln!(s, "#!ipxe");
|
||||
let _ = writeln!(s, "set base-url {base}");
|
||||
let _ = writeln!(s, ":menu");
|
||||
let _ = writeln!(s, "menu OpenPXE - {title}");
|
||||
let filter: fn(DistroFamily) -> bool =
|
||||
if is_windows { is_windows_family } else { is_linux_family };
|
||||
let filter: fn(DistroFamily) -> bool = if is_windows {
|
||||
is_windows_family
|
||||
} else {
|
||||
is_linux_family
|
||||
};
|
||||
let mut count = 0;
|
||||
for iso in isos {
|
||||
if !filter(iso.introspection.family) { continue; }
|
||||
if !filter(iso.introspection.family) {
|
||||
continue;
|
||||
}
|
||||
for entry in &iso.boot_entries {
|
||||
let size_label = fmt_size_mib(iso.size_bytes);
|
||||
let key = hotkey_for_index(count);
|
||||
// Visual hint: a leading `*` marks password-protected entries.
|
||||
// ASCII only — iPXE's menu console mangles non-ASCII on some
|
||||
// firmwares.
|
||||
let lock = if iso.is_password_protected() { "*" } else { " " };
|
||||
let lock = if iso.is_password_protected() {
|
||||
"*"
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
let _ = writeln!(
|
||||
s, "item {}{} {}[{:>6}] {}",
|
||||
s,
|
||||
"item {}{} {}[{:>6}] {}",
|
||||
key,
|
||||
entry.id,
|
||||
lock,
|
||||
@@ -131,7 +178,10 @@ pub fn render_family_menu(isos: &[IsoMeta], base_url: &str, is_windows: bool) ->
|
||||
let _ = writeln!(s, "item --gap");
|
||||
let _ = writeln!(s, "item --key b back < Back to main menu");
|
||||
let _ = writeln!(s, "choose target || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} back && chain {base}/boot.ipxe || goto menu");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} back && chain {base}/boot.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(s, "chain {base}/boot/${{target}}.ipxe || goto menu");
|
||||
s
|
||||
}
|
||||
@@ -171,12 +221,30 @@ pub fn render_tools_menu(base_url: &str) -> String {
|
||||
let _ = writeln!(s, "item --gap");
|
||||
let _ = writeln!(s, "item --key b back < Back to main menu");
|
||||
let _ = writeln!(s, "choose target || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} util && chain {base}/boot/_util.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} shell && chain {base}/boot/_shell.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} nic && chain {base}/boot/_nic.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} reboot && reboot || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} firmware && exit 0 || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} back && chain {base}/boot.ipxe || goto menu");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} util && chain {base}/boot/_util.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} shell && chain {base}/boot/_shell.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} nic && chain {base}/boot/_nic.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} reboot && reboot || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} firmware && exit 0 || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} back && chain {base}/boot.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(s, "goto menu");
|
||||
s
|
||||
}
|
||||
@@ -190,8 +258,15 @@ pub fn render_local_hdd(base_url: &str) -> String {
|
||||
let mut s = String::new();
|
||||
let _ = writeln!(s, "#!ipxe");
|
||||
let _ = writeln!(s, "# Boot from Local HDD - platform-sensitive");
|
||||
let _ = writeln!(s, "iseq ${{platform}} pcbios && sanboot --no-describe --drive 0x80 || ");
|
||||
let _ = writeln!(s, "# UEFI path: fall through to the firmware's next boot entry");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{platform}} pcbios && sanboot --no-describe --drive 0x80 || goto uefi"
|
||||
);
|
||||
let _ = writeln!(s, ":uefi");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"# UEFI path: fall through to the firmware's next boot entry"
|
||||
);
|
||||
let _ = writeln!(s, "exit 0");
|
||||
let _ = writeln!(s, "# If the above exit returns, loop back to the main menu");
|
||||
let _ = writeln!(s, "chain {base}/boot.ipxe");
|
||||
@@ -212,8 +287,14 @@ pub fn render_util(base_url: &str) -> String {
|
||||
let _ = writeln!(s, "item --gap");
|
||||
let _ = writeln!(s, "item back < Back");
|
||||
let _ = writeln!(s, "choose target || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} memtest && chain {base}/ipxe/memtest.bin || ");
|
||||
let _ = writeln!(s, "iseq ${{target}} back && chain {base}/boot/_tools_menu.ipxe || ");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} memtest && chain {base}/ipxe/memtest.bin || goto menu"
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{target}} back && chain {base}/boot/_tools_menu.ipxe || goto menu"
|
||||
);
|
||||
let _ = writeln!(s, "goto menu");
|
||||
s
|
||||
}
|
||||
@@ -260,7 +341,10 @@ pub fn render_queue_entry(base_url: &str) -> String {
|
||||
let base = base_url.trim_end_matches('/');
|
||||
let mut s = String::new();
|
||||
let _ = writeln!(s, "#!ipxe");
|
||||
let _ = writeln!(s, "# Queued Deployment - join the queue and wait for operator");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"# Queued Deployment - join the queue and wait for operator"
|
||||
);
|
||||
let _ = writeln!(s, "echo Joining deployment queue...");
|
||||
// imgfetch writes the body to a file in iPXE's transient FS; we read
|
||||
// the queue entry id out of the Location-style header by asking the server
|
||||
@@ -277,7 +361,11 @@ pub fn render_entry(entry: &BootEntry, settings: &Settings, base_url: &str) -> S
|
||||
let _ = writeln!(s, "#!ipxe");
|
||||
let _ = writeln!(s, "set base-url {base}");
|
||||
match &entry.kind {
|
||||
BootKind::LinuxKernel { kernel_url, initrd_urls, args } => {
|
||||
BootKind::LinuxKernel {
|
||||
kernel_url,
|
||||
initrd_urls,
|
||||
args,
|
||||
} => {
|
||||
let mut cmdline = args.cmdline.replace("${base-url}", base);
|
||||
if !settings.extra_kernel_args.trim().is_empty() {
|
||||
cmdline.push(' ');
|
||||
@@ -328,7 +416,12 @@ fn has_family(isos: &[IsoMeta], pred: fn(DistroFamily) -> bool) -> bool {
|
||||
}
|
||||
|
||||
fn escape_label(s: &str) -> String {
|
||||
s.chars().map(|c| match c { '\n' | '\r' => ' ', c => c }).collect()
|
||||
s.chars()
|
||||
.map(|c| match c {
|
||||
'\n' | '\r' => ' ',
|
||||
c => c,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Render the password-prompt script for a protected boot entry.
|
||||
@@ -362,7 +455,10 @@ pub fn render_password_prompt(entry_id: &str, iso_filename: &str, base_url: &str
|
||||
let _ = writeln!(s, "echo ==========================================");
|
||||
let _ = writeln!(s, "set password ");
|
||||
let _ = writeln!(s, "read --secret password");
|
||||
let _ = writeln!(s, "iseq ${{password}} \"\" && chain {base}/boot.ipxe || goto submit");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"iseq ${{password}} \"\" && chain {base}/boot.ipxe || goto submit"
|
||||
);
|
||||
let _ = writeln!(s, ":submit");
|
||||
let _ = writeln!(s, "echo Verifying...");
|
||||
let _ = writeln!(
|
||||
@@ -384,7 +480,10 @@ pub fn render_password_failed(entry_id: &str, base_url: &str) -> String {
|
||||
let _ = writeln!(s, "echo");
|
||||
let _ = writeln!(s, "echo Wrong password.");
|
||||
let _ = writeln!(s, "sleep 2");
|
||||
let _ = writeln!(s, "chain {base}/boot/{entry_id}.ipxe || chain {base}/boot.ipxe");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"chain {base}/boot/{entry_id}.ipxe || chain {base}/boot.ipxe"
|
||||
);
|
||||
s
|
||||
}
|
||||
|
||||
@@ -414,4 +513,27 @@ mod password_tests {
|
||||
// Re-target the entry so the prompt flow runs again.
|
||||
assert!(s.contains("chain http://10.0.0.5/boot/alpha-linux.ipxe"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_scripts_do_not_emit_bare_or_trailing_fallbacks() {
|
||||
let settings = Settings::default();
|
||||
let scripts = [
|
||||
render_menu(&[], &settings, "http://10.0.0.5"),
|
||||
render_tools_menu("http://10.0.0.5"),
|
||||
render_local_hdd("http://10.0.0.5"),
|
||||
render_util("http://10.0.0.5"),
|
||||
render_shell("http://10.0.0.5"),
|
||||
render_nic_info("http://10.0.0.5"),
|
||||
render_queue_entry("http://10.0.0.5"),
|
||||
render_password_failed("alpha-linux", "http://10.0.0.5"),
|
||||
];
|
||||
for script in scripts {
|
||||
for line in script.lines() {
|
||||
assert!(
|
||||
!line.trim_end().ends_with("||"),
|
||||
"bare iPXE fallback operator in line: {line}\nscript:\n{script}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user