diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 4707b38..5915663 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1803,6 +1803,28 @@ record, the `juju-controller-` tag, or the tofu MAC pin** -- all still gated. And the controller CANNOT be commissioned yet: its `power_type` measured EMPTY at enlistment, the same state that blocked all nine role nodes on 2026-07-20. +- **juju-01 MACs PINNED + both controllers in `lib-hosts` at the ruled `.5`, 2026-07-29.** + Twelve measured MACs (six per DC, `virsh domiflist`) pinned into both substrate roots; + `-juju-01` added to `lib-hosts.sh` with `HOST_OCTET=5` and its boot MAC. Done NOW because + both VMs are ENLISTED -- the exact boundary `modules/node-vm` warns about, past which an + in-place apply can regenerate unpinned MACs and strand the node. No race with the running + agents: they work on the **voffice1** clone, this edit is on **vcloud**'s. + **Recorded rather than smoothed over: dc1's juju-01 MACs are libvirt-generated `52:54:00:` + values, NOT dc1's schematic `52:54:01:d1:` scheme** -- a direct consequence of the deliberate + `macs = []` at first apply, written into the config so a later reader does not call it a defect. + **THREE COUNT ASSERTIONS RE-POINTED, kept EXACT not relaxed:** dc-selector fleet 9 -> 10 (label + corrected to `D-121 Option C 9 role + D-104 juju-01`, since Option C is a 9-node ruling); + node-vm T8 macs lists 9 -> 10 and T9 MAC literals 54 -> 60. **A real harness defect surfaced: + T8's `grep -c 'macs = \['` counts COMMENTS** -- my own comment explaining the deliberate + `macs = []` inflated it to 11, reading exactly like a spurious extra node. Fixed to strip + comments; same class as the `ledger-scan` next-free defect where a doc QUOTING a token + inflated the counter. + **`maas-role-tags.sh` taught the utility band:** adding juju-01 to HOSTS made it refuse + (octet `.5` is in no ROLE band) -- correct behaviour meeting a new fact. Per the amendment, + a utility-band host with no role token is now SKIPPED and REPORTED, while the refusal keeps + its teeth for genuine disagreement. New cases T10 (skip is reported) and T11 (a ROLE-named + host in the utility band still REFUSES); harness 9 -> 11. + Gauntlet **ALL GREEN (87) on vcloud**; opentofu-validate PASS; repo-lint 0 fail. - **THREE PLATFORM BEHAVIOURS GRADUATED to `references/platform-traps.md`** at session close, having been recorded only in this status document (which is consolidated over time, so a durable trap does not belong here alone): MAAS auto-reserves `::1`-`::ffff:ffff` on EVERY diff --git a/docs/changelog-20260728-vip-arity-gate.md b/docs/changelog-20260728-vip-arity-gate.md index a7c5f04..46c702a 100644 --- a/docs/changelog-20260728-vip-arity-gate.md +++ b/docs/changelog-20260728-vip-arity-gate.md @@ -931,3 +931,55 @@ returned to Ready with its configuration intact. **Remaining:** dc0 storage-02/03/04, then dc1 (apply + re-commission). + +## 37. juju-01 MACs PINNED and both controllers added to lib-hosts at the ruled .5 + +**What.** The twelve measured MACs (six per DC, `virsh domiflist`) are pinned into both +substrate roots, and `-juju-01` is added to `lib-hosts.sh` with `HOST_OCTET=5` per the +D-134 amendment and its measured boot MAC. + +**Why now.** Both VMs are ENLISTED in MAAS, which is the exact boundary `modules/node-vm` +warns about: past enlistment an in-place apply can regenerate unpinned MACs and strand the +node. The nine role nodes went through this on 2026-07-21; these two were carrying the same +exposure. + +**No race with the running agents:** they operate on the **voffice1** clone, this edit is on +**vcloud**'s. Nothing reaches their working tree until someone pulls there, which is +deliberately not being done until they report. + +**Recorded, not smoothed over: dc1's juju-01 MACs are libvirt-generated `52:54:00:` values, +NOT dc1's schematic `52:54:01:d1:NN:PP` scheme.** That is a direct consequence of the +deliberate `macs = []` at first apply. Written into the config comment so a later reader does +not diagnose it as a defect. + +## 38. Three harness assertions RE-POINTED, one harness mechanism fixed + +Adding a 10th node per DC broke three exact-count assertions. All were re-pointed to the NEW +invariant and kept EXACT rather than relaxed to `>=` -- an exact count is the entire value of +these checks: + +- `tests/dc-selector` fleet count 9 -> **10**, with the label corrected to say why + (`D-121 Option C 9 role + D-104 juju-01`) rather than leaving it attributed to Option C, + which is a 9-node ruling. +- `tests/node-vm` T8 `macs` lists 9 -> **10**, T9 pinned MAC literals 54 -> **60**. + +**And a real harness defect found on the way:** T8's `grep -c 'macs = \['` counts COMMENTS. +My own config comment explaining the deliberate `macs = []` inflated the count to 11 -- which +reads exactly like a spurious extra node. Fixed to strip comment lines before counting. Same +class as the `ledger-scan` next-free defect, where a doc merely QUOTING a token inflated the +counter. + +## 39. `maas-role-tags.sh` taught the utility band + +Adding `juju-01` to `HOSTS` broke the role-tag script: it iterates `HOSTS` and refused, +because octet `.5` is in no ruled ROLE band. That refusal was correct behaviour meeting a new +fact. + +Per the D-134 amendment, `.4-.49` is per-DC INFRASTRUCTURE, not OpenStack roles. A host there +with no role token in its name is now **SKIPPED and REPORTED** (never silently), while the +refusal keeps its teeth for genuine disagreement. Both directions are locked by new cases: +**T10** a utility-band host is skipped and reported; **T11** a ROLE-named host placed in the +utility band still REFUSES. Harness 9 -> **11**. + +**Evidence.** Gauntlet **ALL GREEN (87) on vcloud**; `opentofu-validate` PASS; repo-lint +0 fail / 614 files. `lib-hosts` resolves both DCs: 10 hosts, juju octet 5, boot MACs present. diff --git a/opentofu/vr1-dc0-substrate/main.tf b/opentofu/vr1-dc0-substrate/main.tf index d4ce1cc..632da6c 100644 --- a/opentofu/vr1-dc0-substrate/main.tf +++ b/opentofu/vr1-dc0-substrate/main.tf @@ -149,7 +149,16 @@ # Its MAAS tag is `juju-controller-vr1-dc0` and it carries NO role tag, so # `juju bootstrap --constraints tags=juju-controller-vr1-dc0` targets it # deterministically and no OpenStack role constraint can cross-grab it. - "vr1-dc0-juju-01" = { vcpu = 4, mem = 8192, disk_gib = 100, macs = [] } + # MACs PINNED 2026-07-29 from measurement (`virsh domiflist`) after the VM's + # first apply, exactly as the nine role nodes were on 2026-07-21. It is now + # ENLISTED in MAAS, which is the boundary modules/node-vm warns about: past + # enlistment an in-place apply can regenerate unpinned MACs and strand the node. + # NOTE these are libvirt-generated `52:54:00:` values, NOT dc1's schematic + # `52:54:01:d1:NN:PP` scheme -- a direct consequence of the deliberate `macs = []` + # at first apply. Recorded so the difference is not later read as a defect. + "vr1-dc0-juju-01" = { vcpu = 4, mem = 8192, disk_gib = 100, macs = [ + "52:54:00:48:e7:1e", "52:54:00:ed:cd:74", "52:54:00:0f:ee:3a", + "52:54:00:b0:1e:dd", "52:54:00:24:26:0a", "52:54:00:00:a1:37"] } } } diff --git a/opentofu/vr1-dc1-substrate/main.tf b/opentofu/vr1-dc1-substrate/main.tf index 897148c..e182720 100644 --- a/opentofu/vr1-dc1-substrate/main.tf +++ b/opentofu/vr1-dc1-substrate/main.tf @@ -152,7 +152,16 @@ # Its MAAS tag is `juju-controller-vr1-dc1` and it carries NO role tag, so # `juju bootstrap --constraints tags=juju-controller-vr1-dc1` targets it # deterministically and no OpenStack role constraint can cross-grab it. - "vr1-dc1-juju-01" = { vcpu = 4, mem = 8192, disk_gib = 100, macs = [] } + # MACs PINNED 2026-07-29 from measurement (`virsh domiflist`) after the VM's + # first apply, exactly as the nine role nodes were on 2026-07-21. It is now + # ENLISTED in MAAS, which is the boundary modules/node-vm warns about: past + # enlistment an in-place apply can regenerate unpinned MACs and strand the node. + # NOTE these are libvirt-generated `52:54:00:` values, NOT dc1's schematic + # `52:54:01:d1:NN:PP` scheme -- a direct consequence of the deliberate `macs = []` + # at first apply. Recorded so the difference is not later read as a defect. + "vr1-dc1-juju-01" = { vcpu = 4, mem = 8192, disk_gib = 100, macs = [ + "52:54:00:53:12:70", "52:54:00:3e:1a:f2", "52:54:00:e1:4c:d5", + "52:54:00:fe:ae:80", "52:54:00:e2:eb:41", "52:54:00:55:32:e1"] } } } diff --git a/scripts/lib-hosts.sh b/scripts/lib-hosts.sh index 1a1db14..a8d4ec7 100644 --- a/scripts/lib-hosts.sh +++ b/scripts/lib-hosts.sh @@ -110,11 +110,13 @@ # octets applied live per D-134 amendment; tag applied 9/9 same day. HOSTS=( vr1-dc0-control-01 vr1-dc0-control-02 vr1-dc0-control-03 vr1-dc0-compute-01 vr1-dc0-compute-02 - vr1-dc0-storage-01 vr1-dc0-storage-02 vr1-dc0-storage-03 vr1-dc0-storage-04 ) + vr1-dc0-storage-01 vr1-dc0-storage-02 vr1-dc0-storage-03 vr1-dc0-storage-04 + vr1-dc0-juju-01 ) HOST_OCTET=( [vr1-dc0-control-01]=100 [vr1-dc0-control-02]=101 [vr1-dc0-control-03]=102 [vr1-dc0-compute-01]=120 [vr1-dc0-compute-02]=121 [vr1-dc0-storage-01]=150 [vr1-dc0-storage-02]=151 - [vr1-dc0-storage-03]=152 [vr1-dc0-storage-04]=153 ) + [vr1-dc0-storage-03]=152 [vr1-dc0-storage-04]=153 + [vr1-dc0-juju-01]=5 ) HOST_BOOT_MAC=( [vr1-dc0-control-01]=52:54:00:be:69:c5 [vr1-dc0-control-02]=52:54:00:02:ff:57 [vr1-dc0-control-03]=52:54:00:4f:de:a9 @@ -123,7 +125,8 @@ [vr1-dc0-storage-01]=52:54:00:5f:8d:42 [vr1-dc0-storage-02]=52:54:00:48:86:2c [vr1-dc0-storage-03]=52:54:00:b1:94:d1 - [vr1-dc0-storage-04]=52:54:00:2b:ed:ab ) + [vr1-dc0-storage-04]=52:54:00:2b:ed:ab + [vr1-dc0-juju-01]=52:54:00:48:e7:1e ) VIRSH_POWER_ADDRESS="qemu+ssh://jessea123@172.31.0.2/system" HOST_TAG="openstack-vr1-dc0" ;; @@ -133,11 +136,13 @@ # octets applied live per D-134 amendment; tag applied 9/9 same day. HOSTS=( vr1-dc1-control-01 vr1-dc1-control-02 vr1-dc1-control-03 vr1-dc1-compute-01 vr1-dc1-compute-02 - vr1-dc1-storage-01 vr1-dc1-storage-02 vr1-dc1-storage-03 vr1-dc1-storage-04 ) + vr1-dc1-storage-01 vr1-dc1-storage-02 vr1-dc1-storage-03 vr1-dc1-storage-04 + vr1-dc1-juju-01 ) HOST_OCTET=( [vr1-dc1-control-01]=100 [vr1-dc1-control-02]=101 [vr1-dc1-control-03]=102 [vr1-dc1-compute-01]=120 [vr1-dc1-compute-02]=121 [vr1-dc1-storage-01]=150 [vr1-dc1-storage-02]=151 - [vr1-dc1-storage-03]=152 [vr1-dc1-storage-04]=153 ) + [vr1-dc1-storage-03]=152 [vr1-dc1-storage-04]=153 + [vr1-dc1-juju-01]=5 ) HOST_BOOT_MAC=( [vr1-dc1-control-01]=52:54:01:d1:01:01 [vr1-dc1-control-02]=52:54:01:d1:02:01 [vr1-dc1-control-03]=52:54:01:d1:03:01 @@ -146,7 +151,8 @@ [vr1-dc1-storage-01]=52:54:01:d1:06:01 [vr1-dc1-storage-02]=52:54:01:d1:07:01 [vr1-dc1-storage-03]=52:54:01:d1:08:01 - [vr1-dc1-storage-04]=52:54:01:d1:09:01 ) + [vr1-dc1-storage-04]=52:54:01:d1:09:01 + [vr1-dc1-juju-01]=52:54:00:53:12:70 ) VIRSH_POWER_ADDRESS="qemu+ssh://jessea123@172.31.0.6/system" HOST_TAG="openstack-vr1-dc1" ;; diff --git a/scripts/maas-role-tags.sh b/scripts/maas-role-tags.sh index 85726cb..231b63d 100755 --- a/scripts/maas-role-tags.sh +++ b/scripts/maas-role-tags.sh @@ -68,6 +68,12 @@ PLAN="$(printf '%s\n' "$EXPECT" | python3 -c ' import sys BANDS = (("control", 100, 119), ("compute", 120, 149), ("storage", 150, 200)) +# D-134 AMENDMENT 2026-07-29: .4-.49 is the UTILITY band -- per-DC INFRASTRUCTURE the +# OpenStack nodes consume (the artifact service at .4, the Juju controller at .5), NOT +# OpenStack role nodes. A host there with no role token in its name is not a role node +# and is SKIPPED, not refused. The refusal keeps its teeth for genuine DISAGREEMENT: +# a utility-band octet on a role-named host, or a role-band octet whose name disagrees. +UTILITY = (4, 49) rc = 0 for line in sys.stdin.read().splitlines(): if not line.strip(): @@ -76,6 +82,12 @@ o = int(octet) by_band = next((r for r, lo, hi in BANDS if lo <= o <= hi), None) by_name = next((r for r, _, _ in BANDS if "-%s-" % r in host), None) + if UTILITY[0] <= o <= UTILITY[1]: + if by_name is not None: + print("REFUSE\t%s\t%s\tutility-band octet .%d but name says %s" % (host, mac, o, by_name)); rc = 1 + else: + print("SKIP\t%s\t%s\tutility band .%d -- per-DC infrastructure, not an OpenStack role node" % (host, mac, o)) + continue if by_band is None: print("REFUSE\t%s\t%s\toctet .%d is in NO ruled D-134 band" % (host, mac, o)); rc = 1 elif by_name is None: @@ -91,6 +103,9 @@ echo "== maas-role-tags $ACTION $SITE $([ "$COMMIT" -eq 1 ] && echo '--commit' || echo '(DRY RUN)') ==" echo " role derived from the D-134 band AND the host name; both must agree" +printf '%s\n' "$PLAN" | grep '^SKIP' | while IFS=$'\t' read -r _ h m why; do + printf " [skip] %-22s %s\n" "$h" "$why" +done echo # ---- which role tags exist in MAAS? -------------------------------------------------- diff --git a/tests/dc-selector/run-tests.sh b/tests/dc-selector/run-tests.sh index 770dd3f..d431620 100644 --- a/tests/dc-selector/run-tests.sh +++ b/tests/dc-selector/run-tests.sh @@ -135,7 +135,7 @@ H1_PWR="$(lib_hosts_select_dc vr1-dc1 >/dev/null 2>&1; echo "$VIRSH_POWER_ADDRESS")" chk "hosts vr1-dc1 power addr rack transit .6" "$H1_PWR" "qemu+ssh://jessea123@172.31.0.6/system" H0_N="$(lib_hosts_select_dc vr1-dc0 >/dev/null 2>&1; echo "${#HOSTS[@]}")" -chk "hosts vr1-dc0 fleet count 9 (D-121 Option C)" "$H0_N" "9" +chk "hosts vr1-dc0 fleet count 10 (D-121 Option C 9 role + D-104 juju-01)" "$H0_N" "10" # --- ONE SELECTION PER SHELL (stale cross-DC guard, lib-net precedent) --- ( lib_hosts_select_dc vr1-dc0 >/dev/null 2>&1; lib_hosts_select_dc vr1-dc1 2>/dev/null ); \ diff --git a/tests/maas-role-tags/run-tests.sh b/tests/maas-role-tags/run-tests.sh index baad3e5..6b2c67c 100755 --- a/tests/maas-role-tags/run-tests.sh +++ b/tests/maas-role-tags/run-tests.sh @@ -96,9 +96,13 @@ else bad "T6 absent-client refusal wrong (rc=$rc)"; fi # A node whose pinned MAC has no MAAS record must be reported, and apply must refuse. +# Drop a ROLE node, not the last entry: since the D-134 amendment of 2026-07-29 the last +# lib-hosts entry is -juju-01, which sits in the UTILITY band and is correctly SKIPPED, +# so removing it produces no finding at all. python3 - "$TMP/m-short.json" "$TMP/m-all.json" <<'PY' import json, sys -m = json.load(open(sys.argv[2])); json.dump(m[:-1], open(sys.argv[1], "w")) +m = json.load(open(sys.argv[2])) +json.dump([x for x in m if "juju" not in x["hostname"]][1:], open(sys.argv[1], "w")) PY run "T7 a pinned MAC missing from MAAS is reported" 1 '\[MISS\].*not found in MAAS' "check vr1-dc0" \ SC_MACHINES="$TMP/m-short.json" SC_TAGS="$TMP/t-all.json" @@ -116,6 +120,21 @@ ok "T9 band/name disagreement REFUSES rather than picking one" else bad "T9 derivation-conflict refusal wrong (rc=$rc)" "$(printf '%s' "$out" | tail -2)"; fi +# ---- D-134 AMENDMENT 2026-07-29: the utility band is per-DC INFRASTRUCTURE ---------- +# -juju-01 sits at .5 and is NOT an OpenStack role node. It must be SKIPPED (reported, +# never silent) rather than refused -- but the refusal must keep its teeth for a genuine +# disagreement, which T11 proves. +run "T10 a utility-band host is SKIPPED, not refused, and is REPORTED" 0 '\[skip\].*juju-01.*utility band' "check vr1-dc0" \ + SC_MACHINES="$TMP/m-all.json" SC_TAGS="$TMP/t-all.json" + +mkdir -p "$TMP/fake2/scripts"; cp "$REPO"/scripts/*.sh "$TMP/fake2/scripts/" 2>/dev/null +sed -i 's/\[vr1-dc0-storage-01\]=150/[vr1-dc0-storage-01]=7/' "$TMP/fake2/scripts/lib-hosts.sh" +out="$(env PATH="$TMP/bin:$PATH" SC_MACHINES="$TMP/m-all.json" SC_TAGS="$TMP/t-all.json" \ + bash "$TMP/fake2/scripts/maas-role-tags.sh" check vr1-dc0 2>&1)"; rc=$? +if [ "$rc" = 2 ] && printf '%s' "$out" | grep -q 'utility-band octet .7 but name says storage'; then + ok "T11 a ROLE-named host in the utility band still REFUSES (teeth intact)" +else bad "T11 utility/role conflict not refused (rc=$rc)" "$(printf '%s' "$out" | tail -2)"; fi + echo echo "RESULT: PASS=$PASS FAIL=$FAIL" [ "$FAIL" -eq 0 ] && { echo "ALL PASS"; exit 0; } || exit 1 diff --git a/tests/node-vm/run-tests.sh b/tests/node-vm/run-tests.sh index 9512348..b6c7b3d 100755 --- a/tests/node-vm/run-tests.sh +++ b/tests/node-vm/run-tests.sh @@ -47,14 +47,23 @@ # T8: the inner root pins EVERY node: 9 macs lists, 54 MAC literals total, # and the module call passes interface_macs through. if [ -f "$INNER" ]; then - MACS_LISTS="$(grep -c 'macs = \[' "$INNER")" - MAC_LITERALS="$(grep -oE '"([0-9a-f]{2}:){5}[0-9a-f]{2}"' "$INNER" | wc -l)" - [ "$MACS_LISTS" -eq 9 ] \ - && ok "T8 inner root: 9 per-node macs lists" \ - || no "T8 inner root: 9 per-node macs lists (found $MACS_LISTS)" - [ "$MAC_LITERALS" -eq 54 ] \ - && ok "T9 inner root: 54 pinned MAC literals (9 nodes x 6 planes)" \ - || no "T9 inner root: 54 pinned MAC literals (found $MAC_LITERALS)" + # Count CONFIG, not prose: strip comment lines first. Measured 2026-07-29 -- a comment + # explaining the deliberate `macs = []` at first apply contained the literal and inflated + # this count to 11, which reads exactly like a real extra node. Same class as the + # ledger-scan next-free defect, where a doc QUOTING a token inflated the counter. + MACS_LISTS="$(grep -v '^[[:space:]]*#' "$INNER" | grep -c 'macs = \[')" + MAC_LITERALS="$(grep -v '^[[:space:]]*#' "$INNER" | grep -oE '"([0-9a-f]{2}:){5}[0-9a-f]{2}"' | wc -l)" + # RE-POINTED 2026-07-29: was 9 / 54. The D-104 AMENDMENT adds a 10th node per DC -- the + # dedicated Juju controller -juju-01 -- whose six MACs were pinned from measurement + # after its first apply (it is ENLISTED, which is the boundary past which unpinned MACs + # can be regenerated and strand the node). So the ruled shape is 10 nodes x 6 planes. + # Kept EXACT rather than relaxed to >=: an exact count is the whole value of these two. + [ "$MACS_LISTS" -eq 10 ] \ + && ok "T8 inner root: 10 per-node macs lists (9 role + juju controller)" \ + || no "T8 inner root: 10 per-node macs lists (found $MACS_LISTS)" + [ "$MAC_LITERALS" -eq 60 ] \ + && ok "T9 inner root: 60 pinned MAC literals (10 nodes x 6 planes)" \ + || no "T9 inner root: 60 pinned MAC literals (found $MAC_LITERALS)" DUPES="$(grep -oE '"([0-9a-f]{2}:){5}[0-9a-f]{2}"' "$INNER" | sort | uniq -d | wc -l)" [ "$DUPES" -eq 0 ] \ && ok "T10 inner root: no duplicate MACs" \