diff --git a/docs/changelog-20260709-dc-selector-convention.md b/docs/changelog-20260709-dc-selector-convention.md new file mode 100644 index 0000000..7a02b53 --- /dev/null +++ b/docs/changelog-20260709-dc-selector-convention.md @@ -0,0 +1,78 @@ +# Changelog 2026-07-09 -- $DC selector convention for lib-net.sh/lib-hosts.sh (DOCFIX-151) + +No live infrastructure touched -- library-script additions + test harness only. +Closes tooling gap register item #1 in `docs/dc-dc-deployment-workflow.md`, +which explicitly BLOCKED the Stage 5 "high reuse" claim (`phase-01..08` +runbooks reused per-DC) from being operationally true. + +## Item + +### 1. DOCFIX-151 -- `lib_net_select_dc()` / `lib_hosts_select_dc()` +FILES: `scripts/lib-net.sh`, `scripts/lib-hosts.sh`, new +`tests/dc-selector/run-tests.sh`. + +WHAT: added one opt-in selector function to each library, following the same +pattern as the existing `$REPO` convention (DOCFIX-139/141) -- an explicit, +measured selector, never an inferred default. + +- `lib_net_select_dc()` in `lib-net.sh`: `dc0`/`dc1` are both a no-op (D-101, + ADOPTED 2026-07-09, rules DC1 inherits DC0's v4 plane layout UNCHANGED -- + so there is deliberately no second copy of the CIDR literals to drift out + of sync with the first); `dc2` FAILS LOUD (no NetBox-assigned literals + exist yet -- gap #3, still open); an unknown token FAILS LOUD. +- `lib_hosts_select_dc()` in `lib-hosts.sh`: `dc0` is a no-op (the real, + enrolled VR0 four-host set); `dc1` AND `dc2` BOTH fail loud -- unlike the + network side, DC1's host *inventory* does not exist yet either (its node + VMs haven't been created by `opentofu/modules/node-vm` or enrolled in + MAAS), so aliasing dc1 to dc0's real hosts would be a silent-reuse bug, + not a valid inheritance. This asymmetry (`lib_net_select_dc dc1` no-ops, + `lib_hosts_select_dc dc1` fails) is intentional and is called out in both + the changelog and the source comments so a future reader isn't confused by + the two libraries behaving differently for the same DC token. + +Backward compatible by construction: sourcing either file with no further +action is completely unaffected -- every existing script that sources +`lib-net.sh`/`lib-hosts.sh` today never calls the new functions, so nothing +about its behavior changes. This is an additive, opt-in mechanism. + +VERIFIED: +- New harness `tests/dc-selector/run-tests.sh`: 21/21 PASS. Covers: backward + compatibility (flat vars unaffected by sourcing alone), dc0/dc1 no-op + + dc2/unknown-token/missing-arg fail-loud for the net selector, dc0 no-op + + dc1/dc2/unknown-token/missing-arg fail-loud for the hosts selector, error + messages cite the correct blocking reason (NetBox gap vs. no-enrollment), + and an explicit assertion that the dc1 asymmetry between the two libraries + is real and intentional (not a bug). +- Caught and fixed a real bug in the harness itself while writing it: the + first draft piped `lib_net_select_dc dc2 2>&1 >/dev/null | grep -q + "NetBox"` under `set -o pipefail` -- since the selector function's own + exit code is 1 (by design, for the fail-loud case), `pipefail` made the + whole pipeline report failure even when `grep` matched correctly. Fixed by + capturing stderr into a variable via command substitution instead of + piping through grep, so the assertion checks message content independent + of the (expected-nonzero) selector exit code. +- `bash scripts/repo-lint.sh`: 0 fail, 1 documented legacy WARN (unchanged). +- `bash scripts/run-tests-all.sh`: confirmed via `git stash` A/B that the + SAME 23 pre-existing failures (all either "jq required" -- not installed + on this Windows workstation -- or live juju/cloud-dependent suites like + `cloud-assert`/`preflight`/`tenant-*`, none of which touch + `lib-net.sh`/`lib-hosts.sh`) exist identically with and without this + change. `dc-selector` itself and `vault-kv-health` (the only other harness + that sources either library) both PASS cleanly. This session is running + from the operator's workstation, not the vopenstack-jesse jumphost -- no + live cloud/juju/MAAS access is expected or required for tonight's prep + work (operator instruction, 2026-07-09 evening). + +REVERT: `git checkout HEAD~ -- scripts/lib-net.sh scripts/lib-hosts.sh && +rm -rf tests/dc-selector` (safe -- no other script calls either new +function yet, so nothing downstream depends on their presence). + +## Next actionable step + +Tooling gap register item #1 CLOSED. Stage 5's runbook (Juju controller + +OpenStack bundle, per DC) can now honestly claim the `phase-01..08` reuse +path IS DC-safe once it's written to call `lib_net_select_dc "$DC"` / +`lib_hosts_select_dc "$DC"` explicitly -- that call-site wiring is part of +authoring the Stage 5 runbook itself (task list item), not this delivery. +Gap #3 (NetBox multi-DC import pipeline) is the next dependency: `dc2` +remains fail-loud in both libraries until it closes. diff --git a/docs/dc-dc-deployment-workflow.md b/docs/dc-dc-deployment-workflow.md index c6b0a77..45db3e8 100644 --- a/docs/dc-dc-deployment-workflow.md +++ b/docs/dc-dc-deployment-workflow.md @@ -174,15 +174,20 @@ not by size -- #1 is cheap and unblocks a reuse claim made elsewhere in this doc; #2 is the single largest build. -1. **`$DC` selector convention (BLOCKS Stage 5's reuse claim).** `scripts/lib-net.sh` - and `lib-hosts.sh` are hardcoded to ONE deployment's literals (one set of six - plane CIDRs, one `KEYSTONE_VIP_DEFAULT`, a fixed 4-host list) -- confirmed by - reading both files directly, no DC parameter exists anywhere in them. Running - the "reused" `phase-01..08` runbooks against DC2 today would silently reuse - DC1's values. Needs the same treatment as the 2026-07-09 `$REPO` convention: - a `$DC` (or `--dc`) selector, with both libraries becoming DC-keyed lookups - instead of flat constants. Do this BEFORE writing the Stage 5 runbook, or the - "high reuse" claim above is not actually true operationally. +1. **`$DC` selector convention -- CLOSED 2026-07-09 (DOCFIX-151).** + `scripts/lib-net.sh` gained `lib_net_select_dc()` (dc0/dc1 no-op, since + D-101 rules DC1 inherits DC0's v4 layout unchanged; dc2 fails loud -- + NetBox literals not yet assigned, gap #3) and `scripts/lib-hosts.sh` + gained `lib_hosts_select_dc()` (dc0 no-op; dc1 AND dc2 BOTH fail loud -- + no real per-DC host enrollment exists yet for either, an intentional + asymmetry vs. the net selector, documented in both files' comments and + `docs/changelog-20260709-dc-selector-convention.md`). Backward compatible + by construction -- every existing caller of either library is unaffected. + New harness `tests/dc-selector/run-tests.sh`: 21/21 PASS. The mechanism + exists; Stage 5's runbook still needs to actually CALL + `lib_net_select_dc "$DC"` / `lib_hosts_select_dc "$DC"` when it's + authored -- that call-site wiring is part of writing that runbook, not + this delivery. 2. **OpenTofu -- network/pool/node-VM/cloud-init/MAAS/netem layer scaffolded 2026-07-09 (was: zero files).** `opentofu/` now has: `modules/dc-planes` (six per-DC planes), `modules/mesh-link` (the D-100 dark-fiber triangle @@ -320,5 +325,5 @@ | `tc netem` mechanism (`modules/netem-link`) | BUILT 2026-07-09, UNVALIDATED -- real latency/jitter/loss/rate parameters still an unruled D-100 item, see `opentofu/README.md` | | `opentofu/` (DC2 planes) | NOT DONE -- deliberately deferred pending NetBox CIDR assignment, see `opentofu/README.md` | | Reusable as-is, repo-agnostic (2026-07-09 sweep) | `bundle.yaml`, `phase-01..08` VR0 DC0 runbooks (Stage 5/7 template, once DC-parameterized -- gap #1), `preflight.sh`, `cloud-assert.sh`, `repo-lint.sh`, `run-logged.sh`, `ledger-scan.sh` | -| `lib-net.sh` / `lib-hosts.sh` | NOT yet DC-agnostic -- single-deployment hardcoded literals; see gap #1 | +| `lib-net.sh` / `lib-hosts.sh` | `$DC` selector mechanism ADDED 2026-07-09 (DOCFIX-151, gap #1 CLOSED) -- `lib_net_select_dc()`/`lib_hosts_select_dc()`, backward compatible, 21/21 tests; Stage 5 runbook still needs to call it | | Environment/skill docs (`environment.md`, README.md) | Still describe VR0 DC0/v1 single-DC scope wholesale -- flagged, not yet rewritten for VR1; not a blocker for Stage 0 but will need doing before/during Stage 1 | diff --git a/docs/session-ledger.md b/docs/session-ledger.md index bb479bb..1b02c63 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -21,7 +21,7 @@ ## Machine-derived (re-seed from `scripts/ledger-scan.sh`; do not hand-edit) -_As of 2026-07-09 scan (post-DOCFIX-149, Stage 0/D-100..D-110 ratified); +_As of 2026-07-09 scan (post-DOCFIX-151, $DC selector convention closed); re-run the scan to refresh:_ - **PROPOSED / OPEN decisions:** D-068 (Vault substrate hardening, Roosevelt -- 1.16 @@ -37,8 +37,8 @@ - **OPEN security rows:** SEC-001 (rotate at v1 close, re-ruled 2026-07-06), SEC-003 (custodians + second-person unseal -- DEFERRED; keeps d011-06 MANUAL), SEC-004 (flip repo to private at v1 close -- deferral reaffirmed). -- **Next-free numbers:** D = 111, DOCFIX = 150, BUNDLEFIX = 012. - (Seeded from the 2026-07-09 scan, post Stage-0 ratification.) +- **Next-free numbers:** D = 111, DOCFIX = 151, BUNDLEFIX = 012. + (Seeded from the 2026-07-09 scan, post DOCFIX-151.) --- @@ -921,6 +921,36 @@ under PROPOSED/OPEN** -- only D-068/D-071 remain (separate, pre-existing, unrelated to this gate). repo-lint 0 fail/1 documented warn, harness 34/34. Stage 0 of `docs/dc-dc-deployment-workflow.md` is CLEARED. +- **Operator context, 2026-07-09 evening:** operator stepped away from the + workstation for the night, granted blanket git permission for the + session, and authorized autonomous subagent use to make maximum progress + toward an end-to-end-ready project by morning. Operator clarified this + session runs from the WORKSTATION, not the vopenstack-jesse jumphost -- + tonight's scope is PREP ONLY (docs, scripts, tests, hardened runbooks); + no live infra (juju/MAAS/OpenStack APIs) is reachable or in scope tonight. + Operator will switch to the vcloud host in the morning to run actual + installs/deployments. This explains (and validates) why + `run-tests-all.sh` shows 23 pre-existing failures in this environment -- + all either "jq required" (not installed on the workstation) or + live-cloud-dependent suites (`cloud-assert`, `preflight`, `tenant-*`, + etc.) -- confirmed via `git stash` A/B to be identical with and without + this session's own changes, i.e. an environment gap, not a regression. +- **DONE -- Tooling gap register #1 CLOSED 2026-07-09 ($DC selector + convention, changelog `docs/changelog-20260709-dc-selector-convention.md`, + DOCFIX-151):** added `lib_net_select_dc()` to `scripts/lib-net.sh` + (dc0/dc1 no-op per D-101's inherited-layout ruling; dc2 fails loud -- + gap #3 still open) and `lib_hosts_select_dc()` to `scripts/lib-hosts.sh` + (dc0 no-op; dc1 AND dc2 BOTH fail loud -- no real per-DC host enrollment + exists for either yet, an intentional asymmetry vs. the net selector, + documented in-line in both files). Backward compatible by construction -- + no existing caller of either library is affected. New harness + `tests/dc-selector/run-tests.sh`: 21/21 PASS (caught and fixed a real + `pipefail`-vs-nonzero-exit-code bug in the harness itself while writing + it). repo-lint 0 fail/1 documented warn (unchanged); full gauntlet + confirmed via `git stash` A/B to introduce zero new failures. Stage 5's + runbook (not yet authored) still needs to actually CALL + `lib_net_select_dc "$DC"` / `lib_hosts_select_dc "$DC"` -- the mechanism + exists, the call sites don't yet. diff --git a/scripts/lib-hosts.sh b/scripts/lib-hosts.sh index 4bec6f6..07ac646 100644 --- a/scripts/lib-hosts.sh +++ b/scripts/lib-hosts.sh @@ -56,3 +56,32 @@ maas "${MAAS_PROFILE:-admin}" machines read 2>/dev/null \ | jq -r --arg h "$hn" '.[] | select(.hostname==$h) | .system_id' | head -1 } + +# --- $DC selector convention (2026-07-09, tooling gap register #1) --- +# +# Mirrors lib-net.sh's lib_net_select_dc -- but the answer here is honestly +# different, not a parallel no-op: HOSTS / HOST_OCTET / HOST_BOOT_MAC above +# are VR0/DC0's real, measured, enrolled hosts. DC1's and DC2's node VMs are +# created by opentofu/modules/node-vm (Stage 3 / buildout-design Phase 2), +# which has not run -- their hostnames, boot MACs, and octets do not exist +# yet, and VIRSH_POWER_ADDRESS above is DC0's own vcloud host connection +# (the buildout design's VR1 vcloud host is a distinct, not-yet-measured +# endpoint -- see opentofu's root `libvirt_uri` variable, also unset). +# Selecting dc1 or dc2 FAILS LOUDLY rather than inventing placeholder host +# data or silently reusing DC0's real hosts for a different DC. +lib_hosts_select_dc() { + local dc="${1:?usage: lib_hosts_select_dc }" + case "$dc" in + dc0) + : # already the sourced defaults above -- explicit no-op, not a guess. + ;; + dc1|dc2) + echo "FAIL: $dc has no enrolled hosts yet -- Phase 2/3 node-VM creation (opentofu/modules/node-vm) and MAAS enrollment haven't run for it. Do not invent HOSTS/HOST_OCTET/HOST_BOOT_MAC/VIRSH_POWER_ADDRESS values; populate this file with a real \$dc block once those hosts are actually enrolled and measured." >&2 + return 1 + ;; + *) + echo "FAIL: unknown DC '$dc' (expected dc0, dc1, or dc2)" >&2 + return 1 + ;; + esac +} diff --git a/scripts/lib-net.sh b/scripts/lib-net.sh index ac61ad7..5db72bd 100644 --- a/scripts/lib-net.sh +++ b/scripts/lib-net.sh @@ -71,6 +71,43 @@ # change here once, not in 6 scripts. KEYSTONE_VIP_DEFAULT="10.12.4.50" +# --- $DC selector convention (2026-07-09, tooling gap register #1) --- +# +# Backward compatible by construction: sourcing this file with no further +# action continues to populate PLANE_CIDRS / PLANE_NAME / PLANE_GW / etc. +# exactly as above (VR0/DC0's real, measured values) -- every existing +# script that sources lib-net.sh keeps working completely unchanged. +# +# DC-aware scripts call `lib_net_select_dc "$DC"` ONCE, immediately after +# sourcing this file, before using any of the flat variables. This mirrors +# the $REPO convention (DOCFIX-139/141): one explicit, measured selector, +# never an inferred default. +# +# dc0 and dc1 are the SAME values: D-101 (ADOPTED 2026-07-09) rules that +# DC1 inherits DC0's v4 layout UNCHANGED, so there is deliberately no +# second copy of these literals to drift out of sync with the ones above -- +# selecting either is a no-op against what sourcing this file already set. +# dc2 has NO assigned values yet (D-101's NetBox-literals open item, gap #3 +# in the tooling gap register) -- selecting it FAILS LOUDLY. It does not +# silently fall back to dc0/dc1's values or invent one; that silent-reuse +# failure mode is exactly what this convention exists to prevent. +lib_net_select_dc() { + local dc="${1:?usage: lib_net_select_dc }" + case "$dc" in + dc0|dc1) + : # already the sourced defaults above -- explicit no-op, not a guess. + ;; + dc2) + echo "FAIL: dc2 has no assigned network literals yet (D-101 NetBox-literals open item, tooling gap register #3) -- do not select dc2 until NetBox assigns real CIDRs for it" >&2 + return 1 + ;; + *) + echo "FAIL: unknown DC '$dc' (expected dc0, dc1, or dc2)" >&2 + return 1 + ;; + esac +} + # --- tiny read-only helpers --- # need_jq: jq is required (present on the jumphost). Returns non-zero if absent. diff --git a/tests/dc-selector/run-tests.sh b/tests/dc-selector/run-tests.sh new file mode 100644 index 0000000..4c2caed --- /dev/null +++ b/tests/dc-selector/run-tests.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# tests/dc-selector/run-tests.sh -- unit tests for the $DC selector +# convention (2026-07-09, tooling gap register #1): lib_net_select_dc() in +# scripts/lib-net.sh and lib_hosts_select_dc() in scripts/lib-hosts.sh. +# +# Asserts: +# - sourcing either file with no further action is unaffected (backward +# compatible by construction -- the flat vars populate exactly as before). +# - lib_net_select_dc: dc0/dc1 no-op (D-101: DC1 inherits DC0's v4 layout +# unchanged), dc2 fails loud, unknown token fails loud. +# - lib_hosts_select_dc: dc0 no-op, dc1 AND dc2 both fail loud (no real +# per-DC host enrollment exists yet for either -- this is the documented +# asymmetry vs. lib-net.sh's dc0|dc1 no-op). +# - Neither function ever silently invents/reuses a value across DCs. +set -uo pipefail +SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NET="$SD/../../scripts/lib-net.sh" +HOSTSLIB="$SD/../../scripts/lib-hosts.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')"; } + +# guard: both libs still refuse direct execution (unchanged by this addition) +( bash "$NET" >/dev/null 2>&1 ); chk "lib-net direct-exec-refused" "$?" 2 +( bash "$HOSTSLIB" >/dev/null 2>&1 ); chk "lib-hosts direct-exec-refused" "$?" 2 + +# shellcheck source=/dev/null +. "$NET" +# shellcheck source=/dev/null +. "$HOSTSLIB" + +# --- backward compatibility: sourcing alone still populates the flat vars --- +chk "PLANE_CIDRS unaffected" "${PLANE_CIDRS[0]}" "10.12.4.0/22" +chk "KEYSTONE_VIP_DEFAULT unaffected" "$KEYSTONE_VIP_DEFAULT" "10.12.4.50" +chk "HOSTS array unaffected" "${HOSTS[0]}" "openstack0" +chk "HOST_OCTET unaffected" "${HOST_OCTET[openstack0]}" "40" + +# --- lib_net_select_dc: dc0/dc1 no-op, dc2 fails loud, unknown fails loud --- +( lib_net_select_dc dc0 ); chk "net dc0 no-op rc" "$?" 0 +( lib_net_select_dc dc1 ); chk "net dc1 no-op rc" "$?" 0 +( lib_net_select_dc dc2 2>/dev/null ); chk "net dc2 fails-loud rc" "$?" 1 +NET_ERR="$(lib_net_select_dc dc2 2>&1 1>/dev/null || true)" +grep -q "NetBox" <<<"$NET_ERR" && ok "net dc2 error cites NetBox gap" || no "net dc2 error cites NetBox gap" +( lib_net_select_dc bogus 2>/dev/null ); chk "net unknown-token fails-loud rc" "$?" 1 +( lib_net_select_dc 2>/dev/null ); chk "net missing-arg fails-loud rc" "$?" 1 + +# no-op really means no-op: values identical after selecting either dc0 or dc1 +DC0_VAL="$(lib_net_select_dc dc0 >/dev/null 2>&1; echo "${PLANE_CIDRS[0]}")" +DC1_VAL="$(lib_net_select_dc dc1 >/dev/null 2>&1; echo "${PLANE_CIDRS[0]}")" +chk "net dc0/dc1 identical values (D-101)" "$DC0_VAL" "$DC1_VAL" + +# --- lib_hosts_select_dc: dc0 no-op, dc1 AND dc2 both fail loud --- +( lib_hosts_select_dc dc0 ); chk "hosts dc0 no-op rc" "$?" 0 +( lib_hosts_select_dc dc1 2>/dev/null ); chk "hosts dc1 fails-loud rc" "$?" 1 +( lib_hosts_select_dc dc2 2>/dev/null ); chk "hosts dc2 fails-loud rc" "$?" 1 +HOSTS_ERR1="$(lib_hosts_select_dc dc1 2>&1 1>/dev/null || true)" +grep -q "no enrolled hosts" <<<"$HOSTS_ERR1" && ok "hosts dc1 error cites no-enrollment" || no "hosts dc1 error cites no-enrollment" +HOSTS_ERR2="$(lib_hosts_select_dc dc2 2>&1 1>/dev/null || true)" +grep -q "no enrolled hosts" <<<"$HOSTS_ERR2" && ok "hosts dc2 error cites no-enrollment" || no "hosts dc2 error cites no-enrollment" +( lib_hosts_select_dc bogus 2>/dev/null ); chk "hosts unknown-token fails-loud rc" "$?" 1 +( lib_hosts_select_dc 2>/dev/null ); chk "hosts missing-arg fails-loud rc" "$?" 1 + +# --- the documented asymmetry itself: dc1 differs between the two libs --- +( lib_net_select_dc dc1 ); NET_DC1_RC=$? +( lib_hosts_select_dc dc1 2>/dev/null ); HOSTS_DC1_RC=$? +[ "$NET_DC1_RC" = 0 ] && [ "$HOSTS_DC1_RC" = 1 ] && ok "dc1 asymmetry: net no-ops, hosts fails (documented, not a bug)" || no "dc1 asymmetry broken (net=$NET_DC1_RC hosts=$HOSTS_DC1_RC)" + +echo; [ "$F" = 0 ] && { echo "ALL PASS ($P checks)"; exit 0; } || { echo "FAILURES: $F"; exit 1; }