Files
felhom-agent/REPORT.md
T
2026-06-30 11:40:08 +02:00

8.6 KiB
Raw Blame History

REPORT — agent v0.50.0: NAS network storage Part A1 (NFS/SMB automount foundation)

Date: 2026-06-30 Task: TASK.md — NAS network storage Part A1 (agent foundation: automount + bind + liveness). Class: Risky / supervised (creates real network mounts on the Proxmox host). Implemented on main, sim-validated live, sim torn down.

Baseline (confirmed live)

value
Agent main before v0.49.0 (commit 7aeb7ca), live on felhom-pve
Agent after v0.50.0 (commit 63aa63d)
Deployed binary sha256 (felhom-pve) 6ddb6ff2efe6b10377e00ff7612f738ff437dfbcbcc35e3fc566e40d139aedf1
Host client tooling mount.nfs (nfs-common) + mount.cifs (cifs-utils) present

What was built

The agent foundation of SPIKE-nas-storage-2026-06-29.md (verdict READY) — a customer NAS can serve bulk media to a media app. The agent mounts a NAS share host-side under /mnt/felhom-drives/<name> via a systemd .automount (+ .mount) pair; it propagates into guest 9201 for free through the existing shared mp8 bind. A NAS is a distinct storage class — no durable-id, never in the drive enroll/eject/decommission/wipe/SMART/watchdog machinery. Bulk-media class only.

Files changed (commit 63aa63d, +1351/-5)

  • internal/storage/netmount.go (NEW, 511) — NetworkMountSpec, the locked NFS/SMB option sets, the +100000 uid recipe, .mount/.automount rendering, validation, per-share liveness, and the SudoHostOps EnsureNetworkMount/RemoveNetworkMount/ListNetworkMounts methods.
  • internal/storage/mountunit.go — drive-machinery guard: parseFelhomMountUnit explicitly refuses any unit carrying the network marker (Scenario D).
  • internal/localapi/netstorage.go (NEW, 204) — self-scoped POST /netstorage/add, GET /netstorage, POST /netstorage/remove; role gate; out-of-band 0600 SMB creds file.
  • internal/localapi/server.go — wire NetStorage + SmbCredsDir into Options + routes.
  • cmd/felhom-agent/main.gonewHostOps returns *storage.SudoHostOps; wire NetStorage; v0.50.0.
  • internal/config/config.goprivileged.smb_creds_dir (default /var/lib/felhom-agent/smb-creds).
  • configs/felhom-agent.sudoers — new narrow FELHOM_NETMOUNT alias (.automount install/enable/ disable/stop + felhom mount-unit removal); visudo -cf clean on the live host.

Tests — go build ./... && go vet ./... && go test ./... GREEN (build server, Linux)

Unit tests with companion red-proofs:

  • Exact option-set string-asserts — NFS vers=4.1,soft,timeo=50,retrans=2,noatime,_netdev (no hard, no client uid); SMB vers=3.0,credentials=…,uid=101000,gid=101000,forceuid,forcegid,file_mode=0664, dir_mode=0775,_netdev.
  • +100000 companion — container uid 1000 → the SMB unit renders uid=101000; a +0 impl (uid=1000) FAILS the test (the documented non-writable trap). dir_mode must be plain 0775, never setgid 2775.
  • Validation matrix — bad name/traversal/slash/space, bad protocol, server metachar, NFS relative/ traversal export, uid/gid range, SMB-without-creds, SMB bad share name.
  • Role gate — under /mnt/felhom-drives → user-data; anything else → system (refused).
  • Drive-machinery guard (Scenario D) + companionparseFelhomMountUnit refuses a network unit even when contrived with a by-uuid What=; the same content with the drive marker DOES parse — proving the guard (not luck) is the discriminator.
  • Unit round-trip + health + isNetworkMounted; Ensure/Remove command-sequence (mkdir → install ×2 → daemon-reload → enable --now the .automount, never the .mount; remove = stop/disable automount → stop mount → rm ×2 → daemon-reload); bad spec → ZERO commands.
  • localapi: add NFS/SMB happy-path; SMB writes a 0600 creds file + no secret in the response; SMB missing creds → 400; role-gate refusal → 403, surface never touched; list; remove (creds cleaned); not-configured → 503; auth required → 401.

(The two SudoHostOps command-sequence tests skip on Windows — the systemd-escaped unit filename embeds a backslash — and run on the Linux build server, where the full suite is green.)

Live sim validation (Scenarios AD)

Isolated sim NAS: a throwaway privileged LXC (VMID 9300, nas-sim, 192.168.0.114) on felhom-pve running nfs-kernel-server + samba with its own config — it did NOT modify any production host's /etc/exports or smb.conf (the spike's near-miss avoided). Export squashed to anonuid/anongid=101000; SMB force user/group = nasguest (uid/gid 101000). Throwaway creds, out-of-band. Sim destroyed after. Endpoints driven exactly as A2 will (the controller's own local-api token + the real agent server pipeline).

A — NFS host-side mount → propagates → container read+write (the +100000 recipe)

  • POST /netstorage/add (nfs) → host automount installed; first access mounted nfs4 with effective opts vers=4.1,soft,timeo=50,retrans=2,noatime (exact recipe).
  • Guest 9201 saw the nfs4 mount with NO restart (shared-bind propagation); guest view ownership 1000:1000 (the +100000 idmap).
  • A uid-1000 Docker container READ readme.txt AND WROTE ctest.txt (WRITE_OK); the new file landed on the NAS as nasguest:nasguest (101000:101000).

B — SMB fallback read+write

  • POST /netstorage/add (smb) → creds staged 0600 (felhom-agent-owned, out-of-band); mounted cifs with vers=3.0,uid=101000,forceuid,gid=101000,forcegid,file_mode=0664,dir_mode=0775 (exact recipe).
  • uid-1000 container READ + WROTE (SMB_WRITE_OK); landed on the NAS as nasguest (101000). No secret in any response, log, or committed file.

C — failure isolation (the risk)

  • NAS black-holed from felhom-pve (iptables DROP, both directions — a dead-NAS black hole).
  • Clean fail-soft: uncached stat → error No such device in 15s; cat data read → error in 16s (≈ the spike's ~16s). It errors, does not hang.
  • df / did NOT hang (box-wide health intact); guest 9201 responsive + felhom-controller healthy throughout — blast radius contained to the process touching the mount.
  • Per-share liveness reported unreachable for the affected shares only (never box-wide); the endpoint TCP-probe never touched the wedged mount.
  • Automatic recovery on NAS return: a fresh read succeeded with no remount; liveness returned to ok/idle.
  • Note: a first C run hit a 30s ceiling — it coincided with a concurrent scheduled vzdump of 9201 (load 4.7); the clean re-run after the backup matched the spike (~1516s). The box-wide isolation held even under that backup load.

D — the drive machinery ignores the NAS mount

  • GET /disks listed only real drives (felhom-pbs, felhom-usb, local, felhom-flash, local-lvm) — never media/vids; the NAS shares carry no durable-id.
  • The 20 s drive reconcile/ReassertEnrolledMounts tick re-bound only the enrolled drives (felhom-flash/felhom-usb) and never touched the NAS mounts; they remained present and unmodified.

Lifecycle + teardown

  • POST /netstorage/remove (both) → units gone, creds file gone, mounts gone, list empty.
  • Sim LXC 9300 destroyed; all iptables DROP rules removed; helper scripts removed; /mnt/felhom-drives back to only felhom-flash + felhom-usb; agent v0.50.0 active; guest 9201 + controller healthy.

Deploy + clean restart

sudoers (FELHOM_NETMOUNT, visudo -cf clean) and the v0.50.0 binary installed on felhom-pve (prior binary backed up .bak-0.49.0). Restart: capabilities self-check ok=46 total=46 degraded=0, local-api server listening 192.168.0.162:8443, no errors — ReassertEnrolledMounts ran without touching any network mount.

Operational note (pre-existing, not this feature)

During validation df / on felhom-pve showed the root fs at ~100% (vzdump output under /var/lib/vz/ dump on pve-root). Unrelated to network storage (the NAS data lives on the sim, the agent state is tiny) — flagging it as a host backup-retention/disk-pressure item for follow-up.

STOP — not yet built (per the task)

  • A2 (controller "network storage" registry kind + UI + per-share health surface) — NOT built; A2 will drive these A1 endpoints.
  • B (restic-over-SFTP NAS backup target, class 2) — NOT built (separate task).
  • No real customer media app wired to a NAS path — awaiting A2 + a real app.
  • Real-Synology/QNAP (virtual-dsm) GA-fidelity confirmation — out of scope (later pass).

SMB/NFS test credentials were throwaway and out-of-band only. No secrets in any committed file.