The dominant field failure from v0.4.63 was "mount.nfs: failed to apply
fstab options" (exit 32), surfaced verbatim by the Storage tab. The
message is misleading — it has nothing to do with /etc/fstab; it comes
from nfs-utils 2.6.x's nfs_options2string() and most commonly indicates
the container is missing CAP_SYS_ADMIN, /etc/mtab is unwritable, or an
auxiliary option triggered an option-transform edge case.
Backend (crates/iso-store/src/nfs.rs):
- TCP pre-flight probe to server:port (4s timeout) before shelling out.
Catches wrong-IP / firewall cases as "cannot reach NFS port" instead
of letting mount.nfs spit out an unhelpful message.
- proto=tcp explicit on NFSv3 (UDP is widely deprecated, modern NAS
appliances often don't bind UDP at all).
- Optional `port` field on NfsAddRequest (defaults to 2049), persisted
on NfsMount.
- On "failed to apply fstab options" / "internal option parsing error"
retry with a minimal option set (vers=N,ro/rw only) — bypasses the
nfs-utils transformation bug; if it still fails we get a real kernel
error to translate.
- hint_for() translates well-known stderr patterns into actionable
guidance — CAP_SYS_ADMIN for option-transform failures, exports-table
for access-denied, export-path hint for "no such file or directory"
(calling out the UniFi UNAS Pro /var/nfs/shared/<name> convention),
etc.
- normalize_server() strips http://, https://, nfs:// schemes the
operator may have pasted by mistake, plus trailing slashes.
API (crates/http-api/src/app.rs):
- api_nfs_add now returns a structured {error, stderr, hint} JSON body
on failure instead of plain text. UI renders the error in bold with
the hint as a dimmer second line.
UI (crates/webui/src/app.js):
- Storage tab's "Mount failed" banner now shows the raw error + hint on
two lines. Each persisted mount row also surfaces last_hint under
last_error.
Terminal (crates/http-api/src/terminal.rs):
- `nfs mount` command prints "hint: ..." on a follow-up line when the
manager returns one.
Tests:
- 8 new tests covering option string (incl. proto=tcp on v3, port=N for
non-default), minimal-options stripping, server normalization, and
hint translation for each well-known stderr pattern.
- All 150 tests pass; clippy -D warnings clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
92 lines
2.4 KiB
TOML
92 lines
2.4 KiB
TOML
[workspace]
|
|
resolver = "2"
|
|
members = [
|
|
"crates/core",
|
|
"crates/dhcp-proxy",
|
|
"crates/tftp",
|
|
"crates/http-api",
|
|
"crates/iso-store",
|
|
"crates/ipxe-assets",
|
|
"crates/webui",
|
|
"crates/openpxe",
|
|
]
|
|
|
|
[workspace.package]
|
|
version = "0.4.64"
|
|
edition = "2021"
|
|
rust-version = "1.95"
|
|
license = "MIT OR Apache-2.0"
|
|
repository = "https://gitea.milesward.dev/mward4/OpenPXE"
|
|
authors = ["OpenPXE contributors"]
|
|
|
|
[workspace.dependencies]
|
|
tokio = { version = "1.40", features = ["full"] }
|
|
tokio-util = { version = "0.7", features = ["io"] }
|
|
tokio-stream = { version = "0.1", features = ["sync"] }
|
|
futures = "0.3"
|
|
async-trait = "0.1"
|
|
|
|
dhcproto = "0.12"
|
|
socket2 = { version = "0.5", features = ["all"] }
|
|
bytes = "1.7"
|
|
nom = "7.1"
|
|
|
|
axum = { version = "0.7", features = ["macros", "multipart", "http2"] }
|
|
tower = "0.5"
|
|
tower-http = { version = "0.6", features = ["fs", "trace", "cors", "limit"] }
|
|
hyper = "1.4"
|
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] }
|
|
mime = "0.3"
|
|
mime_guess = "2.0"
|
|
|
|
serde = { version = "1.0", features = ["derive"] }
|
|
serde_json = "1.0"
|
|
toml = "0.8"
|
|
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
|
|
|
anyhow = "1.0"
|
|
thiserror = "1.0"
|
|
clap = { version = "4.5", features = ["derive", "env"] }
|
|
uuid = { version = "1.10", features = ["v4", "serde"] }
|
|
time = { version = "0.3", features = ["serde", "serde-human-readable", "formatting", "macros"] }
|
|
sha2 = "0.10"
|
|
hex = "0.4"
|
|
bcrypt = "0.15"
|
|
once_cell = "1.19"
|
|
parking_lot = "0.12"
|
|
rust-embed = { version = "8.5", features = ["include-exclude"] }
|
|
|
|
openpxe-core = { path = "crates/core" }
|
|
openpxe-dhcp-proxy = { path = "crates/dhcp-proxy" }
|
|
openpxe-tftp = { path = "crates/tftp" }
|
|
openpxe-http-api = { path = "crates/http-api" }
|
|
openpxe-iso-store = { path = "crates/iso-store" }
|
|
openpxe-ipxe-assets = { path = "crates/ipxe-assets" }
|
|
openpxe-webui = { path = "crates/webui" }
|
|
|
|
[workspace.lints.rust]
|
|
unsafe_code = "deny"
|
|
rust_2018_idioms = { level = "warn", priority = -1 }
|
|
|
|
[workspace.lints.clippy]
|
|
pedantic = { level = "warn", priority = -1 }
|
|
module_name_repetitions = "allow"
|
|
missing_errors_doc = "allow"
|
|
missing_panics_doc = "allow"
|
|
must_use_candidate = "allow"
|
|
doc_markdown = "allow"
|
|
items_after_statements = "allow"
|
|
cast_possible_truncation = "allow"
|
|
cast_lossless = "allow"
|
|
cast_sign_loss = "allow"
|
|
similar_names = "allow"
|
|
too_many_lines = "allow"
|
|
|
|
[profile.release]
|
|
lto = "thin"
|
|
codegen-units = 1
|
|
strip = "symbols"
|
|
opt-level = 3
|