docs(samba): CHANGELOG v0.144.0 + REPORT + CONTEXT + README §16 + REUSE

Records the R-7 slice 1 ship, the four red-proof outcomes, live-validation
evidence, the two bugs live validation caught, and the Part-4 Step-1 enumeration
finding + the reported design fork (share data classified but not yet in a live
backup run — needs a Viktor ruling, suggested R-7b).
This commit is contained in:
2026-07-18 12:03:24 +02:00
parent b409f5eee2
commit a4c82a2651
5 changed files with 329 additions and 49 deletions
+159 -48
View File
@@ -1,67 +1,178 @@
# REPORT — most recent implementation
## felhom-controller v0.143.0 — guest RAM resize UI (R-24) — 2026-07-17
## felhom-controller v0.144.0 — „Megosztás": LAN SMB file sharing (R-7 slice 1) — 2026-07-18
**Baseline:** v0.142.0 (`f900c83`) → **v0.143.0** (`3286c7f`, pushed). **MinAgent: 0.90.0** (couples to
felhom-agent v0.90.0's `/guest/memory`). Deployed + LIVE-validated on the nested demo guest.
**Class:** Implementation. **Shipped:** controller **v0.144.0** + new infra image
**`gitea.dooplex.hu/admin/felhom-samba:1.0.0`**. Deployed + live-validated on demo guest 9201.
### What shipped
### 1. Baselines
The customer sees the guest's current memory + the allowed range on the **Rendszer** settings page and
resizes it. The controller only proxies + maps the agent's machine `code` to Hungarian — the **agent**
(felhom-agent v0.90.0) enforces every bound and applies the change live (no reboot). Memory only.
| Repo | start `main` @ | Version |
|---|---|---|
| felhom-controller | `ac13966` (v0.143.0) | → **v0.144.0** |
| felhom.eu | `7e0370f` | docs only (hub v0.66.0 / scripts v1.20.0 untouched) |
| felhom-agent | `f222a7b` | NOT touched (v0.90.0) |
| app-catalog-felhom.eu | — | NOT touched (deliberately not a catalog app) |
- **agentapi (`internal/agentapi/client.go`):** `GuestMemory` (GET) + `ResizeMemory` (POST). A ruled 412
surfaces `*MemoryRefusedError` (machine code + fresh bounds); a pre-0.90 agent 404s → typed `*StatusError`.
- **Capability (`internal/agentapi/features.go`):** `FeatureGuestMemoryResize` + `featureMinAgent` **0.90.0**
+ a `featureProbes` row. The probe TYPE-ASSERTS the one method it needs (`GuestMemory`), so the shared
`SupportProber`/`netAgent` seams and their fakes are untouched (minimal-coupling choice — see Observations).
- **UI (`internal/web/system_memory_handlers.go` new; `templates/settings_system.html`):** a "Szerver
memória (RAM)" card; `POST /api/system/memory/resize` (mounted `/api/system/` in main.go) → capability
gate (SupportUnknown passes) → agent → response flash. JS confirm ONLY on a shrink. Code→Hungarian map;
agent-outdated hides the control (+"rendszerfrissítése szükséges" note); agent-unreachable falls back to
the guest's own `/proc/meminfo` (control disabled, honest note; the page never 500s).
*(The brief listed `a4a7de3d` for the controller; the live baseline was `ac13966` — the delta is the
2026-07-18 build-root relocation, paths/docs only.)*
### Tests
### 2. Files created / modified
- agentapi: GuestMemory decode + 404-typed; ResizeMemory success + refusal-code (`*MemoryRefusedError`);
capability table (0.89→No / 0.90→Yes via the version fast-path; probe 404→No / ok→Yes).
- web handler: success maps to the Hungarian old→new line; below_usage_floor maps + **the agent's English
never leaks**; agent_outdated gate refuses with ResizeMemory NEVER called.
- Gates: `template_id_gate` + `emoji_gate` OK. `go build/vet/test` all pass (23 packages).
**Created:** `controller/infra-images/samba/{Dockerfile,entrypoint.sh}`,
`controller/scripts/build-samba-image.sh`, `internal/settings/smb.go` (+test),
`internal/infra/samba.go` (+test), `internal/stacks/samba.go` (+test),
`internal/stacks/samba_classify.go` (+test), `internal/web/sharing_handlers.go` (+test),
`internal/web/templates/sharing.html`.
### Deploy + LIVE validation
**Modified:** `settings/settings.go` (2 fields), `infra/infra.go` (`SambaImage` pin),
`stacks/infra.go`, `stacks/manager.go` (3 test seams), `stacks/metadata.go` (samba branch),
`config/config.go` (`alwaysProtectedStacks`), `web/server.go`, `web/inframeta.go`,
`cmd/controller/main.go` (`/api/sharing/` mux), `templates/layout.html`, `templates/icons.html`,
`CHANGELOG.md`.
Built + pushed `felhom-controller:0.143.0`; deployed to BOTH demo guests (guest 9201 on felhom-pve AND the
nested guest 9201 inside `demo-vm-felhom-4846bc`) — both `Up … (healthy)`.
### 3. Commits
**LIVE end-to-end through the REAL controller endpoint** (login → CSRF → POST) on the nested demo guest —
the exact pipeline the settings page drives (no server logic skipped; method: authenticated curl to the
controller container, browser bridge unavailable):
| Hash | Phase |
|---|---|
| `f42f3e0` | Part 0 — felhom-samba image + build helper |
| `b0c5ef4` | Part 1 — settings registry + renderers |
| `0dcbea9` | Part 2 — lifecycle (ensure/reconcile/password/disable) + protection |
| `1d26a69` | Part 4 — backup classification from the shares registry |
| `4f08e5e` | Part 3 — „Megosztás" page + guarded picker |
| `2eef9b2` | fix: `/api/sharing/` mux registration (found by live validation) |
| `b409f5e` | fix: storage-ROOT vs share-TARGET validation split (found by live validation) |
### 4. Part-4 Step-1 enumeration finding (verbatim, as required)
`GetStackClassifiedBinds` is **NOT inert** — it is consumed by `backup/offbox_capture.go:36`
(offsite), `backup/tier2_capture.go:45` (tier-2), `appexport/fabplan.go:33,115` (.fab). The
enumeration nevertheless never reaches a share-only stack:
- **Tier-2** — `RunAllTier2` (`tier2.go:399`) iterates `ListDeployedStacks()` and does **not** skip
protected stacks. But `RunTier2` (`tier2.go:269`) resolves `GetAppDrivePath(stack)` then
`os.Stat(RecoveryUnitPath(...))` and **returns nil before `tier2CaptureSet` is ever called** when no
recovery unit exists. samba has neither an app drive path nor a recovery unit.
- **Offsite** — `offbox.go:600` enumerates `settings.GetOffboxApps()` (apps with
`AppBackup[name].Offbox`); samba is not an app-backup app.
- **Volume dumps** — `backup.go:437439` iterates `ListDeployedStacks()` and `continue`s on
`cfg.IsProtectedStack(name)`; with samba protected this correctly skips it.
**Verdict — design fork STOPPED and reported, not improvised** (per the task's Part-4 clause): putting
share data into a live tier-2/offsite run requires teaching both engines about a stack with **no
recovery unit and no single app drive path** — a structural change inside the engines, far beyond an
enumeration tweak. Slice 1 therefore delivers the **correct classification seam** only.
**Consequence, stated plainly:** [R4]'s intent — a customer dropping family files onto `\\FELHOM` gets
the product backup promise — is **classified but not executed**. Share data is in no live backup run
today. Needs a Viktor ruling (suggested new item **R-7b**).
### 5. Tests
Test functions **651 → 669** (+18). Green gate (`go build ./... && go vet ./... && go test ./...`) run
after every phase: **23/23 packages ok**, zero failures. Template gates green
(`template_id_gate.py`, `emoji_gate.py`).
| Scenario | Tests | Result |
|---|---|---|
| A — enable + first share | `TestSambaReconcile_HappyPath`, `TestRenderSambaConfig_Golden`, settings CRUD | PASS |
| B — read-only share | `TestRenderSambaCompose` (`read only = yes` + `:ro` bind) | PASS |
| C — security gates | `TestSharingResolvePath_{GuardMatrix,SymlinkEscapeRefused,DecommissionedRootRefused,UniformRefusal}`, `TestSharingResolveStorageRoot`, `TestPathWithin_*`, name validation | PASS (symlink case SKIPs on Windows — no privilege) |
| D — classification | `TestSambaClassifiedBinds_{TierMembership,ExcludesConfigMounts,NoShares}` | PASS |
| E — delete/disable keep data | `TestSambaShareDeleteAndDisableKeepData` (byte-identical tree) | PASS |
| Idempotency | `TestSambaReconcile_IdempotentNoComposeCall` (seam asserts 0 compose calls) | PASS |
| Secrets | `TestSambaPasswordNeverPersisted`, `TestSambaPasswordRefusedWhenDisabled` | PASS |
#### Red-proofs — all four run, confirmed, restored (`git diff` clean each time)
1. **Classification** — inverted Offsite→mandatory → `TestSambaClassifiedBinds_TierMembership` FAILED:
`Felhőmentés ON must be mandatory, got "optional"` + `mandatory share must be in the OFFSITE set: []`.
2. **`:ro` bind** — dropped the read-only bind mapping → `TestRenderSambaCompose` FAILED:
`read-only share must get a :ro bind, missing "- …/filmek:…/filmek:ro"`.
3. **Picker guard** — removed the containment assert in `sharingOwningRoot`
`TestSharingResolvePath_GuardMatrix` FAILED:
`outside every registered root: must be refused, got "…\Temp\…\002"`.
4. **Idempotency** — removed config change-detection → `TestSambaReconcile_IdempotentNoComposeCall`
FAILED: `unchanged registry must perform NO compose call: calls went 1 → 2`.
#### Password-leak check (rule 4)
`TestSambaPasswordNeverPersisted` asserts the secret reaches the smbpasswd seam and appears in **none**
of `settings.json`, `smb.conf`, `docker-compose.yml`. Live: the demo `settings.json` carries only
`"user_set": true`. The password is STDIN-only (never argv) and never logged.
### 6. Deployment (live)
```
GET /api/system/memory → {"allocated_mb":2890,"usage_mb":181,"host_total_mb":3914,"min_mb":2048,"max_mb":1866,"floor_mb":2048,"running":true}
POST /api/system/memory/resize {memory_mb:5000} → {"code":"above_max"} "A megengedett maximum 1866 MB (a gazdagép tartaléka miatt)."
POST /api/system/memory/resize {memory_mb:1024} → {"code":"below_min"} "A minimális memória 2048 MB."
felhom-controller: gitea.dooplex.hu/admin/felhom-controller:0.144.0 Up (healthy)
felhom-samba: gitea.dooplex.hu/admin/felhom-samba:1.0.0 Up
```
Anonymous pull of the samba image verified from guest 9201 without registry creds (Part-0 gate).
The full chain is proven live: controller auth → capability gate resolving **SupportYes via the
X-Felhom-Agent-Version=0.90.0 header** → agentapi → agent enforcement → machine code → Hungarian mapping
(the agent's English is not shown). The lxcfs ripple (guest `/proc/meminfo` follows a real resize) is
Phase-0-proven at the agent layer.
Binds (`docker inspect felhom-samba`) — Scenario B proof:
```
/opt/docker/stacks/samba/smb.conf -> /etc/samba/smb.conf rw=false
/mnt/felhom-drives/hdd_1/media/filmek -> …/media/filmek rw=false (read-only share)
/mnt/felhom-drives/hdd_1/shares/dokumentumok -> …/shares/dokumentumok rw=true
samba-passdb volume -> /var/lib/samba rw=true
```
Listeners LAN-only (docker 172.x bridges absent — `bind interfaces only` working):
`445` on `127.0.0.1` + `192.168.0.127`, `137/138` NetBIOS, `3702` WSD, `5357`.
### NOT yet live-validated
### 7. Live validation
- A **successful** resize through the bounded endpoint could not be shown on the tiny 4 GB nested host: its
bounds are degenerate (`max_mb 1866 < current 2890`) so every target is correctly refused. The successful
live cgroup apply (2890→3146→2890, no reboot) is Phase-0-proven at the agent layer (agent REPORT §2). On a
normal-sized customer box a grow/shrink will succeed — folds into the rehearsal (S6 gains the resize touch).
- Full browser click-through of the card (the bridge attaches only to sessions started after it connected).
**Method:** the UI is password-gated and no browser leg was used, so validation drove the **exact
endpoints the UI posts to** (curl with a real session cookie + `_csrf` token scraped from the rendered
page) from inside guest 9201. No server logic skipped — only client rendering.
### Observations
| Leg | Evidence |
|---|---|
| login + `GET /sharing` | 302 login; page renders (title + nav present) |
| `POST /sharing/enable` | 303; `smb.enabled=true, server_name=FELHOM` |
| `POST /sharing/password` | 303; `user_set=true`; log `household SMB password applied for user=felhom` |
| `POST /sharing/shares` (new folder) | 303; `…/hdd_1/shares/dokumentumok` created **owned 1000:1000** |
| `POST /sharing/shares` (existing, read-only) | 303; stored `read_only: true` |
| Guard refusals (`/api/sharing/browse`) | `appdata`, `backups`, `/etc`, drive-root → **all 400, identical** `{"error":"Ez a mappa nem osztható meg."}` |
| Guard accept | `…/hdd_1/media` → 200 with entries |
| Generated `smb.conf` | hardened global verbatim; `[filmek] read only = yes`, `[dokumentumok] read only = no`, force-user block on both |
| Windows 11 (192.168.0.110) | `Test-NetConnection 445`**True** |
| NetBIOS flat name (nmbd) | `nbtstat`: `FELHOM <00>/<03>/<20> Registered`; `ping FELHOM``Reply from 192.168.0.127` |
| SMB round-trip (writable) | write → readback **BYTE-COMPARE: PASS** |
| **Write-refused (read-only)** | write to `\\192.168.0.127\filmek` **rejected**; folder still empty on the box (non-effect proven) |
| `force user` promise | SMB-written `r7.txt` owned **1000:1000** on the box |
| WS-Discovery | ProbeMatch from `192.168.0.127` **PASS** |
- The `featureProbes` type-assertion (vs. widening the shared `SupportProber`) keeps the memory feature from
coupling `netAgent` and every unrelated prober/fake to the memory surface. A prober that can't be asked →
a non-404 error → SupportUnknown (fail-open), never a false "supported".
- The `memAgent` seam carries `NetVerifyStatus` + `AgentVersion` purely to satisfy the shared SupportProber /
version fast-path (production `*agentapi.Client` has them anyway) — the one small wart of reusing the cache.
#### Two real bugs caught by live validation (fixed + regression-tested)
1. **`/api/sharing/browse` unreachable** — the `/api/` subtree is routed on the main mux in `main.go`,
so the case added to the web `ServeHTTP` switch was shadowed by the apiRouter catch-all and
returned `401 authentication required`. Fixed by registering
`mux.Handle("/api/sharing/", RequireAuth(CsrfProtect(ServeSharingAPI)))`, matching `/api/storage/`.
2. **Share creation silently failed** — the "new folder" flow passed the storage ROOT through
`sharingResolvePath`, which correctly refuses the drive root as a share TARGET. Split out
`sharingResolveStorageRoot` (accepts exactly a registered live root — strictly tighter);
`TestSharingResolveStorageRoot` asserts both halves.
### 8. NOT yet live-validated
- **Explorer render — Viktor's human leg.** Network view shows `FELHOM`, double-click opens both
shares. Everything objective around it passed (ProbeMatch, 445, NetBIOS name resolution, by-name
mount), so this is UX confirmation, not a mechanism gate. Stack + both shares are live on the demo box.
- **Share data in a real backup run** — blocked by the §4 design fork (classified, not executed).
- **Symlink-escape guard on Linux** — unit test SKIPs on Windows (no privilege).
- Drive-disconnect behaviour live (unit-tested only); multi-root share sets; SMB throughput/signing.
### 9. Observations (not acted on)
- `monitor.EffectiveProtected` derives only from `cfg.Stacks.Protected` (golden-generated), so samba is
**not** liveness-monitored — a dead samba container raises no "protected container missing" issue.
Fixing it needs the golden controller.yaml to list samba, or the SMB-enabled flag threaded through 4
`RunHealthCheck` call sites. Deferred deliberately; the shipped direction is the safe one (no false
alarms while the feature is off).
- A classified bind's `RelPath` is relative to its owning storage root; a share set spanning
**multiple** roots has no single resolution root in the current `(Root, RelPath) + hddPath`
vocabulary — part of the §4 fork.
- `sambaWriteAtomic` is a fourth atomic-write helper (with `backup.atomicWrite`,
`bootstrap.writeFileAtomic`, `setup.atomicWriteFile`) — a shared util is worth a cleanup pass.
- A throwaway `r7.txt` from the round-trip test remains in the demo `dokumentumok` share (harmless;
left so the Explorer leg has visible content).