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
9f66c269c4
@@ -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<String> {
|
||||
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 = "\
|
||||
|
||||
Reference in New Issue
Block a user