#!/usr/bin/env bash
# tenant-onboard.sh -- Option-3 multi-tenant onboarding (D-066), Omega Cloud v1
# STATUS: DRAFT 2026-07-02. Stages 0-5 validated live (tenant acme). Stage 6 gate CLEARED:
# D-067 (barbican<->Vault) FIXED + validated live 2026-07-02 (see D-067 amendment / BUNDLEFIX-007).
# KEYPAIR FIX (2026-07-02 pre-first-clean-room-run): nova keypairs are USER-scoped and magnum
# validates + boots the keypair in the CLUSTER-CREATOR/trustor context (magnum 18.0.0
# attr_validator.validate_keypair via cluster.py:545; trust app-cred impersonates the trustor).
# So the keypair is created by -cluster (stage3), and the template (stage5) omits --keypair
# (keypair_id optional, default=None); stage6 supplies it. A -svc-owned key would 400 at stage6.
#
# Model (D-066): operator creates domain + manager; manager creates project + -cluster (password,
# trust-capable, cluster lifecycle) + -svc (unrestricted app cred, non-trust automation). Cluster
# create MUST be password (keystone blocks app-cred trust creation; see appendix-D/D-066).
#
# Hardening conventions (learned 2026-07-02): validate raw output WHOLE (never extract-then-check);
# whitelist-write secrets to 0600 files, never echo; dynamic ID resolution; CA threaded; verify
# before mutate. This is an EXECUTED script (bash tenant-onboard.sh ...), so exit-on-error is correct.
set -uo pipefail
SDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; . "$SDIR/lib-net.sh"  # KEYSTONE_VIP_DEFAULT (single source of truth)

# ---- inputs ----
CLIENT="${1:-}"; STAGE="${2:-all}"
TENANT_CIDR="${TENANT_CIDR:-}"          # required for stage 4 (e.g. 10.20.24.0/24); must not collide
KEYSTONE_VIP="${KEYSTONE_VIP:-$KEYSTONE_VIP_DEFAULT}"
CA="${OS_CACERT:-$HOME/vault-init/vault-ca-root.pem}"
OUT="$HOME/tenant-${CLIENT}"            # 0600 credential handover dir
AUTH_URL="https://${KEYSTONE_VIP}:5000/v3"
# Web dashboard URL for the handover cred files. The dashboard is a web UI, NOT
# an API service, so it is not in the keystone catalog and cannot be discovered;
# it is a tagged, env-overridable literal (as-built value; override per cloud).
DASHBOARD_URL="${DASHBOARD_URL:-https://10.12.4.58/horizon}"
die(){ echo "FATAL: $*" >&2; exit 1; }
[ -n "$CLIENT" ] || die "usage: tenant-onboard.sh <client> [stage0|1|2|3|4|5|6|7|all]  (7 = optional canary access-proof, explicit only)"
[ -s "$CA" ] || die "OS_CACERT not found: $CA"
openssl x509 -in "$CA" -noout -checkend 0 >/dev/null 2>&1 || die "CA expired/unreadable: $CA"
umask 077; mkdir -p "$OUT"; chmod 700 "$OUT"

is_id(){ [[ "$1" =~ ^[0-9a-f]{32}$ ]]; }         # keystone id
newpw(){ python3 -c 'import secrets;print(secrets.token_urlsafe(24))'; }

# ---- admin context helper (operator) ----
command -v jq >/dev/null 2>&1 || die "jq required (image-ID resolution uses -f json | jq)"
admin_env(){ for v in $(env|awk -F= '/^OS_/{print $1}'); do unset "$v"; done; source ~/admin-openrc; }
# ---- named-identity password context (subshell-scoped by callers) ----

stage0(){ # operator preflight (read-only)
  admin_env; local fail=0
  echo "== stage0: preflight =="
  juju status keystone -m openstack --format=yaml 2>/dev/null | python3 -c 'import sys,yaml;m=yaml.safe_load(sys.stdin)["applications"]["keystone"]["units"]["keystone/0"].get("workload-status",{}).get("message","");print("keystone:",m);sys.exit(0 if m.startswith("PO:") else 1)' || { echo "  keystone override not PO: active"; fail=1; }
  for R in manager member load-balancer_member; do is_id "$(openstack role show "$R" -f value -c id </dev/null 2>&1)" || { echo "  role $R MISSING"; fail=1; }; done
  local ierr; ierr=$(mktemp)   # DOCFIX-094: stdout-only capture; merged stderr killed the jq parse
  local img; img=$(openstack image list --public -f json </dev/null 2>"$ierr" | jq -r '[.[]|select(.Name|test("kube"))][0].ID // empty' || true)   # -f json (column-order trap); ID-by-position rode alphabetical luck
  is_id "${img//-/}" 2>/dev/null || [[ "$img" =~ ^[0-9a-f-]{36}$ ]] || { echo "  no public kube image$( [ -s "$ierr" ] && echo " (stderr: $(head -1 "$ierr"))" )"; fail=1; }
  rm -f "$ierr"
  is_id "$(openstack domain show "$CLIENT" -f value -c id </dev/null 2>&1)" && { echo "  domain $CLIENT already EXISTS -- decide reuse/clean"; fail=1; } || true
  [ "$fail" = 0 ] && echo "  PREFLIGHT PASS" || die "preflight failed"
}

stage1(){ # operator: domain + manager + quota
  admin_env
  echo "== stage1: operator provisions domain + manager =="
  openstack domain create --description "Client: $CLIENT" "$CLIENT" </dev/null >/dev/null 2>&1 || true
  local DOM; DOM=$(openstack domain show "$CLIENT" -f value -c id </dev/null 2>&1); is_id "$DOM" || die "domain create failed"
  local MPW; MPW=$(newpw)
  openstack user create --domain "$DOM" --password "$MPW" --description "$CLIENT domain manager (D-051/D-064)" "${CLIENT}-domain-admin" </dev/null >/dev/null 2>&1 || true
  local MUID; MUID=$(openstack user show "${CLIENT}-domain-admin" --domain "$DOM" -f value -c id </dev/null 2>&1); is_id "$MUID" || die "manager create failed"
  openstack role add --domain "$DOM" --user "$MUID" manager </dev/null 2>&1 || true
  local RA; RA=$(openstack role assignment list --user "$MUID" --names -f value -c Role </dev/null 2>&1 || true)
  grep -qw manager <<<"$RA" || die "manager grant failed"   # capture-then-test: the raced form false-died on a SUCCESSFUL grant
  local MF="$OUT/${CLIENT}-domain-admin-cred.txt"; : > "$MF"; chmod 600 "$MF"
  printf 'domain=%s\ndomain_id=%s\nusername=%s-domain-admin\nuser_id=%s\npassword=%s\nauth_url=%s\ndashboard_url=%s\n' "$CLIENT" "$DOM" "$CLIENT" "$MUID" "$MPW" "$AUTH_URL" "$DASHBOARD_URL" > "$MF"; chmod 600 "$MF"
  echo "  domain=$DOM manager=$MUID  cred -> $MF"
  echo "  (set project quota after stage2 creates ${CLIENT}-prod, or pre-create the project as operator)"
}

stage2(){ # manager self-service: project + -cluster + -svc + grants
  admin_env
  local DOM; DOM=$(openstack domain show "$CLIENT" -f value -c id </dev/null 2>&1); is_id "$DOM" || die "no domain"
  local MF="$OUT/${CLIENT}-domain-admin-cred.txt"; [ -s "$MF" ] || die "run stage1 first"
  echo "== stage2: manager self-services project + -cluster + -svc + grants (D-064 G3) =="
  ( for v in $(env|awk -F= '/^OS_/{print $1}'); do unset "$v"; done
    export OS_AUTH_URL="$AUTH_URL" OS_IDENTITY_API_VERSION=3 OS_CACERT="$CA"
    export OS_USERNAME="${CLIENT}-domain-admin" OS_USER_DOMAIN_ID="$DOM" OS_DOMAIN_ID="$DOM"
    export OS_PASSWORD="$(awk -F= '/^password=/{print $2}' "$MF")"
    [ "$(openstack token issue -f value -c domain_id </dev/null 2>&1)" = "$DOM" ] || { echo "manager auth FAIL"; exit 1; }
    openstack project create --domain "$DOM" --description "$CLIENT production" "${CLIENT}-prod" </dev/null >/dev/null 2>&1 || true
    local PID; PID=$(openstack project show "${CLIENT}-prod" --domain "$DOM" -f value -c id </dev/null 2>&1); [[ "$PID" =~ ^[0-9a-f]{32}$ ]] || { echo "project FAIL"; exit 1; }
    # -cluster (password, trust-capable) and -svc (app-cred automation)
    for U in cluster svc; do
      local PW; PW=$(python3 -c 'import secrets;print(secrets.token_urlsafe(24))')
      openstack user create --domain "$DOM" --password "$PW" --description "$CLIENT $U" "${CLIENT}-$U" </dev/null >/dev/null 2>&1 || true
      local UID2; UID2=$(openstack user show "${CLIENT}-$U" --domain "$DOM" -f value -c id </dev/null 2>&1); [[ "$UID2" =~ ^[0-9a-f]{32}$ ]] || { echo "user $U FAIL"; exit 1; }
      for R in member load-balancer_member; do openstack role add --project "$PID" --user "$UID2" "$R" </dev/null 2>&1 || true; done
      local F="$OUT/${CLIENT}-$U-cred.txt"; umask 077; : > "$F"; chmod 600 "$F"
      printf 'username=%s-%s\nuser_id=%s\nuser_domain_id=%s\nproject_id=%s\nauth_url=%s\ndashboard_url=%s\npassword=%s\n' "$CLIENT" "$U" "$UID2" "$DOM" "$PID" "$AUTH_URL" "$DASHBOARD_URL" "$PW" > "$F"; chmod 600 "$F"
      echo "  ${CLIENT}-$U=$UID2 (member+load-balancer_member)  cred -> $F"
    done
    # anti-escalation self-check (must be DENIED)
    local SU; SU=$(openstack user show "${CLIENT}-svc" --domain "$DOM" -f value -c id </dev/null 2>&1)
    openstack role add --project "$PID" --user "$SU" admin </dev/null >/dev/null 2>&1 || true
    # capture-then-test, stdout-only (DOCFIX-112): the raced live-pipe form
    # (list | grep -qw admin) failed OPEN under pipefail -- SIGPIPE on a match,
    # or a failed list, both printed "anti-escalation OK". FAIL CLOSED instead.
    local ESERR ESRA; ESERR=$(mktemp)
    if ! ESRA=$(openstack role assignment list --project "$PID" --user "$SU" --names -f value -c Role </dev/null 2>"$ESERR"); then
      echo "  *** anti-escalation check UNVERIFIABLE (role assignment list failed: $(head -1 "$ESERR")) -- STOP ***"
      rm -f "$ESERR"; exit 1
    fi
    rm -f "$ESERR"
    if grep -qw admin <<<"$ESRA"; then echo "  *** ESCALATION: manager granted admin -- STOP ***"; exit 1; else echo "  anti-escalation OK (admin grant denied)"; fi
  ) || die "stage2 failed"
}

stage3(){ # -svc mints unrestricted app cred; -cluster owns the keypair (trustor-owned)
  admin_env
  local DOM; DOM=$(openstack domain show "$CLIENT" -f value -c id </dev/null 2>&1)
  local SF="$OUT/${CLIENT}-svc-cred.txt"; local PID; PID=$(awk -F= '/^project_id=/{print $2}' "$SF")
  echo "== stage3: -svc mints unrestricted app cred + keypair =="
  ( for v in $(env|awk -F= '/^OS_/{print $1}'); do unset "$v"; done
    export OS_AUTH_URL="$AUTH_URL" OS_IDENTITY_API_VERSION=3 OS_CACERT="$CA"
    export OS_USERNAME="${CLIENT}-svc" OS_USER_DOMAIN_ID="$DOM" OS_PROJECT_ID="$PID"
    export OS_PASSWORD="$(awk -F= '/^password=/{print $2}' "$SF")"
    [ "$(openstack token issue -f value -c project_id </dev/null 2>&1)" = "$PID" ] || { echo "svc auth FAIL"; exit 1; }
    local ACF="$OUT/${CLIENT}-svc-appcred.txt"
    # H2 re-run guard (addendum 23): app-cred create is NOT idempotent and the secret is
    # unrecoverable after create. If it exists AND the local secret file is intact: SKIP.
    # If it exists WITHOUT a usable local secret: STOP -- delete-and-recreate rotates a
    # live credential, which is an operator decision, never an implicit re-run side effect.
    local EXID; EXID=$(openstack application credential show "${CLIENT}-svc-cred" -f value -c id </dev/null 2>&1 || true)
    if [[ "$EXID" =~ ^[0-9a-f]{32}$ ]]; then
      if [ -s "$ACF" ] && grep -qE '^secret=' "$ACF"; then
        echo "  app cred ${CLIENT}-svc-cred exists + local secret intact -- SKIP (idempotent re-run)"
        exit 0
      fi
      echo "  app cred ${CLIENT}-svc-cred EXISTS but the local secret file is missing/incomplete."
      echo "  The secret is unrecoverable. OPERATOR DECISION required (rotates a live credential):"
      echo "    as ${CLIENT}-svc: openstack application credential delete ${CLIENT}-svc-cred ; re-run stage3"
      exit 1
    fi
    umask 077; : > "$ACF"; chmod 600 "$ACF"
    openstack application credential create "${CLIENT}-svc-cred" --unrestricted --description "$CLIENT non-trust automation" -f shell </dev/null > "$ACF" 2>&1
    grep -qE '^id=' "$ACF" || { echo "appcred FAIL"; cat "$ACF"; exit 1; }; chmod 600 "$ACF"
    echo "  app cred -> $ACF (unrestricted; secret len $(awk -F'"' '/^secret=/{print length($2)}' "$ACF"))"
  ) || die "stage3 (svc app cred) failed"
  # keypair as -CLUSTER: magnum validates it in the cluster-creator nova context at cluster
  # create, and node boot resolves it as the trustor -- both are the -cluster identity.
  local CF="$OUT/${CLIENT}-cluster-cred.txt"; [ -s "$CF" ] || die "run stage2 first (no cluster cred)"
  ( for v in $(env|awk -F= '/^OS_/{print $1}'); do unset "$v"; done
    export OS_AUTH_URL="$AUTH_URL" OS_IDENTITY_API_VERSION=3 OS_CACERT="$CA"
    export OS_USERNAME="${CLIENT}-cluster" OS_USER_DOMAIN_ID="$DOM" OS_PROJECT_ID="$PID"
    export OS_PASSWORD="$(awk -F= '/^password=/{print $2}' "$CF")"
    [ "$(openstack token issue -f value -c project_id </dev/null 2>&1)" = "$PID" ] || { echo "cluster-user auth FAIL"; exit 1; }
    local KF="$OUT/${CLIENT}-key.pem"
    # H2 re-run guard: keypair create is NOT idempotent and the private key is only
    # returned once. Same skip/stop split as the app cred above.
    local EXK; EXK=$(openstack keypair show "${CLIENT}-key" -f value -c name </dev/null 2>&1 || true)
    if [ "$EXK" = "${CLIENT}-key" ]; then
      if [ -s "$KF" ] && head -1 "$KF" | grep -q 'PRIVATE KEY'; then
        echo "  keypair ${CLIENT}-key exists + local private key intact -- SKIP (idempotent re-run)"
        exit 0
      fi
      echo "  keypair ${CLIENT}-key EXISTS but the local private key is missing/invalid."
      echo "  The private key is unrecoverable. OPERATOR DECISION required (breaks existing node access):"
      echo "    as ${CLIENT}-cluster: openstack keypair delete ${CLIENT}-key ; re-run stage3"
      exit 1
    fi
    umask 077; openstack keypair create "${CLIENT}-key" </dev/null > "$KF" 2>&1
    head -1 "$KF" | grep -q 'PRIVATE KEY' && { chmod 600 "$KF"; echo "  keypair -> $KF (owner: ${CLIENT}-cluster)"; } || { echo "keypair FAIL"; cat "$KF"; exit 1; }
  ) || die "stage3 (cluster keypair) failed"
}

stage4(){ # tenant L3 via app cred
  [ -n "$TENANT_CIDR" ] || die "set TENANT_CIDR (client choice; fallback default 10.100.0.0/24 per D-074)"
  admin_env
  # D-074 guard (replaces the exact-string cloud-wide check): tenant-vs-tenant
  # overlap is ALLOWED under the hard-isolation model; reject ONLY (a) non-RFC1918
  # and (b) real containment overlap -- either direction, net_overlap.py -- against
  # the reserved ranges: PLANE_CIDRS (lib-net.sh), the capi-mgmt tenant net
  # (D-035; resolved live by stable name, fail-closed on lookup miss), metadata.
  # capture-then-test throughout (the raced form failed open once already).
  local SDIR; SDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  # shellcheck disable=SC1091
  source "$SDIR/lib-net.sh"
  local CAPI_CIDR; CAPI_CIDR=$(openstack subnet show capi-mgmt-subnet -f value -c cidr </dev/null 2>&1 || true)
  [[ "$CAPI_CIDR" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] || die "capi-mgmt-subnet CIDR lookup failed: '$CAPI_CIDR' (guard fails closed)"
  local GOUT; GOUT=$(python3 "$SDIR/net_overlap.py" "$TENANT_CIDR" "${PLANE_CIDRS[@]}" "$CAPI_CIDR" "169.254.169.254/32" 2>&1 || true)
  [ "$GOUT" = "OK" ] || die "CIDR $TENANT_CIDR rejected: ${GOUT:-guard-error} (reserved ranges + RFC1918 only; tenant-vs-tenant overlap is allowed per D-074)"
  local ACF="$OUT/${CLIENT}-svc-appcred.txt"
  echo "== stage4: tenant L3 (net/subnet/router/ext-gw) via app cred =="
  ( for v in $(env|awk -F= '/^OS_/{print $1}'); do unset "$v"; done
    export OS_AUTH_TYPE=v3applicationcredential OS_AUTH_URL="$AUTH_URL" OS_IDENTITY_API_VERSION=3 OS_CACERT="$CA"
    export OS_APPLICATION_CREDENTIAL_ID="$(awk -F'"' '/^id=/{print $2}' "$ACF")"
    export OS_APPLICATION_CREDENTIAL_SECRET="$(awk -F'"' '/^secret=/{print $2}' "$ACF")"
    [[ "$(openstack token issue -f value -c project_id </dev/null 2>&1)" =~ ^[0-9a-f]{32}$ ]] || { echo "appcred auth FAIL"; exit 1; }
    openstack network create "${CLIENT}-net" </dev/null >/dev/null 2>&1 && echo "  net ok"
    openstack subnet create "${CLIENT}-subnet" --network "${CLIENT}-net" --subnet-range "$TENANT_CIDR" --dns-nameserver 8.8.8.8 </dev/null >/dev/null 2>&1 && echo "  subnet ok"
    openstack router create "${CLIENT}-router" </dev/null >/dev/null 2>&1 && echo "  router ok"
    openstack router set "${CLIENT}-router" --external-gateway provider-ext </dev/null 2>&1 && echo "  ext-gw ok" || echo "  *** ext-gw FAILED (operator may need to attach) ***"
    openstack router add subnet "${CLIENT}-router" "${CLIENT}-subnet" </dev/null 2>&1 && echo "  interface ok"
  ) || die "stage4 failed"
}

stage5(){ # tenant template (image by UUID)
  admin_env
  local IERR5; IERR5=$(mktemp)   # DOCFIX-094: stdout-only capture
  local IMG; IMG=$(openstack image list --public -f json </dev/null 2>"$IERR5" | jq -r '[.[]|select(.Name|test("kube"))][0].ID // empty' || true)   # -f json (column-order trap)
  [[ "$IMG" =~ ^[0-9a-f-]{36}$ ]] || { sed 's/^/  /' "$IERR5" 2>/dev/null; rm -f "$IERR5"; die "kube image uuid resolve failed"; }
  rm -f "$IERR5"
  local ACF="$OUT/${CLIENT}-svc-appcred.txt"
  echo "== stage5: tenant cluster template (image by UUID) =="
  ( for v in $(env|awk -F= '/^OS_/{print $1}'); do unset "$v"; done
    export OS_AUTH_TYPE=v3applicationcredential OS_AUTH_URL="$AUTH_URL" OS_IDENTITY_API_VERSION=3 OS_CACERT="$CA"
    export OS_APPLICATION_CREDENTIAL_ID="$(awk -F'"' '/^id=/{print $2}' "$ACF")"
    export OS_APPLICATION_CREDENTIAL_SECRET="$(awk -F'"' '/^secret=/{print $2}' "$ACF")"
    openstack coe cluster template show "${CLIENT}-k8s" -f value -c uuid </dev/null >/dev/null 2>&1 && openstack coe cluster template delete "${CLIENT}-k8s" </dev/null 2>&1 || true
    # NO --keypair here: template is -svc-created but the key is -cluster-owned; keypair_id is
    # optional (magnum default=None) and stage6 supplies --keypair in the -cluster context.
    openstack coe cluster template create "${CLIENT}-k8s" --image "$IMG" --external-network provider-ext \
      --master-flavor gp.mid --flavor capi.node --coe kubernetes --network-driver calico \
      --docker-storage-driver overlay2 --master-lb-enabled --floating-ip-enabled \
      --fixed-network "${CLIENT}-net" --fixed-subnet "${CLIENT}-subnet" </dev/null 2>&1
    [[ "$(openstack coe cluster template show "${CLIENT}-k8s" -f value -c uuid </dev/null 2>&1)" =~ ^[0-9a-f-]{36}$ ]] && echo "  template ${CLIENT}-k8s ok" || { echo "template FAIL"; exit 1; }
  ) || die "stage5 failed"
}

stage6(){ # cluster create as -cluster PASSWORD  [GATED ON D-067]
  echo "== stage6: cluster create as ${CLIENT}-cluster (PASSWORD) =="
  echo "  NOTE: D-067 RESOLVED 2026-07-02 (vault external binding -> metal-internal; BUNDLEFIX-007)."
  admin_env
  local DOM; DOM=$(openstack domain show "$CLIENT" -f value -c id </dev/null 2>&1)
  local CF="$OUT/${CLIENT}-cluster-cred.txt"; local PID; PID=$(awk -F= '/^project_id=/{print $2}' "$CF")
  ( for v in $(env|awk -F= '/^OS_/{print $1}'); do unset "$v"; done
    export OS_AUTH_URL="$AUTH_URL" OS_IDENTITY_API_VERSION=3 OS_CACERT="$CA"
    export OS_USERNAME="${CLIENT}-cluster" OS_USER_DOMAIN_ID="$DOM" OS_PROJECT_ID="$PID"
    export OS_PASSWORD="$(awk -F= '/^password=/{print $2}' "$CF")"
    [ "$(openstack token issue -f value -c project_id </dev/null 2>&1)" = "$PID" ] || { echo "cluster-user auth FAIL"; exit 1; }
    openstack coe cluster create "${CLIENT}-cluster" --cluster-template "${CLIENT}-k8s" --keypair "${CLIENT}-key" --master-count 1 --node-count 1 </dev/null 2>&1
    sleep 15
    openstack coe cluster show "${CLIENT}-cluster" -f value -c uuid -c status -c status_reason </dev/null 2>&1 | sed 's/^/  /'
  ) || die "stage6 failed"
}

stage7(){ # OPTIONAL canary access-proof (H3; tenant-onboarding-contract section 5.4).
  # Proves the tenant can boot on their network and reach the instance over SSH with
  # the -cluster-owned key (the ruled meaning of "confirm tenant SSH access", A1.4).
  # MUTATES tenant-scoped resources (1 SG + 1 server + 1 FIP) and ALWAYS tears them
  # down, pass or fail. Explicit invocation only -- never part of 'all'.
  # VERIFY-LIVE: CANARY_SSH_USER (default 'ubuntu') must match the public image's
  # login user; CANARY_FLAVOR (default gp.mid) must fit remaining quota.
  admin_env
  local DOM; DOM=$(openstack domain show "$CLIENT" -f value -c id </dev/null 2>&1)
  local CF="$OUT/${CLIENT}-cluster-cred.txt"; [ -s "$CF" ] || die "run stage2 first (no cluster cred)"
  local PID; PID=$(awk -F= '/^project_id=/{print $2}' "$CF")
  local KF="$OUT/${CLIENT}-key.pem"
  [ -s "$KF" ] && head -1 "$KF" | grep -q 'PRIVATE KEY' || die "no local private key $KF (run stage3)"
  local IERR="$OUT/.canary-img-err"
  local IMG; IMG=$(openstack image list --public -f json </dev/null 2>"$IERR" | jq -r '[.[]|select(.Name|test("kube"))][0].ID // empty' || true)
  [[ "$IMG" =~ ^[0-9a-f-]{36}$ ]] || { sed 's/^/  /' "$IERR" 2>/dev/null; rm -f "$IERR"; die "kube image uuid resolve failed"; }
  rm -f "$IERR"
  local FLV="${CANARY_FLAVOR:-gp.mid}" SSHU="${CANARY_SSH_USER:-ubuntu}"
  echo "== stage7: canary access-proof as ${CLIENT}-cluster (flavor=$FLV sshuser=$SSHU) =="
  ( for v in $(env|awk -F= '/^OS_/{print $1}'); do unset "$v"; done
    export OS_AUTH_URL="$AUTH_URL" OS_IDENTITY_API_VERSION=3 OS_CACERT="$CA"
    export OS_USERNAME="${CLIENT}-cluster" OS_USER_DOMAIN_ID="$DOM" OS_PROJECT_ID="$PID"
    export OS_PASSWORD="$(awk -F= '/^password=/{print $2}' "$CF")"
    [ "$(openstack token issue -f value -c project_id </dev/null 2>&1)" = "$PID" ] || { echo "cluster-user auth FAIL"; exit 1; }
    SRV="${CLIENT}-canary"; SG="${CLIENT}-canary-sg"; FIP=""
    cleanup(){
      openstack server delete --wait "$SRV" </dev/null >/dev/null 2>&1 || true
      [ -n "$FIP" ] && { openstack floating ip delete "$FIP" </dev/null >/dev/null 2>&1 || true; }
      openstack security group delete "$SG" </dev/null >/dev/null 2>&1 || true
      echo "  teardown done (server/FIP/SG removed)"
    }
    trap cleanup EXIT
    openstack security group create "$SG" --description "canary ssh (short-lived)" </dev/null >/dev/null 2>&1 || true
    openstack security group rule create "$SG" --protocol tcp --dst-port 22 --remote-ip 0.0.0.0/0 </dev/null >/dev/null 2>&1 || true
    openstack server create "$SRV" --image "$IMG" --flavor "$FLV" --network "${CLIENT}-net" \
      --key-name "${CLIENT}-key" --security-group "$SG" </dev/null >/dev/null 2>&1 || { echo "  CANARY FAIL: server create"; exit 1; }
    ST=""
    for _ in $(seq 1 30); do
      ST=$(openstack server show "$SRV" -f value -c status </dev/null 2>&1 || true)
      [ "$ST" = "ACTIVE" ] && break; [ "$ST" = "ERROR" ] && break; sleep 10
    done
    [ "$ST" = "ACTIVE" ] || { echo "  CANARY FAIL: server status '$ST' (want ACTIVE)"; exit 1; }
    FIP=$(openstack floating ip create provider-ext -f value -c floating_ip_address </dev/null 2>&1 || true)
    [[ "$FIP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "  CANARY FAIL: FIP allocate: $FIP"; FIP=""; exit 1; }
    openstack server add floating ip "$SRV" "$FIP" </dev/null 2>&1 || { echo "  CANARY FAIL: FIP attach"; exit 1; }
    OKSSH=0
    for _ in $(seq 1 18); do
      if ssh -i "$KF" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
             -o ConnectTimeout=10 -o BatchMode=yes "${SSHU}@${FIP}" hostname </dev/null >/dev/null 2>&1; then
        OKSSH=1; break
      fi
      sleep 10
    done
    [ "$OKSSH" = "1" ] || { echo "  CANARY FAIL: no SSH to ${SSHU}@${FIP} within ~3 min of ACTIVE"; exit 1; }
    echo "  CANARY PASS: booted on ${CLIENT}-net, SSH OK via $FIP with ${CLIENT}-key"
  ) || die "stage7 canary FAILED (teardown was attempted; verify no orphans named ${CLIENT}-canary*)"
}

case "$STAGE" in
  stage0|0) stage0 ;;
  stage1|1) stage1 ;;
  stage2|2) stage2 ;;
  stage3|3) stage3 ;;
  stage4|4) stage4 ;;
  stage5|5) stage5 ;;
  stage6|6) stage6 ;;
  stage7|7) stage7 ;;
  all) stage0; stage1; stage2; stage3; stage4; stage5; echo "== stages 0-5 done. run stage6 (cluster) explicitly: tenant-onboard.sh $CLIENT 6 == (stage7 canary is optional, also explicit)" ;;
  *) die "unknown stage: $STAGE" ;;
esac
echo "handover creds in: $OUT (0600)"
