host-install v1.17.0: appliance guest auto-sizing (F5) + stale operator-key comment fix (R-16)

- F5: appliance mode auto-sizes guest RAM=clamp(host-4096,min 4096,max host-2048,ceil host-1024) + cores=host-1 min 2 when no explicit cap; explicit --memory/--cores always win. Harness F5 red-proof (8/16/32GB + edge + explicit).
- R-16 doc-drift: operator signing keys 'EMPTY by default' comment corrected (keys are PINNED).
This commit is contained in:
2026-07-17 09:21:24 +02:00
parent 0c708a3bd0
commit 698fac04a3
3 changed files with 108 additions and 6 deletions
+15
View File
@@ -1,5 +1,20 @@
# Felhom scripts — Changelog
## felhom-host-install.sh v1.17.0 — appliance guest auto-sizing (F5) + doc-drift fix (2026-07-17)
Closes `VALIDATION-n100-baremetal-2026-07-16.md` **F5 (MEDIUM):** appliance mode provisioned the
golden default (2 GB RAM) on a 16 GB host and never surfaced the `--memory`/`--cores` caps. Now, in
**appliance** mode with no explicit cap, the guest is auto-sized from the host (LXC limits are cheap →
err generous): RAM = `clamp(host-4096, min 4096, max host-2048)` then a hard ceiling `host-1024`
(never over-commit); cores = `host-1, min 2`. An explicit `--memory`/`--cores` ALWAYS wins untouched;
byo mode still requires explicit caps (never auto-sized). Host reads (`MemTotal`/`nproc`) are
overridable via `FELHOM_FAKE_MEMTOTAL_MIB`/`FELHOM_FAKE_NPROC` for the mode harness. Sizing table:
8 GB→4096, 16 GB→12288, 32 GB→28672 MiB; a 4 GB host→3072 (min capped at host-1024).
Red-proof: `hostinstall-mode-harness.sh` new F5 section (8/16/32 GB + small-host edge + explicit-flag
precedence) — pre-fix (auto-size disabled) FAILs, post-fix PASSes. **Same commit (R-16 doc-drift
leftover):** the operator-signing-keys "EMPTY by default" comment was stale (the keys are PINNED to
the real `felhom-op-1`/`felhom-rec-1` ceremony keypairs) — corrected, comment-only, no behavior.
## felhom-poke.sh v1.0.0 — NEW (agent-plane immediate-sync, Direction-2a, 2026-07-16)
The offsite endpoint's THIRD hub forced-command surface (`documentation/runbooks/offsite-endpoint.md`
+52 -6
View File
@@ -182,7 +182,7 @@
set -euo pipefail
SCRIPT_VERSION="1.16.0" # the SINGLE version source (F-1): -h, the run banners, and the hub
SCRIPT_VERSION="1.17.0" # the SINGLE version source (F-1): -h, the run banners, and the hub
# Setup-tab copy (hub internal/web/configs.go hostInstallVersion —
# scripts/hostinstall_gates.py asserts the two stay equal) all follow it.
# 1.16.0: the FELHOM_ESCROW sudoers alias (controller-driven escrow
@@ -190,11 +190,11 @@ SCRIPT_VERSION="1.16.0" # the SINGLE version source (F-1): -h, the run banners
# CANONICAL sudoers fetch below (configs/felhom-agent.sudoers from the
# agent repo, visudo-gated), no separate installer step.
# Operator signing keys pinned at day-0 (GL-4; doc 04 §3 two-key model). EMPTY by default — the pin
# CEREMONY is an operator step: generate the real keypairs OFFLINE, then fill these four constants
# in one commit (or pass --operator-pubkey-file at install time, which overrides them). Empty =
# no authz.signers written = agent self-update stays DORMANT (the safe default; the verify step
# warns). PUBLIC keys only — this script never generates, reads, or references private key material.
# Operator signing keys pinned at day-0 (GL-4; doc 04 §3 two-key model). PINNED below to the real
# ceremony keypairs (the pin ceremony is DONE — felhom-op-1 / felhom-rec-1); --operator-pubkey-file
# at install time overrides them. If these were ever cleared to empty, no authz.signers is written
# and agent self-update stays DORMANT (the safe fallback; the verify step warns). PUBLIC keys only —
# this script never generates, reads, or references private key material.
OPERATOR_KEY_OPERATIONAL_ID="felhom-op-1"
OPERATOR_KEY_OPERATIONAL_LINE="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8z0qCNgA3x2xxAB0Qj5ro8waFjGZ8Ta/sWB63tlLw+ felhom-op-1"
OPERATOR_KEY_RECOVERY_ID="felhom-rec-1"
@@ -1152,6 +1152,49 @@ fi
# --resume variant), so the profile is decided HERE, before the passphrase prompt and any step. The
# refusals are deliberately argv-time (the harness relies on them firing on a non-PVE machine too).
#===============================================================================
# --- F5 (VALIDATION-n100): appliance guest auto-sizing --------------------------
# The golden default (2 GB RAM) was too small on a 16 GB host. In APPLIANCE mode, when the operator
# passes no explicit cap, size the guest generously from the host — LXC limits are cheap, so err
# generous. An explicit --cores/--memory ALWAYS wins (never overwritten). byo mode requires explicit
# caps (never auto-sized). The host reads are overridable for the mode harness (no real /proc dep).
host_total_mib() {
if [[ -n "${FELHOM_FAKE_MEMTOTAL_MIB:-}" ]]; then echo "$FELHOM_FAKE_MEMTOTAL_MIB"; return; fi
awk '/^MemTotal:/{print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0
}
host_core_count() {
if [[ -n "${FELHOM_FAKE_NPROC:-}" ]]; then echo "$FELHOM_FAKE_NPROC"; return; fi
nproc 2>/dev/null || echo 0
}
autosize_guest_caps() {
[[ "$MODE" == "appliance" ]] || return 0
if [[ -z "$MEM_MIB" ]]; then
local total; total=$(host_total_mib)
if [[ "${total:-0}" -gt 0 ]]; then
# clamp(host-4096, min 4096, max host-2048), then the hard ceiling host-1024 (never
# over-commit). Order matters: the min floor is applied AFTER the generous max so that on
# a host too small for both the MIN wins (edge rule), bounded only by the host-1024 ceiling.
local mem=$(( total - 4096 ))
local max2=$(( total - 2048 ))
(( mem > max2 )) && mem=$max2
(( mem < 4096 )) && mem=4096
local ceil=$(( total - 1024 ))
(( mem > ceil )) && mem=$ceil
MEM_MIB=$mem
log_info " auto-sized guest RAM: ${MEM_MIB} MiB (host ${total} MiB; clamp(host-4096, min 4096, max host-2048), ceiling host-1024)"
fi
fi
if [[ -z "$CPU_CORES" ]]; then
local cores; cores=$(host_core_count)
if [[ "${cores:-0}" -gt 0 ]]; then
local c=$(( cores - 1 ))
(( c < 2 )) && c=2
CPU_CORES=$c
log_info " auto-sized guest cores: ${CPU_CORES} (host ${cores} cores; host-1, min 2)"
fi
fi
}
case "$MODE" in
appliance|byo) ;;
"")
@@ -1165,6 +1208,9 @@ case "$MODE" in
*) die "Unknown --mode: $MODE (appliance|byo)" ;;
esac
# F5: fill the appliance guest caps from the host when the operator gave none (explicit flags win).
autosize_guest_caps
# BYO argument refusals (C1/C2) — before the passphrase prompt, before any step.
if [[ "$MODE" == "byo" ]]; then
if [[ -z "$CPU_CORES" || -z "$MEM_MIB" ]]; then
+41
View File
@@ -347,6 +347,47 @@ else
verdict FAIL "GL8-F1b glob removal clears agent.json + every .bak* + the empty dir" "residue: $(ls -A "$f1dir" 2>/dev/null | tr '\n' ' ')"
fi
echo ""
echo "--- F5 appliance guest auto-sizing (VALIDATION-n100) ---"
# The auto-size log line is emitted right after mode validation (BEFORE any PVE/hub contact), so it
# is asserted portably on ANY host — the later pipeline dies for lack of PVE, but the sizing already
# happened. RED-PROOF: pre-fix, appliance passes NO cap → the golden 2 GB default reaches the guest
# (no auto-size line at all); post-fix, the formula value is logged (and flows into the provision
# args via cap_args). Host reads are faked via FELHOM_FAKE_MEMTOTAL_MIB / FELHOM_FAKE_NPROC.
f5_run() { # <memtotal_mib> <nproc> [extra install args...]
local mt="$1" np="$2"; shift 2
FELHOM_INSTALL_STATE_DIR="$STATE_OVERRIDE_ENV" FELHOM_FAKE_MEMTOTAL_MIB="$mt" FELHOM_FAKE_NPROC="$np" \
bash "$SCRIPT" --customer-id t --mode appliance --dry-run "$@" </dev/null 2>&1 || true
}
while read -r f5_mt f5_np f5_mem f5_cores; do
[[ -z "$f5_mt" ]] && continue
o=$(f5_run "$f5_mt" "$f5_np")
if echo "$o" | grep -q "auto-sized guest RAM: ${f5_mem} MiB" && echo "$o" | grep -q "auto-sized guest cores: ${f5_cores} "; then
verdict PASS "F5 auto-size host ${f5_mt}MiB/${f5_np}c -> guest ${f5_mem}MiB/${f5_cores} cores"
else
verdict FAIL "F5 auto-size host ${f5_mt}MiB/${f5_np}c -> guest ${f5_mem}MiB/${f5_cores} cores" \
"got: $(echo "$o" | grep -i auto-sized | tr '\n' '; ')"
fi
done <<'F5TABLE'
8192 8 4096 7
16384 8 12288 7
32768 16 28672 15
F5TABLE
# Edge: a host below the min clamp — the min (4096) wins but is capped at host-1024.
o=$(f5_run 4096 2)
if echo "$o" | grep -q "auto-sized guest RAM: 3072 MiB"; then
verdict PASS "F5 auto-size small host 4096MiB -> 3072MiB (min capped at host-1024)"
else
verdict FAIL "F5 auto-size small host 4096MiB -> 3072MiB" "got: $(echo "$o" | grep -i 'auto-sized guest RAM' | tr '\n' '; ')"
fi
# Explicit flags ALWAYS win: NO auto-size line (byte-identical to the pre-fix no-autosize behavior).
o=$(f5_run 16384 8 --memory 8192 --cores 4)
if ! echo "$o" | grep -qi "auto-sized"; then
verdict PASS "F5 explicit --memory/--cores suppress auto-sizing (explicit wins untouched)"
else
verdict FAIL "F5 explicit --memory/--cores suppress auto-sizing" "auto-size line present despite explicit flags"
fi
echo ""
echo "--- PVE tier ---"
if ! command -v pveum >/dev/null 2>&1 || [[ "$(id -u)" != 0 ]]; then