From 9f66c269c4e9891fb95ea53d56d0ead22f70a8a8 Mon Sep 17 00:00:00 2001 From: Miles Ward Date: Thu, 28 May 2026 11:58:51 -0400 Subject: [PATCH] v0.4.66: ship `smbclient` in the runtime image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.4.65 added the SmbShareManager but the Dockerfile only installed the `samba` package — in Debian 12 that ships the SERVER (smbd) only, not the `smbclient` CLI the new manager shells out to. Every "Add share" attempt surfaced: could not exec smbclient: No such file or directory (os error 2) Fix is two lines: add `smbclient` to the runtime apt install, drop the leftover `nfs-common` (no kernel-mount NFS anymore so the helpers aren't needed). While in the area, harden the manager so future stripped-down runtime images get a useful error instead of a bare exec failure: - `list_isos` and `stream_iso` both detect `ErrorKind::NotFound` on spawn and emit "smbclient binary not found on $PATH". - `hint_for` translates the missing-binary pattern into an actionable hint: "pull OpenPXE v0.4.66+ or add the Debian `smbclient` package to your runtime stage." So even on a custom build the UI still surfaces a clear remediation. Tests: 150 passing (+1 for the new hint). clippy clean. The image is still ~98 MB — `smbclient` adds <1 MB on top of the already-installed samba server. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 16 ++++---- Cargo.toml | 2 +- crates/iso-store/src/smb_share.rs | 68 +++++++++++++++++++++++++++++-- deploy/docker/Dockerfile | 28 +++++++++---- 4 files changed, 94 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1dfb95e..37aff42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1140,7 +1140,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openpxe" -version = "0.4.65" +version = "0.4.66" dependencies = [ "anyhow", "axum", @@ -1162,7 +1162,7 @@ dependencies = [ [[package]] name = "openpxe-core" -version = "0.4.65" +version = "0.4.66" dependencies = [ "anyhow", "bcrypt", @@ -1181,7 +1181,7 @@ dependencies = [ [[package]] name = "openpxe-dhcp-proxy" -version = "0.4.65" +version = "0.4.66" dependencies = [ "anyhow", "bytes", @@ -1195,7 +1195,7 @@ dependencies = [ [[package]] name = "openpxe-http-api" -version = "0.4.65" +version = "0.4.66" dependencies = [ "anyhow", "axum", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "openpxe-ipxe-assets" -version = "0.4.65" +version = "0.4.66" dependencies = [ "openpxe-core", "rust-embed", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "openpxe-iso-store" -version = "0.4.65" +version = "0.4.66" dependencies = [ "anyhow", "bcrypt", @@ -1260,7 +1260,7 @@ dependencies = [ [[package]] name = "openpxe-tftp" -version = "0.4.65" +version = "0.4.66" dependencies = [ "anyhow", "bytes", @@ -1274,7 +1274,7 @@ dependencies = [ [[package]] name = "openpxe-webui" -version = "0.4.65" +version = "0.4.66" [[package]] name = "parking_lot" diff --git a/Cargo.toml b/Cargo.toml index c9d2c00..ca4e699 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ ] [workspace.package] -version = "0.4.65" +version = "0.4.66" edition = "2021" rust-version = "1.95" license = "MIT OR Apache-2.0" diff --git a/crates/iso-store/src/smb_share.rs b/crates/iso-store/src/smb_share.rs index b683884..fa41231 100644 --- a/crates/iso-store/src/smb_share.rs +++ b/crates/iso-store/src/smb_share.rs @@ -400,7 +400,20 @@ impl SmbShareManager { if share.guest { cmd.arg("-N"); } - let mut child = cmd.spawn().map_err(|e| Error::Other(e.into()))?; + // v0.4.66: surface a useful error if smbclient isn't on the + // PATH. Shouldn't happen on the stock image but custom + // builds may strip it. + let mut child = cmd.spawn().map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + Error::Invalid( + "smbclient binary not found on $PATH — install the \ + Debian `smbclient` package or pull OpenPXE v0.4.66+" + .into(), + ) + } else { + Error::Other(e.into()) + } + })?; let stdout = child .stdout .take() @@ -517,9 +530,22 @@ impl SmbShareManager { let output = match cmd.output().await { Ok(o) => o, Err(e) => { + // v0.4.66: distinguish ENOENT (missing binary) from + // other exec failures and pre-fill the hint so the + // UI shows a clear remediation instead of the bare + // "No such file or directory (os error 2)". This + // shouldn't fire on the stock image — the Dockerfile + // installs the `smbclient` package — but is a useful + // breadcrumb for anyone running OpenPXE in a stripped + // base image. + let stderr = if e.kind() == std::io::ErrorKind::NotFound { + "smbclient binary not found on $PATH".to_string() + } else { + String::new() + }; return Err(( format!("could not exec smbclient: {e}"), - String::new(), + stderr, )); } }; @@ -762,7 +788,25 @@ async fn tcp_probe( /// hints. Returns `None` when we don't have a translation. fn hint_for(text: &str) -> Option { let s = text.to_ascii_lowercase(); - if s.contains("nt_status_logon_failure") || s.contains("logon_failure") { + if s.contains("smbclient binary not found") + || s.contains("smbclient: no such file") + || (s.contains("could not exec smbclient") && s.contains("os error 2")) + { + // v0.4.66: this only fires on a stripped / custom runtime + // image — the stock OpenPXE container ships `smbclient` from + // the Debian `smbclient` package. The error surfaced on + // v0.4.65 specifically because that release's Dockerfile + // installed `samba` (the server) but not `smbclient` (the + // client CLI). Operators on the stock image should never see + // this; if they do, the fix is to upgrade. + Some( + "smbclient isn't installed in this container. Pull the \ + official OpenPXE image v0.4.66 or newer — the stock image \ + ships smbclient. If you're running a custom build, add the \ + Debian `smbclient` package to your runtime stage." + .into(), + ) + } else if s.contains("nt_status_logon_failure") || s.contains("logon_failure") { Some( "the server rejected the credentials. Double-check the username \ and password — many NAS appliances use a separate SMB account \ @@ -880,6 +924,24 @@ mod tests { assert!(hint_for("some unrelated error text").is_none()); } + #[test] + fn hint_for_missing_smbclient_calls_out_upgrade() { + // The exec-side path sets `stderr` to "smbclient binary not + // found on $PATH" when ENOENT lands. + let h = hint_for("smbclient binary not found on $PATH").unwrap(); + assert!( + h.contains("smbclient") && h.to_lowercase().contains("install"), + "expected upgrade/install guidance, got: {h}" + ); + // The raw error path on the API side passes the verbatim + // exec error in `error` plus an empty `stderr`. The + // SmbShareError constructor's hint_for fallback checks error + // too, so this pattern needs to translate as well. + let h2 = hint_for("could not exec smbclient: No such file or directory (os error 2)") + .unwrap(); + assert!(h2.contains("smbclient")); + } + #[test] fn parse_ls_iso_finds_one_iso_and_skips_directories() { let out = "\ diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 32344ff..fd2fa56 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -81,23 +81,30 @@ FROM debian:12-slim AS runtime RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates libcap2-bin tini gosu iproute2 \ - wimtools samba nfs-common \ + wimtools samba smbclient \ && rm -rf /var/lib/apt/lists/* \ && useradd --system --uid 10001 --home-dir /var/lib/openpxe --shell /usr/sbin/nologin openpxe \ && mkdir -p /var/lib/openpxe/isos /var/lib/openpxe/work /var/lib/openpxe/smb \ && chown -R openpxe:openpxe /var/lib/openpxe # v0.4.5: the openpxe binary itself is now built against musl and is # fully static — no glibc dependency. The runtime stage still ships -# Debian slim because OpenPXE shells out to the four packages below for +# Debian slim because OpenPXE shells out to the packages below for # functionality we deliberately don't reimplement in-process: +# # wimtools - `wimlib-imagex`, used to inject startnet.cmd into boot.wim. # samba - `smbd` serves extracted Windows install media on :445 so # WinPE can `net use`. Guest read-only, scoped to -# /var/lib/openpxe/smb. -# nfs-common - `mount.nfs` / `mount.nfs4` for the Storage tab's NFS -# share manager. Mount requires CAP_SYS_ADMIN; without it -# mount(2) returns EPERM and the manager surfaces a clear -# error in the UI. +# /var/lib/openpxe/smb. This package provides the SERVER +# side only; the client CLI is a separate package below. +# smbclient - v0.4.66: Samba's `smbclient` userspace CLI, used by +# the Storage tab's SMB shares manager to list and stream +# ISOs from remote SMB servers without ever mounting them +# in the kernel. In Debian 12 `smbclient` is NOT pulled +# in by the `samba` package — they're siblings, not +# parent/child. v0.4.65 shipped without this line and +# every "Add share" attempt surfaced +# `could not exec smbclient: No such file or directory` +# until this landed. # iproute2 - `ip addr` / `ip route` for the auto-detected Network # tab fields (NIC name, subnet mask, default gateway). # Tiny, always available; we don't pull in netlink crates @@ -105,8 +112,13 @@ RUN apt-get update \ # gosu - drops privileges cleanly from root after the entrypoint # fixes bind-mount ownership (common OpenShift/Docker UX # issue). +# +# v0.4.65 dropped `nfs-common` — kernel-mount NFS is gone. The SMB +# shares replacement uses userspace `smbclient` and needs no kernel +# helpers. +# # A future "openpxe-static" variant could drop everything except the -# binary onto distroless once we move the Windows + NFS legs to +# binary onto distroless once we move the Windows + SMB legs to # in-process Rust crates. COPY --from=build /openpxe /usr/local/bin/openpxe