Go-live package: Day-0 install runbook + clean-room drill + host-install v1.9.1

- documentation/runbooks/day0-install.md (NEW): operator Day-0 guide, every
  command drill-executed verbatim (Parts A-F incl. the mandatory one-time
  controller update D.1b and the OQ-3 prereq checklist).
- documentation/audits/DRILL-day0-cleanroom-2026-07-03.md (NEW): nested-PVE
  clean-room drill evidence D0-D6, residue tables, OQ-1/2/3 answers, findings
  ledger (B1-B5), go/no-go for Peti (GO).
- scripts/felhom-host-install.sh v1.9.1: residue-free uninstall (agent config,
  shared-parent unit/script/mount, mkfs wrapper, hook snippet, dnsmasq
  snippets; zero-residue diff proven) + post-provision guest reboot (R6) +
  header/version sync.
- CONTEXT.md, REPORT.md, scripts/CHANGELOG.md updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-03 15:14:47 +02:00
parent 996526273a
commit 0fa79cd90d
6 changed files with 690 additions and 34 deletions
+23
View File
@@ -1,5 +1,28 @@
# Felhom scripts — Changelog
## felhom-host-install.sh v1.9.1 — clean-room drill fixes: residue-free uninstall + post-provision reboot (2026-07-03)
Companion to the Day-0 go-live package (`documentation/runbooks/day0-install.md` +
`documentation/audits/DRILL-day0-cleanroom-2026-07-03.md`). Every fix was found by the clean-room
drill (virgin nested PVE 9.2.2) and re-verified there (v1.9.1 uninstall → **zero-Felhom-residue
diff vs the pre-install baseline**; v1.9.1 install → controller up with no manual intervention).
- **Header/version sync** (the header said v1.8.0 while `SCRIPT_VERSION` said 1.9.0); keep-in-sync
note on `SCRIPT_VERSION`; usage sed range follows the header (2,95).
- **Uninstall now removes the drill-found residue (R1R5):** the agent **config**
(resolved from the unit's `-config` BEFORE the unit is removed — it holds the per-host hub
api_key), the `felhom-shared-parent` unit + wants links + `/usr/local/sbin/felhom-shared-parent.sh`
+ the `/mnt/felhom-drives` self-bind/dir, `/usr/local/sbin/felhom-mkfs-guarded`,
`/var/lib/vz/snippets/felhom-guest-hook.sh`, and `/etc/dnsmasq.d/felhom-*.conf`
(+ dnsmasq restart when touched). All tolerate-absent; summary lines updated (`sudo` AND
`dnsmasq` packages are the documented package remnants).
- **Post-provision guest reboot (R6):** the golden's `felhom-controller-bootstrap.service`
evaluates `ConditionPathExists=/etc/felhom-bootstrap/bootstrap.json` at BOOT, but the agent
back-half hot-plugs the mount into the running guest — on slower hardware the first boot loses
that race deterministically and the controller never deploys. `step_provision` now reboots the
guest once (the agent's own output says "next: reboot the guest"); `step_verify` waits bounded
(180 s) for the controller container instead of a momentary look.
## felhom-host-install.sh v1.9.0 — Pool.Audit for the stale-lock reaper (A1) (2026-07-03)
Companion to felhom-agent v0.62.0 (audit A1: pool-membership ownership check). `PVE_PRIVS_GUEST`
+63 -11
View File
@@ -1,6 +1,6 @@
#!/bin/bash
#===============================================================================
# felhom-host-install.sh v1.8.0
# felhom-host-install.sh v1.9.1
# Day-0 host-bootstrap for a Felhom Proxmox host (operator-deploy model).
#
# Run by the operator on a FRESHLY-PVE-INSTALLED box (after a manual PVE install
@@ -71,8 +71,9 @@
#
# Uninstall (local host teardown — no hub contact, no passphrase):
# --uninstall cleanly revert an install: destroy the Felhom guest, remove the agent
# (unit/sudoers/binary/state/user), the pveum role/user/token/ACL, and the
# install state file. Guarded: refuses a non-Felhom guest (no
# (unit/sudoers/binary/state/config/user + runtime artifacts: shared-parent
# unit, mkfs wrapper, hook snippet, dnsmasq snippets), the pveum
# role/user/token/ACL, and the install state file. Refuses a non-Felhom guest (no
# /etc/felhom-bootstrap mount) and skips host-level removal if OTHER Felhom
# guests remain (both overridable with --force). Typed vmid confirmation
# required. Reuses --vmid (else the recorded provisioned_vmid), --force,
@@ -104,7 +105,7 @@
set -euo pipefail
SCRIPT_VERSION="1.9.0"
SCRIPT_VERSION="1.9.1" # keep in sync with the header line at the top of this file
#-------------------------------------------------------------------------------
# Logging (mirrors felhom-controller/scripts/docker-setup.sh)
@@ -211,7 +212,7 @@ ART_GOLDEN_SHA=""
#-------------------------------------------------------------------------------
# Helpers
#-------------------------------------------------------------------------------
usage() { sed -n '2,94p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }
usage() { sed -n '2,95p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }
run() { # simple (no pipes/redirects) mutating command
if $DRY_RUN; then log_dry "$*"; else "$@"; fi
@@ -552,7 +553,15 @@ run_uninstall() {
# ── host-level removal (reverse of install) ──────────────────────────────────────────────────
log_step "host-level removal"
# 4. Agent removal — service, unit(+.bak), sudoers, binary(+.bak), state dir, user. NEVER `sudo`.
# 4. Agent removal — service, unit(+.bak), sudoers, binary(+.bak), state dir, config, user.
# NEVER `sudo`. Resolve the agent config path BEFORE the unit is removed (mirrors preflight —
# the unit's -config arg is the truth, else the default); the config holds the per-host hub
# api_key and must not survive an uninstall (drill finding R1).
local agent_cfg=""
if systemctl cat felhom-agent >/dev/null 2>&1; then
agent_cfg=$(systemctl cat felhom-agent 2>/dev/null | grep -oP '(?<=-config )\S+' | head -1)
fi
[[ -n "$agent_cfg" ]] || agent_cfg="/etc/felhom-agent/agent.json"
if systemctl list-unit-files felhom-agent.service >/dev/null 2>&1; then
systemctl is-active --quiet felhom-agent 2>/dev/null && run systemctl stop felhom-agent
systemctl is-enabled --quiet felhom-agent 2>/dev/null && run systemctl disable felhom-agent
@@ -569,6 +578,36 @@ run_uninstall() {
if [[ -d "$AGENT_STATE_DIR" ]]; then run rm -rf "$AGENT_STATE_DIR"; else log_skip " $AGENT_STATE_DIR already absent"; fi
if id "$AGENT_USER" >/dev/null 2>&1; then run userdel "$AGENT_USER"; else log_skip " service user $AGENT_USER already absent"; fi
# 4b. Agent config (pve token + per-host hub api_key — secrets must not survive; drill R1).
if [[ -f "$agent_cfg" ]]; then run rm -f "$agent_cfg"; else log_skip " $agent_cfg already absent"; fi
run rmdir "$(dirname "$agent_cfg")" 2>/dev/null || true
# 4c. Shared-parent unit + wrapper + /mnt/felhom-drives (agent-installed at runtime; drill R2).
# Stop/disable, remove unit + script, unbind + remove the (empty) parent dir. Tolerate-absent.
if systemctl list-unit-files felhom-shared-parent.service 2>/dev/null | grep -q felhom-shared-parent; then
systemctl is-active --quiet felhom-shared-parent 2>/dev/null && run systemctl stop felhom-shared-parent
systemctl is-enabled --quiet felhom-shared-parent 2>/dev/null && run systemctl disable felhom-shared-parent
else
log_skip " felhom-shared-parent unit not loaded — skip stop/disable"
fi
if [[ -f /etc/systemd/system/felhom-shared-parent.service ]]; then run rm -f /etc/systemd/system/felhom-shared-parent.service; else log_skip " felhom-shared-parent.service already absent"; fi
if [[ -f /usr/local/sbin/felhom-shared-parent.sh ]]; then run rm -f /usr/local/sbin/felhom-shared-parent.sh; fi
run systemctl daemon-reload
if mountpoint -q /mnt/felhom-drives 2>/dev/null; then run umount /mnt/felhom-drives; fi
if [[ -d /mnt/felhom-drives ]]; then run rmdir /mnt/felhom-drives 2>/dev/null || true; fi
# 4d. Guarded-mkfs wrapper, guest-hook snippet, lan-resolver dnsmasq snippets (drill R3-R5).
if [[ -f /usr/local/sbin/felhom-mkfs-guarded ]]; then run rm -f /usr/local/sbin/felhom-mkfs-guarded; else log_skip " felhom-mkfs-guarded already absent"; fi
if [[ -f /var/lib/vz/snippets/felhom-guest-hook.sh ]]; then run rm -f /var/lib/vz/snippets/felhom-guest-hook.sh; fi
local dconf _dnsmasq_touched=false
for dconf in /etc/dnsmasq.d/felhom-*.conf; do
[[ -e "$dconf" ]] || continue
run rm -f "$dconf"; _dnsmasq_touched=true
done
if $_dnsmasq_touched && systemctl is-active --quiet dnsmasq 2>/dev/null; then
run systemctl restart dnsmasq || true
fi
# 5. pveum removal (presence-checked; tolerate-absent; roles deleted only after their grants).
# Remove the 3-role scoped grants+roles (3b) AND the pre-3b single-role broad grant if present —
# both tolerate-absent so --uninstall works on a box of either shape.
@@ -624,9 +663,9 @@ run_uninstall() {
# 8. Summary.
echo ""
log_success "UNINSTALL complete — removed: guest $vmid, the felhom-agent (unit/sudoers/binary/state/user), the pveum role/user/token/ACL,$( $pool_removed && printf ' the %s pool,' "$PVE_POOL") and $STATE_FILE."
log_success "UNINSTALL complete — removed: guest $vmid, the felhom-agent (unit/sudoers/binary/state/config/user + shared-parent/mkfs-wrapper/hook-snippet/dnsmasq-snippets), the pveum role/user/token/ACL,$( $pool_removed && printf ' the %s pool,' "$PVE_POOL") and $STATE_FILE."
if $REMOVE_GOLDEN; then log_info " golden vzdump: removed."; else log_info " golden vzdump: left in place (--remove-golden to remove)."; fi
log_info " NOTE: the 'sudo' package was left installed (system package); the host record still exists in the hub — remove it there if desired."
log_info " NOTE: the 'sudo' and 'dnsmasq' packages were left installed (system packages); the host record still exists in the hub — remove it there if desired."
$DRY_RUN && log_warn " DRY-RUN: nothing above was actually executed."
return 0
}
@@ -1439,6 +1478,14 @@ step_provision() {
# Record the provisioned vmid so a later --uninstall resolves the target automatically + safely.
_state_put provisioned_vmid "$VMID"
# (No pool_add_guest here — the agent's restore --pool already made the guest a member.)
# Reboot the guest ONCE: the golden's controller-bootstrap unit evaluates its
# ConditionPathExists=/etc/felhom-bootstrap/bootstrap.json at BOOT, and the back-half attaches
# the bootstrap mount to the ALREADY-RUNNING guest — without a reboot the unit stays skipped and
# the controller never deploys (drill finding R6; the agent's own provision output says
# "next: reboot the guest"). On fast hosts the first boot sometimes wins the race — the reboot
# is idempotent either way (the unit no-ops when the controller already runs).
log_info " rebooting guest $VMID so the baked controller-bootstrap unit picks up the mount"
run pct reboot "$VMID"
}
#-------------------------------------------------------------------------------
@@ -1452,9 +1499,14 @@ step_verify() {
[[ "$st" == "running" ]] && log_success " pct status: running" || { log_error " pct status: $st"; ok=false; }
if pct config "$VMID" 2>/dev/null | grep -q '^onboot: 1'; then log_success " onboot: 1"; else log_error " onboot NOT 1"; ok=false; fi
pct config "$VMID" 2>/dev/null | grep -E '^(rootfs|mp0|mp1|mp8):' | sed 's/^/ /'
# controller container healthy in-guest
local cstat; cstat=$(pct exec "$VMID" -- docker ps --filter name=felhom-controller --format '{{.Status}}' 2>/dev/null | head -1)
if [[ -n "$cstat" ]]; then log_success " controller: $cstat"; else log_warn " controller container not visible yet (may still be starting)"; fi
# controller container healthy in-guest — bounded wait (the post-provision reboot + docker start
# take a while, especially on modest hardware; drill R6 re-verify)
local cstat="" _waited=0
while [[ -z "$cstat" && $_waited -lt 180 ]]; do
cstat=$(pct exec "$VMID" -- docker ps --filter name=felhom-controller --format '{{.Status}}' 2>/dev/null | head -1)
[[ -n "$cstat" ]] || { sleep 5; _waited=$((_waited+5)); }
done
if [[ -n "$cstat" ]]; then log_success " controller: $cstat (after ~${_waited}s)"; else log_warn " controller container not visible after ${_waited}s — check 'pct exec $VMID -- journalctl -u felhom-controller-bootstrap'"; fi
local cver; cver=$(pct exec "$VMID" -- docker ps --filter name=felhom-controller --format '{{.Image}}' 2>/dev/null | head -1)
[[ -n "$cver" ]] && log_info " controller image: $cver"
# tunnel