Files
felhom.eu/documentation/audits/DRILL-day0-cleanroom-2026-07-03.md
T

198 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DRILL — Day-0 clean-room install / uninstall / reinstall (2026-07-03)
**Class:** Clean-room drill (empirical validation of `documentation/runbooks/day0-install.md` +
residue-proof of `--uninstall`). Companion deliverables: the runbook itself and
`scripts/felhom-host-install.sh` **v1.9.1**.
**Verdict (short):** the Day-0 guide is **executable verbatim end-to-end** on a virgin PVE 9.2 box:
install → verify → app deploy → uninstall → **zero-Felhom-residue diff** → reinstall all pass.
The drill found **one installer gap that mattered** (the controller never deployed without a guest
reboot — fixed in v1.9.1), **five uninstall residue items** (all fixed in v1.9.1, re-proven by a
second residue-diff), and produced a decisive **OQ-2 answer** (a fresh box does NOT self-update its
controller — a one-time manual update is a mandatory install-day step). **Go for Peti** with the
go/no-go list in §8.
No secrets appear in this document. Drill credentials lived only on the build server (0600) and in
the drill VM; the drill VM was reverted to its virgin snapshot afterwards.
---
## 1. Environment (Phase 0 — primary mechanism, validated)
| Item | Value |
|---|---|
| Host | build server 192.168.0.180 (Debian 13, /dev/kvm, QEMU 10.0.8 installed for the drill) |
| VM | nested **PVE 9.2.2** (unattended install: `proxmox-auto-install-assistant` 9.2.7 answer file, ext4/LVM-thin layout), 4 vCPU / 8 GiB RAM / 150 GiB sparse qcow2 |
| Resulting storage | `local` 46 GiB dir + `local-lvm` **76 GiB** thin — deliberately BELOW the 120 GiB floor, so the warn path is exercised |
| Network | QEMU user-mode NAT (slirp); outbound to hub.felhom.eu / gitea.dooplex.hu / felhom.eu confirmed from host AND guest; SSH via hostfwd |
| Restore point | qcow2 internal snapshot `virgin` taken post-install, pre-first-boot |
| Hub side | live hub v0.31.0; throwaway customer **drill-1** (domain `drill.demo-felhom.eu`, fake CF tunnel token, real Gitea read credential copied from the demo customer's row, hub-generated-format passphrase) inserted directly into `customer_configs` (the web create form is operator-password-gated — CC cannot drive it; the row is byte-identical to `SaveCustomerConfig`, spike-day0 precedent) |
| Versions under test | script v1.9.0 → v1.9.1, agent v0.62.0, golden v0.85.1 (hub artifact manifest), controller floor 0.87.0, latest 0.98.3 |
**Environment quirk (drill-only, not product):** slirp's DHCP pool starts at 10.0.2.15 — the same
address the PVE host holds statically — so the freshly-provisioned guest's DHCP grabbed the host's
IP, intermittently breaking the drill SSH (and truncating the D2 transcript). Worked around by
pinning the guest to 10.0.2.40. A real LAN with a real DHCP server does not have this failure mode.
## 2. D0 — baseline
Captured off-box before any Felhom artifact touched the box: sorted file list of
`/etc /usr/local/bin /usr/local/sbin /var/lib` (5 076 entries), `systemctl list-unit-files`
(379 units), `pveum user/role/acl/pool list`, `pvesm status`, `pct list; qm list` (empty).
Stored at `~/drill/D0/` on the build server.
## 3. D1 — hub onboarding (guide Part A)
- Customer-create field set verified against `hub/internal/web/configs.go`
(`handleConfigCreate` + `buildConfigJSON`): customer_id/name/domain/email +
`infrastructure.cf_tunnel_token`, `infrastructure.cf_api_token`, `git.username`, `git.token`;
passphrase (5 words) + api-key hub-generated. **`git.token` is load-bearing for Day-0** — the
installer dies at step 5/8 without it (guide Part A marks it required).
- Box-facing contracts probed for drill-1: `GET /api/v1/config/drill-1`**200**;
`GET /api/v1/artifacts/drill-1` → agent 0.62.0 + golden 0.85.1; wrong passphrase → **401**.
- **UI caveat:** the operator-password-gated create form itself was not click-driven (CC has no
operator password); the row was inserted store-identically. Part A's field list is code-verified
and its outputs are live-verified.
## 4. D2 — install (guide Parts B/C, script v1.9.0)
Part B checklist ran verbatim (two guide corrections folded in: hub root answers **302**, and a
cleaner node-count one-liner). Script fetched from `https://felhom.eu/scripts/felhom-host-install.sh`
(text/plain, the git-synced repo file). `--dry-run` previewed all 8 steps correctly. Real run:
```
./felhom-host-install.sh --customer-id drill-1 --vmid 120 --cores 2 --memory 4096 \
--force-gitea-golden --acl-storages "local local-lvm" --passphrase-file /root/.drill-pass
```
- pre-flight: PVE 9.2.2, node auto (single), **`local-lvm ~75 GiB < 120 GiB` warn fired as designed**,
passphrase validated, grows auto-computed for the small box (docker +34G, sys +12G).
- token + pool + scoped 3-role ACL applied (Base@/, Guest@/pool/felhom, Store@[local local-lvm]).
- host-enroll: **201 MINTED** (`drill-1-c867f3`).
- agent 0.62.0 fetched + **sha256-verified** against the hub manifest; sudo installed; non-root
user/sudoers/unit installed; selftest passed; service active.
- golden 0.85.1 fetched + sha256-verified from Gitea (`--force-gitea-golden` — the real fresh-box path).
- provision: guest 120 up in 1m26s, cores=2/memory=4096 applied, 32/50/20G volumes, mp9 bootstrap
mount, onboot=1, pool member. State file recorded `provisioned_vmid=120`
(**confirms** the v0.62.0-era open question: a fresh install DOES record it).
## 5. D3/D4 — verify + app deploy
- Agent: `--selftest` all-OK **including `pool read pool "felhom", 1 member`** (the v0.62.0
`pve:pool-read` capability); service active as `felhom-agent`.
- Hub: host row reporting (agent 0.62.0), guest 120 `running`; events pipeline live from the fresh
box (`controller_started`, `app_deployed`, `controller_updated`, 2× `host_leaf_changed` warnings
from the reinstalls — correct behavior).
- **Finding R6 (the big one):** after provision the controller was NOT running —
`felhom-controller-bootstrap.service` was skipped
(`ConditionPathExists=/etc/felhom-bootstrap/bootstrap.json` unmet at boot): the agent back-half
**hot-plugs** the bootstrap mount into the already-running guest and its own output says
*"next: reboot the guest"*, but the v1.9.0 script never rebooted. Fast hardware (demo) usually
won this race; the slow clean-room box lost it deterministically. **Fixed in v1.9.1**
(post-provision `pct reboot` + bounded verify wait); re-proven in D6b (§7).
- Dashboard: the controller sits behind traefik — correct probe is
`curl -k -H "Host: felhom.<domain>" https://127.0.0.1/`**200** (Hungarian dashboard). The
guide's draft `:8080` probe was wrong and was corrected.
- **D4 app deploy:** `bentopdf` deployed through the exact UI pipeline (page fetch → the same
`POST /api/stacks/bentopdf/deploy` + empty-CSRF headers the dashboard JS sends — endpoint-proxy
method per the live-validation rule; no browser bridge was available in this session). Container
healthy; app answers **200** at `pdf.drill.demo-felhom.eu` via traefik. Public Cloudflare leg not
drill-testable (fake tunnel token) — that leg is live-proven daily on the demo box.
- **OQ-2 evidence:** fresh box landed controller **0.85.1** (golden-baked) and did NOT
self-update (floor 0.87.0 ignored — 0.85.1 predates the v0.86.0 floor code; its one report ACK
carried no consequence). Manual update (`/api/selfupdate/check` + `/update`, the settings-page
button flow) first **failed with the known fresh-install local-API 401** (`POST /controller/swap:
HTTP 401`, stale token map); after `systemctl restart felhom-agent` the same trigger pulled
0.98.3 and the agent swap landed it: **0.85.1 → 0.98.3, healthy**. Both the failure and the
recovery are now guide steps (D.1b).
## 6. D5 — uninstall + residue diff (v1.9.0 → the gap list)
`--uninstall --vmid 120 --remove-golden` ran with the typed-vmid confirmation; guest/agent/pveum/
pool/state/golden all removed. Full re-capture + diff vs D0 classified every delta:
| Delta (v1.9.0 uninstall) | Class |
|---|---|
| `/etc/felhom-agent/agent.json` (+dir) — **holds the live hub api_key** | **RESIDUE R1** → fixed v1.9.1 |
| `felhom-shared-parent.service` (+wants links) + `/usr/local/sbin/felhom-shared-parent.sh` + `/mnt/felhom-drives` self-bind still mounted | **RESIDUE R2** → fixed v1.9.1 |
| `/usr/local/sbin/felhom-mkfs-guarded` | **RESIDUE R3** (suspected by the spec — confirmed) → fixed v1.9.1 |
| `/var/lib/vz/snippets/felhom-guest-hook.sh` | **RESIDUE R4** (would remain where the snippet install succeeded; on this box it never installed — see B2) → fixed v1.9.1 |
| `/etc/dnsmasq.d/felhom-resolver-base.conf` | **RESIDUE R5** → fixed v1.9.1 |
| `sudo` + `dnsmasq` packages (+ their files/units) | expected remnant (documented in the guide; dnsmasq newly noted) |
| hub host record + report/guest/event history | expected remnant (no hub delete path — B4) |
| LVM archive/backup metadata, dpkg lists, PVE rrd + `/var/lib/lxc/120` scratch, `.pve-staged-mounts` | system noise (PVE/dpkg-owned) |
## 7. D6 / D6b — reinstall proofs
- **D6 (v1.9.0, post-uninstall box):** full reinstall succeeded — host-enroll **200 REUSED**
(mint-once-reuse proven), fresh leaf minted (box had no guests → the populated-host guard
correctly stayed silent), golden re-fetched, guest provisioned. Uninstall leaves a reinstallable
box. (Controller again required the reboot — the R6 evidence repeated deterministically, incl.
the agent's own "next: reboot the guest" line and the guesthook warn, see B2.)
- **v1.9.1 re-verification (all fixes):**
- `bash -n` + `-h` (header now v1.9.1) + full `--dry-run` previews of the new removal steps.
- **Uninstall re-drill:** v1.9.1 `--uninstall --remove-golden` → re-diff vs D0:
**zero `felhom`-named entries** in the file diff, zero pveum objects, no mounts, both dirs gone.
- **D6b install re-drill:** full v1.9.1 install on the clean box → the script rebooted the guest
itself → verify reported `controller: Up … (after ~0s)`, cloudflared up — **no manual
intervention**. R6 closed at the installer.
## 8. OQ answers + go/no-go for Peti (sajatfelhom.hu)
**OQ-1 (offsite backup): recommendation stands — go live with local backups only**
(`--acl-storages "local local-lvm"`, exactly the drilled canonical command). The PBS box is
LAN-only until Headscale/WireGuard lands; the ACL/`--rescope-acl` path retrofits the offsite grant
later without reinstalling. The guide documents this as the current standard. (Operator holds the
pen — if offsite-first is wanted instead, install day waits on the VPN spike.)
**OQ-2 (fresh-install controller version): answered with evidence** — lands 0.85.1, never
self-updates (pre-floor code). Raising the floor does NOT fix a fresh box. Install-day step D.1b
(restart agent → trigger update → verify 0.98.3) is mandatory and drill-proven. Structural fix =
rebuild + re-vouch the golden with a ≥ 0.86.0 controller (operator follow-up, not a Peti blocker).
**OQ-3 (pre-install facts):** the guide's Part B checklist is the collection instrument (PVE 9.x,
single-node vs cluster → `--node`, ≥120 GiB local-lvm, free vmid from `pct list`+`qm list`, host
cores/RAM for the cap, root SSH, outbound reach incl. the three exact URLs+expected codes, python3).
**Go/no-go list:**
| # | Item | State |
|---|---|---|
| 1 | Install path proven verbatim on a virgin box (twice + reinstall) | ✅ GO |
| 2 | Uninstall residue-clean (v1.9.1) — safe on a shared box | ✅ GO (deploy/serve v1.9.1 BEFORE install day; drilled from the served URL at v1.9.0 + locally at v1.9.1) |
| 3 | D.1b one-time controller update executed on install day | ⚠️ REQUIRED STEP (guide covers it; 2 minutes) |
| 4 | Day-0 manifest vouches agent **0.62.0** + golden **0.85.1** | ✅ current; **but hub Day-0 manifest bump to agent 0.62.0 was already flagged as operator follow-up — CONFIRMED DONE (drill fetched 0.62.0)** |
| 5 | Real CF tunnel token created + entered in Part A (drill used a fake) | ⚠️ operator does A.1 for sajatfelhom.hu; tunnel leg is demo-proven, not drill-proven |
| 6 | `git.username`/`git.token` set in the customer config | ⚠️ REQUIRED (installer dies without it) |
| 7 | Multi-node? If Peti's box is a cluster, add `--node` | per Part B answer |
| 8 | Known non-blockers on the box: B2 (no pre-start hook on fresh boxes), B3 (401-until-agent-restart) | **both FIXED in agent v0.63.0** (2026-07-03, Gitea-published); effective for fresh installs once the Day-0 manifest vouches ≥ 0.63.0 — until then the D.1b restart step covers B3 |
**Verdict: GO** — with items 3/5/6 executed on/before install day.
## 9. Findings ledger (blockers + follow-ups, per the §9 gap policy)
| ID | Where | Severity | What | Status |
|---|---|---|---|---|
| R1R5 | installer | — | uninstall residue (config w/ secrets, shared-parent unit+script+mount, mkfs wrapper, hook snippet, dnsmasq snippets) | **FIXED v1.9.1**, residue-diff-proven |
| R6 | installer | — | controller-bootstrap unit skipped: no post-provision reboot (agent hot-plugs the mount; unit condition is boot-time) | **FIXED v1.9.1** (reboot + bounded verify wait), D6b-proven. NOTE (2026-07-03, golden-098 task): goldens ≥ 0.98.3 bake a `.path` unit that makes the reboot redundant — the reboot is RETAINED as a belt; its removal is a recorded candidate cleanup, not done |
| B1 | agent/golden | LOW | structural fix for R6 belongs in the product too (path unit in the golden, or agent-side restart after back-half) — installer reboot is a correct but external crutch | **FIXED** (build-golden.sh v2.0.0, golden 0.98.3): baked `felhom-controller-bootstrap.path` starts the service on the bootstrap-mount hot-plug — isolated + full-install proven, `DRILL-golden-098-2026-07-03.md` §4/§5 |
| B2 | agent | LOW | `guesthook.InstallSnippet` fails on a fresh box: `/var/lib/vz/snippets` doesn't exist and `install` won't create it → no pre-start self-heal hook, warn-only (`install: cannot create regular file … No such file or directory`, agent journal 14:58:40) | **FIXED agent v0.63.0** (fenced `mkdir -p` precedes the install + the one sudoers grant; red-proofed — felhom-agent REPORT 2026-07-03) |
| B3 | agent | MED | fresh-install local-API **401 until `systemctl restart felhom-agent`** — root cause: the daemon's TokenStore index is built once at open, while provisioning is a SEPARATE one-shot process minting into the shared file; NEW consequence found: it blocks the controller self-update agent-swap | **FIXED agent v0.63.0** (`Lookup` reload-on-miss, red-proofed + run on felhom-pve — felhom-agent REPORT 2026-07-03). Guide D.1b restart-step narrowed to "agent < v0.63.0" — applies until the Day-0 manifest vouches ≥ 0.63.0 |
| B4 | hub | LOW | **no host-delete path** (UI or API) — drill host rows removed via direct SQL (spike precedent); customer-delete exists but leaves hosts/reports/guests/events orphaned | RECORDED (candidate small hub follow-up) |
| B5 | golden/fleet | MED | golden bakes a pre-floor controller (0.85.1) → every fresh install needs D.1b manually | **FIXED** (build-golden.sh v2.0.0: mandatory controller tag; golden **0.98.3** baked, published, vouched): fresh install lands 0.98.3 on first boot, self-update reports up-to-date, guide D.1b retired — `DRILL-golden-098-2026-07-03.md` §5/§6 |
## 10. Cleanup confirmation
- **Hub:** all `drill-1` rows deleted (customer_configs / hosts / host_reports / reports / guests /
events / notification_log / customer_notifications → remaining **0**); `demo-felhom` confirmed
intact. (The web customer-delete would have left the host/report rows — B4 — hence SQL, spike
precedent.)
- **Drill VM:** powered off; disk reverted to the `virgin` snapshot and kept
(**virgin-snapshotted-and-stopped**, not removed — reusable for future drills). Footprint
~6.7 GiB at `~/drill/` on the build server (ISO + qcow2 + evidence logs + keys, keys 0600);
the baked auto-install ISO variant was deleted.
- Evidence archived off-box at `~/drill/{D0,D5,D5b}/` + `D2-install.log`, `D5-uninstall.log`,
`D5b-uninstall.log`, `D6-install.log`, `D6b-install.log` on the build server.
- QEMU/`proxmox-auto-install-assistant` packages remain installed on the build server (useful for
re-drills; noted).