Full-feature validation across the running demo: PBS backup, komga/gitea HC fixes, deploy sweep, backup+restore, DR rebuild, storage, monitoring/alerts/hub, guest reboot. Headline finding: controller->agent local-API (8443) unreachable from the controller container, gating storage UI + host metrics + whole-guest backup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
18 KiB
Unattended test campaign — full-feature validation (demo guest 9201) — 2026-06-22
Status: empirical findings from an unattended live test campaign (2026-06-22). Operator away, glancing remotely. Goal: maximize feature coverage across the running demo (Felhom on Proxmox: host agent
felhom-agent v0.39.0onfelhom-pve, in-guest controllerfelhom-controller v0.73.0in LXC 9201, hubfelhom-hubon k3s/DooPlex) without leaving the demo unrecoverably broken. Only code change permitted: the komga catalog healthcheck fix (+ one analogous gitea HC fix the sweep surfaced, which the runbook sanctions). No controller/agent build. All destructive DR work targeted throwaway guests; the live demo was restored to baseline (and is healthier — komga fixed).Companion live report (per-check raw evidence, continuously updated during the run):
felhom-controller/TEST-REPORT.md. This document is the consolidated findings writeup.
Topology under test
- Host / agent:
felhom-pve(demo-felhom, PVE 9.2.2, Intel N100, 16 GB),felhom-agent v0.39.0(systemd active). SSHroot@felhom-pve. - Guest / controller: LXC 9201 (
demo-felhom, 12 GB, 32 GB rootfs + 256 GB/var/lib/dockervol),felhom-controller:0.73.0(docker container, no auth wall currently). Customer iddemo-felhom. - Hub:
felhom-hubon k3s @ DooPlex192.168.0.180;hub.felhom.eu. - Backup target: PBS
felhom-pbs(datastorefelhom-spikeon DooPlex), 9.6 TB, 21 % used. - Drives (enrolled):
felhom-flash(USB,uuid:81a26531…,/mnt/felhom-drives/felhom-flash, 117 GB, holds app userdata for 8 apps) ·felhom-usb(local-dir,uuid:da9e7089…,/mnt/felhom-drives/felhom-usb, 916 GB, empty) ·local-lvm·local·felhom-pbs. - Baseline apps: 14 deployed app stacks (25 containers incl. 4 infra: controller, traefik,
cloudflared, filebrowser); 55 in catalog, 38 installable. komga
unhealthyat baseline.
Access note — how the UI was driven
On the LAN, split-horizon DNS (resolver 192.168.0.250) maps *.demo-felhom.eu → guest 9201
(192.168.0.141), bypassing Cloudflare. The guest's traefik :443 (websecure) returns Go
404 page not found for every host (controller and apps alike) while serving a valid LE
*.demo-felhom.eu cert; :80 301-redirects to https → so a browser dead-ends at the 404 on-LAN.
Workaround: a short-lived python TCP forwarder inside 9201 (0.0.0.0:18080 → controller
<container-ip>:8080); the browser hit http://felhom.demo-felhom.eu:18080/ (LAN DNS → guest:18080),
and the controller's CatchAllMiddleware strips the port so the Host matches felhom.<domain> and the
dashboard serves over plain HTTP. Forwarder removed at end of run. The controller dashboard is
reachable internally at traefik file-route Host(felhom.demo-felhom.eu) → felhom-controller:8080.
Phase 0 — Baseline + PBS safety-net backup
| Check | Result | Evidence |
|---|---|---|
| Baseline capture | ✅ | agent v0.39.0; ctrl v0.73.0; 25 containers, komga unhealthy; drives + pvesm healthy |
| Fresh PBS whole-CT backup of 9201 | ✅ | felhom-agent --selftest=backup -vmid=9201 → felhom-pbs:backup/ct/9201/2026-06-22T12:45:37Z, 19.62 GB, crash-consistent, 161 s. Uncovered (by design): /etc/felhom-bootstrap, /mnt/felhom-drives (drives backed up separately) |
| PBS verify | ✅ | --selftest=pbs-verify → verify_state: ok for the new snapshot |
The verified backup is the DR source for Phase 4. Gate to destructive phases: opened.
Phase 1 — komga healthcheck fix + git-sync + health state machine
Diagnosis (live): the docker HC curl -f http://localhost:25600/api/v1/actuator/health exited 22
(HTTP 401 — the /api/v1 prefix is auth-gated) → unhealthy for 18 h while komga served fine.
Endpoint probe matrix (wget inside the image): /, /actuator/health, /api/v1/oauth2/providers,
/login → all 200; only /api/v1/actuator/health → 401. Komga's actuator is served unauthenticated
at /actuator/health, off the /api/v1 API prefix. A second probe matters too: the controller's own
.felhom.yml healthcheck.checks[].path (which drives the dashboard badge + route publishing) pointed
at the same 401 path. The gotson/komga:1.20.0 image ships curl (verified).
Fix (app-catalog main): repoint both probes to /actuator/health — docker HC
(3faa5ae) and .felhom.yml (9b066de).
Validation (end-to-end through the real UI + server pipeline):
- GUI "Sablonok frissítése" → controller git-sync:
[sync] Updated komga/docker-compose.yml+komga/.felhom.yml; content-hash copy landed in/opt/docker/stacks/komga/. - GUI "Frissítés" →
POST /api/stacks/komga/update200 →docker compose up -d --remove-orphansrecreated the container (4.2 s). - Result:
docker ps→komga Up (healthy), HC now/actuator/health; controller logHealth probes: N ok; dashboard badgeNem egészséges→ Fut; the "⚠ URL nem elérhető – útvonal nincs publikálva" route warning cleared.
This one phase exercised: catalog fix, git-sync (content-hash copy), redeploy, the health-aware state machine, and route publishing.
Phase 2 — App deployment sweep
Scope note: catalog is 55 apps, 38 not deployed (the runbook's "14 enabled" was stale). Deploying all 38 would exhaust the 12 GB guest, so a representative sample was deployed via the GUI; the heavy multi-container case (immich, 4 containers) is already deployed + healthy (cited).
| App | Result | Evidence |
|---|---|---|
| gitea (GUI deploy) | ✅ after HC fix | GUI deploy page → memory-gate bar (two-segment current+new) → 3-step progress panel → containers up. Initially unhealthy: HC curl -f /api/v1/version → 404 (404 until install-lock) while /api/healthz → 200. Fixed catalog (17e00b7, start_period 30→90 s) → re-sync → redeploy → healthy; endpoint :3000/ → 200 |
| glance (GUI deploy) | ❌ logged, not fixed | crash-loop Restarting (1). Log: open /app/config/glance.yml: no such file or directory. Catalog template mounts an empty glance_config named volume but never seeds the required glance.yml. Not a trivial HC fix → logged. Removed cleanly (stop+remove) |
| immich (pre-deployed) | ✅ cited | 4 containers healthy = heavy-app coverage |
Bonus coverage: the stop + remove endpoints (glance + gitea teardown). Both test deployments were removed at cleanup to restore the baseline app set.
Phase 3 — Backup + restore (per-app)
| Check | Result | Evidence |
|---|---|---|
| Manual DB-dump backup | ✅ | POST /api/backup/run → 3 DBs: romm-mariadb (14 tbl, 38.5 KB), paperless-postgres (67 tbl, 360 KB), immich-postgres (60 tbl, 43.2 MB); 43.6 MB / 2.18 s. On-disk fresh (…/primary/<app>/db-dumps/*.sql) |
| Tier-2 cross-drive copy | ✅ | POST /api/backup/tier2 → 8 HDD apps → /mnt/sys_drive/felhom-data/backups/secondary/<app> (immich 156 MB on disk; romm appdata + recovery-unit{db-dump,manifest,compose}); crossdrive_completed events pushed to hub |
| Per-app restore | ✅ | POST /backup/restore stack_name=romm → RestoreFromRecoveryUnit: compose down → redeploy (6 env, 3 encrypted recovered) → Imported DB dump romm-mariadb.sql → containers up → "Restore-from-unit completed" |
| Restore non-hollow | ✅ | planted a marker table pre-restore; post-restore romm DB has the real platforms + 17 tables, 3 containers healthy; marker dropped afterward to restore baseline |
Findings:
GET /api/backup/snapshotshas no server handler (the api-router returns "endpoint not found"). The restore UI's snapshot dropdown (backups.html:682) therefore cannot populate → a pure-UI restore is blocked at snapshot selection. The restore itself works via the form actionPOST /backup/restore(which callsRestoreFromRecoveryUnit(stack)and ignoressnapshot_id's value).- Restore DB import is additive —
RestoreFromRecoveryUnitreplays the dump but does not drop tables absent from it (the planted marker table survived). Snapshot data is restored; stale/extra tables are not pruned. Minor behavioral note.
Phase 4 — DR rebuild from backup (non-destructive to 9201)
Proven twice, both collision-safe (no tunnel/hub identity clash with the live 9201):
- Agent DR primitive —
felhom-agent --selftest=restore-test -archive=<Phase-0 volid>: restored into scratch 990000 → bind-mounts neutralized → net link-down → boot →verified: boot+running→ torn down.pass:true, 2 m 56 s, self-cleaning. - Deep manual restore into throwaway 9300 —
pct restore 9300 <archive> --storage local-lvm. Safety before start: removed the shared bindsmp3:/mnt/felhom-drives+mp9:/etc/felhom-bootstrap, removednet0, setonboot 0(kept only the restoredmp0docker-data + rootfs — independent copies). Then started and inspected:pct status: running;felhom-controller Up (healthy)inside./opt/docker/stacks/had all 57 stack dirs; rommapp.yamlrestored (deployed:true, env, locked_fields).docker psinside 9300: 25 containers up from restored volumes — identical set to 9201.- Restored data non-hollow: 9300
romm-dbhas 17 tables. - No collision: 9300 had no eth0; live 9201
cloudflared Upunchanged throughout. - Cleanup:
pct stop 9300 && pct destroy 9300(both LVs removed); local-lvm freed.
Core DR claim proven end-to-end: PBS whole-CT restore → guest boots → controller + all app stacks +
DB data recovered from restored volumes. The bind-mount-neutralization recipe (remove host-path binds +
net0 + onboot before start) is the safe way to inspect a restored customer image alongside a live
one.
Phase 5 — Storage lifecycle — BLOCKED by a controller→agent connectivity defect
🔴 FINDING #1 (highest priority) — controller cannot reach the agent local API (8443)
The controller container's calls to the agent local API fail: agentapi: GET /disks: dial tcp 192.168.0.162:8443: connect: cannot assign requested address (EADDRNOTAVAIL, persistent — 6/6). The
same error surfaces in the Settings UI ("Meghajtók (ügynök nézet)") and in the backup page
("A host-ügynök jelenleg nem elérhető").
Reachability matrix from the controller container:
192.168.0.162:8006(pveproxy, binds0.0.0.0) → 200 (reachable)192.168.0.162:8443(felhom-agent local API) → refused / EADDRNOTAVAIL192.168.0.141:443(guest's own traefik) → 404 (reachable)
From the guest host namespace, 192.168.0.162:8443 is OPEN. Confirmed: ss shows the agent
LISTEN 192.168.0.162:8443 — bound to the specific host LAN IP, not 0.0.0.0 like pveproxy;
no iptables rule references 8443; pve-firewall disabled. The agent itself is healthy
(--selftest=storage enumerates all 5 targets with SMART/FS/durable_id fine). So this is a
container→host-IP routing / source-bind mismatch on port 8443, not a firewall and not an agent
crash.
Impact — the entire agent-backed feature set is down in this demo: storage management UI
(/api/disks, /api/storage: scan/label/eject/enroll/format/migrate/decommission), the live
host-metrics API (/api/host-metrics), and whole-guest backup trigger. (Monitoring still shows host
CPU/mem/temp — those are read from /proc, since the LXC shares the host kernel, not via the agent.)
Fix direction (supervised): bind the agent local API to 0.0.0.0 (or the vmbr0 bridge address the
guest routes through), or repoint the controller's local_api.endpoint (bootstrap.json) at a
container-reachable address; then re-run Phase 5's mutating checks. Not changed in this run (network
reconfig is out of scope for an unattended test).
| Check | Result | Evidence |
|---|---|---|
| Scan / health / FS / model / durable_id (agent tier) | ✅ | --selftest=storage: 5 targets; felhom-usb SMART PASSED temp=35 poh=2736 realloc=0; felhom-flash uuid:81a26531; thin-pool data=9.7 % |
| Label edit + revert (UI) | ⛔ blocked | controller→agent down (finding #1) |
| Eject → re-enroll cycle (UI) | ⛔ blocked | same |
| Destructive loopback (scan→format→mount→migrate→decommission) | ⛔ skipped | wizard drives the agent via the broken path; agent-direct needs the per-guest token + TLS-pin reconstruction → high-effort/low-confidence unattended. No scratch loopback created (nothing to clean up). Deferred to supervised. |
Phase 6 — Monitoring / alerts / notifications / settings / hub
| Check | Result | Evidence |
|---|---|---|
| Monitoring — host metrics | ✅ | CPU/load/mem (7.6/15.4 GB — host's 16 GB via /proc/LXC kernel-share)/temp/uptime |
| Monitoring — container/app metrics | ✅ | per-container memory breakdown (26 containers), per-app CPU/MEM charts, time-range charts 1h–30d |
| Alerts — state-based | ✅ | komga 0 ok, 1 unhealthy before → N ok after the Phase-1 fix; badge red→green |
| Notification path (exercised ONCE) | ✅ | set email+events+cooldown → hub Notification preferences updated for demo-felhom. Test → hub Event from demo-felhom: test → Test email sent to nagyfenyvesi.viktor@gmail.com = controller→hub→Resend→email confirmed. One email only (§0.5) |
| Settings persistence | ✅ | notification prefs set → persisted to settings.json → reverted (both directions) |
| Settings — password change | n/a | password protection is operator-only and not configured ("A jelszavas védelem nincs beállítva"); dashboard currently serves with no auth wall |
| Hub — report received | ✅ | Received report from demo-felhom (11983 bytes) every 15 min; host-report (3 guests, 5 storage targets, 1 backups, 1 restore-tests, 9 pbs-snapshots); UI "Hub: Kapcsolódva" |
| Hub — DR recipe carries the PBS coord | ✅ | dr_recipe (demo-felhom) host-half pbs={"repo_id":"felhom-pbs","namespace":"root","latest_snapshot_id":"9201"}; app-half 15 apps; no secrets in either half |
| Hub — update panel | ✅ | version 0.73.0, "naprakész", last check timestamp shown |
| Hub GUI tiles (analytics/healthchecks) | skipped | no operator GUI creds — verified hub-side state via logs + DB instead |
The DR-recipe app-half + host-half are both stored at the hub each report (DR-recipe …-half stored for customer demo-felhom (v1)), and the assembled recipe carries the live PBS coordinate (the agent v0.39.0
live-reporter feature) — verified directly in the hub DB.
Phase 7 — Resilience
| Check | Result | Evidence |
|---|---|---|
| Guest 9201 reboot | ✅ | pct reboot 9201; recovered to 26→25 containers, 0 unhealthy in ~2 min |
| Drives re-bind | ✅ | felhom-flash (sdc1) + felhom-usb (sdb1) remounted ext4 at correct mountpoints |
| FileBrowser convergence | ✅ | binds reconverged to <drive>/userdata → /srv/<drive> |
| Tunnel reconnect | ✅ | cloudflared QUIC + HTTP/2 prechecks to region1/2 argotunnel pass |
| Controller + apps + Phase-1/2 fixes persist | ✅ | komga + gitea still healthy post-reboot |
| Host reboot | ⏭ skipped (§7) | unattended risk — deferred to supervised |
Incidental: the controller's docker container IP changed across the reboot
(172.18.0.10→172.18.0.8); harmless to the system (traefik routes by docker DNS name), only required
re-pointing the test forwarder.
Consolidated findings (for supervised follow-up)
- 🔴 controller→agent local-API (8443) unreachable (Phase 5) — gates storage UI + host-metrics API
- whole-guest backup. Agent binds
192.168.0.162not0.0.0.0; no firewall; container reaches:8006but not:8443on the same host. Highest priority. Fix: bind0.0.0.0/bridge or repointlocal_api.endpoint.
- whole-guest backup. Agent binds
GET /api/backup/snapshotshas no handler (Phase 3) — restore UI snapshot dropdown can't populate (restore works viaPOST /backup/restore).- glance catalog template never seeds
glance.yml→ crash-loop on deploy (Phase 2). Needs a default-config seed (init step / entrypoint), not a trivial HC fix. - Restore DB import is additive (Phase 3) — doesn't drop tables absent from the dump. Minor.
- On-LAN HTTPS to dashboard/apps 404s at traefik
:443for all hosts (cert valid) — supported path is Cloudflare; on-LAN HTTPS appears non-functional. Worth confirming the Cloudflare path is the intended sole ingress. - Minor: a few catalog logo SVGs 404 (adventurelog, cloudflared, crafty-controller); dashboard "Adatbázisok mentve" count flickered 3→2 post-reboot (cosmetic).
Code shipped (app-catalog main, trunk)
- komga HC
/api/v1/actuator/health(401) →/actuator/health(docker HC +.felhom.ymlprobe) —3faa5ae,9b066de. - gitea HC
/api/v1/version(404 pre-install) →/api/healthz, start_period 90 s —17e00b7. - CHANGELOG + REPORT updated. No felhom-controller / felhom-agent code changed (catalog-only).
Cleanup confirmation
- Throwaway guest 9300 destroyed; restore-test scratch 990000 auto-torn-down;
pct list= 9201 (running) + pre-existing 9001/9999 (stopped, not created by this run). - No scratch loopback devices created (destructive storage sub-phase skipped).
- Test apps removed (glance, gitea) → baseline app set.
- Notification settings reverted; password never set; drives never ejected.
- Test forwarder (
/tmp/felhom-test-fwd.py) removed from the guest; temphub.dbcopy removed from DooPlex. DooPlex used in normal roles only (PBS target, gitea/hub reads). - komga healthy (improvement over the unhealthy baseline).
After-state vs before
9201 running; 25 containers Up, 0 unhealthy (before: komga unhealthy); controller v0.73.0; tunnel up; 5 storage targets reachable. Net result: demo restored to baseline + komga fixed.
Deferred — needs a supervised run
- Agent 8443 reachability root-cause + fix (finding #1) — gates the whole agent-backed subsystem.
- Storage lifecycle UI (label edit, eject→re-enroll, init-wizard format/migrate/decommission on a loopback) — re-run once finding #1 is fixed.
- Host reboot resilience (full host AC-recovery + remount-by-UUID under sdX reshuffle).
- glance config-seeding fix (finding #3);
/api/backup/snapshotshandler + UI snapshot selection (finding #2).