Name update

This commit is contained in:
Miles Ward
2026-05-21 02:13:08 -04:00
parent 91848e02e3
commit 115ba779da
37 changed files with 1387 additions and 589 deletions
+38 -11
View File
@@ -78,7 +78,9 @@ pub fn introspect(path: &Path) -> IntrospectionReport {
let mut haystack = Vec::with_capacity(scan_bytes.min(32 * 1024 * 1024));
while read_total < scan_bytes {
let n = f.read(&mut buf).unwrap_or(0);
if n == 0 { break; }
if n == 0 {
break;
}
haystack.extend_from_slice(&buf[..n]);
read_total += n;
}
@@ -107,8 +109,11 @@ fn family_from_label(label: &str) -> DistroFamily {
let l = label.to_ascii_lowercase();
if l.contains("ubuntu") || l.contains("debian") || l.contains("mint") {
DistroFamily::DebianUbuntu
} else if l.contains("rhel") || l.contains("centos") || l.contains("fedora")
|| l.contains("rocky") || l.contains("alma")
} else if l.contains("rhel")
|| l.contains("centos")
|| l.contains("fedora")
|| l.contains("rocky")
|| l.contains("alma")
{
DistroFamily::RhelFedora
} else if l.contains("suse") || l.contains("opensuse") {
@@ -127,17 +132,30 @@ fn family_from_label(label: &str) -> DistroFamily {
fn guess_kernel_initrd(family: DistroFamily) -> (Option<&'static str>, Vec<&'static str>) {
match family {
DistroFamily::DebianUbuntu => (Some("/casper/vmlinuz"), vec!["/casper/initrd"]),
DistroFamily::RhelFedora => (Some("/images/pxeboot/vmlinuz"), vec!["/images/pxeboot/initrd.img"]),
DistroFamily::OpenSuse => (Some("/boot/x86_64/loader/linux"), vec!["/boot/x86_64/loader/initrd"]),
DistroFamily::Arch => (Some("/arch/boot/x86_64/vmlinuz-linux"), vec!["/arch/boot/x86_64/initramfs-linux.img"]),
DistroFamily::RhelFedora => (
Some("/images/pxeboot/vmlinuz"),
vec!["/images/pxeboot/initrd.img"],
),
DistroFamily::OpenSuse => (
Some("/boot/x86_64/loader/linux"),
vec!["/boot/x86_64/loader/initrd"],
),
DistroFamily::Arch => (
Some("/arch/boot/x86_64/vmlinuz-linux"),
vec!["/arch/boot/x86_64/initramfs-linux.img"],
),
DistroFamily::Alpine => (Some("/boot/vmlinuz-lts"), vec!["/boot/initramfs-lts"]),
DistroFamily::WindowsPe | DistroFamily::Unknown => (None, Vec::new()),
}
}
fn contains_ascii(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || haystack.len() < needle.len() { return false; }
haystack.windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle))
if needle.is_empty() || haystack.len() < needle.len() {
return false;
}
haystack
.windows(needle.len())
.any(|w| w.eq_ignore_ascii_case(needle))
}
#[cfg(test)]
@@ -146,9 +164,18 @@ mod tests {
#[test]
fn label_matching() {
assert_eq!(family_from_label("Ubuntu 24.04"), DistroFamily::DebianUbuntu);
assert_eq!(family_from_label("Rocky-9-x86_64-dvd"), DistroFamily::RhelFedora);
assert_eq!(family_from_label("openSUSE-Leap-15.6"), DistroFamily::OpenSuse);
assert_eq!(
family_from_label("Ubuntu 24.04"),
DistroFamily::DebianUbuntu
);
assert_eq!(
family_from_label("Rocky-9-x86_64-dvd"),
DistroFamily::RhelFedora
);
assert_eq!(
family_from_label("openSUSE-Leap-15.6"),
DistroFamily::OpenSuse
);
assert_eq!(family_from_label("ARCH_202604"), DistroFamily::Arch);
assert_eq!(family_from_label("weird-custom"), DistroFamily::Unknown);
}
+3 -1
View File
@@ -27,5 +27,7 @@ pub use entry::{BootEntry, BootKind, KernelArgs};
pub use introspect::{DistroFamily, IntrospectionReport};
pub use nfs::{NfsAddRequest, NfsManager, NfsMount, NfsVersion};
pub use smb::{extract_windows_iso, SmbManager, SmbState};
pub use store::{generate_boot_entries_for, slugify_str, IsoMeta, IsoSource, IsoStore, UploadHandle};
pub use store::{
generate_boot_entries_for, slugify_str, IsoMeta, IsoSource, IsoStore, UploadHandle,
};
pub use windows::{WimPatcher, WinPatchState};
+3 -9
View File
@@ -36,8 +36,8 @@
use crate::introspect::{introspect, IntrospectionReport};
use crate::store::{generate_boot_entries_for, slugify_str, IsoSource, IsoStore};
use parking_lot::Mutex;
use openpxe_core::{Error, Result};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
@@ -403,14 +403,8 @@ impl NfsManager {
mount_id: m.id.clone(),
relative_path: filename.clone(),
};
self.iso_store.register_external(
id,
filename,
size,
report,
boot_entries,
source,
);
self.iso_store
.register_external(id, filename, size, report, boot_entries, source);
count += 1;
}
Ok(count)
+80 -17
View File
@@ -25,11 +25,11 @@
//! Samba), we return `SmbState::SmbdMissing` and the UI surfaces the
//! gap. No panics, no retries, no silent failure.
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::Arc;
use parking_lot::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case", tag = "state")]
@@ -72,7 +72,9 @@ impl SmbManager {
/// ISO under `smb_dir/<slug>/` becomes a share named `<slug>`. Returns
/// the sorted list.
pub fn discover_shares(&self) -> Vec<String> {
let Ok(rd) = std::fs::read_dir(&self.smb_dir) else { return vec![]; };
let Ok(rd) = std::fs::read_dir(&self.smb_dir) else {
return vec![];
};
let mut out: Vec<String> = rd
.flatten()
.filter(|e| e.path().is_dir())
@@ -130,7 +132,9 @@ impl SmbManager {
let shares = match self.write_conf() {
Ok(v) => v,
Err(e) => {
let s = SmbState::Failed { reason: format!("write smb.conf: {e}") };
let s = SmbState::Failed {
reason: format!("write smb.conf: {e}"),
};
*self.state.lock() = s.clone();
return s;
}
@@ -139,7 +143,8 @@ impl SmbManager {
.args([
"--foreground",
"--no-process-group",
"--configfile", self.conf_path.to_str().unwrap_or(""),
"--configfile",
self.conf_path.to_str().unwrap_or(""),
"--log-stdout",
])
.stdin(Stdio::null())
@@ -156,7 +161,9 @@ impl SmbManager {
s
}
Err(e) => {
let s = SmbState::Failed { reason: format!("spawn smbd: {e}") };
let s = SmbState::Failed {
reason: format!("spawn smbd: {e}"),
};
*self.state.lock() = s.clone();
s
}
@@ -168,11 +175,15 @@ impl SmbManager {
#[allow(unsafe_code)]
pub fn reconcile(&self) -> SmbState {
let mut g = self.child.lock();
if g.is_none() { return self.state.lock().clone(); }
if g.is_none() {
return self.state.lock().clone();
}
let shares = match self.write_conf() {
Ok(v) => v,
Err(e) => {
let s = SmbState::Failed { reason: format!("write smb.conf: {e}") };
let s = SmbState::Failed {
reason: format!("write smb.conf: {e}"),
};
*self.state.lock() = s.clone();
return s;
}
@@ -191,8 +202,13 @@ impl SmbManager {
// covers this is `nix`, which pulls ~40 transitive deps for a
// single signal send. One documented unsafe call is the better
// tradeoff for a container-first project.
unsafe { libc::kill(pid, libc::SIGHUP); }
let s = SmbState::Running { pid: pid as u32, shares };
unsafe {
libc::kill(pid, libc::SIGHUP);
}
let s = SmbState::Running {
pid: pid as u32,
shares,
};
*self.state.lock() = s.clone();
s
} else {
@@ -212,15 +228,19 @@ impl SmbManager {
}
fn smbd_present() -> bool {
let Ok(paths) = std::env::var("PATH") else { return false; };
let Ok(paths) = std::env::var("PATH") else {
return false;
};
for dir in std::env::split_paths(&paths) {
if dir.join("smbd").is_file() { return true; }
if dir.join("smbd").is_file() {
return true;
}
}
false
}
const SMB_CONF_GLOBAL: &str = r"[global]
workgroup = PXEFORGE
workgroup = OPENPXE
server min protocol = SMB2
smb ports = 445
log level = 1
@@ -235,6 +255,15 @@ lock directory = /tmp
state directory = /tmp
cache directory = /tmp
pid directory = /tmp
# WinPE reconnect hardening. Windows Setup can reboot mid-install and
# reconnect from the same IP; stale sessions/oplocks otherwise cause
# intermittent `net use` failures on the second stage.
reset on zero vc = yes
oplocks = no
kernel oplocks = no
level2 oplocks = no
strict locking = no
deadtime = 1
";
/// Extract a Windows ISO at `iso_path` into `smb_dir/<slug>/`. Uses
@@ -245,7 +274,11 @@ pid directory = /tmp
/// Idempotent: if the target dir already contains `sources/boot.wim`, we
/// skip extraction. Callers who want a forced re-extract should remove the
/// dir first.
pub fn extract_windows_iso(iso_path: &Path, smb_dir: &Path, slug: &str) -> std::io::Result<PathBuf> {
pub fn extract_windows_iso(
iso_path: &Path,
smb_dir: &Path,
slug: &str,
) -> std::io::Result<PathBuf> {
let target = smb_dir.join(slug);
if target.join("sources").join("boot.wim").is_file() {
tracing::debug!(target: "openpxe::smb", slug, "ISO already extracted, skipping");
@@ -263,7 +296,9 @@ pub fn extract_windows_iso(iso_path: &Path, smb_dir: &Path, slug: &str) -> std::
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()?;
if out.status.success() { return Ok(target); }
if out.status.success() {
return Ok(target);
}
tracing::warn!(
target: "openpxe::smb",
stderr=%String::from_utf8_lossy(&out.stderr),
@@ -278,7 +313,9 @@ pub fn extract_windows_iso(iso_path: &Path, smb_dir: &Path, slug: &str) -> std::
.args(["-C"])
.arg(&target)
.output()?;
if out.status.success() { return Ok(target); }
if out.status.success() {
return Ok(target);
}
return Err(std::io::Error::other(format!(
"bsdtar failed: {}",
String::from_utf8_lossy(&out.stderr)
@@ -294,7 +331,9 @@ fn which(cmd: &str) -> Option<PathBuf> {
let paths = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&paths) {
let p = dir.join(cmd);
if p.is_file() { return Some(p); }
if p.is_file() {
return Some(p);
}
}
None
}
@@ -320,7 +359,9 @@ mod tests {
let m = SmbManager::new(dir.path().into());
let st = m.start();
// Restore PATH before asserting so any subsequent failure is legible.
if let Some(p) = saved { std::env::set_var("PATH", p); }
if let Some(p) = saved {
std::env::set_var("PATH", p);
}
assert_eq!(st, SmbState::SmbdMissing);
}
@@ -347,5 +388,27 @@ mod tests {
assert!(conf.contains("guest ok = yes"));
assert!(conf.contains("read only = yes"));
assert!(conf.contains("server min protocol = SMB2"));
assert!(conf.contains("workgroup = OPENPXE"));
}
#[test]
fn write_conf_includes_winpe_reconnect_tuning() {
let dir = tempdir().unwrap();
let m = SmbManager::new(dir.path().into());
m.write_conf().unwrap();
let conf = std::fs::read_to_string(dir.path().join("smb.conf")).unwrap();
for expected in [
"reset on zero vc = yes",
"oplocks = no",
"kernel oplocks = no",
"level2 oplocks = no",
"strict locking = no",
"deadtime = 1",
] {
assert!(
conf.contains(expected),
"missing Windows reconnect Samba option {expected} in:\n{conf}"
);
}
}
}
+37 -13
View File
@@ -3,8 +3,8 @@
use crate::entry::{BootEntry, BootKind, KernelArgs};
use crate::introspect::{introspect, DistroFamily, IntrospectionReport};
use bytes::Bytes;
use parking_lot::RwLock;
use openpxe_core::{Error, Result};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
@@ -170,7 +170,9 @@ impl IsoStore {
let mut entries = tokio::fs::read_dir(self.iso_dir.as_path()).await?;
while let Some(e) = entries.next_entry().await? {
let p = e.path();
if p.extension().and_then(|s| s.to_str()) != Some("json") { continue; }
if p.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
if !p
.file_name()
.and_then(|s| s.to_str())
@@ -276,7 +278,10 @@ impl IsoStore {
/// share or unmount the NFS share entirely.
pub async fn delete(&self, id: &str) -> Result<()> {
let meta = self.get(id);
let is_local = matches!(meta.as_ref().map(|m| &m.source), Some(IsoSource::Local) | None);
let is_local = matches!(
meta.as_ref().map(|m| &m.source),
Some(IsoSource::Local) | None
);
if is_local {
let iso = self.iso_path(id);
let meta_path = self.meta_path(id);
@@ -319,9 +324,9 @@ impl IsoStore {
/// to clean out stale entries.
pub fn drop_external_source(&self, mount_id: &str) {
let mut g = self.inner.write();
g.isos.retain(|_, m| {
!matches!(&m.source, IsoSource::Nfs { mount_id: mid, .. } if mid == mount_id)
});
g.isos.retain(
|_, m| !matches!(&m.source, IsoSource::Nfs { mount_id: mid, .. } if mid == mount_id),
);
}
/// Set or clear an ISO's boot password.
@@ -371,7 +376,7 @@ impl IsoStore {
/// Verify a candidate password against the stored bcrypt hash.
/// Returns:
/// - `Ok(true)` — match (or the ISO has no password set; gate is open)
/// - `Ok(true)` — match (or the ISO has no password set; boot is open)
/// - `Ok(false)` — mismatch
/// - `Err(_)` — id not found, or bcrypt error
pub fn verify_password(&self, id: &str, candidate: &str) -> Result<bool> {
@@ -427,7 +432,10 @@ pub fn generate_boot_entries_for(
/// Build `BootEntry`s from the introspection report. URLs are relative —
/// the HTTP layer rewrites them with the public base URL per request.
fn generate_boot_entries(id: &str, filename: &str, r: &IntrospectionReport) -> Vec<BootEntry> {
let title = r.volume_label.clone().unwrap_or_else(|| filename.to_string());
let title = r
.volume_label
.clone()
.unwrap_or_else(|| filename.to_string());
match r.family {
DistroFamily::WindowsPe if r.has_boot_wim => {
// Standard wimboot chain. Paths are in-ISO; the HTTP layer maps
@@ -451,12 +459,22 @@ fn generate_boot_entries(id: &str, filename: &str, r: &IntrospectionReport) -> V
fam if r.kernel_path.is_some() => {
let base = format!("iso/{id}");
let kernel_url = format!("{base}{}", r.kernel_path.as_deref().unwrap_or(""));
let initrd_urls = r.initrd_paths.iter().map(|p| format!("{base}{p}")).collect();
let args = KernelArgs { cmdline: linux_cmdline(fam, id) };
let initrd_urls = r
.initrd_paths
.iter()
.map(|p| format!("{base}{p}"))
.collect();
let args = KernelArgs {
cmdline: linux_cmdline(fam, id),
};
vec![BootEntry {
id: format!("{id}-linux"),
title,
kind: BootKind::LinuxKernel { kernel_url, initrd_urls, args },
kind: BootKind::LinuxKernel {
kernel_url,
initrd_urls,
args,
},
}]
}
_ => {
@@ -465,7 +483,9 @@ fn generate_boot_entries(id: &str, filename: &str, r: &IntrospectionReport) -> V
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") },
kind: BootKind::SanBootIso {
iso_url: format!("iso/{id}.iso"),
},
}]
}
}
@@ -536,7 +556,11 @@ mod tests {
let dir = tempdir().unwrap();
let store = IsoStore::new(dir.path().to_path_buf());
store.ensure_dirs().await.unwrap();
store.inner.write().isos.insert("alpha".into(), fake_meta("alpha"));
store
.inner
.write()
.isos
.insert("alpha".into(), fake_meta("alpha"));
// No password set — verify_password returns Ok(true) for any input.
assert!(store.verify_password("alpha", "anything").unwrap());
+50 -13
View File
@@ -54,7 +54,10 @@ pub struct WimPatcher {
impl WimPatcher {
#[must_use]
pub fn new(smb_host: String, smb_share: String) -> Self {
Self { smb_host, smb_share }
Self {
smb_host,
smb_share,
}
}
/// Apply WinPE patches to `boot.wim` inside `extracted_iso_dir`. Returns
@@ -72,31 +75,40 @@ impl WimPatcher {
let work = match tempfile::tempdir() {
Ok(d) => d,
Err(e) => return WinPatchState::Failed { reason: format!("tempdir: {e}") },
Err(e) => {
return WinPatchState::Failed {
reason: format!("tempdir: {e}"),
}
}
};
// Stage the two files we want present at /Windows/System32/.
let staging = work.path().join("stage/Windows/System32");
if let Err(e) = std::fs::create_dir_all(&staging) {
return WinPatchState::Failed { reason: format!("staging mkdir: {e}") };
return WinPatchState::Failed {
reason: format!("staging mkdir: {e}"),
};
}
if let Err(e) = std::fs::write(staging.join("winpeshl.ini"), WINPESHL_INI) {
return WinPatchState::Failed { reason: format!("write winpeshl.ini: {e}") };
return WinPatchState::Failed {
reason: format!("write winpeshl.ini: {e}"),
};
}
let startnet = render_startnet(&self.smb_host, &self.smb_share);
if let Err(e) = std::fs::write(staging.join("startnet.cmd"), startnet) {
return WinPatchState::Failed { reason: format!("write startnet.cmd: {e}") };
return WinPatchState::Failed {
reason: format!("write startnet.cmd: {e}"),
};
}
// Build a wimlib update command file:
// add <stage>/Windows/System32 /Windows/System32
let update_file = work.path().join("update.cmd");
let update_cmd = format!(
"add \"{}\" \"/Windows/System32\"\n",
staging.display()
);
let update_cmd = format!("add \"{}\" \"/Windows/System32\"\n", staging.display());
if let Err(e) = std::fs::write(&update_file, update_cmd) {
return WinPatchState::Failed { reason: format!("write update.cmd: {e}") };
return WinPatchState::Failed {
reason: format!("write update.cmd: {e}"),
};
}
// Run wimlib-imagex update against image index 2 (WinPE).
@@ -120,7 +132,9 @@ impl WimPatcher {
String::from_utf8_lossy(&o.stderr)
),
},
Err(e) => WinPatchState::Failed { reason: format!("spawn wimlib-imagex: {e}") },
Err(e) => WinPatchState::Failed {
reason: format!("spawn wimlib-imagex: {e}"),
},
}
}
}
@@ -133,7 +147,9 @@ fn which(cmd: &str) -> Option<PathBuf> {
let paths = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&paths) {
let p = dir.join(cmd);
if p.is_file() { return Some(p); }
if p.is_file() {
return Some(p);
}
}
None
}
@@ -174,7 +190,11 @@ fn render_startnet(host: &str, share: &str) -> String {
)
.unwrap();
s.push_str(":havenet\r\n");
writeln!(s, "echo Mapping install media from \\\\{host}\\{share}...\r").unwrap();
writeln!(
s,
"echo Mapping install media from \\\\{host}\\{share}...\r"
)
.unwrap();
writeln!(
s,
":mapshare\r\nnet use Z: \\\\{host}\\{share} /user:guest \"\" /persistent:no && goto mapped\r\n\
@@ -205,6 +225,23 @@ mod tests {
assert!(s.contains("setup.exe"));
}
#[test]
fn startnet_primes_workstation_and_surfaces_mapping_errors() {
let s = render_startnet("10.0.0.5", "win11");
assert!(
s.contains("net start Workstation"),
"WinPE should explicitly start the SMB client before net use:\n{s}"
);
let net_use_line = s
.lines()
.find(|line| line.contains("net use Z:"))
.expect("net use line");
assert!(
!net_use_line.contains(">nul"),
"net use errors must remain visible in WinPE console: {net_use_line}"
);
}
#[test]
fn patcher_reports_wimlib_missing_gracefully() {
// We don't assume wimlib is present in CI; this checks the missing