v0.4.65: Local directory ISO source (bind-mount workaround for Unraid)
Field report: even with CAP_SYS_ADMIN and full --privileged, NFS mounts
inside the OpenPXE container fail on Unraid with the same
"failed to apply fstab options" error v0.4.64 added diagnostics for.
The root cause is the host kernel: Unraid's base kernel ships without
the nfs/nfsv4 client modules loaded. Capabilities are necessary but
not sufficient; the modules have to be present on the host kernel for
in-container mount(2) to do anything. No container-side change can
fix that.
This is exactly the case every other PXE/imaging tool sidesteps
(Bootimus uses SMB; iVentoy, FOG, MAAS, Cobbler all rely on the host
to mount network storage and bind-mount the path into the imaging
service). v0.4.65 brings OpenPXE in line with that pattern.
What's new:
* `IsoSource::LocalDir { dir_id, relative_path }` — third source kind
alongside `Local` (uploaded) and `Nfs` (in-container mount).
* `LocalDirManager` (crates/iso-store/src/local_dir.rs) — registers
bind-mounted directories, validates them (absolute path, exists, is
a directory, readable), scans for *.iso files, registers them with
IsoStore. Persisted to <work_dir>/local_dirs.json so the relationship
survives restarts.
* `NfsHostCaps::detect()` — pure read of /proc/filesystems on startup.
Surfaced via GET /api/nfs/capabilities and used by the Storage tab to
show a prominent red banner above the NFS form when in-container
mounts cannot possibly work, pointing the operator at the Local
Directories card as the recommended path.
* Four new API routes:
GET /api/nfs/capabilities
GET /api/local-dirs
POST /api/local-dirs { path, label? }
DELETE /api/local-dirs/:id
POST /api/local-dirs/:id/scan
UI changes (crates/webui/src/app.js):
* Storage tab: new "Local directories" card under the NFS card with
the bind-mount form, an explainer paragraph (with the Docker
`-v /mnt/user/isos:/mnt/external-isos` command), and the list of
registered directories with rescan + remove actions.
* When NFS host caps are unavailable, the NFS card sprouts a red
banner explaining what's wrong and pointing at the local-dir
workaround. The card sub-header also flips to "N registered ·
recommended on this host".
* ISO table: new "dir:<id>" source badge; on-disk ISOs show "on disk"
in the actions column instead of a delete button (same pattern as
NFS — OpenPXE doesn't own those bytes).
* API reference table picks up the four new endpoints + a hint about
the new `port` field on NFS add.
Tests (+12, total 162):
* iso-store: 7 local_dir unit tests covering relative-path rejection,
missing path, non-directory file, empty-directory success, default
label, idempotent re-add, remove + iso-path-resolution clear.
* iso-store: 1 nfs unit test confirming NfsHostCaps::detect() never
panics and the boolean accessors are consistent.
* http-api: 4 integration tests covering /api/nfs/capabilities,
/api/local-dirs list/add/remove + relative-path 400.
`cargo clippy --workspace --all-targets -- -D warnings` clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0afbe860e8
commit
761489761c
@@ -17,8 +17,15 @@ use tokio::io::AsyncWriteExt;
|
||||
///
|
||||
/// The default is `Local` — uploaded ISOs sit in `<iso_dir>/<id>.iso`.
|
||||
/// `Nfs` entries point at a file inside a remote share that the
|
||||
/// `NfsManager` is keeping mounted. We resolve the on-disk path lazily
|
||||
/// in [`IsoStore::iso_path_for`] using the `nfs_root` set at startup.
|
||||
/// `NfsManager` is keeping mounted. `LocalDir` (v0.4.65) entries point
|
||||
/// at a file inside an operator-registered directory that's
|
||||
/// **bind-mounted into the container** from the host — the workaround
|
||||
/// for environments (Unraid, OpenShift restricted SCC) where the host
|
||||
/// kernel doesn't have NFS client modules loaded and in-container
|
||||
/// mounts can't succeed regardless of CAP_SYS_ADMIN.
|
||||
///
|
||||
/// We resolve every variant's on-disk path lazily in
|
||||
/// [`IsoStore::iso_path_for`].
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum IsoSource {
|
||||
@@ -29,6 +36,13 @@ pub enum IsoSource {
|
||||
/// Path relative to the mount point — typically just the filename.
|
||||
relative_path: String,
|
||||
},
|
||||
/// v0.4.65: operator-registered host directory bind-mounted into the
|
||||
/// container. Path resolution looks up `dir_id` in the IsoStore's
|
||||
/// `local_dir_roots` map, then joins `relative_path`.
|
||||
LocalDir {
|
||||
dir_id: String,
|
||||
relative_path: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Where the ISO lands in the PXE menu hierarchy.
|
||||
@@ -168,6 +182,10 @@ pub struct IsoStore {
|
||||
/// [`IsoStore::set_nfs_root`]; required for resolving any
|
||||
/// `IsoSource::Nfs` entry.
|
||||
nfs_root: Arc<RwLock<Option<PathBuf>>>,
|
||||
/// v0.4.65: map of operator-registered directory id → absolute path
|
||||
/// inside the container. Used to resolve `IsoSource::LocalDir`
|
||||
/// entries to a real on-disk path. Maintained by `LocalDirManager`.
|
||||
local_dir_roots: Arc<RwLock<HashMap<String, PathBuf>>>,
|
||||
inner: Arc<RwLock<Inner>>,
|
||||
}
|
||||
|
||||
@@ -176,10 +194,26 @@ impl IsoStore {
|
||||
Self {
|
||||
iso_dir: Arc::new(iso_dir),
|
||||
nfs_root: Arc::new(RwLock::new(None)),
|
||||
local_dir_roots: Arc::new(RwLock::new(HashMap::new())),
|
||||
inner: Arc::new(RwLock::new(Inner::default())),
|
||||
}
|
||||
}
|
||||
|
||||
/// v0.4.65: register the bind-mounted host path under `dir_id` so
|
||||
/// `iso_path_for` can resolve `IsoSource::LocalDir` entries. Called
|
||||
/// by `LocalDirManager` when a directory is added or reloaded from
|
||||
/// persisted state.
|
||||
pub fn register_local_dir(&self, dir_id: String, path: PathBuf) {
|
||||
self.local_dir_roots.write().insert(dir_id, path);
|
||||
}
|
||||
|
||||
/// v0.4.65: drop the path mapping for `dir_id`. Existing
|
||||
/// `IsoSource::LocalDir` entries referencing this id will start
|
||||
/// resolving to `None` from `iso_path_for`.
|
||||
pub fn unregister_local_dir(&self, dir_id: &str) {
|
||||
self.local_dir_roots.write().remove(dir_id);
|
||||
}
|
||||
|
||||
/// Tell the store where NFS mounts live. Without this set,
|
||||
/// `IsoSource::Nfs` entries cannot be resolved to a file path.
|
||||
pub fn set_nfs_root(&self, root: PathBuf) {
|
||||
@@ -297,6 +331,19 @@ impl IsoStore {
|
||||
let root = self.nfs_root.read().clone()?;
|
||||
root.join(mount_id).join(relative_path)
|
||||
}
|
||||
// v0.4.65: bind-mounted host directory. We look the dir_id
|
||||
// up in local_dir_roots which is maintained by
|
||||
// LocalDirManager. If LocalDirManager hasn't loaded yet (or
|
||||
// the operator removed the directory) the resolution
|
||||
// returns None and the HTTP handler 404s, same as a missing
|
||||
// NFS mount.
|
||||
IsoSource::LocalDir {
|
||||
dir_id,
|
||||
relative_path,
|
||||
} => {
|
||||
let root = self.local_dir_roots.read().get(dir_id).cloned()?;
|
||||
root.join(relative_path)
|
||||
}
|
||||
};
|
||||
if path.exists() {
|
||||
Some(path)
|
||||
@@ -362,6 +409,16 @@ impl IsoStore {
|
||||
);
|
||||
}
|
||||
|
||||
/// v0.4.65: drop every entry that belongs to a registered local
|
||||
/// directory. Used by `LocalDirManager` when an operator removes a
|
||||
/// directory or before re-scanning to clean out stale entries.
|
||||
pub fn drop_local_dir_source(&self, dir_id: &str) {
|
||||
let mut g = self.inner.write();
|
||||
g.isos.retain(
|
||||
|_, m| !matches!(&m.source, IsoSource::LocalDir { dir_id: did, .. } if did == dir_id),
|
||||
);
|
||||
}
|
||||
|
||||
/// Set or clear an ISO's boot password.
|
||||
///
|
||||
/// `Some("plaintext")` hashes via bcrypt (cost 10 — fast enough for
|
||||
|
||||
Reference in New Issue
Block a user