hub v0.61.0 + felhom-tenantsync v1.1.0: Customer RESET (middle lifecycle tier)

One operator action returns a customer to pre-first-install: all operational
state dies (offsite repo, PBS namespace+backups, DR recipe, one-time secret,
claim state, retained escrow custody); identity + basic config + provenance +
events survive. Sits between host delete and customer Delete.

- store/customer_reset.go: customer_resets journal, live inventory, ack-gated
  purge (never touches identity/provenance/events), DeleteClaim.
- claim.ResetToUnclaimed: delete claim row -> fresh code next onboarding.
- offsite.Deprovision (idempotent) + OffsiteIdentifier + ClearProvisionedDescriptor.
- tenantsync.Deprovision + felhom-tenantsync.sh deprovision op (destroys ns +
  backup groups + token; shared user untouched; idempotent).
- web/customer_reset.go: GET reset -> inventory JSON; POST -> orchestration
  (external teardown FIRST, DB purge LAST; refuse-while-hosts; typed-id +
  separate escrow ack). Amber RESET card distinct from red Danger-zone Delete.
- Red-proofs: ack-gate + partial-failure resumability (both proven red);
  store ack-gating + journal round-trip; offsite idempotency + descriptor clear;
  RESET-card render. Green: build + vet + test.
This commit is contained in:
2026-07-17 13:09:04 +02:00
parent 6b1fbca51d
commit 4009401f46
17 changed files with 1193 additions and 13 deletions
+34 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# felhom-tenantsync v1.0.0 — the offsite endpoint's per-customer PBS tenancy surface (PBS DR tier
# felhom-tenantsync v1.1.0 — the offsite endpoint's per-customer PBS tenancy surface (PBS DR tier
# SLICE 1; spike SPIKE-pbs-tier-provisioning-2026-07-10 §3).
#
# Runs as the SSH forced command for the hub's SECOND `felhom-peersync` key (via sudo — its own
@@ -14,11 +14,16 @@
# → {"status":"ok","token_id","token_secret","fingerprint","datastore","namespace"}
# {"op":"reissue","customer_id":"<id>"} → delete-token (its ACLs purge with it — spike) →
# recreate → re-grant BOTH → self-check → same ok-shape with the FRESH secret.
# {"op":"deprovision","customer_id":"<id>"} → the customer-RESET teardown (v0.61.0, hub-side
# ack-gated). Delete the token (ACLs purge with it) → delete the residual namespace ACLs →
# DESTROY the namespace AND all its backup groups (`namespace delete --delete-groups true`).
# IDEMPOTENT: a missing token / missing namespace is success, not an error (a re-run after a
# partial reset converges). The shared felhom@pbs USER is NEVER touched (other tenants ride it).
# → {"status":"ok","namespace","datastore","deleted":<bool ns existed>}.
# This is the DELIBERATE, gated data-destruction the slice-1 note reserved — the operator RESET
# confirm (typed customer-id + separate escrow-custody ack) is the human decision it demanded.
# {"op":"fingerprint"} → {"status":"ok","fingerprint":"<PBS cert sha256>"}
#
# NO deprovision op in slice 1 — namespace/data deletion is a deliberate, separate decision (the
# offsite-disable precedent: disable never destroys data).
#
# Secret hygiene (load-bearing):
# - The token secret exists ONLY in memory and in the final stdout JSON — never a file, never
# stderr (the hub embeds remote stderr in error logs), never an argument.
@@ -63,7 +68,7 @@ if [ "$OP" = "fingerprint" ]; then
exit 0
fi
case "$OP" in provision|reissue) ;; *) err_json bad_request "unknown op" ;; esac
case "$OP" in provision|reissue|deprovision) ;; *) err_json bad_request "unknown op" ;; esac
# customer_id → the namespace AND the token name. Conservative charset (PBS ns + token grammar,
# no leading dash/dot so it can never parse as an option).
@@ -91,6 +96,30 @@ proxmox-backup-manager acl update "/datastore/$DS" DatastoreAdmin \
ADMIN_REPO="root@pam!$ADMIN_TOKEN_NAME@$REPO_HOST:$DS"
# 2b. deprovision (v0.61.0 customer-RESET teardown): destroy this ONE tenant's token + namespace +
# backup groups. Every step is idempotent (missing = already gone = ok). The shared felhom@pbs
# user survives (co-tenants). Returns before the provision/reissue create-path below.
if [ "$OP" = "deprovision" ]; then
# token (its ACLs purge with it — spike); ignore "no such token".
proxmox-backup-manager user delete-token "$PBS_USER" "$CID" >&2 2>/dev/null || true
# residual namespace ACLs (belt-and-suspenders — the user grant is not token-scoped).
proxmox-backup-manager acl update "/datastore/$DS/$CID" DatastoreBackup --auth-id "$PBS_USER" --delete >&2 2>/dev/null || true
proxmox-backup-manager acl update "/datastore/$DS/$CID" DatastoreBackup --auth-id "$TOKEN_ID" --delete >&2 2>/dev/null || true
ns_existed=false
if PBS_PASSWORD="$ADM" proxmox-backup-client namespace list --repository "$ADMIN_REPO" \
--output-format json | jq -e --arg ns "$CID" '(.data // .) | any(.[]; .ns == $ns)' >/dev/null; then
ns_existed=true
# --delete-groups true destroys every backup group under the namespace (the deliberate data kill).
PBS_PASSWORD="$ADM" proxmox-backup-client namespace delete "$CID" --repository "$ADMIN_REPO" --delete-groups true >&2
log "deprovision: namespace $CID destroyed (all backup groups deleted)"
else
log "deprovision: namespace $CID absent — already gone"
fi
jq -cn --arg ns "$CID" --arg ds "$DS" --argjson del "$ns_existed" \
'{"status":"ok","namespace":$ns,"datastore":$ds,"deleted":$del}'
exit 0
fi
# 3. Ensure the shared user + the namespace (both idempotent).
if ! proxmox-backup-manager user list --output-format json | jq -e --arg u "$PBS_USER" \
'any(.[]; .userid == $u)' >/dev/null; then