From 741f322d6ad321b89602549086b5bf45dd0140ae Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Wed, 1 Jul 2026 12:46:08 +0200 Subject: [PATCH] host-install v1.6.0: pool-scoped token ACL (3-role) + --rescope-acl retrofit step_token now grants 3 scoped roles (Guest@/pool/felhom incl. Pool.Allocate, Store@each PVE_STORAGES incl. felhom-pbs, Base=Sys.Audit+SDN.Use@/) to both user and token, pool-before-token, and removes any pre-3b broad / grant. New --rescope-acl migrates existing installs (add-before-remove; supervised). --uninstall removes both scoped + old shapes. Post-provision pool_add_guest dropped (agent restore --pool owns membership, v0.53.0). Validated dry-run on felhom-pve; bash -n + shellcheck clean. LIVE rescope+swap is the supervised STOP. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/CHANGELOG.md | 31 +++++ scripts/felhom-host-install.sh | 229 ++++++++++++++++++++++++++------- 2 files changed, 217 insertions(+), 43 deletions(-) diff --git a/scripts/CHANGELOG.md b/scripts/CHANGELOG.md index 768c90d..5c07639 100644 --- a/scripts/CHANGELOG.md +++ b/scripts/CHANGELOG.md @@ -1,5 +1,36 @@ # Felhom scripts — Changelog +## felhom-host-install.sh v1.6.0 — pool-scoped token ACL (3-role) + `--rescope-acl` retrofit (2026-07-01) + +Colleague-safety batch #4 phase b (script half; agent half = v0.53.0). Moves the agent token's dangerous +privileges off `/` (which spanned every guest + storage) to `/pool/felhom` + `/storage/`, so on +a shared box the token can only touch Felhom's own guests + storages. Validated by +`documentation/audits/SPIKE-pool-scoped-acl-2026-07-01.md` (PASS) — implemented here. + +- **3-role scoped ACL (`step_token` rewrite).** Replaces the single `FelhomAgent` role granted at `/` + with three roles, each granted to BOTH the user AND the token (privsep intersection): `FelhomAgentGuest` + (`VM.*` + `Pool.Allocate`) @ `/pool/felhom`; `FelhomAgentStore` (`Datastore.*`) @ each of + `PVE_STORAGES` (default `local local-lvm felhom-pbs` — the offsite PBS MUST be included, SPIKE + residual #1; `--acl-storages` overrides); `FelhomAgentBase` (`Sys.Audit SDN.Use`) @ `/`. Helpers + `apply_scoped_acl`/`remove_scoped_acl`/`_grant`/`_ensure_role`. +- **Pool before token.** `ensure_felhom_pool` runs at the top of `step_token` (always, incl. + `--skip-provision`) so `/pool/felhom` exists before it's granted on. +- **Re-install safety.** `step_token` also removes the pre-3b broad `/` grant + `FelhomAgent` role if + present (`remove_old_broad_acl`, tolerate-absent), so a re-install can't leave the old grant unioned + with the scoped one. The post-provision `pool_add_guest` is gone (the agent's `restore --pool` makes + the guest a member atomically — v0.53.0). +- **`--rescope-acl` retrofit** (new mode, mirrors `--adopt-pool`): migrate an existing install — ensure + the pool + guest membership, apply the scoped grants, THEN remove the old broad grant (add-before- + remove: the token is never grant-less mid-migration). Prints the "now deploy agent ≥ v0.53.0" + ordering reminder. Idempotent + dry-run-aware. **SUPERVISED** (run with the agent stopped — the scoped + ACL and the pool-param agent are mutually dependent; §13 of the task). +- **`--uninstall`** now removes the scoped grants + 3 roles AND the pre-3b broad grant/role (both + tolerate-absent → works on either shape), keeping the pool delete-if-empty (v1.5.0). +- **Validated on felhom-pve** (dry-run): T-A fresh install (pool-before-token, 3 roles once, scoped + grants incl. `/storage/felhom-pbs`), `--rescope-acl` (add scoped → remove old `FelhomAgent`), T-F + uninstall (old-shape cleanup + pool not-empty skip). `bash -n` + `shellcheck` clean (0 new warnings). + **The live rescope + agent swap is the supervised STOP** — not run here. + ## 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** diff --git a/scripts/felhom-host-install.sh b/scripts/felhom-host-install.sh index 2006afa..9779767 100644 --- a/scripts/felhom-host-install.sh +++ b/scripts/felhom-host-install.sh @@ -1,6 +1,6 @@ #!/bin/bash #=============================================================================== -# felhom-host-install.sh v1.5.0 +# felhom-host-install.sh v1.6.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 @@ -85,6 +85,13 @@ # 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.) +# --rescope-acl migrate an existing install from the pre-3b broad-`/` token grant to the +# pool-scoped ACL (Guest@/pool/felhom + Store@each storage + Sys.Audit/SDN.Use@/). +# ACL-only (no data touched). SUPERVISED: run with felhom-agent STOPPED, then +# deploy agent >= v0.53.0, then start (the scoped ACL + pool-param agent are +# mutually dependent). +# --acl-storages "a b c" override the storages the scoped ACL grants Datastore.* on +# (default: "local local-lvm felhom-pbs"). Used by fresh install + --rescope-acl. # # State (idempotent/resumable): /var/lib/felhom-install/state.json # Agent config written 0600 to the systemd unit's -config path @@ -97,7 +104,7 @@ set -euo pipefail -SCRIPT_VERSION="1.5.0" +SCRIPT_VERSION="1.6.0" #------------------------------------------------------------------------------- # Logging (mirrors felhom-controller/scripts/docker-setup.sh) @@ -144,6 +151,7 @@ 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) +RESCOPE_ACL=false # --rescope-acl: migrate an existing install from the broad-/ token to the scoped ACL # --- Gitea (artifact source) + agent install model (BUNDLE slice) --- GITEA_BASE="https://gitea.dooplex.hu" @@ -157,10 +165,24 @@ 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" +PVE_POOL="felhom" # dedicated pool every Felhom-managed guest joins (fleet uniformity + ACL scope) +PVE_ROLE="FelhomAgent" # the PRE-3b single broad role (removed on rescope/uninstall if present — legacy) + +# Pool-scoped ACL (3b, validated by SPIKE-pool-scoped-acl-2026-07-01): the agent's privileges are split +# across THREE roles applied at scoped paths so the token can only touch Felhom's own guests + storages +# (blast-radius containment). `pveum acl` grants a whole role per path, hence 3 roles not 1. Each role is +# granted to BOTH the user AND the token (privsep intersection). Guest privs (incl. Pool.Allocate so the +# agent restores INTO the pool) live at /pool/felhom; Datastore privs at each agent-touched storage; only +# Sys.Audit+SDN.Use box-wide at /. +PVE_ROLE_GUEST="FelhomAgentGuest" +PVE_ROLE_STORE="FelhomAgentStore" +PVE_ROLE_BASE="FelhomAgentBase" +PVE_PRIVS_GUEST="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 Pool.Allocate" +PVE_PRIVS_STORE="Datastore.Allocate Datastore.AllocateSpace Datastore.Audit" +PVE_PRIVS_BASE="Sys.Audit SDN.Use" +# Storages the agent reads/writes (archive+dump=local, restore=local-lvm, offsite DR=felhom-pbs). The +# offsite felhom-pbs MUST be included or the agent's DR backup 403s (SPIKE residual #1). --acl-storages overrides. +PVE_STORAGES=(local local-lvm felhom-pbs) STATE_DIR="/var/lib/felhom-install" STATE_FILE="${STATE_DIR}/state.json" @@ -182,7 +204,7 @@ ART_GOLDEN_SHA="" #------------------------------------------------------------------------------- # Helpers #------------------------------------------------------------------------------- -usage() { sed -n '2,87p' "$0" | sed 's/^# \{0,1\}//'; exit 0; } +usage() { sed -n '2,94p' "$0" | sed 's/^# \{0,1\}//'; exit 0; } run() { # simple (no pipes/redirects) mutating command if $DRY_RUN; then log_dry "$*"; else "$@"; fi @@ -355,6 +377,82 @@ pool_add_guest() { fi } +#------------------------------------------------------------------------------- +# Pool-scoped ACL helpers (3b). All ops run as root@pam (installer) — no privilege change to the agent. +#------------------------------------------------------------------------------- +# _role_exists NAME — true if a pveum role NAME exists. +_role_exists() { + pveum role list --output-format json 2>/dev/null \ + | python3 -c "import json,sys;sys.exit(0 if any(r['roleid']==sys.argv[1] for r in json.load(sys.stdin)) else 1)" "$1" 2>/dev/null +} + +# _ensure_role NAME "PRIVS" — create the role, or modify it to the exact priv set (idempotent). +_ensure_role() { + local name="$1" privs="$2" + if _role_exists "$name"; then + log_info " role $name exists — ensuring exact privileges" + run pveum role modify "$name" -privs "$privs" + else + run pveum role add "$name" -privs "$privs" + fi +} + +# _grant PATH ROLE — grant ROLE at PATH to BOTH the user AND the token (privsep intersection). `acl +# modify` is idempotent so this is safe to repeat / re-apply after a token rotation. +_grant() { + local path="$1" role="$2" + run pveum acl modify "$path" -user "$PVE_USER" -role "$role" + run pveum acl modify "$path" -token "${PVE_USER}!${PVE_TOKENID}" -role "$role" +} + +# apply_scoped_acl — create the 3 scoped roles and grant each at its path(s). Requires the pool to exist. +apply_scoped_acl() { + _ensure_role "$PVE_ROLE_GUEST" "$PVE_PRIVS_GUEST" + _ensure_role "$PVE_ROLE_STORE" "$PVE_PRIVS_STORE" + _ensure_role "$PVE_ROLE_BASE" "$PVE_PRIVS_BASE" + _grant "/pool/$PVE_POOL" "$PVE_ROLE_GUEST" + local s + for s in "${PVE_STORAGES[@]}"; do + _grant "/storage/$s" "$PVE_ROLE_STORE" + done + _grant / "$PVE_ROLE_BASE" + log_success " scoped ACL applied (Guest@/pool/$PVE_POOL, Store@[${PVE_STORAGES[*]}], Base@/)" +} + +# _acl_grant_present PATH TYPE UGID ROLE — true if that exact ACL grant exists. +_acl_grant_present() { + pveum acl list --output-format json 2>/dev/null | python3 -c "import json,sys +p,t,u,r=sys.argv[1:5] +sys.exit(0 if any(e.get('path')==p and e.get('type')==t and e.get('ugid')==u and e.get('roleid')==r for e in json.load(sys.stdin)) else 1)" "$1" "$2" "$3" "$4" 2>/dev/null +} + +# remove_scoped_acl — delete the 3-role scoped grants (user+token at each path), then the 3 roles +# (roles last — PVE refuses to delete a referenced role). Presence-checked, tolerate-absent. +remove_scoped_acl() { + local s + if _acl_grant_present "/pool/$PVE_POOL" user "$PVE_USER" "$PVE_ROLE_GUEST"; then run pveum acl delete "/pool/$PVE_POOL" --users "$PVE_USER" --roles "$PVE_ROLE_GUEST"; fi + if _acl_grant_present "/pool/$PVE_POOL" token "${PVE_USER}!${PVE_TOKENID}" "$PVE_ROLE_GUEST"; then run pveum acl delete "/pool/$PVE_POOL" --tokens "${PVE_USER}!${PVE_TOKENID}" --roles "$PVE_ROLE_GUEST"; fi + for s in "${PVE_STORAGES[@]}"; do + if _acl_grant_present "/storage/$s" user "$PVE_USER" "$PVE_ROLE_STORE"; then run pveum acl delete "/storage/$s" --users "$PVE_USER" --roles "$PVE_ROLE_STORE"; fi + if _acl_grant_present "/storage/$s" token "${PVE_USER}!${PVE_TOKENID}" "$PVE_ROLE_STORE"; then run pveum acl delete "/storage/$s" --tokens "${PVE_USER}!${PVE_TOKENID}" --roles "$PVE_ROLE_STORE"; fi + done + if _acl_grant_present / user "$PVE_USER" "$PVE_ROLE_BASE"; then run pveum acl delete / --users "$PVE_USER" --roles "$PVE_ROLE_BASE"; fi + if _acl_grant_present / token "${PVE_USER}!${PVE_TOKENID}" "$PVE_ROLE_BASE"; then run pveum acl delete / --tokens "${PVE_USER}!${PVE_TOKENID}" --roles "$PVE_ROLE_BASE"; fi + local name + for name in "$PVE_ROLE_GUEST" "$PVE_ROLE_STORE" "$PVE_ROLE_BASE"; do + if _role_exists "$name"; then run pveum role delete "$name"; else log_skip " role $name already absent"; fi + done +} + +# remove_old_broad_acl — remove the PRE-3b single FelhomAgent role granted at / (user+token) + the role. +# Tolerate-absent (fresh 3b installs have none). Used by --rescope-acl (migration) and step_token (so a +# re-install can't leave the old broad grant unioned with the new scoped one). +remove_old_broad_acl() { + if _acl_grant_present / user "$PVE_USER" "$PVE_ROLE"; then run pveum acl delete / --users "$PVE_USER" --roles "$PVE_ROLE"; fi + if _acl_grant_present / token "${PVE_USER}!${PVE_TOKENID}" "$PVE_ROLE"; then run pveum acl delete / --tokens "${PVE_USER}!${PVE_TOKENID}" --roles "$PVE_ROLE"; fi + if _role_exists "$PVE_ROLE"; then run pveum role delete "$PVE_ROLE"; else log_skip " old broad role $PVE_ROLE already absent"; 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. @@ -461,19 +559,12 @@ 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 - # 5. pveum removal (reverse order; presence-checked; tolerate-absent). The role delete must come - # AFTER its ACL grants are gone — PVE refuses to delete a referenced role. - local acls; acls=$(pveum acl list --output-format json 2>/dev/null || echo '[]') - if echo "$acls" | python3 -c "import json,sys;sys.exit(0 if any(e.get('path')=='/' and e.get('type')=='user' and e.get('ugid')=='$PVE_USER' and e.get('roleid')=='$PVE_ROLE' for e in json.load(sys.stdin)) else 1)" 2>/dev/null; then - run pveum acl delete / --users "$PVE_USER" --roles "$PVE_ROLE" - else - log_skip " ACL user grant ($PVE_USER @ /) already absent" - fi - if echo "$acls" | python3 -c "import json,sys;sys.exit(0 if any(e.get('path')=='/' and e.get('type')=='token' and e.get('ugid')=='${PVE_USER}!${PVE_TOKENID}' and e.get('roleid')=='$PVE_ROLE' for e in json.load(sys.stdin)) else 1)" 2>/dev/null; then - run pveum acl delete / --tokens "${PVE_USER}!${PVE_TOKENID}" --roles "$PVE_ROLE" - else - log_skip " ACL token grant (${PVE_USER}!${PVE_TOKENID} @ /) already absent" - 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. + remove_scoped_acl + remove_old_broad_acl + # token then user (token-remove purges its ACL; user-delete purges anything else). if pveum user token list "$PVE_USER" --output-format json 2>/dev/null | python3 -c "import json,sys;sys.exit(0 if any(t['tokenid']=='$PVE_TOKENID' for t in json.load(sys.stdin)) else 1)" 2>/dev/null; then run pveum user token remove "$PVE_USER" "$PVE_TOKENID" else @@ -484,11 +575,6 @@ run_uninstall() { else log_skip " user $PVE_USER already absent" fi - if pveum role list --output-format json 2>/dev/null | python3 -c "import json,sys;sys.exit(0 if any(r['roleid']=='$PVE_ROLE' for r in json.load(sys.stdin)) else 1)" 2>/dev/null; then - run pveum role delete "$PVE_ROLE" - else - 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 @@ -573,6 +659,50 @@ run_adopt_pool() { return 0 } +# run_rescope_acl — migrate an EXISTING install from the pre-3b broad-/ token to the pool-scoped ACL. +# Non-destructive to data (ACL-only): ensure the pool + the guest is a member, apply the 3-role scoped +# grants, THEN remove the old broad grant. Idempotent + dry-run-aware. Does NOT touch the guest or hub. +# ORDERING (see §13): run this with the agent STOPPED, then deploy agent >= v0.53.0, then start — the +# scoped ACL and the pool-param agent are mutually dependent. +run_rescope_acl() { + log_step "RESCOPE-ACL — migrate to the pool-scoped token ACL" + + 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" + 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, rescoping anyway" + else + die "vmid $vmid does not look like a Felhom-provisioned guest (no /etc/felhom-bootstrap mount). + Refusing to rescope. Pass --force to override." + fi + + # The guest MUST be a pool member before the scoped token can touch it — ensure it first. + ensure_felhom_pool + pool_add_guest "$vmid" + # Apply the scoped grants, THEN remove the old broad grant (add-before-remove: never leave the token + # with NO grant mid-migration). + apply_scoped_acl + remove_old_broad_acl + + echo "" + log_success "RESCOPE-ACL complete — token scoped to /pool/$PVE_POOL + /storage/[${PVE_STORAGES[*]}] + Sys.Audit/SDN.Use@/." + log_warn " NOW deploy agent >= v0.53.0 (restore-into-pool) and (re)start felhom-agent — the scoped ACL needs it." + $DRY_RUN && log_warn " DRY-RUN: nothing above was actually executed." + return 0 +} + #------------------------------------------------------------------------------- # Arg parse #------------------------------------------------------------------------------- @@ -602,6 +732,8 @@ while [[ $# -gt 0 ]]; do --uninstall) UNINSTALL=true; shift ;; --remove-golden) REMOVE_GOLDEN=true; shift ;; --adopt-pool) ADOPT_POOL=true; shift ;; + --rescope-acl) RESCOPE_ACL=true; shift ;; + --acl-storages) read -ra PVE_STORAGES <<< "$2"; shift 2 ;; --dry-run) DRY_RUN=true; shift ;; --resume) RESUME=true; shift ;; -h|--help) usage ;; @@ -637,6 +769,20 @@ if $ADOPT_POOL; then exit 0 fi +#=============================================================================== +# RESCOPE-ACL MODE — migrate an existing install to the pool-scoped token ACL (ACL-only, no hub, no +# passphrase). Supervised: run with the agent stopped, then deploy agent >= v0.53.0 (see §13). +#=============================================================================== +if $RESCOPE_ACL; then + [[ $EUID -eq 0 ]] || die "must run as root" + echo "" + log_info "felhom-host-install v${SCRIPT_VERSION} — mode=rescope-acl" + $DRY_RUN && log_warn "DRY-RUN: no mutations will be performed" + echo "" + run_rescope_acl + exit 0 +fi + #=============================================================================== # DR MODE — documented seam only (10D). NOT implemented. #=============================================================================== @@ -841,13 +987,10 @@ step_token() { log_step "2/8 Proxmox API token" if should_skip token && [[ -n "$PVE_TOKEN" ]]; then return 0; fi - # role: create or modify to the exact 16 privs - if pveum role list --output-format json 2>/dev/null | python3 -c "import json,sys;sys.exit(0 if any(r['roleid']=='$PVE_ROLE' for r in json.load(sys.stdin)) else 1)"; then - log_info " role $PVE_ROLE exists — ensuring exact privileges" - run pveum role modify "$PVE_ROLE" -privs "$PVE_PRIVS" - else - run pveum role add "$PVE_ROLE" -privs "$PVE_PRIVS" - fi + # Pool BEFORE the ACL: /pool/felhom must exist before apply_scoped_acl grants on it (3b). Always — + # even under --skip-provision (the token exists now; a later provision-into-pool needs pool + grant). + # The 3 scoped roles + grants are created by apply_scoped_acl below (AFTER the token exists). + ensure_felhom_pool # user: tolerate-exists if pveum user list --output-format json 2>/dev/null | python3 -c "import json,sys;sys.exit(0 if any(u['userid']=='$PVE_USER' for u in json.load(sys.stdin)) else 1)"; then @@ -887,11 +1030,12 @@ step_token() { fi fi - # Both ACL grants — AFTER the token exists (the single most common 403 cause). - # `pveum user token remove` PURGES the token's ACL, so re-applying here (post-rotate) - # is mandatory; `acl modify` is idempotent so this is also safe on the reuse path. - run pveum acl modify / -user "$PVE_USER" -role "$PVE_ROLE" - run pveum acl modify / -token "${PVE_USER}!${PVE_TOKENID}" -role "$PVE_ROLE" + # Scoped ACL grants — AFTER the token exists (`pveum user token remove` purges the token's ACL, so + # re-applying post-rotate is mandatory; `acl modify` is idempotent so this is safe on the reuse path). + apply_scoped_acl + # If this box previously ran the pre-3b broad grant (re-install/upgrade), remove it — else the old + # FelhomAgent role at / would UNION with the scoped grant and defeat containment. Tolerate-absent. + remove_old_broad_acl _state_mark token } @@ -1251,12 +1395,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. + # felhom pool: ensure it exists before the restore (step_token already created it; this is a + # belt-and-suspenders no-op that also covers a --resume path). The AGENT adds the guest to the pool + # atomically via restore --pool (v0.53.0) — no separate script-side pool_add_guest. 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 "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[*]} # agent restores INTO pool $PVE_POOL" log_dry "record provisioned_vmid=$VMID in $STATE_FILE (for a later automatic --uninstall)" _state_mark provision; return 0 fi @@ -1271,8 +1415,7 @@ 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" + # (No pool_add_guest here — the agent's restore --pool already made the guest a member.) } #-------------------------------------------------------------------------------