Files
felhom-controller/REPORT.md
T

124 lines
10 KiB
Markdown

# 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-agent`
`groups=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 ps`
`gitea.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 | **PASS**`smb_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 refused**`not_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.