host-install v1.3.0: --uninstall (clean revert) + pre-flight guards
Colleague-safety batch #1+#2 (script-only). Adds a guarded, dry-run-aware --uninstall local host teardown (guest -> agent -> pveum(ACL,token,user,role) -> golden(opt-in) -> state file), with ours-check on the /etc/felhom-bootstrap mount, typed vmid confirm, other-Felhom-guests safe-skip, and idempotent tolerate-absent. Records customer_id + provisioned_vmid into the install state (_state_put/_state_get) so uninstall resolves its target automatically. Adds three provision pre-flight guards: multi-node (--node required), archive-storage exists, and a RAM-floor WARN. Validated dry-run-only on felhom-pve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,49 @@
|
||||
# Felhom scripts — Changelog
|
||||
|
||||
## felhom-host-install.sh v1.3.0 — `--uninstall` (clean revert) + pre-flight guards (2026-07-01)
|
||||
|
||||
Colleague-safety batch #1+#2. Adds a first-class, guarded **`--uninstall`** teardown so an operator can
|
||||
cleanly back out of a trial install, plus three provision pre-flight guards that stop common footguns.
|
||||
Script-only; no agent/hub/controller change.
|
||||
|
||||
- **`--uninstall` (local host teardown — no hub contact, no passphrase).** Reverses an install in the
|
||||
install-order's reverse: **guest → agent(unit/sudoers/binary/state/user) → pveum(ACL,token,user,role)
|
||||
→ golden(opt-in) → state file.** Every mutation goes through `run()` so `--dry-run` prints the full
|
||||
plan and executes nothing. Safety:
|
||||
- **Ours-check:** refuses to destroy a guest that lacks the `/etc/felhom-bootstrap` bind mount (matched
|
||||
by the constant guest *path*, not a hardcoded `mpN` slot — on the demo host it's `mp9`), unless
|
||||
`--force`.
|
||||
- **Typed confirmation:** must type the vmid to confirm PERMANENT destruction (read from `/dev/tty`;
|
||||
skipped only under `--dry-run`, where nothing is destroyed).
|
||||
- **Other-guests guard:** if any OTHER Felhom guest remains, destroys only the target and **leaves the
|
||||
agent + PVE token + state in place** (re-run with `--force` to remove host-level anyway — orphans the
|
||||
others).
|
||||
- **Never removes the `sudo` package**; never contacts the hub (the host record intentionally persists).
|
||||
- Presence-checked + idempotent: an already-absent guest/unit/sudoers/binary/user/ACL/token/role is a
|
||||
tolerated skip, not an error. The `pveum role delete` runs only after its ACL grants are gone (PVE
|
||||
refuses to delete a referenced role). Confirmed PVE 9 ACL-delete form:
|
||||
`pveum acl delete / --users|--tokens <x> --roles FelhomAgent`.
|
||||
- Target vmid resolves from `--vmid`, else the recorded `provisioned_vmid` (else dies). A `--vmid` that
|
||||
disagrees with the recorded one needs `--force`.
|
||||
- **`--remove-golden`:** with `--uninstall`, also delete the golden vzdump from the archive storage
|
||||
(`pvesm free`); otherwise it is left in place.
|
||||
- **Install state now records `customer_id` + `provisioned_vmid`** (new `_state_put`/`_state_get` helpers,
|
||||
dry-run-guarded like `_state_mark`; the `completed[]` shape is untouched) so a later `--uninstall`
|
||||
resolves its target automatically and safely.
|
||||
- **Pre-flight guards (provision mode):**
|
||||
- **Multi-node guard** — on a 2+-node cluster, `die` (naming the nodes) unless `--node` is explicit
|
||||
(new `NODE_EXPLICIT`); single-node keeps the current auto-pick. No-op under `--skip-provision`.
|
||||
- **Archive-storage-exists guard** — verify `--archive-storage` appears in `pvesm status` (else `die`);
|
||||
no-op under `--skip-provision`.
|
||||
- **RAM floor (WARN, never fatal)** — warn when `MemAvailable < 2048 MiB`.
|
||||
All three run inside `step_preflight` (before any mutation) so they also fire under `--dry-run`.
|
||||
- **Validated dry-run-only on felhom-pve** (single-node, live guest 9201): T-A full uninstall plan, T-C
|
||||
not-ours refusal (red-proof), archive-missing `die`, RAM line, other-guests detector, state round-trip;
|
||||
confirmed 9201 + agent + pveum + state untouched after all dry-runs. `bash -n` + `shellcheck` clean
|
||||
(0 new warnings vs. baseline; the 2 pre-existing SC2015 in `step_verify` are unchanged). **NOT yet
|
||||
live-validated (awaiting a supervised run):** a real live `--uninstall` (guest destroy + pveum removal)
|
||||
and the multi-node guard on an actual cluster.
|
||||
|
||||
## felhom-host-install.sh v1.2.0 — /dev/tty passphrase read + vmid auto-detect (2026-07-01)
|
||||
|
||||
Two operator-experience fixes so a colleague can install online (via the hub's new "Option 1: Online
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
#===============================================================================
|
||||
# felhom-host-install.sh v1.2.0
|
||||
# felhom-host-install.sh v1.3.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
|
||||
@@ -64,6 +64,16 @@
|
||||
# --resume skip steps already recorded in the state file
|
||||
# -h, --help this help
|
||||
#
|
||||
# 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
|
||||
# /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,
|
||||
# --archive-storage, --golden-vmid, --dry-run.
|
||||
# --remove-golden with --uninstall, also delete the golden vzdump from the archive storage
|
||||
#
|
||||
# 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).
|
||||
@@ -75,7 +85,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_VERSION="1.2.0"
|
||||
SCRIPT_VERSION="1.3.0"
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Logging (mirrors felhom-controller/scripts/docker-setup.sh)
|
||||
@@ -103,6 +113,7 @@ GOLDEN_VOLID=""
|
||||
GOLDEN_VMID="9100"
|
||||
ARCHIVE_STORAGE="local"
|
||||
NODE=""
|
||||
NODE_EXPLICIT=false # set true when --node is given; gates the multi-node wrong-node guard
|
||||
BRIDGE_ADDR=""
|
||||
ROOTFS_GROW=""
|
||||
DATAVOL_GROW=""
|
||||
@@ -116,6 +127,8 @@ FORCE_GITEA_GOLDEN=false
|
||||
SKIP_PROVISION=false
|
||||
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
|
||||
|
||||
# --- Gitea (artifact source) + agent install model (BUNDLE slice) ---
|
||||
GITEA_BASE="https://gitea.dooplex.hu"
|
||||
@@ -153,7 +166,7 @@ ART_GOLDEN_SHA=""
|
||||
#-------------------------------------------------------------------------------
|
||||
# Helpers
|
||||
#-------------------------------------------------------------------------------
|
||||
usage() { sed -n '2,65p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }
|
||||
usage() { sed -n '2,75p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }
|
||||
|
||||
run() { # simple (no pipes/redirects) mutating command
|
||||
if $DRY_RUN; then log_dry "$*"; else "$@"; fi
|
||||
@@ -196,6 +209,18 @@ should_skip() { # returns 0 (skip) if --resume AND step already done
|
||||
if $RESUME && _state_has "$1"; then log_skip "step '$1' already completed"; return 0; fi
|
||||
return 1
|
||||
}
|
||||
# _state_put KEY VALUE — set a top-level string key in state.json (creates the file if absent).
|
||||
# Mirrors _state_mark: dry-run no-ops (writes nothing), robust JSON via python3.
|
||||
_state_put() {
|
||||
$DRY_RUN && return 0
|
||||
mkdir -p "$STATE_DIR"
|
||||
STATE_FILE="$STATE_FILE" python3 -c "import json,os,sys;f=os.environ['STATE_FILE'];d=json.load(open(f)) if os.path.exists(f) else {'completed':[]};d[sys.argv[1]]=sys.argv[2];json.dump(d,open(f,'w'),indent=2)" "$1" "$2"
|
||||
}
|
||||
# _state_get KEY — print the top-level string value for KEY (empty if the file/key is absent).
|
||||
_state_get() {
|
||||
[[ -f "$STATE_FILE" ]] || return 0
|
||||
STATE_FILE="$STATE_FILE" python3 -c "import json,os,sys;d=json.load(open(os.environ['STATE_FILE']));print(d.get(sys.argv[1],''))" "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
http_code() { # GET, prints status code only (read-only preflight)
|
||||
curl -fsS -o /dev/null -w '%{http_code}' "$@" 2>/dev/null || curl -sS -o /dev/null -w '%{http_code}' "$@" 2>/dev/null
|
||||
@@ -258,6 +283,184 @@ fetch_raw() {
|
||||
[[ -s "$dest" ]] || die "raw fetch empty: $path"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Uninstall (local host teardown) — reverse of install; no hub contact, no passphrase
|
||||
#-------------------------------------------------------------------------------
|
||||
# felhom_guests — every vmid on this host that carries the /etc/felhom-bootstrap bind mount (the
|
||||
# read-only bootstrap mount an agent-provisioned guest always has). Matched by the CONSTANT guest
|
||||
# PATH, not a hardcoded mpN slot (the slot drifts; on the demo host it's mp9).
|
||||
felhom_guests() {
|
||||
local id
|
||||
for id in $(used_vmids); do
|
||||
pct config "$id" 2>/dev/null | grep -q 'mp=/etc/felhom-bootstrap' && echo "$id"
|
||||
done
|
||||
}
|
||||
|
||||
# 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.
|
||||
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
|
||||
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
|
||||
|
||||
# state-mismatch: an explicit --vmid that disagrees with the recorded one needs --force.
|
||||
if $VMID_EXPLICIT && [[ -n "$state_vmid" && "$state_vmid" != "$vmid" ]]; then
|
||||
if $FORCE; then
|
||||
log_warn " --vmid $vmid differs from the recorded provisioned_vmid=$state_vmid — --force given, proceeding"
|
||||
else
|
||||
die "--vmid $vmid differs from the recorded provisioned_vmid=$state_vmid. Pass --force to override."
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Guest teardown (guarded: ours-check + typed confirm).
|
||||
if _vmid_in_use "$vmid"; then
|
||||
# ours-check: a Felhom guest carries the /etc/felhom-bootstrap bind mount (constant guest path).
|
||||
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, destroying anyway"
|
||||
else
|
||||
die "vmid $vmid does not look like a Felhom-provisioned guest (no /etc/felhom-bootstrap mount).
|
||||
Refusing to destroy. Pass --force to override."
|
||||
fi
|
||||
|
||||
# show the config so the operator can eyeball what is about to be destroyed
|
||||
log_info " pct config $vmid:"
|
||||
pct config "$vmid" 2>/dev/null | sed 's/^/ /'
|
||||
|
||||
# typed confirmation — mandatory, never skipped except in --dry-run (nothing is destroyed there).
|
||||
if $DRY_RUN; then
|
||||
log_dry "would prompt: Type the vmid ($vmid) to confirm PERMANENT destruction"
|
||||
else
|
||||
local ans
|
||||
read -rp "Type the vmid ($vmid) to confirm PERMANENT destruction: " ans < /dev/tty
|
||||
[[ "$ans" == "$vmid" ]] || die "confirmation mismatch (got '$ans', expected '$vmid') — aborting, nothing destroyed"
|
||||
fi
|
||||
|
||||
# stop (tolerate already-stopped) then destroy
|
||||
local gstat; gstat=$(pct status "$vmid" 2>/dev/null | awk '{print $2}')
|
||||
if [[ "$gstat" == "running" ]]; then
|
||||
run pct stop "$vmid"
|
||||
else
|
||||
log_skip " guest $vmid not running (status: ${gstat:-unknown}) — skip stop"
|
||||
fi
|
||||
run pct destroy "$vmid"
|
||||
log_success " guest $vmid destroyed"
|
||||
else
|
||||
log_skip " guest $vmid already absent — skipping guest teardown"
|
||||
# host-level removal is still ours-gated: allowed if state's provisioned_vmid matches; else --force.
|
||||
if [[ -n "$state_vmid" && "$state_vmid" == "$vmid" ]]; then
|
||||
log_info " recorded provisioned_vmid matches $vmid — host-level removal permitted"
|
||||
elif ! $FORCE; then
|
||||
die "guest $vmid is absent and is not the recorded provisioned_vmid ('${state_vmid:-none}') — refusing host-level removal without --force."
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Other-Felhom-guests detector — the safe default. If any OTHER Felhom guest remains and no
|
||||
# --force, stop after the guest teardown and leave every host-level component in place.
|
||||
local others others_csv
|
||||
others=$(felhom_guests | grep -vx "$vmid" || true)
|
||||
if [[ -n "$others" ]] && ! $FORCE; then
|
||||
others_csv=$(echo "$others" | tr '\n' ' ' | sed 's/ */ /g;s/^ //;s/ $//;s/ /, /g')
|
||||
echo ""
|
||||
log_warn "Other Felhom guests remain (${others_csv}); leaving the agent + PVE token + state in place."
|
||||
log_warn "Re-run --uninstall --force to remove host-level components anyway (this orphans ${others_csv})."
|
||||
log_success "UNINSTALL (guest-only) complete — removed guest $vmid; host-level components preserved."
|
||||
log_info " NOTE: 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
|
||||
fi
|
||||
|
||||
# ── 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`.
|
||||
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
|
||||
else
|
||||
log_skip " felhom-agent unit not loaded — skip stop/disable"
|
||||
fi
|
||||
if [[ -f "$AGENT_UNIT" ]]; then run rm -f "$AGENT_UNIT"; else log_skip " $AGENT_UNIT already absent"; fi
|
||||
local bak
|
||||
for bak in "${AGENT_UNIT}".bak-*; do [[ -e "$bak" ]] && run rm -f "$bak"; done
|
||||
run systemctl daemon-reload
|
||||
if [[ -f "$AGENT_SUDOERS" ]]; then run rm -f "$AGENT_SUDOERS"; else log_skip " $AGENT_SUDOERS already absent"; fi
|
||||
if [[ -f "$AGENT_BIN" ]]; then run rm -f "$AGENT_BIN"; else log_skip " $AGENT_BIN already absent"; fi
|
||||
for bak in "${AGENT_BIN}".bak-*; do [[ -e "$bak" ]] && run rm -f "$bak"; done
|
||||
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
|
||||
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
|
||||
log_skip " token ${PVE_USER}!${PVE_TOKENID} already absent"
|
||||
fi
|
||||
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)" 2>/dev/null; then
|
||||
run pveum user delete "$PVE_USER"
|
||||
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
|
||||
|
||||
# 6. Golden vzdump (opt-in via --remove-golden; else left in place).
|
||||
if $REMOVE_GOLDEN; then
|
||||
local gvols gv
|
||||
gvols=$(pvesm list "$ARCHIVE_STORAGE" --content backup 2>/dev/null | awk -v v="$GOLDEN_VMID" '$0 ~ ("vzdump-lxc-" v "-"){print $1}')
|
||||
if [[ -n "$gvols" ]]; then
|
||||
while IFS= read -r gv; do
|
||||
[[ -n "$gv" ]] || continue
|
||||
run pvesm free "$gv"
|
||||
done <<<"$gvols"
|
||||
log_success " removed golden vzdump(s) from $ARCHIVE_STORAGE"
|
||||
else
|
||||
log_skip " no golden vzdump (vzdump-lxc-${GOLDEN_VMID}-*) on $ARCHIVE_STORAGE"
|
||||
fi
|
||||
else
|
||||
log_skip " golden vzdump left in place (pass --remove-golden to remove)"
|
||||
fi
|
||||
|
||||
# 7. Install state file (only reached when host-level removal ran — safe-skip returned earlier).
|
||||
if [[ -f "$STATE_FILE" ]]; then run rm -f "$STATE_FILE"; else log_skip " $STATE_FILE already absent"; fi
|
||||
run rmdir "$STATE_DIR" 2>/dev/null || true
|
||||
|
||||
# 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."
|
||||
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
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Arg parse
|
||||
#-------------------------------------------------------------------------------
|
||||
@@ -270,7 +473,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--golden) GOLDEN_VOLID="$2"; shift 2 ;;
|
||||
--golden-vmid) GOLDEN_VMID="$2"; shift 2 ;;
|
||||
--archive-storage) ARCHIVE_STORAGE="$2"; shift 2 ;;
|
||||
--node) NODE="$2"; shift 2 ;;
|
||||
--node) NODE="$2"; NODE_EXPLICIT=true; shift 2 ;;
|
||||
--bridge-ip) BRIDGE_ADDR="$2"; shift 2 ;;
|
||||
--rootfs-grow) ROOTFS_GROW="$2"; shift 2 ;;
|
||||
--datavol-grow) DATAVOL_GROW="$2"; shift 2 ;;
|
||||
@@ -282,6 +485,8 @@ while [[ $# -gt 0 ]]; do
|
||||
--force) FORCE=true; shift ;;
|
||||
--force-gitea-golden) FORCE_GITEA_GOLDEN=true; shift ;;
|
||||
--skip-provision) SKIP_PROVISION=true; shift ;;
|
||||
--uninstall) UNINSTALL=true; shift ;;
|
||||
--remove-golden) REMOVE_GOLDEN=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--resume) RESUME=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
@@ -289,6 +494,20 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
#===============================================================================
|
||||
# UNINSTALL MODE — local host teardown (no hub contact, no passphrase). Dispatched early,
|
||||
# before any provision/DR logic, and does not require --customer-id.
|
||||
#===============================================================================
|
||||
if $UNINSTALL; then
|
||||
[[ $EUID -eq 0 ]] || die "must run as root"
|
||||
echo ""
|
||||
log_info "felhom-host-install v${SCRIPT_VERSION} — mode=uninstall"
|
||||
$DRY_RUN && log_warn "DRY-RUN: no mutations will be performed"
|
||||
echo ""
|
||||
run_uninstall
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#===============================================================================
|
||||
# DR MODE — documented seam only (10D). NOT implemented.
|
||||
#===============================================================================
|
||||
@@ -353,11 +572,19 @@ step_preflight() {
|
||||
[[ "$pvever" == *"/9."* ]] || log_warn "expected PVE 9.x, got: $pvever"
|
||||
log_info " $pvever"
|
||||
|
||||
# node
|
||||
if [[ -z "$NODE" ]]; then
|
||||
NODE=$(pvesh get /nodes --output-format json 2>/dev/null | python3 -c "import json,sys;print(json.load(sys.stdin)[0]['node'])" 2>/dev/null || hostname)
|
||||
# node — on a MULTI-NODE cluster, auto-selecting nodes[0] is a wrong-node footgun. Require an
|
||||
# explicit --node unless there is exactly one node (or no guest will be provisioned).
|
||||
local nodes_json node_count node_names
|
||||
nodes_json=$(pvesh get /nodes --output-format json 2>/dev/null || echo "[]")
|
||||
node_count=$(python3 -c "import json,sys;print(len(json.loads(sys.argv[1])))" "$nodes_json" 2>/dev/null || echo 0)
|
||||
if [[ "${node_count:-0}" -gt 1 ]] && ! $NODE_EXPLICIT && ! $SKIP_PROVISION; then
|
||||
node_names=$(python3 -c "import json,sys;print(', '.join(n['node'] for n in json.loads(sys.argv[1])))" "$nodes_json" 2>/dev/null || echo "?")
|
||||
die "this is a ${node_count}-node cluster (${node_names}); pass --node explicitly — auto-selecting nodes[0] risks provisioning on the wrong node."
|
||||
fi
|
||||
log_info " node: $NODE"
|
||||
if [[ -z "$NODE" ]]; then
|
||||
NODE=$(python3 -c "import json,sys;print(json.loads(sys.argv[1])[0]['node'])" "$nodes_json" 2>/dev/null || hostname)
|
||||
fi
|
||||
if $NODE_EXPLICIT; then log_info " node: $NODE (explicit)"; else log_info " node: $NODE (auto)"; fi
|
||||
|
||||
# agent config path: honor the existing systemd unit's -config, else default
|
||||
if systemctl cat felhom-agent >/dev/null 2>&1; then
|
||||
@@ -383,6 +610,26 @@ step_preflight() {
|
||||
log_warn " could not read local-lvm free space (continuing)"
|
||||
fi
|
||||
|
||||
# RAM floor (soft): a big appliance guest can pressure existing guests on a small box. WARN only.
|
||||
local mem_avail_mib
|
||||
mem_avail_mib=$(awk '/^MemAvailable:/{print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)
|
||||
if [[ "${mem_avail_mib:-0}" -gt 0 ]]; then
|
||||
if [[ "$mem_avail_mib" -lt 2048 ]]; then
|
||||
log_warn " low free RAM (~${mem_avail_mib} MiB); the appliance guest may pressure existing guests — consider the CPU/mem cap."
|
||||
else
|
||||
log_info " free RAM: ~${mem_avail_mib} MiB"
|
||||
fi
|
||||
fi
|
||||
|
||||
# archive-storage-exists guard (provision only — the golden lives there + the restore reads it).
|
||||
if ! $SKIP_PROVISION; then
|
||||
if pvesm status --storage "$ARCHIVE_STORAGE" >/dev/null 2>&1; then
|
||||
log_info " archive storage '$ARCHIVE_STORAGE' present"
|
||||
else
|
||||
die "archive storage '$ARCHIVE_STORAGE' not found (pvesm status). Pass --archive-storage NAME."
|
||||
fi
|
||||
fi
|
||||
|
||||
# hub reachable
|
||||
local hc; hc=$(http_code "$HUB_URL/api/v1/config/$CUSTOMER_ID" -H "X-Retrieval-Password: preflight-no-op" || echo 000)
|
||||
[[ "$hc" != "000" ]] || die "hub unreachable at $HUB_URL"
|
||||
@@ -435,6 +682,8 @@ step_preflight() {
|
||||
log_success " using auto-selected vmid $VMID"
|
||||
fi
|
||||
fi
|
||||
# Record the customer into the install state (foundation for a later automatic --uninstall).
|
||||
_state_put customer_id "$CUSTOMER_ID"
|
||||
_state_mark preflight
|
||||
log_success "pre-flight passed"
|
||||
}
|
||||
@@ -853,6 +1102,7 @@ step_provision() {
|
||||
# visible in ps. Tracked as an Observation (candidate: env/stdin in the agent).
|
||||
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"
|
||||
log_dry "record provisioned_vmid=$VMID in $STATE_FILE (for a later automatic --uninstall)"
|
||||
_state_mark provision; return 0
|
||||
fi
|
||||
if ! felhom-agent --config "$AGENT_CONFIG" --selftest=provision \
|
||||
@@ -863,6 +1113,8 @@ step_provision() {
|
||||
fi
|
||||
log_success " provision completed"
|
||||
_state_mark provision
|
||||
# Record the provisioned vmid so a later --uninstall resolves the target automatically + safely.
|
||||
_state_put provisioned_vmid "$VMID"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user