diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index abaa224..3e04562 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -429,6 +429,44 @@ `running = true`. **STILL OWED for dc0:** carve metal-admin `10.12.8.6` + provider-public `10.12.4.6` (gw `10.12.4.1`) once Ready, deploy an OS, then install regiond + PostgreSQL. **dc1's VM is authored but NOT applied.** + **MIGRATION PREREQ 3 DONE 2026-07-30 -- THE WRONG-REGION HAZARD IS CLOSED BY A GATE.** + Capture `docs/audit/dc0-region-profile-assert-20260730.txt`. MEASURED: voffice1's MAAS + profile store is `~/snap/maas/current/.maascli.db` (snap-confined and per-revision -- the + same refresh fragility already recorded for the SEC-012 power key; the `~/.maascli.db` at + `$HOME` is a zero-byte inert residue), and it held EXACTLY ONE profile, `admin` -> + `http://10.10.0.20:5240/` = the OFFICE1 region. Thirteen repo scripts default to + `MAAS_PROFILE=admin`. Because the two regions hold SEPARATE databases, a carve recreated + against the wrong profile is an idempotent NO-OP that prints PASS, and `machine delete` + against it destroys the real record with no undo. **`scripts/maas-profile-assert.sh` (NEW, + harness 20/20) asserts which region a profile resolves to by RACK-CONTROLLER IDENTITY** -- + a machine COUNT is not proof, since two regions can hold the same number. Live, both + directions: `admin` -> 23 machines / racks `voffice1,vvr1-dc0,vvr1-dc1` exit 0; + `vr1-dc0-region` -> 0 machines / rack `hot-kid` exit 0; `admin` asserted as `hot-kid` + exit **1**. Its mutation pass found that one of its OWN new assertions could not fail + (nameless rack entries were caught by a different branch); fixed and re-proven. + **HOST MATRIX MEASURED -- no single host has everything the migration needs:** voffice1 + has repo + CLI + NetBox but CANNOT reach `10.12.8.6:5240` (curl exit 28); the dc0 rack + reaches BOTH regions (200/200) but has no repo clone and CANNOT reach the NetBox apex + (`10.10.1.10:8000` CLOSED); `dc-plane-ipam.sh` derives the v6 plane prefixes FROM the apex + (D-136 (D)). Resolved with an SSH tunnel from voffice1 through the rack + (`-L 127.0.0.1:5241:10.12.8.6:5240`, answering 200) and profile `vr1-dc0-region`. + **SEC-010 IS NOT PUNCTURED** -- the tunnel is application-layer and rack-ORIGINATED, which + is the exact distinction SEC-010's own justification draws; no route, nftables rule or + libvirt net was changed. Safe failure direction by construction: if the tunnel dies the + profile REFUSES (exit 2) and cannot silently fall back to Office1. + **SEC-026 ADDENDUM, OWED:** this places dc0's region admin API key on voffice1. Today that + is a strict SUBSET of the blast radius already there (voffice1 holds the Office1 admin key, + which currently administers BOTH DCs' nodes) -- but the migration INVERTS that, so removal + of the `vr1-dc0-region` profile from voffice1 is an obligation once dc0's control host is + settled. **CONSEQUENCE OF D-132 q1, recorded not ruled:** D-128's "Plane 2 executes on + voffice1" clause was written when there was ONE region; per-DC regions need a tunnel or a + DC-side host. That is a consequence of an already-ruled decision, and a DOCFIX is owed + against D-128's Plane-2 wording (joining F5's run-location item). + **AS-EXECUTED LOG NOT USED FOR THIS WINDOW:** `run-logged.sh` opens an INTERACTIVE + `script(1)` subshell, unusable from a non-interactive session (F6 already records the + classifier refusing the wrapped form). Captures go to `docs/audit/*`, which is what GA-R6 + requires of a gate; an index row is owed at close. A log that looks complete and is not is + worse than one declaring its gap. - Project: Omega Cloud, VR1 DC-DC rehearsal -- a two-DC + Office1-headend virtual rehearsal on KVM (vcloud host), rehearsing the future bare-metal diff --git a/docs/audit/dc0-region-profile-assert-20260730.txt b/docs/audit/dc0-region-profile-assert-20260730.txt new file mode 100644 index 0000000..bcf0c9a --- /dev/null +++ b/docs/audit/dc0-region-profile-assert-20260730.txt @@ -0,0 +1,38 @@ +MAAS PROFILE / REGION RESOLUTION -- dc0 per-DC region migration (D-132 q1) +Captured 2026-07-30 ON voffice1 (D-128 Plane-2 host). Read-only. +====================================================================== + +-- profile store (measured, not assumed) -- +-rw-r--r-- 1 jessea123 jessea123 0 Jul 29 07:26 /home/jessea123/.maascli.db +-rw------- 1 jessea123 jessea123 1093632 Jul 30 20:02 /home/jessea123/snap/maas/current/.maascli.db + +NOTE: the LIVE profile store is ~/snap/maas/current/.maascli.db (snap-confined, +per-revision -- the same refresh fragility already recorded for the SEC-012 +power key). The zero-byte ~/.maascli.db is inert residue. + +-- profile -> region URL (names + URLs only; no credential material) -- +admin -> http://10.10.0.20:5240/MAAS/api/2.0/ +vr1-dc0-region -> http://127.0.0.1:5241/MAAS/api/2.0/ + +-- discriminator: machine count + rack-controller identity -- +profile=admin machines=23 racks=voffice1,vvr1-dc0,vvr1-dc1 +profile=vr1-dc0-region machines=0 racks=hot-kid + +-- scripts/maas-profile-assert.sh, live, BOTH directions -- +$ maas-profile-assert.sh vr1-dc0-region hot-kid +OK profile 'vr1-dc0-region' -> region with racks [hot-kid] (as expected) + exit=0 + +$ maas-profile-assert.sh admin voffice1,vvr1-dc0,vvr1-dc1 +OK profile 'admin' -> region with racks [voffice1,vvr1-dc0,vvr1-dc1] (as expected) + exit=0 + +$ maas-profile-assert.sh admin hot-kid # NEGATIVE -- must fail +FAIL profile 'admin' resolves to the WRONG region + expected racks: [hot-kid] + actual racks: [voffice1,vvr1-dc0,vvr1-dc1] + exit=1 + +-- harness -- + +maas-profile-assert: 20 passed, 0 failed diff --git a/docs/changelog-20260730-dc0-region-migration.md b/docs/changelog-20260730-dc0-region-migration.md new file mode 100644 index 0000000..c7fc504 --- /dev/null +++ b/docs/changelog-20260730-dc0-region-migration.md @@ -0,0 +1,140 @@ +# Changelog 2026-07-30 -- dc0 MAAS region migration (Stage 5 unblock) + +Session changelog (GA-R2/D1: ONE per session). Branch `dc-dc-stage5-preconditions`. +Status claims live ONLY in `docs/CURRENT-STATE.md`. + +Context: Stage 5 is OPEN and BLOCKED. D-132 q1 (RULED 2026-07-30) puts a MAAS region +in each DC; the dc0 region VM is LIVE at `10.12.8.6:5240`, but the Office1 region still +owns all 9 dc0 role nodes plus the Juju controller. MAAS cannot move machines between +regions, so the carve must be RECREATED and the machines re-enrolled. This changelog +covers that migration. + +**Operator authorization for this session, verbatim: "Begin working through all tasks and +continue deployment. Work as autonomously as possible, deploy agents as needed."** Under +blanket approval this changelog IS the review surface (GA-R2): every item states what, +why (evidence), and how to revert. + +--- + +## Item 1 -- `scripts/maas-profile-assert.sh` + `tests/maas-profile-assert/` (NEW) + +**What.** A guard that proves WHICH MAAS REGION a CLI profile resolves to, by +rack-controller identity, before any command that reads or writes that region. +Exit 0 match / 1 wrong region / 2 refuse. + +**Why (measured, not reasoned).** Every MAAS call in this repo defaults to +`MAAS_PROFILE=admin` (13 scripts: `maas-fabric-prune`, `phase-00-teardown-destroy`, +`pre-flight-checks`, `maas-role-tags`, `carve-host-interfaces`, `dc-plane-ipam`, +`phase-00-teardown-release`, `lib-hosts`, `maas-node-power`, `phase-00-maas-standup`, +`reenroll-hosts`, and two more). MEASURED on voffice1: the sole profile `admin` +resolves to `http://10.10.0.20:5240/` -- the OFFICE1 region. The two regions hold +SEPARATE databases, so during this migration: + +- a carve recreated against the wrong profile is an idempotent NO-OP that prints PASS + (the state was already there -- in the wrong region); +- `machine delete` against the wrong profile DESTROYS the real record, unrecoverably. + +This is the wrong-HOST class this repo has been bitten by three times already +(`dc-mirror.sh` run on voffice1; the egress probe run from the rack; P5 probing the +wrong filesystem) -- now wrong-REGION, where the destructive direction has no undo. + +**A count alone is not proof** -- two regions can hold the same number of machines. +Rack-controller identity is the discriminator, because a rack registers to exactly one +region. + +**Live verification, both directions** (capture +`docs/audit/dc0-region-profile-assert-20260730.txt`): + + admin -> machines=23 racks=voffice1,vvr1-dc0,vvr1-dc1 (Office1) + vr1-dc0-region -> machines=0 racks=hot-kid (new dc0 region) + + $ maas-profile-assert.sh vr1-dc0-region hot-kid exit=0 + $ maas-profile-assert.sh admin voffice1,vvr1-dc0,vvr1-dc1 exit=0 + $ maas-profile-assert.sh admin hot-kid NEGATIVE -> exit=1 + +**Harness 20/20, and the MUTATION PASS FOUND A REAL DEFECT IN MY OWN TEST.** Three +mutations were applied to the script and the suite re-run: + +- A (comparison always true) -- killed 4 tests. Good. +- B (empty rack list accepted) -- killed 4 tests. Good. +- C (nameless rack entries accepted) -- **suite stayed fully GREEN (19/19)**. The + assertion was decoration: a nameless entry fell through to the generic empty-`ACTUAL` + guard and refused for a different reason, so deleting the branch it claimed to test + changed nothing. Fixed by giving the nameless case its own exit code and refusal + message and asserting on THAT; mutation C now kills a test. This is exactly the + "prove each new assertion can FAIL" rule, and it caught me writing decoration. + +**Revert.** `git rm scripts/maas-profile-assert.sh tests/maas-profile-assert/ && +git checkout ^ -- .` Nothing else depends on it yet; it is additive. + +--- + +## Item 2 -- named MAAS profile `vr1-dc0-region` on voffice1 (LIVE, config) + +**What.** `maas login vr1-dc0-region http://127.0.0.1:5241/MAAS/ -`, key streamed from +`~/vr1-dc0-creds/maas-region-api-key.txt` on vcloud via stdin, never printed. The +existing `admin` profile is UNTOUCHED and still points at Office1. + +**Why the tunnel.** Neither candidate host has everything the migration needs: + +| host | repo | maas CLI | reaches new region | reaches NetBox apex | +|---|---|---|---|---| +| voffice1 | yes | yes | **NO** (curl exit 28) | yes | +| dc0 rack | no | yes | yes (200) | **NO** (10.10.1.10:8000 CLOSED) | +| region VM | no | yes | yes | not measured | + +`dc-plane-ipam.sh` derives the v6 plane prefixes from the NetBox apex (D-136 option D), +so it must run where NetBox is reachable. An SSH tunnel from voffice1 through the rack +(`-L 127.0.0.1:5241:10.12.8.6:5240`) gives voffice1 both. Measured: `localhost:5241` +answers **200**. + +**This does not puncture SEC-010.** SEC-010 forbids the rack FORWARDING between its +transit leg and the plane nets. The tunnel is an application-layer, rack-ORIGINATED +connection -- exactly the distinction SEC-010's own justification draws ("a MAAS rack +proxies at the application layer and needs no kernel forwarding"). No route, no nftables +rule, and no libvirt net was changed. The rack's `table inet sec010` drops are intact. + +**Safe failure direction, verified by construction:** if the tunnel dies, the +`vr1-dc0-region` profile becomes unreachable and `maas-profile-assert.sh` REFUSES +(exit 2). It cannot silently fall back to Office1, because the fallback would have to +be a different URL. + +**CREDENTIAL RESIDENCY -- SEC-026 addendum, flagged not glossed.** This puts dc0's +region admin API key on voffice1, where it was not before. Today that is a strict +SUBSET of the blast radius already present: voffice1 holds the Office1 region admin +key, which currently administers BOTH DCs' nodes. **After the migration that inverts** -- +Office1's key stops covering dc0, and dc0's region key becomes the broader credential +on a shared headend. **Obligation recorded: remove the `vr1-dc0-region` profile from +voffice1 once dc0's control host is settled.** A security-ledger row update is owed at +session close. + +**Revert.** On voffice1: `maas logout vr1-dc0-region`, and kill the tunnel +(`pkill -f '5241:10.12.8.6'`). Both are local to voffice1; no cloud state is touched. + +--- + +## Item 3 -- OPERATING NOTE: MAAS CLI for a per-DC region cannot run from voffice1 + +**Measured.** voffice1 -> `10.12.8.6:5240` times out (curl exit 28); voffice1 -> +`10.10.0.20:5240` is fine. The dc0 rack reaches BOTH (200 / 200). + +**Consequence.** D-128's "Plane 2 (MAAS/NetBox/inner tofu) executes on voffice1" clause +was written when there was ONE region, at Office1. D-132 q1 makes the MAAS control +surface DC-local, so for the per-DC regions that clause needs either a tunnel (what this +session does) or a DC-side host. **This is a consequence of an already-ruled decision, +not a new one** -- recorded here, and owed as a DOCFIX against D-128's Plane-2 wording +and the phase-4 runbook's run-location banner (which F5 already flags as wrong for the +juju half after D-138). + +**No revert** -- this item is a record, not a change. + +--- + +## Item 4 -- as-executed log NOT used for this window, and why + +`scripts/run-logged.sh` opens an INTERACTIVE `script(1)` subshell and is unusable from a +non-interactive agent session. F6 (2026-07-30) already records the harness classifier +refusing the wrapped `script -aqe ... -c` form. Rather than produce a log that looks +complete and is not, this session captures to `docs/audit/*` files -- which is what +GA-R6 actually requires of a gate -- and says so here. A `logs/as-executed-index.md` +row is owed at close. diff --git a/scripts/maas-profile-assert.sh b/scripts/maas-profile-assert.sh new file mode 100644 index 0000000..6aa8e85 --- /dev/null +++ b/scripts/maas-profile-assert.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# scripts/maas-profile-assert.sh [,...] +# +# Prove WHICH MAAS REGION a CLI profile resolves to, before any command that +# reads or writes that region. Exit 0 only if the profile's rack-controller set +# matches the expected set EXACTLY. +# +# WHY THIS EXISTS (measured 2026-07-30, D-132 q1 per-DC region migration). +# Every MAAS call in this repo defaults to `MAAS_PROFILE=admin` (13 scripts). +# On voffice1 that profile resolves to the OFFICE1 region +# (http://10.10.0.20:5240/), while the per-DC regions ruled by D-132 q1 live at +# 10.12.8.6 / 10.12.68.6. The two regions hold SEPARATE databases, so: +# - a carve recreated against the wrong profile is an idempotent NO-OP that +# prints PASS -- the fixed state was already there, in the wrong region; +# - `machine delete` against the wrong profile DESTROYS the real record. +# This is the wrong-HOST class this repo has been bitten by three times +# (dc-mirror.sh run on voffice1; the egress probe run from the rack; P5 probing +# the wrong filesystem) -- now wrong-REGION, where the destructive direction is +# unrecoverable. A count alone is NOT proof: two regions can hold the same +# number of machines. Rack-controller IDENTITY is the discriminator, because a +# rack controller registers to exactly one region. +# +# ASSERT ON CONTENT, NEVER ON EXISTENCE: a reachable API that returns an empty +# or unparseable rack list REFUSES (exit 2) rather than passing. "Could not +# look" is never "nothing there". +# +# Exit: 0 profile resolves to the expected region +# 1 profile resolves SOMEWHERE ELSE (or the rack set differs) +# 2 could not evaluate (unknown profile, unreachable API, bad JSON) +# ASCII + LF. +set -uo pipefail + +PROFILE="${1:-}" +EXPECTED="${2:-}" + +if [ -z "$PROFILE" ] || [ -z "$EXPECTED" ]; then + echo "usage: maas-profile-assert.sh [,...]" >&2 + echo " e.g. maas-profile-assert.sh vr1-dc0-region hot-kid" >&2 + echo " maas-profile-assert.sh admin voffice1,vvr1-dc0,vvr1-dc1" >&2 + exit 2 +fi + +command -v maas >/dev/null 2>&1 || { echo "REFUSE: no 'maas' CLI on this host" >&2; exit 2; } + +RAW="$(maas "$PROFILE" rack-controllers read 2>&1)" +RC=$? +if [ "$RC" -ne 0 ]; then + # Do not echo $RAW unfiltered -- a maas CLI error can carry the profile URL and, + # on some failures, credential fragments. Report the shape, not the body. + echo "REFUSE: 'maas $PROFILE rack-controllers read' exited $RC (unknown profile or unreachable region)" >&2 + exit 2 +fi + +ACTUAL="$(printf '%s' "$RAW" | python3 -c ' +import json, sys +try: + d = json.load(sys.stdin) +except Exception: + sys.exit(3) +if not isinstance(d, list) or not d: + sys.exit(4) +names = sorted(x.get("hostname", "") for x in d) +if not all(names): + sys.exit(5) +print(",".join(names)) +' 2>/dev/null)" +PRC=$? +if [ "$PRC" -eq 3 ]; then + echo "REFUSE: profile '$PROFILE' returned unparseable JSON" >&2; exit 2 +fi +if [ "$PRC" -eq 4 ]; then + echo "REFUSE: profile '$PROFILE' returned an EMPTY rack-controller set" >&2 + echo " (a region with no rack controller cannot be identified -- refusing rather than passing)" >&2 + exit 2 +fi +if [ "$PRC" -eq 5 ]; then + echo "REFUSE: profile '$PROFILE' returned a NAMELESS rack-controller entry" >&2 + echo " (an unnamed rack cannot discriminate one region from another)" >&2 + exit 2 +fi +if [ "$PRC" -ne 0 ] || [ -z "$ACTUAL" ]; then + echo "REFUSE: could not derive the rack-controller set for profile '$PROFILE'" >&2; exit 2 +fi + +WANT="$(printf '%s' "$EXPECTED" | tr ',' '\n' | sed '/^$/d' | sort | paste -sd, -)" + +if [ "$ACTUAL" = "$WANT" ]; then + echo "OK profile '$PROFILE' -> region with racks [$ACTUAL] (as expected)" + exit 0 +fi + +echo "FAIL profile '$PROFILE' resolves to the WRONG region" >&2 +echo " expected racks: [$WANT]" >&2 +echo " actual racks: [$ACTUAL]" >&2 +exit 1 diff --git a/tests/maas-profile-assert/run-tests.sh b/tests/maas-profile-assert/run-tests.sh new file mode 100644 index 0000000..595792c --- /dev/null +++ b/tests/maas-profile-assert/run-tests.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# tests/maas-profile-assert/run-tests.sh -- unit tests for +# scripts/maas-profile-assert.sh (the wrong-REGION guard, 2026-07-30). +# +# Asserts, with a STUB `maas` on PATH so no live region is touched: +# - a matching rack-controller set exits 0; a DIFFERENT region exits 1. +# - expected-set ORDER does not matter (the script sorts both sides). +# - EVERY refusal direction exits 2 rather than passing: no CLI, unknown +# profile (maas rc!=0), unparseable JSON, EMPTY rack list, nameless entries. +# - missing arguments exit 2. +# +# THE EMPTY-LIST CASE IS THE POINT OF THE HARNESS. A region that answers but +# reports no rack controllers must REFUSE, not pass -- otherwise the guard +# green-lights a `machine delete` against an unidentified region, which is the +# exact "a checker that cannot fail is not a gate" class this repo keeps +# finding. +# +# MUTATION PASS, 2026-07-30 -- each assertion was verified to FAIL when its +# branch is removed. Recorded because the pass FOUND ONE: mutation C (accept +# nameless rack entries) left the suite fully GREEN, because a nameless entry +# fell through to the generic empty-ACTUAL guard and refused for a different +# reason. That assertion was decoration until it was made branch-specific by +# asserting the refusal's own message. Mutations A (comparison always true) +# and B (empty list accepted) each killed 4 tests. +set -uo pipefail +SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="$SD/../../scripts/maas-profile-assert.sh" +P=0; F=0 +ok(){ echo "PASS: $1"; P=$((P+1)); } +no(){ echo "FAIL: $1"; F=$((F+1)); } +chk(){ [ "$2" = "$3" ] && ok "$1" || no "$1 (got '$2' want '$3')"; } + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +mkdir -p "$TMP/bin" + +# The stub reads $STUB_MODE and emits a canned rack-controllers payload. +cat > "$TMP/bin/maas" <<'STUB' +#!/usr/bin/env bash +case "${STUB_MODE:-}" in + office1) echo '[{"hostname":"voffice1"},{"hostname":"vvr1-dc0"},{"hostname":"vvr1-dc1"}]' ;; + dc0) echo '[{"hostname":"hot-kid"}]' ;; + empty) echo '[]' ;; + nameless) echo '[{"system_id":"abc123"}]' ;; + garbage) echo 'not json at all' ;; + notalist) echo '{"error":"nope"}' ;; + fail) echo 'Profile "bogus" is not known' >&2; exit 2 ;; + *) echo '[]' ;; +esac +STUB +chmod +x "$TMP/bin/maas" + +run(){ # run -> sets RC, OUT + local mode="$1"; shift + OUT="$(STUB_MODE="$mode" PATH="$TMP/bin:$PATH" bash "$SCRIPT" "$@" 2>&1)" + RC=$? +} + +# --- usage --- +run dc0; chk "no args -> 2" "$RC" 2 +run dc0 vr1-dc0-region; chk "one arg -> 2" "$RC" 2 + +# --- the happy paths --- +run dc0 vr1-dc0-region hot-kid +chk "dc0 region matches hot-kid -> 0" "$RC" 0 +case "$OUT" in *"as expected"*) ok "match prints an OK line" ;; *) no "match prints an OK line (got '$OUT')" ;; esac + +run office1 admin voffice1,vvr1-dc0,vvr1-dc1 +chk "office1 region matches its three racks -> 0" "$RC" 0 + +# order of the EXPECTED set must not matter +run office1 admin vvr1-dc1,voffice1,vvr1-dc0 +chk "expected-set order is irrelevant -> 0" "$RC" 0 + +# --- the wrong-region direction (exit 1) --- +run office1 admin hot-kid +chk "office1 region asserted as dc0 -> 1" "$RC" 1 +case "$OUT" in *"WRONG region"*) ok "mismatch names the failure" ;; *) no "mismatch names the failure (got '$OUT')" ;; esac + +run dc0 vr1-dc0-region voffice1,vvr1-dc0,vvr1-dc1 +chk "dc0 region asserted as office1 -> 1" "$RC" 1 + +# a SUBSET must not pass -- office1 has three racks, not one of them +run office1 admin voffice1 +chk "subset of the rack set -> 1" "$RC" 1 + +# --- refusals: every one of these must be 2, never 0 --- +run empty vr1-dc0-region hot-kid +chk "EMPTY rack list REFUSES (does not pass) -> 2" "$RC" 2 +case "$OUT" in *EMPTY*) ok "empty-list refusal says why" ;; *) no "empty-list refusal says why (got '$OUT')" ;; esac + +run nameless vr1-dc0-region hot-kid +chk "nameless rack entries REFUSE -> 2" "$RC" 2 +# Branch-specific: the first version of this test passed for the WRONG REASON -- +# a nameless entry fell through to the generic empty-ACTUAL guard, so deleting +# the nameless branch left the suite green (mutation C, 2026-07-30). Asserting +# the branch's OWN message is what makes the assertion able to fail. +case "$OUT" in *NAMELESS*) ok "nameless refusal fires its OWN branch" ;; *) no "nameless refusal fires its OWN branch (got '$OUT')" ;; esac + +run garbage vr1-dc0-region hot-kid +chk "unparseable JSON REFUSES -> 2" "$RC" 2 + +run notalist vr1-dc0-region hot-kid +chk "non-list JSON REFUSES -> 2" "$RC" 2 + +run fail bogus-profile hot-kid +chk "unknown profile (maas rc!=0) REFUSES -> 2" "$RC" 2 + +# an empty EXPECTED set must never be satisfiable by an empty actual set +run empty vr1-dc0-region ,,, +chk "empty expected + empty actual still REFUSES -> 2" "$RC" 2 + +# --- no CLI at all --- +OUT="$(PATH="$TMP/emptybin:/usr/bin:/bin" bash "$SCRIPT" vr1-dc0-region hot-kid 2>&1)"; RC=$? +chk "absent maas CLI REFUSES -> 2" "$RC" 2 + +# --- the script must never leak the payload body on a CLI error --- +run fail bogus-profile hot-kid +case "$OUT" in *"is not known"*) no "CLI error body is echoed verbatim (leak risk)" ;; *) ok "CLI error body is not echoed verbatim" ;; esac + +echo +echo "maas-profile-assert: $P passed, $F failed" +[ "$F" -eq 0 ] || exit 1