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) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 12:46:08 +02:00
parent 554845b44b
commit 741f322d6a
2 changed files with 217 additions and 43 deletions
+31
View File
@@ -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/<targets>`, 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**
+186 -43
View File
@@ -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 <pass> -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 <pass> -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.)
}
#-------------------------------------------------------------------------------