diff --git a/scripts/CHANGELOG.md b/scripts/CHANGELOG.md index bdecfc2..768c90d 100644 --- a/scripts/CHANGELOG.md +++ b/scripts/CHANGELOG.md @@ -1,5 +1,34 @@ # Felhom scripts — Changelog +## felhom-host-install.sh v1.5.0 — `felhom` pool by default + `--adopt-pool` retrofit + uninstall teardown (2026-07-01) + +Colleague-safety batch #4 phase a. Every Felhom-managed guest now joins a dedicated **`felhom` pool** +for fleet uniformity (and as the environment the later pool-scoped ACL — 3b — will spike against). All +pool ops run as `root@pam` from the installer, so there is **NO agent/token/ACL change** and zero +permission-model risk (`PVE_PRIVS` untouched; the `FelhomAgent` token stays scoped at `/`). + +- **New `felhom` pool default.** `step_provision` calls `ensure_felhom_pool` (create if absent, + idempotent) and, after a successful provision, adds the guest via `pveum pool modify felhom -vms + ` (skip-if-already-member). New helpers `pool_exists` / `pool_members` / `ensure_felhom_pool` / + `pool_add_guest`; const `PVE_POOL="felhom"`. PVE 9 syntax + `/pools` JSON shape confirmed live before + wiring (`pveum pool add|delete|modify`; `pvesh get /pools` → `[{poolid,comment}]`, `/pools/` → + `{members:[{vmid,…}]}`). +- **`--adopt-pool` retrofit mode.** Non-destructive: adds an EXISTING Felhom guest to the pool (creating + it if needed), resolving the guest from `--vmid` else the recorded `provisioned_vmid`. Reuses the + ours-check (`/etc/felhom-bootstrap` mount) — refuses a non-Felhom guest unless `--force`. Touches ONLY + pool membership: never reconfigures/restarts the guest, never contacts the hub. Idempotent + (skip-if-member). +- **`--uninstall` pool teardown (step 5b).** After the pveum removal, deletes the `felhom` pool **only + if empty** (a destroyed guest is auto-removed from its pool); a pool that still has members is left + with a `log_skip` naming them. Not reached on the Spec-1 safe-skip path (other Felhom guests remain). +- **Validated on felhom-pve** (dry-run + SAFE live): T-A fresh-install dry-run shows the pool create + + membership lines; T-B **live adopt of guest 9201** → `pvesh get /pools/felhom` lists 9201, guest still + running, config unchanged (the demo node is now pool-uniform); re-run = no-op; T-B' non-Felhom vmid → + refusal; T-C uninstall dry-run → "pool felhom not empty (members: 9201) — leaving it". `bash -n` + + `shellcheck` clean (0 new warnings; the 2 pre-existing SC2015 in `step_verify` unchanged). +- **NOT changed:** `PVE_PRIVS`, the ACL grants, the agent, the provision-call args. 3b (pool-scoped ACL + + agent restore-into-pool under a scoped token) is the separate spike-gated task. + ## felhom-host-install.sh v1.4.0 — appliance CPU/RAM cap passthrough (`--cores` / `--memory`) (2026-07-01) Colleague-safety batch #3 (host-install half; the mechanism is agent v0.52.0). Lets an operator cap the diff --git a/scripts/felhom-host-install.sh b/scripts/felhom-host-install.sh index 0722078..2006afa 100644 --- a/scripts/felhom-host-install.sh +++ b/scripts/felhom-host-install.sh @@ -1,6 +1,6 @@ #!/bin/bash #=============================================================================== -# felhom-host-install.sh v1.4.0 +# felhom-host-install.sh v1.5.0 # 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 @@ -79,6 +79,13 @@ # --archive-storage, --golden-vmid, --dry-run. # --remove-golden with --uninstall, also delete the golden vzdump from the archive storage # +# Retrofit (local, non-destructive — no hub contact, no passphrase): +# --adopt-pool add an EXISTING Felhom guest to the `felhom` pool (creates the pool if +# needed). Resolves the guest from --vmid else the recorded provisioned_vmid; +# refuses a non-Felhom guest unless --force. Touches ONLY pool membership — +# never reconfigures/restarts the guest. (A fresh provision joins the pool +# automatically; this retrofits already-installed boxes.) +# # State (idempotent/resumable): /var/lib/felhom-install/state.json # Agent config written 0600 to the systemd unit's -config path # (auto-detected; else /etc/felhom-agent/agent.json). @@ -90,7 +97,7 @@ set -euo pipefail -SCRIPT_VERSION="1.4.0" +SCRIPT_VERSION="1.5.0" #------------------------------------------------------------------------------- # Logging (mirrors felhom-controller/scripts/docker-setup.sh) @@ -136,6 +143,7 @@ DRY_RUN=false RESUME=false UNINSTALL=false # --uninstall: local host teardown (destroy guest + remove agent/pveum/state) REMOVE_GOLDEN=false # --remove-golden: also delete the golden vzdump during --uninstall +ADOPT_POOL=false # --adopt-pool: retrofit an EXISTING Felhom guest into the felhom pool (non-destructive) # --- Gitea (artifact source) + agent install model (BUNDLE slice) --- GITEA_BASE="https://gitea.dooplex.hu" @@ -150,6 +158,7 @@ AGENT_STATE_DIR="/var/lib/felhom-agent" PVE_USER="felhom-agent@pve" PVE_TOKENID="agent" PVE_ROLE="FelhomAgent" +PVE_POOL="felhom" # dedicated pool every Felhom-managed guest joins (fleet uniformity; 3b will scope the ACL to it) # The authoritative 16 privileges (agent README; VM.Config.CPUMemory is NOT real, SDN.Use IS required). PVE_PRIVS="VM.Allocate VM.Audit VM.Config.Disk VM.Config.CPU VM.Config.Memory VM.Config.Network VM.Config.Options VM.PowerMgmt VM.Snapshot VM.Snapshot.Rollback VM.Backup Datastore.Allocate Datastore.AllocateSpace Datastore.Audit Sys.Audit SDN.Use" @@ -173,7 +182,7 @@ ART_GOLDEN_SHA="" #------------------------------------------------------------------------------- # Helpers #------------------------------------------------------------------------------- -usage() { sed -n '2,80p' "$0" | sed 's/^# \{0,1\}//'; exit 0; } +usage() { sed -n '2,87p' "$0" | sed 's/^# \{0,1\}//'; exit 0; } run() { # simple (no pipes/redirects) mutating command if $DRY_RUN; then log_dry "$*"; else "$@"; fi @@ -303,6 +312,49 @@ felhom_guests() { done } +#------------------------------------------------------------------------------- +# felhom pool (fleet uniformity) — every managed guest joins the `felhom` pool. All pool ops run as +# root@pam from the installer, so NO agent/token/ACL change is involved (that is the separate 3b spike). +# API shapes confirmed on PVE 9: `pvesh get /pools` → [{poolid,comment}]; `pvesh get /pools/` → +# {poolid,comment,members:[{vmid,...}]}. Pool ops: `pveum pool add|delete `, `pveum pool modify +# --vms ` (additive). +#------------------------------------------------------------------------------- +# pool_exists — true if the felhom pool is present. +pool_exists() { + pvesh get /pools --output-format json 2>/dev/null \ + | python3 -c "import json,sys;sys.exit(0 if any(p.get('poolid')=='$PVE_POOL' for p in json.load(sys.stdin)) else 1)" 2>/dev/null +} + +# pool_members — space-separated vmids currently in the felhom pool (empty if none / pool absent). +pool_members() { + pvesh get "/pools/$PVE_POOL" --output-format json 2>/dev/null \ + | python3 -c "import json,sys +try: d=json.load(sys.stdin) +except Exception: sys.exit(0) +print(' '.join(str(m.get('vmid')) for m in d.get('members',[]) if m.get('vmid') is not None))" 2>/dev/null +} + +# ensure_felhom_pool — create the pool if absent (idempotent no-op otherwise). Via run() (dry-run-aware). +ensure_felhom_pool() { + if pool_exists; then + log_skip " pool $PVE_POOL already exists" + else + run pveum pool add "$PVE_POOL" --comment "Felhom-managed guests" + fi +} + +# pool_add_guest VMID — add a guest to the felhom pool unless it is already a member (idempotent). +pool_add_guest() { + local vmid="$1" members + members=" $(pool_members) " + if [[ "$members" == *" $vmid "* ]]; then + log_skip " guest $vmid already in pool $PVE_POOL" + else + run pveum pool modify "$PVE_POOL" -vms "$vmid" + log_success " guest $vmid added to pool $PVE_POOL" + fi +} + # run_uninstall — the full guarded teardown. Every mutation goes through run() so --dry-run prints it # and executes nothing. Ordering is the reverse of install: guest -> agent -> pveum(ACL,token,user, # role) -> golden(opt-in) -> state file. See the TASK spec §7/§8. @@ -310,7 +362,7 @@ run_uninstall() { log_step "UNINSTALL — local host teardown" # 1. Resolve the target vmid: --vmid, else the recorded provisioned_vmid, else die. - local state_vmid vmid + local state_vmid vmid pool_removed=false state_vmid=$(_state_get provisioned_vmid) if $VMID_EXPLICIT; then vmid="$VMID" @@ -438,6 +490,21 @@ run_uninstall() { log_skip " role $PVE_ROLE already absent" fi + # 5b. felhom pool — delete ONLY if empty (a destroyed guest is auto-removed from its pool). Never + # delete a pool that still holds members (someone else's guests, or another Felhom guest kept + # under --force). + if pool_exists; then + local pool_left; pool_left=$(pool_members) + if [[ -z "$pool_left" ]]; then + run pveum pool delete "$PVE_POOL" + pool_removed=true + else + log_skip " pool $PVE_POOL not empty (members: $pool_left) — leaving it" + fi + else + log_skip " pool $PVE_POOL already absent" + fi + # 6. Golden vzdump (opt-in via --remove-golden; else left in place). if $REMOVE_GOLDEN; then local gvols gv @@ -461,13 +528,51 @@ 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, and $STATE_FILE." + 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." 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." $DRY_RUN && log_warn " DRY-RUN: nothing above was actually executed." return 0 } +# run_adopt_pool — retrofit an EXISTING Felhom guest into the felhom pool. Non-destructive: creates the +# pool if absent + adds the guest; never reconfigures/restarts the guest, never contacts the hub. Guest +# resolves from --vmid else the recorded provisioned_vmid (mirrors run_uninstall). +run_adopt_pool() { + log_step "ADOPT-POOL — add an existing Felhom guest to the $PVE_POOL pool" + + local state_vmid vmid + state_vmid=$(_state_get provisioned_vmid) + if $VMID_EXPLICIT; then + vmid="$VMID" + elif [[ -n "$state_vmid" ]]; then + vmid="$state_vmid" + log_info " no --vmid given; using recorded provisioned_vmid=$vmid from $STATE_FILE" + else + die "pass --vmid N (state has no recorded vmid)" + fi + + _vmid_in_use "$vmid" || die "guest $vmid not found on this host (nothing to adopt)" + + # ours-check: only adopt a Felhom guest (has the /etc/felhom-bootstrap mount) unless --force. + if pct config "$vmid" 2>/dev/null | grep -q 'mp=/etc/felhom-bootstrap'; then + log_info " vmid $vmid looks like a Felhom guest (has the /etc/felhom-bootstrap mount)" + elif $FORCE; then + log_warn " vmid $vmid has NO /etc/felhom-bootstrap mount — --force given, adopting anyway" + else + die "vmid $vmid does not look like a Felhom-provisioned guest (no /etc/felhom-bootstrap mount). + Refusing to adopt. Pass --force to override." + fi + + ensure_felhom_pool + pool_add_guest "$vmid" + + echo "" + log_success "ADOPT-POOL complete — guest $vmid is in pool $PVE_POOL (guest not otherwise modified)." + $DRY_RUN && log_warn " DRY-RUN: nothing above was actually executed." + return 0 +} + #------------------------------------------------------------------------------- # Arg parse #------------------------------------------------------------------------------- @@ -496,6 +601,7 @@ while [[ $# -gt 0 ]]; do --skip-provision) SKIP_PROVISION=true; shift ;; --uninstall) UNINSTALL=true; shift ;; --remove-golden) REMOVE_GOLDEN=true; shift ;; + --adopt-pool) ADOPT_POOL=true; shift ;; --dry-run) DRY_RUN=true; shift ;; --resume) RESUME=true; shift ;; -h|--help) usage ;; @@ -517,6 +623,20 @@ if $UNINSTALL; then exit 0 fi +#=============================================================================== +# ADOPT-POOL MODE — retrofit an EXISTING Felhom guest into the felhom pool (non-destructive; no hub +# contact, no passphrase, no guest reconfigure beyond pool membership). Dispatched early. +#=============================================================================== +if $ADOPT_POOL; then + [[ $EUID -eq 0 ]] || die "must run as root" + echo "" + log_info "felhom-host-install v${SCRIPT_VERSION} — mode=adopt-pool" + $DRY_RUN && log_warn "DRY-RUN: no mutations will be performed" + echo "" + run_adopt_pool + exit 0 +fi + #=============================================================================== # DR MODE — documented seam only (10D). NOT implemented. #=============================================================================== @@ -1131,8 +1251,12 @@ step_provision() { local -a cap_args=() [[ -n "$CPU_CORES" ]] && cap_args+=(-cores "$CPU_CORES") [[ -n "$MEM_MIB" ]] && cap_args+=(-memory "$MEM_MIB") + # felhom pool: ensure it exists (root@pam op — no agent/token/ACL change), then add the guest after + # a successful provision. Fleet uniformity + the environment 3b will scope its ACL against. + ensure_felhom_pool if $DRY_RUN; then log_dry "felhom-agent --config $AGENT_CONFIG --selftest=provision -archive $GOLDEN_VOLID -vmid $VMID -customer-id $CUSTOMER_ID -hub-password -rootfs-grow $ROOTFS_GROW -datavol-grow $DATAVOL_GROW -sysdata-grow $SYSDATA_GROW ${cap_args[*]}" + log_dry "add guest $VMID to pool $PVE_POOL" log_dry "record provisioned_vmid=$VMID in $STATE_FILE (for a later automatic --uninstall)" _state_mark provision; return 0 fi @@ -1147,6 +1271,8 @@ step_provision() { _state_mark provision # Record the provisioned vmid so a later --uninstall resolves the target automatically + safely. _state_put provisioned_vmid "$VMID" + # Join the felhom pool (idempotent; skip-if-member). + pool_add_guest "$VMID" } #-------------------------------------------------------------------------------