#!/usr/bin/env bash
# scripts/phase-00-teardown.sh [--apply] [--no-prompt]
#
# Gated teardown for the D-058 reconfigure. Destroys the `openstack` Juju model and
# deletes the orphaned `capi-mgmt` MAAS machine, so the hosts release to Ready for the
# re-CIDR/standup/rebuild. HARD-EXCLUDES the management substrate (juju, lxd, tailscale):
# those system_ids are resolved and asserted OUT of every target set -- the script
# refuses to run if a target ever resolves to a protected machine.
#
# Machine roster (measured from `maas admin machines read`, 2026-06-29):
# PROTECTED (never touched): juju, lxd, tailscale -- management substrate
# HOSTS (released to Ready): openstack0, openstack1, openstack2, openstack3
# ORPHAN (deleted): capi-mgmt -- retired CAPI mgmt machine
# The in-cloud capi-mgmt-v2 Nova VM is NOT a MAAS machine; it dies with the model destroy.
#
# DEFAULT = DRY-RUN AUDIT: resolves every system_id, prints the plan (what is destroyed,
# what is protected), changes nothing. `--apply` executes; the irreversible destroy-model
# requires typing the model name at a /dev/tty prompt first (skip with --no-prompt for
# tested automation). destroy-model releases openstack0-3 to Ready; this script does NOT
# run a blind machine-release loop (avoids racing Juju's own release).
#
# Storage: destroy-model uses --destroy-storage (full teardown; data is discarded -- intended).
# If destroy hangs on stuck units, re-run with FORCE=1 (adds --force --no-wait).
#
# Exit: 0 ok | 1 fatal / unsafe (target intersects substrate, host missing) | 2 aborted by operator
# ASCII + LF.
set -euo pipefail
shopt -s inherit_errexit 2>/dev/null || true
MAAS_PROFILE="${MAAS_PROFILE:-admin}"
MODEL="${OPENSTACK_MODEL:-openstack}"
HOSTS=(openstack0 openstack1 openstack2 openstack3)
ORPHANS=(capi-mgmt)
PROTECTED=(juju lxd tailscale)
MODE="dryrun"; PROMPT=1
for a in "$@"; do
case "$a" in
--apply) MODE="apply" ;;
--no-prompt) PROMPT=0 ;;
*) echo "unknown arg: $a" >&2; exit 1 ;;
esac
done
FATAL=0
hdr() { echo; echo "=== $* ==="; }
note() { echo " - $*"; }
fail() { echo "FAIL: $*" >&2; FATAL=$((FATAL+1)); }
command -v jq >/dev/null || { echo "FATAL: jq required" >&2; exit 1; }
command -v juju >/dev/null || { echo "FATAL: juju not on PATH" >&2; exit 1; }
maas_json() { local o; o="$(maas "$MAAS_PROFILE" "$@" 2>/dev/null || true)"; printf '%s' "$o" | jq empty 2>/dev/null && printf '%s' "$o" || printf '[]'; }
# one read of the machine roster; resolve everything from it
MACHINES_JSON="$(maas_json machines read)"
sid_of() { printf '%s' "$MACHINES_JSON" | jq -r --arg h "$1" '.[]|select(.hostname==$h)|.system_id' | head -1; }
status_of() { printf '%s' "$MACHINES_JSON" | jq -r --arg h "$1" '.[]|select(.hostname==$h)|.status_name' | head -1; }
emit() { # <desc> <cmd...>
local desc="$1"; shift
if [ "$MODE" = apply ]; then
echo " DO: $desc"
local out
if ! out="$("$@" 2>&1)"; then
fail "$desc"; echo " said: $(printf '%s' "$out" | head -3 | tr '\n' ' ')" >&2; return 1
fi
else
echo " WOULD: $desc"; echo " $*"
fi
}
hdr "teardown audit mode=$MODE model=$MODEL"
# --- resolve protected substrate (must exist; will NOT be touched) ---
declare -A PROT_SID
hdr "PROTECTED substrate (never touched)"
for p in "${PROTECTED[@]}"; do
s="$(sid_of "$p")"
if [ -z "$s" ]; then note "$p: not in MAAS (already absent) -- nothing to protect"; continue; fi
PROT_SID["$s"]="$p"; note "$p = $s (status $(status_of "$p")) -- PROTECTED"
done
# --- resolve hosts (must exist; released to Ready via destroy-model) ---
HSID=()
hdr "HOSTS (released to Ready by destroy-model)"
for h in "${HOSTS[@]}"; do
s="$(sid_of "$h")"
if [ -z "$s" ]; then fail "$h: not found in MAAS -- cannot proceed (roster mismatch)"; continue; fi
if [ -n "${PROT_SID[$s]:-}" ]; then fail "$h resolves to PROTECTED sid $s (${PROT_SID[$s]}) -- ABORT"; continue; fi
HSID+=("$s"); note "$h = $s (status $(status_of "$h"))"
done
# --- resolve orphans (deleted; absent is fine) ---
declare -A OSID
hdr "ORPHANS (deleted from MAAS)"
for o in "${ORPHANS[@]}"; do
s="$(sid_of "$o")"
if [ -z "$s" ]; then note "$o: absent -- SKIP (nothing to delete)"; continue; fi
if [ -n "${PROT_SID[$s]:-}" ]; then fail "$o resolves to PROTECTED sid $s (${PROT_SID[$s]}) -- ABORT"; continue; fi
OSID["$s"]="$o"; note "$o = $s (status $(status_of "$o")) -- DELETE"
done
# --- model presence ---
MODEL_PRESENT=0
if juju models --format=json 2>/dev/null | jq -e --arg m "$MODEL" '.models[]?|select(.name==$m or (.name|endswith("/"+$m)))' >/dev/null 2>&1; then
MODEL_PRESENT=1; note "juju model '$MODEL' is PRESENT -- will destroy"
else
note "juju model '$MODEL' not present -- destroy will be skipped"
fi
[ "$FATAL" -eq 0 ] || { echo; echo "ABORT: $FATAL safety failure(s) above -- nothing was changed"; exit 1; }
hdr "PLAN"
echo " 1) destroy juju model '$MODEL' (--destroy-storage${FORCE:+ --force --no-wait}; releases openstack0-3 to Ready)"
echo " [$([ "$MODEL_PRESENT" = 1 ] && echo 'present -> will run' || echo 'absent -> skip')]"
echo " 2) delete orphan MAAS machine(s): ${ORPHANS[*]}"
echo " 3) verify openstack0-3 reach Ready; confirm substrate untouched"
echo " PROTECTED (asserted out of all targets): ${PROTECTED[*]}"
if [ "$MODE" = dryrun ]; then
echo; echo " re-run with --apply to execute (you will be asked to type the model name)."
echo "OK (dryrun)"; exit 0
fi
# ---- apply: irreversible-destroy confirmation ----
if [ "$PROMPT" -eq 1 ] && [ "$MODEL_PRESENT" = 1 ]; then
printf 'Type the model name "%s" to confirm IRREVERSIBLE destroy: ' "$MODEL" > /dev/tty
read -r ans < /dev/tty
[ "$ans" = "$MODEL" ] || { echo "aborted (got '$ans', expected '$MODEL') -- nothing changed"; exit 2; }
fi
hdr "MUTATE 1/3: destroy model"
if [ "$MODEL_PRESENT" = 1 ]; then
if [ -n "${FORCE:-}" ]; then
emit "destroy model $MODEL (FORCE)" juju destroy-model "$MODEL" --destroy-storage --force --no-wait --no-prompt
else
emit "destroy model $MODEL" juju destroy-model "$MODEL" --destroy-storage --no-prompt
fi
else note "model absent -- skip"; fi
[ "$FATAL" -eq 0 ] || { echo; echo "STOP: destroy-model failed -- not deleting orphans. Investigate (FORCE=1 if units are stuck)."; exit 1; }
hdr "MUTATE 2/3: delete orphan machines"
for s in "${!OSID[@]}"; do emit "delete orphan ${OSID[$s]} ($s)" maas "$MAAS_PROFILE" machine delete "$s" || true; done
[ "$FATAL" -eq 0 ] || { echo; echo "completed with $FATAL failure(s)"; exit 1; }
hdr "VERIFY 3/3 (read-only): host status + substrate intact"
MACHINES_JSON="$(maas_json machines read)"
for h in "${HOSTS[@]}"; do
st="$(status_of "$h")"
if [ "$st" = "Ready" ]; then note "$h -> $st"
else note "$h -> $st (Juju release may still be in progress; re-check, or release manually if it stalls)"; fi
done
for p in "${PROTECTED[@]}"; do note "PROTECTED $p -> $(status_of "$p") (unchanged)"; done
echo; echo "next: scripts/phase-00-maas-recidr.sh (audit) -- once openstack0-3 are Ready"
echo "OK (apply)"