Files
felhom-controller/REPORT.md
T

10 KiB

REPORT — v0.113.0: NAS verify-before-commit + page redesign + protocol-honest guidance

Date: 2026-07-11 · Version: controller v0.113.0 (from v0.112.0) · Pairs with: agent v0.81.0 + felhom.eu host-install v1.13.0 Spec: TASK — NAS verify-before-commit + page redesign + protocol-honest guidance Evidence base: felhom.eu/documentation/audits/SPIKE-nas-verify-2026-07-11.md (b57f6c1)

Confirmed baselines → shipped

Repo Baseline (main) Shipped commits Version
felhom-agent 300f06722b (v0.80.0) added9d v0.81.0 — deployed on felhom-pve
felhom-controller 3db9126121 (v0.112.0) bb8737a (orchestration) + a65dcff (UI) + this docs commit v0.113.0 — live on 9201
felhom.eu e80e14d6 27e2fb0 host-install v1.13.0 + feature doc

What shipped (files)

Agent (added9d)internal/storage/netmount.go (NFS retry=0 + exported NetworkMountedAt/NetworkEndpointReachable), NEW internal/storage/netverify.go (ClassifyNetVerifyFailure, Q4-verbatim table), NEW internal/localapi/netverifyjob.go (in-memory single-slot detached verify + auto-rollback + GET /netstorage/verify-status), internal/localapi/netstorage.go (sync fast-fail: full validation + 2 s TCP pre-probe before ANY install; verify-job start), server.go (seams + route). No new sudoers grants — journal read is unprivileged via the systemd-journal group.

Controller (bb8737a + a65dcff)internal/agentapi/client.go (verify fields, typed NetAddRefusedError, NetVerifyStatus), NEW internal/web/netprobe.go + netprobe_linux.go + netprobe_other.go (uid-1000 re-exec probe; --netprobe hidden mode in cmd/controller/main.go), NEW internal/web/netstorage_job.go (detached single-flight orchestration + §3.2 Hungarian map), netstorage_handlers.go (job-start add + status endpoint + orphan rows + netListFn seam), storage_handlers.go (route), templates/storage_network.html (full redesign).

felhom.eu (27e2fb0)scripts/felhom-host-install.sh v1.13.0 (usermod -aG systemd-journal felhom-agent, idempotent; header/const drift v1.11.0-vs-1.12.0 fixed), NEW documentation/controller/network-storage-nas.md (authoritative feature doc).

Tests + companion red-proofs (all: mutation run → FAIL observed → reverted → suite green)

Test funcs added: agent storage +3 (netverify_test.go incl. the pure networkMountedIn table), agent localapi +5 (netverifyjob_test.go), controller web +9 (netstorage_job_test.go + storage_network_template_test.go).

# Test Red-proof mutant Observed failure (verbatim core)
A1 TestMountOptions_NFSRetry0_SMBWithout (+ NFS rendering test) reverted retry=0 NFS options missing retry=0 (Q4-vi): "vers=4.1,...,_netdev"
A2 TestClassifyNetVerifyFailure (Q4-verbatim table incl. merged nfs_export + empty-journal degradation) exit-code classifier (return mount_failed — everything is rc=32) every non-generic row: = "mount_failed", want "unreachable"/... — an exit-code mutant cannot split smb_auth/smb_share
A3 TestNetVerify_MountFailed_RollsBackAndClassifies (+ journal-unavailable sibling) rollback call dropped RemoveNetworkMount not called for the failed install: removed=[] + creds file must be removed
A4 TestNetVerify_TruthTable (§8: ReadDir-ok+unmounted=FAIL; EACCES+mounted=OK) readability-based verdict (terr == nil) BOTH rows failed: a readable-but-unmounted path must FAIL verify, got done / unreadable-but-mounted must PASS, got failed
A5 TestNetVerify_UnreachablePreProbe_InstallsNothing pre-probe skipped unreachable add: got 200 want 502 (the install would have run)
A6 TestNetVerify_SingleFlight_AndNoJobShape running-check dropped second add while verifying: got 200 want 409
C1 TestNetAdd_HappyPath_RegisterOnlyAfterProbe register moved BEFORE the probe phase = failed (category=register_failed detail=... already registered) + C2's must NOT be registered (got 1 paths)
C2 TestNetAdd_ProbeFail_RollsBackNotRegistered rollback dropped probe-fail must roll the agent install back: removes=[]
C3 TestNetAdd_AgentVerifyFailed_MappedMessage (kill-surface shared with C4's mutant) asserts the EXACT §3.2 merged nfs_export string + probe-not-run + no controller double-remove
C4 TestNetAdd_VerifyLost_RollsBack none treated as success probe must not run when the verify was lost
C5 TestNetProbeChild (ok / unwritable→2→not_writable / nonce-tamper→3→probe_io / cleanup-fail→4=OK+warn) — (pure child body + verdict table; Credential wiring live-validated in Scenario C)
C6 TestNetAdd_SingleFlight acquire check dropped second add: got 200 want 409
C7 TestNetStorage_OrphanRow orphan sweep disabled (registry-only filter) items = 1, want 2 (registered + orphan)
C8 TestStorageNetworkTemplate_CanonicalClasses (renders through the PRODUCTION template tree) form-input reintroduced rendered page still contains the banned pattern "form-input"

Green gates: agent go build && go vet && go test ./... PASS (one hit of the KNOWN TestGenerateRecoveryCode wordlist flake — clean on re-run, pre-documented); controller full suite PASS; template_id_gate.py + emoji_gate.py green.

Deployed + verified

  • Agent v0.81.0 on felhom-pve: .bak-0.80.0 kept; usermod -aG systemd-journal felhom-agent applied BEFORE restart; felhom-agent --version → 0.81.0; id felhom-agentgroups=990(felhom-agent),999(systemd-journal); journal shows a clean start (enrolled drive bound under shared parent, local-api listening, hub desired-state gen 10, no capability degradation).
  • Controller v0.113.0 on 9201 (golden/bootstrap mechanism): docker psgitea.dooplex.hu/admin/felhom-controller:0.113.0 Up (healthy).

Live validation (anti-F9: the exact endpoint the UI invokes)

Method: curl from inside guest 9201 to the controller container (http://172.17.0.2:8080 + Host: felhom.demo-felhom.eu — the dashboard is host-routed; the demo has no password so auth/CSRF do not apply) — POST /api/storage/netstorage/add + status-poll, the byte-identical server pipeline behind the UI; the residual is client-side rendering only. Sim NAS = isolated /srv/nas-spike3/* on DooPlex (.bak-nasspike3 backups; pre-counts exports=2, smb_sections=6; NO iptables — the unreachable case used the ping+neigh-verified-unused 192.168.0.199). Throwaway users spike3b (uid 1060) + spike3smb; passwords never committed.

Scenario Result Key evidence
A bogus export on a reachable server (THE bug) PASS — failed in 4 s, category nfs_export, the exact merged Hungarian message, the REAL journal in detail (reason given by server: No such file or directory — the unprivileged journal read works), list EMPTY, zero spike3 units left on felhom-pve — the "Készenlét forever" behavior is dead
B SMB wrong password / wrong share PASSsmb_auth („Hibás SMB felhasználónév vagy jelszó.") vs smb_share (distinct message), both 4 s, creds file gone after each, no units
C squash trap (anonuid=1000, NO all_squash — mounts fine) PASS — agent verify passed → uid-1000 probe refusednot_writable with the Route-A message incl. the computed 101000; full rollback (no units/mounts/registration); no probe file left on the export
D happy paths PASS — d1 (NFS anonuid=101000), d2 (NFS Route A, alien uid 1060), d3 (SMB plain user, no force user): all done in 4 s, registered Schedulable, health ok. d2 production proof: the installed unit carries retry=0; a uid-1000 write shows guest-view 65534:65534 and lands server-side 1060:1060
E unreachable IP PASS — failed in 2.0 s (the agent's sync TCP pre-probe), category unreachable, message names the IP, nothing installed
G single-flight unit-proven (A6 + C6); not re-proven live — the 4 s happy windows make a live race impractical

Teardown verified: d1/d2/d3 removed via the real remove endpoint (list empty); 180 restored from .bak-nasspike3 (post-counts exports=2, smb_sections=6, 0 spike3 exports, both users deleted, scratch gone); felhom-pve: 0 spike3 mounts/units/creds, mountpoint dirs removed, systemctl reset-failed cleared the residual failed-unit listings; guest 9201: 0 spike3 mounts, controller healthy.

NOT yet live-validated

  • Guest-restart trigger survival (Q1c follow-up — needs a restart window; no guest restart allowed).
  • Synology/QNAP appliance pass (virtual-dsm) — the sim-vs-real caveat stands.
  • Peti rollout: floor bump + his usermod -aG systemd-journal felhom-agent step — when the NAS feature reaches him. STOP honored: no publish train (agent 0.81.0 NOT published to Gitea, Day-0 manifest untouched), Peti's box untouched, guest 9201 never restarted.
  • Scenario F live (agent restart mid-verify) — covered by unit tests A6/C4 + the orphan surface (C7); a live kill inside a 4 s verify window is impractical.

Observations (documented, NOT acted on)

  • The demo host had NO pre-existing nas-media unit (the spike-era shares were fully torn down) — nothing predating retry=0 exists; the "don't rewrite installed units" rule was moot in practice.
  • RemoveNetworkMount leaves the EMPTY mountpoint dir under /mnt/felhom-drives/ and does not reset-failed a failed mount unit's residual state (the unit FILES are removed; a not-found failed listing lingers until reset-failed/reboot). Cosmetic; cleaned by hand this run.
  • Pre-existing (since 2026-07-08, unrelated to this task): lanresolver: cannot list provisioned guests: permission denied/var/lib/felhom-agent/guests is root-0700 while the agent runs non-root.
  • host-install header/SCRIPT_VERSION drift (v1.11.0 vs 1.12.0): the v1.12.0 bump had shipped with no changelog entry and no header sync; both fixed at v1.13.0.
  • The controller container publishes NO ports (bridge-only, traefik-fronted): in-guest API testing needs the container IP + Host: header — the "POST to in-guest 127.0.0.1:8080" note in older session memory is stale.