hub v0.15.0: Phase 2 managed updates — per-customer controller-version floor
Operator sets a minimum controller version (FLOOR), per-customer defaulting to a
global floor; the report ACK returns the effective floor + latest_version so the
controller auto-updates to the floor when below it (latest stays the opt-in button).
- store: min_controller_version column + hub_settings global floor + Effective/
Get/SetGlobal/SetMin resolution + config/env DEFAULT_MIN_CONTROLLER_VERSION
- handler: report ACK {min_controller_version, latest_version}; LatestVersionProvider
- web: global floor editor + per-customer override form + Floor column (English)
- tests: floor resolution + ACK + render; override-precedence red-proof verified
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSZmmSFVzGwEzhYmxbkgBK
This commit is contained in:
@@ -2,100 +2,45 @@
|
||||
|
||||
> **Overwrite** this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in [hub/CHANGELOG.md](hub/CHANGELOG.md).
|
||||
|
||||
---
|
||||
## hub v0.15.0 — Phase 2 managed updates: per-customer controller-version floor (2026-06-27)
|
||||
|
||||
## Day-0 host-bootstrap script — built + live wipe-and-provision test — 2026-06-26
|
||||
**Why.** Phase 1 gave enthusiasts an opt-in "update to latest" button. Phase 2 adds the managed
|
||||
default: the operator sets a per-customer **minimum controller version** (defaulting to a global floor),
|
||||
and any box below it **auto-updates** on its next report — so the household that never clicks stays
|
||||
current and patched. Floor = the operator's enforced minimum and the auto-target; latest stays the
|
||||
customer's manual opt-in.
|
||||
|
||||
**Task:** Deliver `scripts/felhom-host-install.sh` (operator-run Day-0 bootstrap) and validate it with
|
||||
an **operator-authorized destructive live test**: wipe demo guest 9201 and re-provision it from scratch
|
||||
via the script. Single repo (`felhom.eu`, new `scripts/`).
|
||||
### Files changed
|
||||
- `internal/store/store.go` — `customer_configs.min_controller_version` column; `hub_settings` KV table;
|
||||
`defaultMinControllerVersion` + `SetDefaultMinControllerVersion`; `Get/SetGlobalMinControllerVersion`,
|
||||
`SetMinControllerVersion`, `EffectiveMinControllerVersion`.
|
||||
- `internal/api/handler.go` — `LatestVersionProvider` interface + `SetLatestVersionProvider`; report ACK
|
||||
returns `{min_controller_version, latest_version}` (both omitted when empty).
|
||||
- `internal/web/configs.go`, `server.go`, `templates/configs.html`, `templates/customer_unified.html` —
|
||||
operator UI: global floor editor + per-customer override form + Floor column / below-floor markers;
|
||||
routes `POST /configs/global-floor`, `POST /customers/{id}/floor`.
|
||||
- `cmd/hub/main.go` — `Config.ControllerUpdates.DefaultMinVersion` (yaml `default_min_version`, env
|
||||
`DEFAULT_MIN_CONTROLLER_VERSION`); seeds the store default; wires the version checker as latest provider.
|
||||
|
||||
### Confirmed baselines
|
||||
- hub `1539746` v0.14.0 (`/host-enroll` option C, just shipped) · felhom-agent `4725396` v0.41.0
|
||||
(`--selftest=provision`) · felhom-controller `e0cf78b` v0.84.0.
|
||||
- Live host `felhom-pve` (192.168.0.162): PVE 9.2.2, node `demo-felhom`, agent v0.41.0 at
|
||||
`/root/.config/felhom-agent/agent.json` (systemd unit `-config` path — **not** `/etc/felhom-agent/`),
|
||||
golden `local:backup/vzdump-lxc-9100-2026_06_23-16_38_49.tar.zst` (rootfs 32 / docker 16 / sysdata 8 GiB base).
|
||||
### Design
|
||||
- Effective floor = per-customer override if non-empty, else global (hub_settings → config/env default),
|
||||
else "" (Phase 2 inert for that customer).
|
||||
- Rides the **existing report cycle** — no new floor-polling endpoint. The actual swap is the
|
||||
controller's Phase 1 flow (in-guest pull → agent `POST /controller/swap` → rollback); the hub only
|
||||
advertises the floor. **No agent change.**
|
||||
|
||||
### Deliverable
|
||||
- `scripts/felhom-host-install.sh` **v1.0.0** — 7 idempotent/resumable steps (pre-flight → pveum token →
|
||||
grows → host-enroll → agent config → provision → verify), single-secret (passphrase no-echo/0600 file),
|
||||
`--dry-run`/`--resume`/`--force`/`--preserve-from`, `--mode dr` documented stub.
|
||||
- `scripts/README.md` (operator runbook), `scripts/CHANGELOG.md` (v1.0.0).
|
||||
- `documentation/audits/SPIKE-…-2026-06-26.md` — pointer added (script ships the gap inventory).
|
||||
- `bash -n` passes (Windows + host). **shellcheck unavailable** on dev box, build box, and felhom-pve — not run.
|
||||
### Tests (`go test ./...` green)
|
||||
- `internal/store/floor_test.go` — empty-when-unset; global-default fallback; DB-overrides-default;
|
||||
**override-beats-global (companion red-proof)**; set-and-preserve-across-save.
|
||||
- `internal/api/floor_ack_test.go` — ACK carries effective floor (override beats global) + latest; omits
|
||||
both when unset.
|
||||
- `internal/web/render_test.go` — template render smoke (catches `template.Must` panics + floor fields).
|
||||
- **Companion red-proof (verified):** making `EffectiveMinControllerVersion` ignore the override →
|
||||
`TestEffectiveFloor_OverrideBeatsGlobal` AND `TestReportACK_FloorAndLatest` FAIL → restored → green.
|
||||
|
||||
### Live supervised test (felhom-pve, customer `demo-felhom`, authorized destructive)
|
||||
Passphrase sourced operator-side from the hub DB into a 0600 file on the host (never printed); deleted
|
||||
(shredded) after. Methods noted per check.
|
||||
### Green gate
|
||||
`go build ./... && go vet ./... && go test ./...` → all pass.
|
||||
|
||||
**A. Pre-wipe snapshot:** saved `pct config 9201`, controller `0.84.0 Up (healthy)`, `agent.json.bak`
|
||||
(1574 B), token list. Confirmed `demo-felhom` host exists in hub (`demo-felhom-01`, plus two stale
|
||||
`dr-drill-host*` cruft rows — `demo-felhom-01` has today's `updated_at`, so `GetHostByCustomer` returns it).
|
||||
|
||||
**B. Wipe (authorized):** `pct stop 9201` + `pct destroy 9201 --purge` (all 3 volumes removed); moved
|
||||
`agent.json` + cleared state to simulate a fresh host.
|
||||
|
||||
**C. Dry-run:** every mutating command printed correctly — pveum role-modify (16 privs), **both** ACL
|
||||
lines, token rotate, enroll curl, agent.json write, provision call. Pre-flight green (golden auto-found,
|
||||
passphrase valid, vmid free).
|
||||
|
||||
**D. Real run — found + fixed a bug, then SUCCESS:**
|
||||
- **Bug found live:** first run failed the post-config agent selftest with `HTTP 403 … missing privilege
|
||||
Sys.Audit`. Root cause: the script applied the **token ACL before rotating the token**, and
|
||||
`pveum user token remove` **purges the token's ACL** — so the new token had no ACL. **Fix:** apply both
|
||||
ACL grants **after** the token is (re)created. (`scripts/felhom-host-install.sh` step 2.)
|
||||
- **Re-run green:** token rotated (old config token failed selftest → rotate → ACL → selftest **passed**);
|
||||
`host-enroll` → **200 REUSE** of `demo-felhom-01` (idempotency confirmed); agent.json written 0600 +
|
||||
read-only selftest passed + daemon restarted; provision front-half bring-up (1m11s) + back-half bootstrap
|
||||
mount `mp9` → `/etc/felhom-bootstrap` + local-api token minted; guest **9201 running, onboot:1**, layout
|
||||
rootfs 32G / mp0 200G / mp1 50G / mp8 bind — exactly the standard appliance.
|
||||
|
||||
**E. Verify (method per check):**
|
||||
| Check | Method | Result |
|
||||
|-------|--------|--------|
|
||||
| guest running + onboot:1 + volumes | `pct status/config` | ✅ running, onboot 1, 32/200/50G + mp8 bind |
|
||||
| controller healthy | `pct exec docker ps` | ✅ `Up (healthy)`, image `felhom-controller:0.77.0` (from golden) |
|
||||
| **controller pulled config from hub** | controller logs (`pct exec`) | ✅ `bootstrap: pulled config from hub for demo-felhom, merged local_api … coming up configured`; `Hub connectivity … HTTP 200` |
|
||||
| serving | `pct exec curl :80` | ✅ HTTP 301 (traefik) |
|
||||
| **Cloudflare tunnel connected** | cloudflared logs | ✅ 4 `Registered tunnel connection` (vie06, bud01) |
|
||||
| **public URL** | curl from build box (public DNS) | ✅ `felhom.demo-felhom.eu → HTTP 200` via Cloudflare `188.114.97.11` (ingress `*.demo-felhom.eu → traefik`) |
|
||||
| real dashboard | curl public | ✅ `<title>Vezérlőpult — Felhom.eu</title>` |
|
||||
| **host-report includes guest 9201** | hub sqlite | ✅ `demo-felhom-01/9201` status `running`, last_seen current |
|
||||
| storage plane (functional touch) | `pct exec ls` | ✅ `/mnt/felhom-drives` bound, `felhom-flash` + `felhom-usb` visible (data intact); local-api drive plane reachable |
|
||||
|
||||
> Public URL returned `000` from felhom-pve and the Windows dev box — split-horizon (the agent's LAN
|
||||
> resolver points `*.demo-felhom.eu` at the guest's LAN IP, which serves `:80` not `:443`). Through the
|
||||
> Cloudflare edge (build box) it is **200**. Not a provision fault.
|
||||
|
||||
**F. Idempotency:** plain re-run **refused** to clobber the live 9201 (exit 1, "pass `--force`");
|
||||
`--resume` re-run **skipped** token/grows/enroll/agent_config/provision → verify-only **no-op**, token
|
||||
**not** rotated (selftest still passes), guest **not** re-provisioned.
|
||||
|
||||
**G. Cleanup:** passphrase file shredded; `agent.json.bak` + snapshot retained; final state — 9201
|
||||
running, host_id `demo-felhom-01`, agent active.
|
||||
|
||||
### pveum / token automation result
|
||||
Role `FelhomAgent` normalised to the authoritative **16 privileges** (incl. `Sys.Audit`, `SDN.Use`); user
|
||||
`felhom-agent@pve`; privsep token rotated; **both** ACL grants (user **and** token) applied **after** token
|
||||
creation. Read-only `felhom-agent --selftest` passed against the live API with the minted token.
|
||||
|
||||
### Observations (out-of-scope — not acted on)
|
||||
1. **`-hub-password` on argv:** the agent's only input for the retrieval passphrase during provision is the
|
||||
`-hub-password` flag → briefly visible in `ps` during `--selftest=provision`. **Candidate agent
|
||||
follow-up:** accept it via env/stdin. (Not fixed here — would modify the agent.)
|
||||
2. **Golden freshness:** the golden bakes controller **0.77.0**; the freshly provisioned demo runs 0.77.0
|
||||
(pre-wipe was 0.84.0, and the v0.78.0 drive-register stable-path fix is newer than the golden). Rebuild
|
||||
the golden at the current controller for parity; full drive **re-enroll** via the UI needs controller
|
||||
≥0.78.0. The Day-0 script itself is version-agnostic.
|
||||
3. **Multi-host cruft:** `demo-felhom` carries two stale `dr-drill-host*` rows. `GetHostByCustomer`
|
||||
(most-recent-updated) correctly returned `demo-felhom-01` here, but the Day-0 invariant is one host per
|
||||
customer — drill artifacts should be cleaned up (left untouched; out of scope).
|
||||
4. **Serving + binary/golden delivery:** the script lives in `scripts/`; serving it at
|
||||
`https://felhom.eu/scripts/…`, plus agent-binary and golden central delivery, are documented
|
||||
productionization hooks (the test used the local binary + golden).
|
||||
|
||||
### Commits
|
||||
- `<this commit>` — script v1.0.0 + scripts/README + scripts/CHANGELOG + spike pointer + REPORT.
|
||||
|
||||
No secrets in any committed file. PVE-token secret + host api_key live only in the host's 0600 agent config.
|
||||
### Deploy
|
||||
Built + pushed `gitea.dooplex.hu/admin/felhom-hub:0.15.0`, manifest bumped + synced (see hub/CHANGELOG
|
||||
and the live-test summary in felhom-controller/REPORT.md for the end-to-end floor auto-update result).
|
||||
|
||||
Reference in New Issue
Block a user