v0.4.66: ship smbclient in the runtime image
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) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
900b65b3ec
commit
1c0c11ccdb
Generated
+8
-8
@@ -1140,7 +1140,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe"
|
name = "openpxe"
|
||||||
version = "0.4.65"
|
version = "0.4.66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1162,7 +1162,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-core"
|
name = "openpxe-core"
|
||||||
version = "0.4.65"
|
version = "0.4.66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
@@ -1181,7 +1181,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-dhcp-proxy"
|
name = "openpxe-dhcp-proxy"
|
||||||
version = "0.4.65"
|
version = "0.4.66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1195,7 +1195,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-http-api"
|
name = "openpxe-http-api"
|
||||||
version = "0.4.65"
|
version = "0.4.66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1226,7 +1226,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-ipxe-assets"
|
name = "openpxe-ipxe-assets"
|
||||||
version = "0.4.65"
|
version = "0.4.66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"openpxe-core",
|
"openpxe-core",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
@@ -1236,7 +1236,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-iso-store"
|
name = "openpxe-iso-store"
|
||||||
version = "0.4.65"
|
version = "0.4.66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
@@ -1260,7 +1260,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-tftp"
|
name = "openpxe-tftp"
|
||||||
version = "0.4.65"
|
version = "0.4.66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1274,7 +1274,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-webui"
|
name = "openpxe-webui"
|
||||||
version = "0.4.65"
|
version = "0.4.66"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.4.65"
|
version = "0.4.66"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -400,7 +400,20 @@ impl SmbShareManager {
|
|||||||
if share.guest {
|
if share.guest {
|
||||||
cmd.arg("-N");
|
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
|
let stdout = child
|
||||||
.stdout
|
.stdout
|
||||||
.take()
|
.take()
|
||||||
@@ -517,9 +530,22 @@ impl SmbShareManager {
|
|||||||
let output = match cmd.output().await {
|
let output = match cmd.output().await {
|
||||||
Ok(o) => o,
|
Ok(o) => o,
|
||||||
Err(e) => {
|
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((
|
return Err((
|
||||||
format!("could not exec smbclient: {e}"),
|
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.
|
/// hints. Returns `None` when we don't have a translation.
|
||||||
fn hint_for(text: &str) -> Option<String> {
|
fn hint_for(text: &str) -> Option<String> {
|
||||||
let s = text.to_ascii_lowercase();
|
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(
|
Some(
|
||||||
"the server rejected the credentials. Double-check the username \
|
"the server rejected the credentials. Double-check the username \
|
||||||
and password — many NAS appliances use a separate SMB account \
|
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());
|
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]
|
#[test]
|
||||||
fn parse_ls_iso_finds_one_iso_and_skips_directories() {
|
fn parse_ls_iso_finds_one_iso_and_skips_directories() {
|
||||||
let out = "\
|
let out = "\
|
||||||
|
|||||||
@@ -81,23 +81,30 @@ FROM debian:12-slim AS runtime
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
ca-certificates libcap2-bin tini gosu iproute2 \
|
ca-certificates libcap2-bin tini gosu iproute2 \
|
||||||
wimtools samba nfs-common \
|
wimtools samba smbclient \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& useradd --system --uid 10001 --home-dir /var/lib/openpxe --shell /usr/sbin/nologin openpxe \
|
&& 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 \
|
&& mkdir -p /var/lib/openpxe/isos /var/lib/openpxe/work /var/lib/openpxe/smb \
|
||||||
&& chown -R openpxe:openpxe /var/lib/openpxe
|
&& chown -R openpxe:openpxe /var/lib/openpxe
|
||||||
# v0.4.5: the openpxe binary itself is now built against musl and is
|
# v0.4.5: the openpxe binary itself is now built against musl and is
|
||||||
# fully static — no glibc dependency. The runtime stage still ships
|
# 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:
|
# functionality we deliberately don't reimplement in-process:
|
||||||
|
#
|
||||||
# wimtools - `wimlib-imagex`, used to inject startnet.cmd into boot.wim.
|
# wimtools - `wimlib-imagex`, used to inject startnet.cmd into boot.wim.
|
||||||
# samba - `smbd` serves extracted Windows install media on :445 so
|
# samba - `smbd` serves extracted Windows install media on :445 so
|
||||||
# WinPE can `net use`. Guest read-only, scoped to
|
# WinPE can `net use`. Guest read-only, scoped to
|
||||||
# /var/lib/openpxe/smb.
|
# /var/lib/openpxe/smb. This package provides the SERVER
|
||||||
# nfs-common - `mount.nfs` / `mount.nfs4` for the Storage tab's NFS
|
# side only; the client CLI is a separate package below.
|
||||||
# share manager. Mount requires CAP_SYS_ADMIN; without it
|
# smbclient - v0.4.66: Samba's `smbclient` userspace CLI, used by
|
||||||
# mount(2) returns EPERM and the manager surfaces a clear
|
# the Storage tab's SMB shares manager to list and stream
|
||||||
# error in the UI.
|
# 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
|
# iproute2 - `ip addr` / `ip route` for the auto-detected Network
|
||||||
# tab fields (NIC name, subnet mask, default gateway).
|
# tab fields (NIC name, subnet mask, default gateway).
|
||||||
# Tiny, always available; we don't pull in netlink crates
|
# 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
|
# gosu - drops privileges cleanly from root after the entrypoint
|
||||||
# fixes bind-mount ownership (common OpenShift/Docker UX
|
# fixes bind-mount ownership (common OpenShift/Docker UX
|
||||||
# issue).
|
# 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
|
# 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.
|
# in-process Rust crates.
|
||||||
|
|
||||||
COPY --from=build /openpxe /usr/local/bin/openpxe
|
COPY --from=build /openpxe /usr/local/bin/openpxe
|
||||||
|
|||||||
Reference in New Issue
Block a user