diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index ae3ce56..c937b1f 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1091,7 +1091,11 @@ bumping both constants breaks the gate. **(iii) the L3-9 collision is worse than recorded and the DANGEROUS merge order is the GREEN one** -- vips-last exits 0 while silently dropping every v6 leg, and `prefer-ipv6: true` SURVIVES as a separate key, so charms would bind - `:::port` with no v6 VIP for pacemaker to manage (measured through the checker's merge MIRROR, + `:::port` with no v6 VIP for pacemaker to manage. **SUPERSEDED 2026-07-29: the "green" + clause has not been true since invariant 9 shipped 2026-07-28 -- measured, NEITHER merge + order passes, and the collision itself is now CLOSED (see the commit-2 entry below). + The merge SEMANTICS described here remain accurate; only the green-over-loss conclusion + is retired.** (measured through the checker's merge MIRROR, not juju, which is absent from this host -- confirm with `--dry-run`). **(iv) CORRECTED, and worth keeping:** the agent flagged dc1 "silently inheriting dc0's band bounds" as a defect; the observation is right and the conclusion is WRONG -- the octet band is DC-INVARIANT by @@ -1504,6 +1508,53 @@ **STILL RED, UNCHANGED AND EXPECTED:** CHECK 1 reports `OK=11 want 13` and `provider-bundle-check` reports designate -- both are R11's `.61`/`.62` being RULED-BUT-NOT-BUILT, which is commit 2's job. `preflight` remains exit 1 as it already was. +- **RULING 3 COMMIT 2 EXECUTED 2026-07-29 -- R2's DUAL-STACK AND R11's VIPs ARE NOW BUILT.** + Operator: "Continue as autonomously as possible. Deploy agents as needed." Two read-only + agents mapped the blast radius first. Changelog `docs/changelog-20260728-vip-arity-gate.md`. + Both per-DC VIP overlays re-rendered dual-family: every app carries `prefer-ipv6: true` and a + six-address `vip`, and **vault `.61` + designate `.62` are BUILT**. Measured both DCs: + **`13 clustered VIP(s) ... (13 dual-family)` + `12 hacluster principal(s) all carry a VIP` -> + PASS**; with `dc-ha-scaleup.yaml` stacked, **13 principals, PASS -- so R6's ruled ordering + (VIPs BEFORE the HA overlay) is now satisfied and executable.** Two RULED-BUT-NEVER-BUILT + decisions became artifacts in one pass. + **THE L3-9 COLLISION IS RESOLVED.** `overlays/dc-dc-ipv6-family-matrix.yaml` is narrowed to + `ceph-mon` ONLY; its ten duplicate `vip` + `prefer-ipv6` pairs (every value an unrendered + `{{TOKEN}}`) are gone, removed TOGETHER because removing the vips alone would have recreated + the defect deliberately. **BOTH merge orders now PASS -- the result is order-independent**, + which is the actual fix. What survives exists in no other file: ceph-mon's ULA-only + `ceph-public-network`/`ceph-cluster-network` and its privacy-extension caveat. Also removed: + the two Launchpad citations arguing octavia lb-mgmt IPv6 was an open risk -- both were read + in full and BOTH failed, and R8 CLOSED that question 2026-07-27, so they were a refuted + citation standing in a live artifact. + **GATE REPAIRS THE AGENTS CAUGHT BEFORE THE EDIT.** `phase-01`'s deploy guard would have + **ABORTED THE DEPLOY AGAIN**: its `HI` regex `(5[0-9]|60)` EXCLUDES `.61`/`.62`, so it would + have read `13/11/0` against a required `11/11/0`. Widened to `(5[0-9]|6[0-2])`, counts moved + to 13, verified `13/13/0 -> DEPLOY`. That is the second near-miss on this one guard in two + commits -- it is that brittle. `pre-flight-checks` CHECK 1 hardcoded `if(n!=3)` and would have + failed all 13; it now takes a triple OR a sextet, asserts a sextet's last three legs really + are v6, and is PROVEN able to fail (4 addresses -> MALFORMED; a v4 in a v6 slot -> NOT-IPv6). + **HARNESSES RE-POINTED, NEVER DELETED.** T16/T17 both asserted R11's gaps were OPEN; inverted + to the surviving invariant and PAIRED with new T16b/T17b that remove a VIP and demand the + check still fires. **T20/T21 had become NO-OPS that would have gone GREEN testing nothing** -- + an agent caught it; both inverted. A v4-only twin fixture keeps the pre-dual-stack cases + testing their own invariants. The renderer harness now proves reproduction on BOTH shapes: + the FROZEN v4 fixture and (new T3b) the LIVE dual-family overlay. + **EVIDENCE:** `provider-bundle-check` **33/33**, `render-dc-overlays` **18/18**, + render-baseline 10/10, preflight 16/16; gauntlet **ALL GREEN (85) on vcloud**; repo-lint + 0 fail / 611 files. **PREFLIGHT: BOTH STANDING REDS CLEARED -- `3 fatal` -> `2 fatal`**, the + remaining two being the deliberately-absent octavia-pki overlay and MAAS unreachable from + vcloud (a missing-binary artifact R10 measured clears on voffice1). + **FLAGGED FOR ONE OPERATOR CONFIRMATION, not hidden: octavia's dual-family API VIP is an + INFERENCE.** R2 ruled dual-stack and R8 ruled octavia's lb-mgmt NETWORK, but nothing + explicitly rules that octavia's own API VIP goes dual-family; a uniform re-render gave it + `prefer-ipv6: true`. That follows R2 applied evenly and matches the retired matrix file's own + reasoning, but it is not a quoted ruling. + **ALSO LOGGED NOT ACTIONED:** `ceph-mon`'s two CIDRs are gated by NOTHING (the checker skips + any app without a `vip`) and are now that overlay's only content -- a header warning was + added, but rejecting `{{...}}` in an option value is the real fix; nothing checks + vip-WITHOUT-hacluster (the inverse of invariant 8), which matters because `vault-hacluster` + exists only in the HA overlay; and `dc-dc-phase4` Step 6 still names two overlays that do not + exist on disk. - **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 ea684b1..b50ed94 100644 --- a/docs/changelog-20260728-vip-arity-gate.md +++ b/docs/changelog-20260728-vip-arity-gate.md @@ -385,3 +385,116 @@ **Still red, unchanged and expected:** `pre-flight-checks` CHECK 1 reports `OK=11 want 13` and `provider-bundle-check` reports designate -- both are R11's `.61`/`.62` VIPs being RULED-BUT-NOT-BUILT, which is commit 2's job, not this one's. + +--- + +# 2026-07-29 -- RULING 3, COMMIT 2: the dual-stack ADD (R2 + R11 built) + +Operator: "Continue as autonomously as possible. Deploy agents as needed." Two read-only +agents mapped the blast radius before any edit -- the same move that caught the deploy-gate +break in commit 1, and it caught another one here. + +## 12. Both per-DC VIP overlays are now DUAL-FAMILY, 13 apps + +**What.** `overlays/vr1-dc0-vips.yaml` and `overlays/vr1-dc1-vips.yaml` re-rendered: +each app carries `prefer-ipv6: true` and a six-address `vip` (3 v4 legs + 3 v6), and +**vault `.61` and designate `.62` are now BUILT** (R11, a D-020 amendment). v6 prefixes +come from the apex; the host part mirrors the v4 octet textually. + +**RESULT, measured both DCs:** `13 clustered VIP(s) ... (13 dual-family)` and +`12 hacluster principal(s) all carry a VIP` -> **PASS**. With `dc-ha-scaleup.yaml` +stacked on top: **13 principals, PASS** -- so **R6's ruled ordering (VIPs before the HA +overlay) is now satisfied and executable**, not just stated. + +**Two decisions that were RULED-BUT-NEVER-BUILT are now artifacts:** R11's vault/designate +VIPs, and R2's dual-stack for the API VIP layer. + +**Revert.** Re-render both overlays from the values files without `--dual-family +--include-unbuilt`, and revert the gate/harness hunks below. + +## 13. THE L3-9 COLLISION IS RESOLVED -- and the recorded description of it was half stale + +`overlays/dc-dc-ipv6-family-matrix.yaml` carried a `vip` + `prefer-ipv6` pair for ten API +charms, every value an unrendered `{{TOKEN}}`. Those duplicated what the per-DC overlays +now own, and take-last merge between them is the collision. **Narrowed to `ceph-mon` only.** + +The vip and prefer-ipv6 keys were removed **TOGETHER**. Removing the vips alone would have +recreated the exact defect on purpose -- which is why this is worth stating. + +**MEASURED, and it corrects this repo's own record:** CURRENT-STATE described L3-9 as "the +dangerous merge order is the one that PASSES". **That has not been true since 2026-07-28.** +Invariant 9 (added by the arity commit, precisely because of L3-9) makes vips-last fail on +the coupling, while matrix-last already failed on the placeholders -- so NEITHER order was +green. The gate closed the SILENT half; this commit closes the collision itself. **Both +merge orders now PASS, i.e. the result is order-independent**, which is the real fix. + +**What survives, because it exists in NO other file:** ceph-mon's `ceph-public-network` / +`ceph-cluster-network` ULA-only config and its privacy-extension caveat. ceph-mon is in +neither VIP overlay and never will be -- it has no VIP. Also removed: the two Launchpad +citations arguing octavia lb-mgmt IPv6 was an open risk. Both were read in full and BOTH +failed (one a different installer's bug, Fix Released 2021; one not an IPv6 defect at all), +and **R8 RULED the question closed on 2026-07-27** -- leaving them standing in a live +artifact was a refuted citation presented as current. + +## 14. Gate repairs the agents caught BEFORE the edit + +- **`runbooks/phase-01-bundle-deploy.md` -- the deploy guard would have ABORTED AGAIN.** + Its `HI` regex is `10\.12\.4\.(5[0-9]|60)`, which **excludes .61 and .62**, so the counts + would have read `13/11/0` against a required `11/11/0` and `juju deploy` would not have + run. Widened to `(5[0-9]|6[0-2])` and the expected counts moved to `13/13/0`. Verified end + to end: **13/13/0 -> DEPLOY**. This is the second time this one guard has nearly blocked + the deploy across two commits; it is worth knowing it is that brittle. +- **`scripts/pre-flight-checks.sh` CHECK 1** hardcoded `if(n!=3)` and would have printed + `MALFORMED(not 3 IPs)` for all 13 and failed `OK=0 bad=13`. Now accepts a v4 triple OR an + R2 sextet, and for a sextet asserts the last three legs are actually v6 literals. Deep v6 + prefix/mirror checking is deliberately NOT duplicated here -- `provider-bundle-check` does + it against the apex. **Proven able to fail:** a 4-address vip -> `MALFORMED(not 3 or 6)`, + a v4 in a v6 slot -> `NOT-IPv6 in v6 leg 1`, and a v4 triple still passes. + +## 15. Harness: 31 -> 33, and 18 for the renderer, with everything RE-POINTED not deleted + +- **T16 and T17 both asserted R11's gaps were OPEN.** R11 is now built, so both were + inverted to the surviving invariant (the deploy input is CLEAN; R6's ordering is + satisfied at 13 principals) and **paired with NEW T16b/T17b that remove a VIP and demand + the check still fires** -- otherwise inverting them would have removed the teeth. +- **T20/T21, the coupling cases, had become NO-OPS** (the overlay already sets what they + were setting) -- they would have gone GREEN while testing nothing, which an agent caught. + Inverted: T20 now truncates the vip while keeping `prefer-ipv6`, T21 drops `prefer-ipv6` + while keeping six legs. +- A **v4-only twin fixture** was added so the pre-dual-stack cases (T5/T8/T26/T28) still + exercise band, octet-uniqueness and apex-independence instead of tripping the coupling + check first and never reaching their own assertion. +- The renderer harness now proves reproduction on **BOTH** shapes: T3 against the FROZEN v4 + fixture (immutable, a stable target) and **new T3b against the LIVE dual-family overlay** + (whose own header mandates it stay renderer-reproducible). + +**Evidence.** `provider-bundle-check` **33/33**, `render-dc-overlays` **18/18**, +`render-baseline` 10/10, `preflight` 16/16. Gauntlet **ALL GREEN (85) on vcloud**; +repo-lint 0 fail / 611 files. + +**PREFLIGHT: BOTH STANDING REDS CLEARED.** It was `3 fatal` with P2 and CHECK 1 among them; +it is now **`2 fatal`** -- the deliberately-absent octavia-pki overlay and MAAS unreachable +from vcloud (a missing-binary artifact that R10 measured clears on voffice1). Nothing this +work introduced is red. + +--- + +## Findings LOGGED, NOT ACTIONED + +12. **`ceph-mon`'s two CIDRs are gated by NOTHING.** `provider-bundle-check` skips any app + without a `vip`, so `{{DC_STORAGE_ULA_CIDR}}` / `{{DC_REPLICATION_ULA_CIDR}}` are + validated by no gate at all -- and they are now the ONLY content of that overlay. A + prominent header warning was added; rejecting `{{...}}` in any option value would be the + real fix and is a gate change out of this commit's scope. +13. **octavia's dual-family API VIP is an INFERENCE, flagged not hidden.** R2 ruled + dual-stack and R8 ruled octavia's lb-mgmt network, but nothing explicitly rules that + octavia's own API VIP goes dual-family. It is in both VIP overlays, so a uniform + re-render gave it `prefer-ipv6: true`. That follows R2 applied evenly and matches the + old matrix file's own reasoning ("no different from the other charms"), but it is not a + quoted ruling -- **worth one operator confirmation.** +14. **Nothing checks vip-WITHOUT-hacluster.** Invariant 8 catches a clustered principal with + no VIP; the inverse is unguarded. Relevant because `vault-hacluster` exists only in + `dc-ha-scaleup.yaml`, so a base+dc0-vips deploy gives vault a VIP and no HA relation. +15. `runbooks/dc-dc-phase4-juju-bundle-per-dc.md` Step 6 still instructs passing the matrix + overlay alongside **two overlays that do not exist on disk** and no VIP overlay. Wrong + before this change; now wrong differently. diff --git a/overlays/dc-dc-ipv6-family-matrix.yaml b/overlays/dc-dc-ipv6-family-matrix.yaml index 3c3f79e..6d39e06 100644 --- a/overlays/dc-dc-ipv6-family-matrix.yaml +++ b/overlays/dc-dc-ipv6-family-matrix.yaml @@ -1,157 +1,48 @@ # overlays/dc-dc-ipv6-family-matrix.yaml # -# D-101 IPv6 family-matrix overlay (tooling gap register item #13). -# Researched 2026-07-09/10 against real charm source/config (not memory -- -# see docs/dc-dc-ipv6-charm-research.md for full sourcing, per-charm -# confidence level, and the one significant open risk this overlay does -# NOT attempt to close). Applied ALONGSIDE bundle.yaml's other overlays, -# e.g.: -# juju deploy ./bundle.yaml \ -# --overlay overlays/octavia-pki.yaml \ -# --overlay "overlays/${DC}-hostnames.yaml" \ -# --overlay overlays/dc-dc-ipv6-family-matrix.yaml \ -# -m "${DC_MODEL}" +# ==== SCOPE NARROWED 2026-07-29 -- THIS FILE IS NOW ceph-mon ONLY ==== # -# NOT YET APPLIED to any live model -- authored prep-only, no live cloud -# reachable this session. Every VIP/CIDR value below is an explicit -# {{TOKEN}} placeholder (this repo's established convention, matching -# opentofu/templates/opnsense-config.xml.tmpl) -- render/substitute with -# real, measured per-DC values before use; do not fill in a guessed -# address. +# It used to carry a `vip` + `prefer-ipv6` pair for ten API charms, every value an +# unrendered {{TOKEN}}. Those are SUPERSEDED: ruling 3 (2026-07-25) plus R2's +# dual-stack ruling put every API VIP in the per-DC overlays, rendered with measured +# values -- `overlays/vr1-dc0-vips.yaml` and `overlays/vr1-dc1-vips.yaml`, generated by +# `scripts/render-dc-overlays.py`, each carrying `prefer-ipv6: true` alongside its own +# six-address vip. # -# CONFIRMED mechanism, sourced 2026-07-09/10: -# - charms.openstack-family API charms (confirmed directly for -# charm-keystone's config.yaml, and for charm-nova-cloud-controller's -# actual "Dual Stack VIPs" commit) expose a `prefer-ipv6` boolean. When -# True, HAProxy binds BOTH `*:port` (v4, wildcard) AND `:::port` (v6, -# wildcard) SIMULTANEOUSLY -- this is genuinely ADDITIVE dual-stack, not -# an either/or switch, confirmed by reading the actual haproxy.cfg -# Jinja template diff in that commit. The existing `vip:` option (already -# a space-separated multi-network list in this bundle, per D-020's triple- -# VIP pattern) takes the ADDITIONAL v6/GUA VIP literals alongside the -# existing v4 ones once `prefer-ipv6: true` is set. -# - charm-ceph-mon exposes `prefer-ipv6` (bool) + `ceph-public-network` / -# `ceph-cluster-network` (space-delimited CIDR list). Per that charm's own -# docs, `prefer-ipv6=False` (default) means IPv4 is expected -- this -# reads as a straight EITHER/OR switch for ceph-mon specifically (unlike -# the additive HAProxy behavior above), which is actually the CORRECT -# shape for storage/replication: D-101 wants those ULA-ONLY, not -# dual-stack, so a clean switch to v6-only is exactly right here. -# - OVN (ovn-central/ovn-chassis, charm-layer-ovn base) has NO explicit -# IPv6/encapsulation config option at all (confirmed: read the full -# config.yaml, no such key exists). geneve-over-v6 for the data-tenant -# plane is therefore NOT a charm-config concern -- it is governed purely -# by which IP family the unit's bound interface on that plane actually -# has. Since data-tenant is ULA-only (no v4 present) per D-101, the -# geneve tunnel endpoint address will naturally be the unit's v6 address -# with NO overlay entry needed for OVN at all. -# - charm-vault's cert-issuance code (`vault_pki.py`'s `sort_sans()`) -# confirmed to have NO IPv4/IPv6 distinction when building IP SANs -- -# D-109's "Vault issues v4+IPv6-SAN certs" requirement is technically -# supported by the charm's own code, not just assumed. +# THIS RESOLVES THE "L3-9 COLLISION": two overlays both setting `options.vip` for the +# same app, merged take-last. Measured, BOTH orders now fail -- matrix-last dies on the +# unrendered placeholders, vips-last dies on provider-bundle-check invariant 9 (which +# exists because of L3-9: `prefer-ipv6` survives as its own key while the v6 legs are +# clobbered). The gate closed the SILENT half of this on 2026-07-28; deleting the +# duplicate keys closes the collision itself. The vip and prefer-ipv6 keys were removed +# TOGETHER and must never be separated -- removing the vips alone would have recreated +# the exact defect deliberately. # -# NOT independently confirmed per-charm (inferred by shared-framework -# pattern only): glance, neutron-api, placement, cinder, barbican, magnum, -# openstack-dashboard, ceph-radosgw all share the same charms.openstack -# base classes as keystone/nova-cloud-controller, so `prefer-ipv6` and the -# additive dual-stack HAProxy behavior is EXPECTED to apply identically -- -# but only keystone's config.yaml and nova-cloud-controller's actual code -# were read directly. Confirm each charm's real `prefer-ipv6` presence via -# `juju config ` once deployed, before assuming this overlay is -# complete for all of them. +# WHAT SURVIVES HERE, because it exists in NO other file: ceph-mon's ULA-only storage +# and replication networks. ceph-mon appears in neither VIP overlay and never will -- +# it is not an HAProxy/pacemaker app and has no vip. Its `prefer-ipv6` is an EITHER/OR +# switch (v6-only), not the additive HAProxy dual-bind the API charms use, which is +# exactly what D-101 wants for these two planes. # -# ONE SIGNIFICANT OPEN RISK, deliberately NOT addressed by this overlay: -# Octavia's lb-mgmt-net (the amphora management network D-101 wants -# ULA-only) is a NEUTRON tenant network the octavia charm itself creates -# (`create-mgmt-network` option), not a Juju-space-bound interface like -# ceph-mon's -- a fundamentally different mechanism. Real, still-open -# upstream bug reports exist describing IPv6 lb-mgmt-net failures -# (Launchpad #1911788 "IPv6 mgmt network not working, octavia can't talk -# to amphora"; #1913409 "octavia_amp_network does not support IPv6" against -# kolla-ansible, the same underlying Octavia limitation). #1911788's root -# cause traces to an OVN/LXD/MAAS hostname-resolution mismatch (marked -# duplicate of #1896630, "managing /etc/hosts for containers") -- the SAME -# CLASS of problem this repo's own D-008 bootstrap order (static /etc/hosts -# -> os-public-hostname -> vault certs -> Designate) already exists to -# harden against, so it is NOT necessarily fatal here, but it is a REAL, -# documented, open upstream risk, not a solved problem. This overlay -# deliberately does NOT attempt an Octavia lb-mgmt-net IPv6 config change -- -# that needs its own decision (present to the operator: accept the risk and -# test it for real once DC1 exists, or keep Octavia's lb-mgmt-net dual-stack/ -# v4 as a deliberate, logged D-101 exception) rather than being silently -# forced through in an overlay file. +# The full per-charm sourcing (charm config.yaml reads, the nova-cloud-controller +# "Dual Stack VIPs" commit, the OVN negative result, vault's sort_sans()) is in +# `docs/archive/dc-dc-ipv6-charm-research.md` -- that memo, not this header, is the +# research record, and it is unaffected by this narrowing. +# +# OCTAVIA: the lb-mgmt-net question this file used to raise as an open risk is CLOSED. +# R8 RULED 2026-07-27 that Octavia creates and owns its own IPv6 network, and the two +# Launchpad citations that argued otherwise were read in full and BOTH failed -- +# #1913409 is a different installer's bug, Fix Released 2021; #1911788 is not an IPv6 +# defect and its actual cause is fixed in the OVS version these nodes install. Octavia's +# own API VIP is dual-family in the per-DC overlays like every other API charm. +# +# ==== STILL NOT RENDERED -- {{TOKEN}} placeholders below ==== +# The two CIDRs are placeholders and NOTHING GATES THEM: provider-bundle-check skips any +# application without a `vip`, so ceph-mon is outside every current check. Substitute +# real measured per-DC values before this file goes anywhere near `juju deploy`, and do +# not assume a green gate says anything about it. (Logged as a gap, not fixed here.) applications: - # ---- Dual-stack API charms (metal-internal ULA leg + provider-public - # GUA leg, per D-101's family matrix) ---- - keystone: - options: - prefer-ipv6: true - # Existing vip (D-020 triple: provider/metal-admin/metal-internal v4) - # gets the v6/GUA legs appended -- do NOT replace the v4 entries, - # append only. Render with real per-DC measured addresses. - vip: "{{KEYSTONE_VIP_PROVIDER_V4}} {{KEYSTONE_VIP_METAL_ADMIN_V4}} {{KEYSTONE_VIP_METAL_INTERNAL_V4}} {{KEYSTONE_VIP_PROVIDER_GUA}} {{KEYSTONE_VIP_METAL_ADMIN_ULA}} {{KEYSTONE_VIP_METAL_INTERNAL_ULA}}" - - glance: - options: - prefer-ipv6: true - vip: "{{GLANCE_VIP_PROVIDER_V4}} {{GLANCE_VIP_METAL_ADMIN_V4}} {{GLANCE_VIP_METAL_INTERNAL_V4}} {{GLANCE_VIP_PROVIDER_GUA}} {{GLANCE_VIP_METAL_ADMIN_ULA}} {{GLANCE_VIP_METAL_INTERNAL_ULA}}" - - neutron-api: - options: - prefer-ipv6: true - vip: "{{NEUTRON_VIP_PROVIDER_V4}} {{NEUTRON_VIP_METAL_ADMIN_V4}} {{NEUTRON_VIP_METAL_INTERNAL_V4}} {{NEUTRON_VIP_PROVIDER_GUA}} {{NEUTRON_VIP_METAL_ADMIN_ULA}} {{NEUTRON_VIP_METAL_INTERNAL_ULA}}" - - nova-cloud-controller: - options: - prefer-ipv6: true # CONFIRMED: this charm's actual "Dual Stack VIPs" commit is the direct source for this whole overlay's design - vip: "{{NCC_VIP_PROVIDER_V4}} {{NCC_VIP_METAL_ADMIN_V4}} {{NCC_VIP_METAL_INTERNAL_V4}} {{NCC_VIP_PROVIDER_GUA}} {{NCC_VIP_METAL_ADMIN_ULA}} {{NCC_VIP_METAL_INTERNAL_ULA}}" - - placement: - options: - prefer-ipv6: true - vip: "{{PLACEMENT_VIP_PROVIDER_V4}} {{PLACEMENT_VIP_METAL_ADMIN_V4}} {{PLACEMENT_VIP_METAL_INTERNAL_V4}} {{PLACEMENT_VIP_PROVIDER_GUA}} {{PLACEMENT_VIP_METAL_ADMIN_ULA}} {{PLACEMENT_VIP_METAL_INTERNAL_ULA}}" - - cinder: - options: - prefer-ipv6: true - vip: "{{CINDER_VIP_PROVIDER_V4}} {{CINDER_VIP_METAL_ADMIN_V4}} {{CINDER_VIP_METAL_INTERNAL_V4}} {{CINDER_VIP_PROVIDER_GUA}} {{CINDER_VIP_METAL_ADMIN_ULA}} {{CINDER_VIP_METAL_INTERNAL_ULA}}" - - barbican: - options: - prefer-ipv6: true - vip: "{{BARBICAN_VIP_PROVIDER_V4}} {{BARBICAN_VIP_METAL_ADMIN_V4}} {{BARBICAN_VIP_METAL_INTERNAL_V4}} {{BARBICAN_VIP_PROVIDER_GUA}} {{BARBICAN_VIP_METAL_ADMIN_ULA}} {{BARBICAN_VIP_METAL_INTERNAL_ULA}}" - - magnum: - options: - prefer-ipv6: true - vip: "{{MAGNUM_VIP_PROVIDER_V4}} {{MAGNUM_VIP_METAL_ADMIN_V4}} {{MAGNUM_VIP_METAL_INTERNAL_V4}} {{MAGNUM_VIP_PROVIDER_GUA}} {{MAGNUM_VIP_METAL_ADMIN_ULA}} {{MAGNUM_VIP_METAL_INTERNAL_ULA}}" - - openstack-dashboard: - options: - prefer-ipv6: true - vip: "{{DASHBOARD_VIP_PROVIDER_V4}} {{DASHBOARD_VIP_METAL_ADMIN_V4}} {{DASHBOARD_VIP_METAL_INTERNAL_V4}} {{DASHBOARD_VIP_PROVIDER_GUA}} {{DASHBOARD_VIP_METAL_ADMIN_ULA}} {{DASHBOARD_VIP_METAL_INTERNAL_ULA}}" - - ceph-radosgw: - options: - prefer-ipv6: true - vip: "{{RADOSGW_VIP_PROVIDER_V4}} {{RADOSGW_VIP_METAL_ADMIN_V4}} {{RADOSGW_VIP_METAL_INTERNAL_V4}} {{RADOSGW_VIP_PROVIDER_GUA}} {{RADOSGW_VIP_METAL_ADMIN_ULA}} {{RADOSGW_VIP_METAL_INTERNAL_ULA}}" - - # octavia: deliberately NOT included here -- see "ONE SIGNIFICANT OPEN - # RISK" above. Its VIP (metal-admin/metal-internal/provider-public, - # matching the pattern above) COULD take the same prefer-ipv6 + appended- - # VIP treatment for its own API endpoint (that part is no different from - # the other charms above) -- what's genuinely unresolved is its SEPARATE - # lb-mgmt-net (amphora management) IPv6 support, a different mechanism - # entirely. If the operator wants Octavia's API endpoint dual-stacked - # while leaving lb-mgmt-net v4-only as a deliberate exception, add: - # octavia: - # options: - # prefer-ipv6: true - # vip: "{{OCTAVIA_VIP_PROVIDER_V4}} {{OCTAVIA_VIP_METAL_ADMIN_V4}} {{OCTAVIA_VIP_METAL_INTERNAL_V4}} {{OCTAVIA_VIP_PROVIDER_GUA}} {{OCTAVIA_VIP_METAL_ADMIN_ULA}} {{OCTAVIA_VIP_METAL_INTERNAL_ULA}}" - # -- but decide this explicitly, don't silently activate it via this file. - - # ---- ULA-only planes (storage, replication) -- Ceph-over-v6 ---- ceph-mon: options: prefer-ipv6: true # CONFIRMED real option, charm-ceph-mon config.yaml @@ -167,3 +58,4 @@ # charm-config toggle exists for ovn-central/ovn-chassis; the plane being # ULA-only (no v4 present, per D-101) is sufficient on its own -- geneve # encapsulation follows whatever address family the bound interface has. + # A deliberate absence is only legible if it is written down. diff --git a/overlays/vr1-dc0-vips.yaml b/overlays/vr1-dc0-vips.yaml index 7d54361..44b4ce0 100644 --- a/overlays/vr1-dc0-vips.yaml +++ b/overlays/vr1-dc0-vips.yaml @@ -1,51 +1,76 @@ # overlays/vr1-dc0-vips.yaml -# Per-DC VIP overlay for vr1-dc0. RULING 3 of 2026-07-25 made bundle.yaml -# VIP-FREE (topology only) and moved every per-DC value into an overlay, dc0 -# INCLUDED -- so this file now carries what used to sit inline in the base -# bundle, and the base is no longer any DC's VIP source of truth: +# Per-DC VIP overlay for vr1-dc0. GENERATED by scripts/render-dc-overlays.py from +# render/values/vr1-dc0-vips.yaml -- edit the values file and re-render; do not +# hand-edit this file. # juju deploy ./bundle.yaml --overlay ./overlays/vr1-dc0-vips.yaml # -# Mapping (MEASURED from scripts/lib-net.sh lib_net_select_dc vr1-dc0, PLANE_NAME): -# provider-public 10.12.4 metal-admin 10.12.8 metal-internal 10.12.12 -# (host octets .50-.60 UNCHANGED by the move -- this commit is a NEUTRAL -# extraction, proven by provider-bundle-check --overlay --dc vr1-dc0.) -# The three VIPs per service are public / admin / internal API endpoints. -# GENERATED by scripts/render-dc-overlays.py from render/values/vr1-dc0-vips.yaml; -# edit the values file and re-render rather than editing this file by hand. -# The B1 / B5 tokens in the per-app comments below are DEFINED in bundle.yaml's -# own header block (B5 at its "Endpoints:" line, B1 at its "HA chain:" line) -- -# they stayed there when the values moved here, so the references still resolve. +# RULING 3 (2026-07-25) made bundle.yaml VIP-FREE, so this file is the ONLY source of +# vr1-dc0's VIPs and the bundle is not deployable without it. +# +# DUAL-FAMILY per R2 (RULED 2026-07-27): each vip carries the three v4 legs THEN the +# three v6 legs, and `prefer-ipv6: true` travels WITH them -- the two must never be +# separated (a merge that keeps prefer-ipv6 while dropping the v6 legs leaves HAProxy +# bound on :::port with no v6 VIP for pacemaker to manage; provider-bundle-check +# invariant 9 now rejects that pair). Column order is +# provider-public / metal-admin / metal-internal, v4 then v6. +# v4 prefixes MEASURED from scripts/lib-net.sh lib_net_select_dc vr1-dc0 +# v6 prefixes READ FROM THE NETBOX APEX (D-136 option (D)); the provider leg uses +# the DEDICATED GUA VIP /64, admin+internal their own plane /64s +# v6 host part MIRRORS the v4 octet textually (RULED 2026-07-27) -- .50 -> ::50 +# +# vault .61 and designate .62 are R11 (a D-020 AMENDMENT, 2026-07-27). Per R6 these +# land BEFORE overlays/dc-ha-scaleup.yaml, which is what gives vault a real HA target. +# The B1 / B5 tokens below are DEFINED in bundle.yaml's own header block. applications: keystone: options: - vip: "10.12.4.50 10.12.8.50 10.12.12.50" # B1 front-loaded VIP; IS the catalog endpoint (B5, no os-public-hostname) + prefer-ipv6: true + vip: "10.12.4.50 10.12.8.50 10.12.12.50 2602:f3e2:f02:11::50 fd50:840e:74e2:220::50 fd50:840e:74e2:221::50" # B1 front-loaded VIP; IS the catalog endpoint (B5, no os-public-hostname) barbican: options: - vip: "10.12.4.51 10.12.8.51 10.12.12.51" # B1 + prefer-ipv6: true + vip: "10.12.4.51 10.12.8.51 10.12.12.51 2602:f3e2:f02:11::51 fd50:840e:74e2:220::51 fd50:840e:74e2:221::51" # B1 cinder: options: - vip: "10.12.4.52 10.12.8.52 10.12.12.52" # B1 + prefer-ipv6: true + vip: "10.12.4.52 10.12.8.52 10.12.12.52 2602:f3e2:f02:11::52 fd50:840e:74e2:220::52 fd50:840e:74e2:221::52" # B1 glance: options: - vip: "10.12.4.53 10.12.8.53 10.12.12.53" # B1 + prefer-ipv6: true + vip: "10.12.4.53 10.12.8.53 10.12.12.53 2602:f3e2:f02:11::53 fd50:840e:74e2:220::53 fd50:840e:74e2:221::53" # B1 magnum: options: - vip: "10.12.4.54 10.12.8.54 10.12.12.54" # B1 + prefer-ipv6: true + vip: "10.12.4.54 10.12.8.54 10.12.12.54 2602:f3e2:f02:11::54 fd50:840e:74e2:220::54 fd50:840e:74e2:221::54" # B1 neutron-api: options: - vip: "10.12.4.55 10.12.8.55 10.12.12.55" # B1 + prefer-ipv6: true + vip: "10.12.4.55 10.12.8.55 10.12.12.55 2602:f3e2:f02:11::55 fd50:840e:74e2:220::55 fd50:840e:74e2:221::55" # B1 nova-cloud-controller: options: - vip: "10.12.4.56 10.12.8.56 10.12.12.56" # B1 + prefer-ipv6: true + vip: "10.12.4.56 10.12.8.56 10.12.12.56 2602:f3e2:f02:11::56 fd50:840e:74e2:220::56 fd50:840e:74e2:221::56" # B1 octavia: options: - vip: "10.12.4.57 10.12.8.57 10.12.12.57" # B1 + prefer-ipv6: true + vip: "10.12.4.57 10.12.8.57 10.12.12.57 2602:f3e2:f02:11::57 fd50:840e:74e2:220::57 fd50:840e:74e2:221::57" # B1 openstack-dashboard: options: - vip: "10.12.4.58 10.12.8.58 10.12.12.58" # B1 -- browse HTTPS by IP (B5); ALLOWED_HOSTS must permit the VIP IP (verify at deploy) + prefer-ipv6: true + vip: "10.12.4.58 10.12.8.58 10.12.12.58 2602:f3e2:f02:11::58 fd50:840e:74e2:220::58 fd50:840e:74e2:221::58" # B1 -- browse HTTPS by IP (B5); ALLOWED_HOSTS must permit the VIP IP (verify at deploy) placement: options: - vip: "10.12.4.59 10.12.8.59 10.12.12.59" # B1 + prefer-ipv6: true + vip: "10.12.4.59 10.12.8.59 10.12.12.59 2602:f3e2:f02:11::59 fd50:840e:74e2:220::59 fd50:840e:74e2:221::59" # B1 ceph-radosgw: options: - vip: "10.12.4.60 10.12.8.60 10.12.12.60" # B1 -- radosgw HA un-deferred for Roosevelt fidelity (decorative HA on testcloud) + prefer-ipv6: true + vip: "10.12.4.60 10.12.8.60 10.12.12.60 2602:f3e2:f02:11::60 fd50:840e:74e2:220::60 fd50:840e:74e2:221::60" # B1 -- radosgw HA un-deferred for Roosevelt fidelity (decorative HA on testcloud) + vault: + options: + prefer-ipv6: true + vip: "10.12.4.61 10.12.8.61 10.12.12.61 2602:f3e2:f02:11::61 fd50:840e:74e2:220::61 fd50:840e:74e2:221::61" # R11 (D-020 amendment) -- was ruled in D-020's own enumeration and never built + designate: + options: + prefer-ipv6: true + vip: "10.12.4.62 10.12.8.62 10.12.12.62 2602:f3e2:f02:11::62 fd50:840e:74e2:220::62 fd50:840e:74e2:221::62" # R11 (D-020 amendment) -- NEW to D-020's enumeration; dnsaas is already provider-bound diff --git a/overlays/vr1-dc1-vips.yaml b/overlays/vr1-dc1-vips.yaml index 1b226ae..1477c51 100644 --- a/overlays/vr1-dc1-vips.yaml +++ b/overlays/vr1-dc1-vips.yaml @@ -1,46 +1,76 @@ # overlays/vr1-dc1-vips.yaml -# Per-DC VIP overlay for vr1-dc1 (the runbook's "DC2"). D-135 amendment / -# phase-4 Step 4: bundle.yaml hardcodes the API VIPs in vr1-dc0's bands, so -# deploying vr1-dc1 re-homes them into ITS bands. Base bundle stays the vr1-dc0 -# source of truth; this overlay is applied only for the dc1 deploy: +# Per-DC VIP overlay for vr1-dc1. GENERATED by scripts/render-dc-overlays.py from +# render/values/vr1-dc1-vips.yaml -- edit the values file and re-render; do not +# hand-edit this file. # juju deploy ./bundle.yaml --overlay ./overlays/vr1-dc1-vips.yaml # -# Mapping (MEASURED from scripts/lib-net.sh lib_net_select_dc vr1-dc1, PLANE_NAME): -# provider-public 10.12.4 -> 10.12.64 metal-admin 10.12.8 -> 10.12.68 -# metal-internal 10.12.12 -> 10.12.72 (host octets .50-.60 UNCHANGED) -# The three VIPs per service are public / admin / internal API endpoints -# (D-101 inherits the family layout; only the network prefix moves per DC). +# RULING 3 (2026-07-25) made bundle.yaml VIP-FREE, so this file is the ONLY source of +# vr1-dc1's VIPs and the bundle is not deployable without it. +# +# DUAL-FAMILY per R2 (RULED 2026-07-27): each vip carries the three v4 legs THEN the +# three v6 legs, and `prefer-ipv6: true` travels WITH them -- the two must never be +# separated (a merge that keeps prefer-ipv6 while dropping the v6 legs leaves HAProxy +# bound on :::port with no v6 VIP for pacemaker to manage; provider-bundle-check +# invariant 9 now rejects that pair). Column order is +# provider-public / metal-admin / metal-internal, v4 then v6. +# v4 prefixes MEASURED from scripts/lib-net.sh lib_net_select_dc vr1-dc1 +# v6 prefixes READ FROM THE NETBOX APEX (D-136 option (D)); the provider leg uses +# the DEDICATED GUA VIP /64, admin+internal their own plane /64s +# v6 host part MIRRORS the v4 octet textually (RULED 2026-07-27) -- .50 -> ::50 +# +# vault .61 and designate .62 are R11 (a D-020 AMENDMENT, 2026-07-27). Per R6 these +# land BEFORE overlays/dc-ha-scaleup.yaml, which is what gives vault a real HA target. +# The B1 / B5 tokens below are DEFINED in bundle.yaml's own header block. applications: keystone: options: - vip: "10.12.64.50 10.12.68.50 10.12.72.50" + prefer-ipv6: true + vip: "10.12.64.50 10.12.68.50 10.12.72.50 2602:f3e2:f03:11::50 fd50:840e:74e2:320::50 fd50:840e:74e2:321::50" barbican: options: - vip: "10.12.64.51 10.12.68.51 10.12.72.51" + prefer-ipv6: true + vip: "10.12.64.51 10.12.68.51 10.12.72.51 2602:f3e2:f03:11::51 fd50:840e:74e2:320::51 fd50:840e:74e2:321::51" cinder: options: - vip: "10.12.64.52 10.12.68.52 10.12.72.52" + prefer-ipv6: true + vip: "10.12.64.52 10.12.68.52 10.12.72.52 2602:f3e2:f03:11::52 fd50:840e:74e2:320::52 fd50:840e:74e2:321::52" glance: options: - vip: "10.12.64.53 10.12.68.53 10.12.72.53" + prefer-ipv6: true + vip: "10.12.64.53 10.12.68.53 10.12.72.53 2602:f3e2:f03:11::53 fd50:840e:74e2:320::53 fd50:840e:74e2:321::53" magnum: options: - vip: "10.12.64.54 10.12.68.54 10.12.72.54" + prefer-ipv6: true + vip: "10.12.64.54 10.12.68.54 10.12.72.54 2602:f3e2:f03:11::54 fd50:840e:74e2:320::54 fd50:840e:74e2:321::54" neutron-api: options: - vip: "10.12.64.55 10.12.68.55 10.12.72.55" + prefer-ipv6: true + vip: "10.12.64.55 10.12.68.55 10.12.72.55 2602:f3e2:f03:11::55 fd50:840e:74e2:320::55 fd50:840e:74e2:321::55" nova-cloud-controller: options: - vip: "10.12.64.56 10.12.68.56 10.12.72.56" + prefer-ipv6: true + vip: "10.12.64.56 10.12.68.56 10.12.72.56 2602:f3e2:f03:11::56 fd50:840e:74e2:320::56 fd50:840e:74e2:321::56" octavia: options: - vip: "10.12.64.57 10.12.68.57 10.12.72.57" + prefer-ipv6: true + vip: "10.12.64.57 10.12.68.57 10.12.72.57 2602:f3e2:f03:11::57 fd50:840e:74e2:320::57 fd50:840e:74e2:321::57" openstack-dashboard: options: - vip: "10.12.64.58 10.12.68.58 10.12.72.58" + prefer-ipv6: true + vip: "10.12.64.58 10.12.68.58 10.12.72.58 2602:f3e2:f03:11::58 fd50:840e:74e2:320::58 fd50:840e:74e2:321::58" placement: options: - vip: "10.12.64.59 10.12.68.59 10.12.72.59" + prefer-ipv6: true + vip: "10.12.64.59 10.12.68.59 10.12.72.59 2602:f3e2:f03:11::59 fd50:840e:74e2:320::59 fd50:840e:74e2:321::59" ceph-radosgw: options: - vip: "10.12.64.60 10.12.68.60 10.12.72.60" + prefer-ipv6: true + vip: "10.12.64.60 10.12.68.60 10.12.72.60 2602:f3e2:f03:11::60 fd50:840e:74e2:320::60 fd50:840e:74e2:321::60" + vault: + options: + prefer-ipv6: true + vip: "10.12.64.61 10.12.68.61 10.12.72.61 2602:f3e2:f03:11::61 fd50:840e:74e2:320::61 fd50:840e:74e2:321::61" + designate: + options: + prefer-ipv6: true + vip: "10.12.64.62 10.12.68.62 10.12.72.62 2602:f3e2:f03:11::62 fd50:840e:74e2:320::62 fd50:840e:74e2:321::62" diff --git a/render/values/vr1-dc0-vips.yaml b/render/values/vr1-dc0-vips.yaml index c1797db..f999eed 100644 --- a/render/values/vr1-dc0-vips.yaml +++ b/render/values/vr1-dc0-vips.yaml @@ -1,6 +1,6 @@ dc: vr1-dc0 output: overlays/vr1-dc0-vips.yaml -family: v4 +family: dual legs: - provider-public - metal-admin @@ -10,6 +10,11 @@ metal-admin: 10.12.8 metal-internal: 10.12.12 source_v4: scripts/lib-net.sh lib_net_select_dc vr1-dc0 +prefixes_v6: + provider-public: 2602:f3e2:f02:11 + metal-admin: fd50:840e:74e2:220 + metal-internal: fd50:840e:74e2:221 +apex_record: netbox/draft/vr1-office1-current-20260725.json apps: - name: keystone octet: 50 @@ -44,38 +49,56 @@ - name: ceph-radosgw octet: 60 comment: '# B1 -- radosgw HA un-deferred for Roosevelt fidelity (decorative HA on testcloud)' +- name: vault + octet: 61 + comment: '# R11 (D-020 amendment) -- was ruled in D-020''s own enumeration and never built' +- name: designate + octet: 62 + comment: '# R11 (D-020 amendment) -- NEW to D-020''s enumeration; dnsaas is already provider-bound' header: '# overlays/vr1-dc0-vips.yaml - # Per-DC VIP overlay for vr1-dc0. RULING 3 of 2026-07-25 made bundle.yaml + # Per-DC VIP overlay for vr1-dc0. GENERATED by scripts/render-dc-overlays.py from - # VIP-FREE (topology only) and moved every per-DC value into an overlay, dc0 + # render/values/vr1-dc0-vips.yaml -- edit the values file and re-render; do not - # INCLUDED -- so this file now carries what used to sit inline in the base - - # bundle, and the base is no longer any DC''s VIP source of truth: + # hand-edit this file. # juju deploy ./bundle.yaml --overlay ./overlays/vr1-dc0-vips.yaml # - # Mapping (MEASURED from scripts/lib-net.sh lib_net_select_dc vr1-dc0, PLANE_NAME): + # RULING 3 (2026-07-25) made bundle.yaml VIP-FREE, so this file is the ONLY source of - # provider-public 10.12.4 metal-admin 10.12.8 metal-internal 10.12.12 + # vr1-dc0''s VIPs and the bundle is not deployable without it. - # (host octets .50-.60 UNCHANGED by the move -- this commit is a NEUTRAL + # - # extraction, proven by provider-bundle-check --overlay --dc vr1-dc0.) + # DUAL-FAMILY per R2 (RULED 2026-07-27): each vip carries the three v4 legs THEN the - # The three VIPs per service are public / admin / internal API endpoints. + # three v6 legs, and `prefer-ipv6: true` travels WITH them -- the two must never be - # GENERATED by scripts/render-dc-overlays.py from render/values/vr1-dc0-vips.yaml; + # separated (a merge that keeps prefer-ipv6 while dropping the v6 legs leaves HAProxy - # edit the values file and re-render rather than editing this file by hand. + # bound on :::port with no v6 VIP for pacemaker to manage; provider-bundle-check - # The B1 / B5 tokens in the per-app comments below are DEFINED in bundle.yaml''s + # invariant 9 now rejects that pair). Column order is - # own header block (B5 at its "Endpoints:" line, B1 at its "HA chain:" line) -- + # provider-public / metal-admin / metal-internal, v4 then v6. - # they stayed there when the values moved here, so the references still resolve. + # v4 prefixes MEASURED from scripts/lib-net.sh lib_net_select_dc vr1-dc0 + + # v6 prefixes READ FROM THE NETBOX APEX (D-136 option (D)); the provider leg uses + + # the DEDICATED GUA VIP /64, admin+internal their own plane /64s + + # v6 host part MIRRORS the v4 octet textually (RULED 2026-07-27) -- .50 -> ::50 + + # + + # vault .61 and designate .62 are R11 (a D-020 AMENDMENT, 2026-07-27). Per R6 these + + # land BEFORE overlays/dc-ha-scaleup.yaml, which is what gives vault a real HA target. + + # The B1 / B5 tokens below are DEFINED in bundle.yaml''s own header block. ' diff --git a/render/values/vr1-dc1-vips.yaml b/render/values/vr1-dc1-vips.yaml index f3234d6..2a4e257 100644 --- a/render/values/vr1-dc1-vips.yaml +++ b/render/values/vr1-dc1-vips.yaml @@ -1,6 +1,6 @@ dc: vr1-dc1 output: overlays/vr1-dc1-vips.yaml -family: v4 +family: dual legs: - provider-public - metal-admin @@ -9,6 +9,12 @@ provider-public: 10.12.64 metal-admin: 10.12.68 metal-internal: 10.12.72 +source_v4: scripts/lib-net.sh lib_net_select_dc vr1-dc1 +prefixes_v6: + provider-public: 2602:f3e2:f03:11 + metal-admin: fd50:840e:74e2:320 + metal-internal: fd50:840e:74e2:321 +apex_record: netbox/draft/vr1-office1-current-20260725.json apps: - name: keystone octet: 50 @@ -32,28 +38,54 @@ octet: 59 - name: ceph-radosgw octet: 60 +- name: vault + octet: 61 +- name: designate + octet: 62 header: '# overlays/vr1-dc1-vips.yaml - # Per-DC VIP overlay for vr1-dc1 (the runbook''s "DC2"). D-135 amendment / + # Per-DC VIP overlay for vr1-dc1. GENERATED by scripts/render-dc-overlays.py from - # phase-4 Step 4: bundle.yaml hardcodes the API VIPs in vr1-dc0''s bands, so + # render/values/vr1-dc1-vips.yaml -- edit the values file and re-render; do not - # deploying vr1-dc1 re-homes them into ITS bands. Base bundle stays the vr1-dc0 - - # source of truth; this overlay is applied only for the dc1 deploy: + # hand-edit this file. # juju deploy ./bundle.yaml --overlay ./overlays/vr1-dc1-vips.yaml # - # Mapping (MEASURED from scripts/lib-net.sh lib_net_select_dc vr1-dc1, PLANE_NAME): + # RULING 3 (2026-07-25) made bundle.yaml VIP-FREE, so this file is the ONLY source of - # provider-public 10.12.4 -> 10.12.64 metal-admin 10.12.8 -> 10.12.68 + # vr1-dc1''s VIPs and the bundle is not deployable without it. - # metal-internal 10.12.12 -> 10.12.72 (host octets .50-.60 UNCHANGED) + # - # The three VIPs per service are public / admin / internal API endpoints + # DUAL-FAMILY per R2 (RULED 2026-07-27): each vip carries the three v4 legs THEN the - # (D-101 inherits the family layout; only the network prefix moves per DC). + # three v6 legs, and `prefer-ipv6: true` travels WITH them -- the two must never be + + # separated (a merge that keeps prefer-ipv6 while dropping the v6 legs leaves HAProxy + + # bound on :::port with no v6 VIP for pacemaker to manage; provider-bundle-check + + # invariant 9 now rejects that pair). Column order is + + # provider-public / metal-admin / metal-internal, v4 then v6. + + # v4 prefixes MEASURED from scripts/lib-net.sh lib_net_select_dc vr1-dc1 + + # v6 prefixes READ FROM THE NETBOX APEX (D-136 option (D)); the provider leg uses + + # the DEDICATED GUA VIP /64, admin+internal their own plane /64s + + # v6 host part MIRRORS the v4 octet textually (RULED 2026-07-27) -- .50 -> ::50 + + # + + # vault .61 and designate .62 are R11 (a D-020 AMENDMENT, 2026-07-27). Per R6 these + + # land BEFORE overlays/dc-ha-scaleup.yaml, which is what gives vault a real HA target. + + # The B1 / B5 tokens below are DEFINED in bundle.yaml''s own header block. ' diff --git a/runbooks/phase-01-bundle-deploy.md b/runbooks/phase-01-bundle-deploy.md index ee76d66..f8c21e3 100644 --- a/runbooks/phase-01-bundle-deploy.md +++ b/runbooks/phase-01-bundle-deploy.md @@ -97,9 +97,9 @@ echo "=== CHECK 1: bundle VIPs (quote-tolerant, octet-anchored) ===" grep -nE '^[[:space:]]+vip:' bundle.yaml overlays/vr1-dc0-vips.yaml TOT=$(grep -hcE '^[[:space:]]+vip:[[:space:]]*"?10\.12\.4\.' bundle.yaml overlays/vr1-dc0-vips.yaml 2>/dev/null | awk '{s+=$1} END{print s+0}') - HI=$(grep -hcE '^[[:space:]]+vip:[[:space:]]*"?10\.12\.4\.(5[0-9]|60)("|$|[[:space:]])' bundle.yaml overlays/vr1-dc0-vips.yaml 2>/dev/null | awk '{s+=$1} END{print s+0}') + HI=$(grep -hcE '^[[:space:]]+vip:[[:space:]]*"?10\.12\.4\.(5[0-9]|6[0-2])("|$|[[:space:]])' bundle.yaml overlays/vr1-dc0-vips.yaml 2>/dev/null | awk '{s+=$1} END{print s+0}') LO=$(grep -hcE '^[[:space:]]+vip:[[:space:]]*"?10\.12\.4\.(1[0-9]|20)("|$|[[:space:]])' bundle.yaml overlays/vr1-dc0-vips.yaml 2>/dev/null | awk '{s+=$1} END{print s+0}') - echo " provider VIPs total=$TOT in .50-.60=$HI in .10-.20(stale)=$LO (want 11/11/0)" + echo " provider VIPs total=$TOT in .50-.60=$HI in .10-.20(stale)=$LO (want 13/13/0)" # RULING 3 (2026-07-25): the counts span bundle.yaml AND its per-DC VIP overlay, # because the base is now VIP-FREE. `grep -c` over two files prints one count PER # FILE, so these sum with awk -- a bare $( ) would capture a two-line string and @@ -173,13 +173,13 @@ ```bash ( { TOT=$(grep -hcE '^[[:space:]]+vip:[[:space:]]*"?10\.12\.4\.' bundle.yaml overlays/vr1-dc0-vips.yaml 2>/dev/null | awk '{s+=$1} END{print s+0}') - HI=$(grep -hcE '^[[:space:]]+vip:[[:space:]]*"?10\.12\.4\.(5[0-9]|60)("|$|[[:space:]])' bundle.yaml overlays/vr1-dc0-vips.yaml 2>/dev/null | awk '{s+=$1} END{print s+0}') + HI=$(grep -hcE '^[[:space:]]+vip:[[:space:]]*"?10\.12\.4\.(5[0-9]|6[0-2])("|$|[[:space:]])' bundle.yaml overlays/vr1-dc0-vips.yaml 2>/dev/null | awk '{s+=$1} END{print s+0}') LO=$(grep -hcE '^[[:space:]]+vip:[[:space:]]*"?10\.12\.4\.(1[0-9]|20)("|$|[[:space:]])' bundle.yaml overlays/vr1-dc0-vips.yaml 2>/dev/null | awk '{s+=$1} END{print s+0}') - if [ "$TOT" = 11 ] && [ "$HI" = 11 ] && [ "$LO" = 0 ]; then + if [ "$TOT" = 13 ] && [ "$HI" = 13 ] && [ "$LO" = 0 ]; then juju deploy ./bundle.yaml --overlay overlays/vr1-dc0-vips.yaml \ --overlay overlays/octavia-pki.yaml -m openstack else - echo "ABORT: VIP guard failed (total=$TOT hi=$HI lo=$LO; want 11/11/0)" + echo "ABORT: VIP guard failed (total=$TOT hi=$HI lo=$LO; want 13/13/0)" fi } ) ``` diff --git a/scripts/pre-flight-checks.sh b/scripts/pre-flight-checks.sh index 60b4aa0..1fd8095 100644 --- a/scripts/pre-flight-checks.sh +++ b/scripts/pre-flight-checks.sh @@ -8,7 +8,8 @@ # Covers: # - repo HEAD / cleanliness (informational) # - octavia-pki overlay sanity (5 keys + ASCII; no key material printed) -# - triple-VIP validator (provider/admin/internal columns, aligned, .50-.60) +# - VIP validator (provider/admin/internal columns, aligned, .50-.99; a v4 triple +# or an R2 dual-family sextet). Deep v6 checking is provider-bundle-check's job. # - MAAS six-plane layout resolved BY CIDR (id/vid/gw/dns; metal-internal VID 103) # - per-host data/storage NIC links resolved BY CIDR (metal-internal on br-internal) # - the four KVM nodes Ready / power state @@ -71,7 +72,7 @@ fi # --------------------------------------------------------------------------- -hdr "CHECK 1: bundle VIPs -- TRIPLE column .${VIP_OCTET_MIN}-.${VIP_OCTET_MAX} (provider/admin/internal)" +hdr "CHECK 1: bundle VIPs -- v4 triple or R2 dual-family sextet, .${VIP_OCTET_MIN}-.${VIP_OCTET_MAX} (provider/admin/internal)" if [ ! -f bundle.yaml ]; then fail "bundle.yaml not found in $REPO" else @@ -98,7 +99,16 @@ BEGIN{ok=0;bad=0} /^[[:space:]]+vip:/{ v=$0; sub(/^[^"]*"/,"",v); sub(/".*/,"",v); n=split(v,a," "); - if(n!=3){print " MALFORMED(not 3 IPs): " $0; bad++; next} + # R2 (RULED 2026-07-27): a vip is the v4 TRIPLE, or a dual-family SEXTET + # appending the three v6 legs. Anything else is malformed. + if(n!=3 && n!=6){print " MALFORMED(not 3 or 6 IPs): " $0; bad++; next} + # For a sextet the last three must be v6 literals. Their PREFIX and octet-mirror + # correctness is asserted by provider-bundle-check against the NetBox apex -- this + # gate deliberately does not duplicate that, it only rejects a v4/v6 mix-up here. + if(n==6){ + for(i=4;i<=6;i++){ if(index(a[i],":")==0){print " NOT-IPv6 in v6 leg " i-3 ": " $0; bad++; next6=1} } + if(next6){next6=0; next} + } if(index(a[1],pp".")!=1){print " WRONG provider col: " $0; bad++; next} if(index(a[2],pa".")!=1){print " WRONG admin col: " $0; bad++; next} if(index(a[3],pi".")!=1){print " WRONG internal col: " $0; bad++; next} diff --git a/tests/provider-bundle-check/run-tests.sh b/tests/provider-bundle-check/run-tests.sh index 51f0382..3b49b8a 100644 --- a/tests/provider-bundle-check/run-tests.sh +++ b/tests/provider-bundle-check/run-tests.sh @@ -30,34 +30,46 @@ # -- is base + overlays/vr1-dc0-vips.yaml. The bare base is no longer a deploy # input at all, and T31 asserts exactly that rather than leaving it untested. python3 - "$REPO/bundle.yaml" "$REPO/overlays/vr1-dc0-vips.yaml" \ - "$TMP/pristine.yaml" "$TMP/good.yaml" <<'PY' -import sys, yaml + "$TMP/pristine.yaml" "$TMP/good.yaml" "$TMP/v4only.yaml" <<'PY' +import sys, yaml, copy base = yaml.safe_load(open(sys.argv[1])) ovl = yaml.safe_load(open(sys.argv[2])) for n, s in (ovl.get("applications") or {}).items(): base["applications"].setdefault(n, {}).setdefault("options", {}).update( (s or {}).get("options") or {}) yaml.safe_dump(base, open(sys.argv[3], "w")) -# `good.yaml` adds the ONE thing R11 ruled and that is not built yet: designate's -# VIP at the ruled octet .62. Without it the new hacluster check fails the real -# input, and every rc=0 case below would assert a failure we already know about. -base["applications"]["designate"].setdefault("options", {})["vip"] = \ - "10.12.4.62 10.12.8.62 10.12.12.62" +# Since R11 landed (2026-07-29) the overlay supplies designate AND vault, so the +# deploy input needs no injection any more -- good == pristine. The case that used +# to assert the .62 gap is RE-POINTED below, not deleted. yaml.safe_dump(base, open(sys.argv[4], "w")) +# A V4-ONLY twin: every vip truncated to its three v4 legs and prefer-ipv6 dropped, +# TOGETHER (dropping one alone IS the L3-9 defect). The v4-shaped cases mutate this +# instead of good.yaml, so they still exercise the band / octet-uniqueness / +# apex-independence invariants they were written for, rather than tripping the +# prefer-ipv6 coupling check first and never reaching them. +v4 = copy.deepcopy(base) +for n, spec in v4["applications"].items(): + o = (spec or {}).get("options") or {} + if o.get("vip"): + o["vip"] = " ".join(str(o["vip"]).split()[:3]) + o.pop("prefer-ipv6", None) +yaml.safe_dump(v4, open(sys.argv[5], "w")) PY -# dc1 twin of that ruled VIP, so the dc1 overlay case (T14) validates the same shape. -cat > "$TMP/dc1-designate.yaml" <<'YAML' -applications: - designate: - options: - vip: "10.12.64.62 10.12.68.62 10.12.72.62" -YAML - # A valid dual-family (R2 dual-stack) VIP for keystone: the three v4 legs plus the # three v6 legs, host part mirroring the v4 octet per the 2026-07-27 ruling. The # provider v6 leg is the DEDICATED GUA VIP /64 (f02:11::), not the node /64. DUAL6="10.12.4.50 10.12.8.50 10.12.12.50 2602:f3e2:f02:11::50 fd50:840e:74e2:220::50 fd50:840e:74e2:221::50" +mutate4() { # mutate the V4-ONLY twin -- for cases whose invariant predates dual-stack + python3 - "$TMP/v4only.yaml" "$TMP/$1" "$2" <<'PY' +import sys, yaml +src, dst, expr = sys.argv[1], sys.argv[2], sys.argv[3] +b = yaml.safe_load(open(src)) +exec(expr) +yaml.safe_dump(b, open(dst, "w")) +PY +} + mutate() { # mutate python3 - "$TMP/good.yaml" "$TMP/$1" "$2" <<'PY' import sys, yaml @@ -95,7 +107,7 @@ run 1 'D-062 requires 3' "T4 mysql num_units=1 FAILS" "$TMP/t4.yaml" # T5 shared VIP octet between two apps -mutate t5.yaml 'b["applications"]["glance"]["options"]["vip"]="10.12.4.50 10.12.8.50 10.12.12.50"' +mutate4 t5.yaml 'b["applications"]["glance"]["options"]["vip"]="10.12.4.50 10.12.8.50 10.12.12.50"' run 1 'shared by' "T5 duplicate VIP octet FAILS" "$TMP/t5.yaml" # T6 DOCFIX-071 regression: keystone resource stanza removed @@ -109,7 +121,7 @@ cp "$REPO/policies/domain-manager-policy.yaml" "$TMP/policies/" # restore # T8 original invariant still guarded: octavia VIP off-band (band widened to 50-99 by R11) -mutate t8.yaml 'b["applications"]["octavia"]["options"]["vip"]="10.12.4.233 10.12.8.233 10.12.12.233"' +mutate4 t8.yaml 'b["applications"]["octavia"]["options"]["vip"]="10.12.4.233 10.12.8.233 10.12.12.233"' run 1 'outside 50-99' "T8 off-band VIP octet FAILS" "$TMP/t8.yaml" # ---- Placement / anti-affinity checks (only fire on a role-separated bundle) ---- @@ -167,8 +179,7 @@ # T14 overlay merge + DC bands: base + dc1 VIPs, --dc vr1-dc1 PASSES runargs 0 'PASS: Pattern A' "T14 dc1-vips overlay + --dc vr1-dc1 passes" \ - "$TMP/good.yaml" --overlay "$REPO/overlays/vr1-dc1-vips.yaml" \ - --overlay "$TMP/dc1-designate.yaml" --dc vr1-dc1 + "$TMP/good.yaml" --overlay "$REPO/overlays/vr1-dc1-vips.yaml" --dc vr1-dc1 # T15 same overlay but default (dc0) bands -> dc1 VIPs off-band FAIL runargs 1 'not in 10.12.4' "T15 dc1 VIPs vs dc0 bands FAILS" \ @@ -177,8 +188,15 @@ # ---- R11 (D-020 amendment): an hacluster principal with no VIP is decorative HA ---- # T16 the REAL repo bundle trips it. This is the ruled-but-not-built designate VIP; # when .62 lands in bundle.yaml this case must be re-pointed, NOT deleted. +# RE-POINTED 2026-07-29: this asserted designate's VIP was MISSING. R11 built it, so +# the assertion is inverted to the surviving invariant -- the real deploy input is now +# CLEAN -- and T16b keeps the check's teeth by removing a VIP and demanding it fire. +run 0 '12 hacluster principal\(s\) all carry a VIP' \ + "T16 the real dc0 deploy input is CLEAN (R11 built)" "$TMP/pristine.yaml" + +mutate t16b.yaml 'b["applications"]["designate"]["options"].pop("vip"); b["applications"]["designate"]["options"].pop("prefer-ipv6",None)' run 1 'hacluster relation but no vip: designate' \ - "T16 the real dc0 deploy input FAILS on the R11 designate VIP gap" "$TMP/pristine.yaml" + "T16b removing designate's VIP still FAILS" "$TMP/t16b.yaml" # T31 ruling 3's new invariant: the BARE base is not a deploy input. Validating it # alone must name every clustered principal, not silently pass -- a VIP-free @@ -188,26 +206,35 @@ # T17 vault is the second R11 gap and it only appears under the HA overlay -- R6 ruled # the VIPs land BEFORE that overlay, so this is the ordering made executable. -runargs 1 'hacluster relation but no vip: .*vault' "T17 vault under dc-ha-scaleup FAILS (R6 ordering)" \ +# RE-POINTED 2026-07-29: R6 ruled the VIPs land BEFORE the HA overlay. They have, so +# the ordering is now SATISFIED and this asserts that -- 13 principals once the overlay +# adds vault-hacluster. T17b proves the check still fires if vault's VIP goes missing. +runargs 0 '13 hacluster principal\(s\) all carry a VIP' \ + "T17 R6 ordering satisfied: VIPs precede dc-ha-scaleup" \ "$TMP/good.yaml" --overlay "$REPO/overlays/dc-ha-scaleup.yaml" +mutate t17b.yaml 'b["applications"]["vault"]["options"].pop("vip"); b["applications"]["vault"]["options"].pop("prefer-ipv6",None)' +runargs 1 'hacluster relation but no vip: .*vault' \ + "T17b removing vault's VIP under the HA overlay still FAILS" \ + "$TMP/t17b.yaml" --overlay "$REPO/overlays/dc-ha-scaleup.yaml" + # T18 the check can also PASS, not only fail: give designate its ruled VIP -> green. run 0 'hacluster principal\(s\) all carry a VIP' \ "T18 hacluster check goes GREEN once the ruled VIP lands" "$TMP/good.yaml" # ---- R2 dual-stack: VIP arity, family coupling, and the apex-derived v6 bands ---- # T19 a valid dual-family sextet PASSES and is counted as dual -mutate t19.yaml "b['applications']['keystone']['options'].update({'vip':'$DUAL6','prefer-ipv6':True})" -run 0 '1 dual-family' "T19 valid dual-family VIP passes" "$TMP/t19.yaml" +run 0 '13 clustered VIP\(s\).*13 dual-family' \ + "T19 every app is dual-family in the real deploy input" "$TMP/good.yaml" # T20 prefer-ipv6 kept while the v6 legs are dropped -- the L3-9 merge order that # exits 0 today. This is the whole reason the coupling exists. -mutate t20.yaml 'b["applications"]["keystone"]["options"]["prefer-ipv6"]=True' +mutate t20.yaml 'o=b["applications"]["keystone"]["options"]; o["vip"]=" ".join(o["vip"].split()[:3])' run 1 'prefer-ipv6 and the v6 VIP legs must land TOGETHER' \ "T20 prefer-ipv6 without v6 legs FAILS" "$TMP/t20.yaml" # T21 the inverse: v6 legs present but nothing binds v6 -mutate t21.yaml "b['applications']['keystone']['options']['vip']='$DUAL6'" +mutate t21.yaml 'b["applications"]["keystone"]["options"].pop("prefer-ipv6")' run 1 'prefer-ipv6 and the v6 VIP legs must land TOGETHER' \ "T21 v6 legs without prefer-ipv6 FAILS" "$TMP/t21.yaml" @@ -228,18 +255,18 @@ # T25 an unreadable apex REFUSES (rc=2) rather than passing -- "could not look" is # never "nothing there". Only fires when a dual-family vip is actually present. APEX_RECORD=/nonexistent/apex.json runargs 2 'cannot evaluate .* no readable apex record' \ - "T25 dual-family + unreadable apex REFUSES (rc=2)" "$TMP/t19.yaml" + "T25 dual-family + unreadable apex REFUSES (rc=2)" "$TMP/good.yaml" # T26 ...and a v4-only bundle does NOT need the apex at all (no spurious refusal) APEX_RECORD=/nonexistent/apex.json runargs 0 'PASS: Pattern A' \ - "T26 v4-only bundle needs no apex record" "$TMP/good.yaml" + "T26 v4-only bundle needs no apex record" "$TMP/v4only.yaml" # ---- R11 band widen 50-60 -> 50-99 ---- # T27 octet .62 (designate's ruled octet) is now IN band -- proven by the band message run 0 'octet 50-99' "T27 R11 band widened to 50-99" "$TMP/good.yaml" # T28 ...but the band is still bounded: .100 is still rejected -mutate t28.yaml 'b["applications"]["glance"]["options"]["vip"]="10.12.4.100 10.12.8.100 10.12.12.100"' +mutate4 t28.yaml 'b["applications"]["glance"]["options"]["vip"]="10.12.4.100 10.12.8.100 10.12.12.100"' run 1 'outside 50-99' "T28 band still bounded (.100 rejected)" "$TMP/t28.yaml" # ---- the dc1 arm of the apex lookup, exercised for real ---- @@ -254,9 +281,10 @@ prefer-ipv6: true vip: "10.12.64.50 10.12.68.50 10.12.72.50 2602:f3e2:f03:11::50 fd50:840e:74e2:320::50 fd50:840e:74e2:321::50" YAML -runargs 0 '1 dual-family' "T29 dual-family VIP resolves the dc1 apex bands" \ +runargs 0 '13 clustered VIP\(s\).*13 dual-family' \ + "T29 dual-family resolves the dc1 apex bands" \ "$TMP/good.yaml" --overlay "$REPO/overlays/vr1-dc1-vips.yaml" \ - --overlay "$TMP/dc1-designate.yaml" --overlay "$TMP/dc1-dual.yaml" --dc vr1-dc1 + --overlay "$TMP/dc1-dual.yaml" --dc vr1-dc1 # T30 the v6 analogue of T15: dc0 v6 legs under --dc vr1-dc1 must FAIL, so a # cross-DC copy-paste of a rendered overlay cannot pass. @@ -270,7 +298,7 @@ runargs 1 'provider v6 leg .* not in 2602:f3e2:f03:11::/64' \ "T30 dc0 v6 legs under --dc vr1-dc1 FAIL (cross-DC leak)" \ "$TMP/good.yaml" --overlay "$REPO/overlays/vr1-dc1-vips.yaml" \ - --overlay "$TMP/dc1-designate.yaml" --overlay "$TMP/dc1-dual-wrong.yaml" --dc vr1-dc1 + --overlay "$TMP/dc1-dual-wrong.yaml" --dc vr1-dc1 echo echo "RESULT: PASS=$PASS FAIL=$FAIL" diff --git a/tests/render-dc-overlays/run-tests.sh b/tests/render-dc-overlays/run-tests.sh index 84c6db4..c560841 100755 --- a/tests/render-dc-overlays/run-tests.sh +++ b/tests/render-dc-overlays/run-tests.sh @@ -4,8 +4,10 @@ # Offline regression harness for scripts/render-dc-overlays.py (D-136 option (D)). # No live infra, no network. Mutates NOTHING outside $TMP. # -# THE CENTRAL ASSERTION is byte-for-byte reproduction of a REVIEWED artifact -# (overlays/vr1-dc1-vips.yaml). A renderer validated only against its own output +# THE CENTRAL ASSERTION is byte-for-byte reproduction of REVIEWED artifacts, on BOTH +# shapes: the FROZEN v4 fixture (immutable, so it stays a stable target) and the LIVE +# dual-family overlay (which is what actually deploys, and whose own header mandates +# that it remain renderer-reproducible). A renderer validated only against its own output # is validated against nothing -- that is the objection D-136 raises against its # own option (A). So the reproduction case is paired with THREE seeded faults # proving the comparison can fail: a wrong octet, a dropped header, and wrong-DC @@ -29,56 +31,64 @@ echo "== render-dc-overlays: deterministic per-DC overlay renderer ==" # ---- derive ------------------------------------------------------------------ -V="$TMP/dc1.yaml" -if python3 "$R" derive --dc vr1-dc1 --from-overlay "$LIVE" \ - --output overlays/vr1-dc1-vips.yaml -o "$V" 2>"$TMP/e1"; then - ok "T1 derive vr1-dc1 from apex + lib-net succeeds" +V4="$TMP/dc1-v4.yaml" # v4 target = the FROZEN fixture (immutable) +V="$TMP/dc1-dual.yaml" # live target = the dual-family overlay the tree deploys +if python3 "$R" derive --dc vr1-dc1 --from-overlay "$FIX" \ + --output overlays/vr1-dc1-vips.yaml -o "$V4" 2>"$TMP/e1" \ + && python3 "$R" derive --dc vr1-dc1 --dual-family --include-unbuilt \ + --from-overlay "$LIVE" --output overlays/vr1-dc1-vips.yaml -o "$V" 2>>"$TMP/e1"; then + ok "T1 derive vr1-dc1 succeeds for both the v4 and dual-family shapes" else bad "T1 derive failed: $(head -1 "$TMP/e1")" fi -n_apps="$(python3 -c "import yaml,sys; print(len(yaml.safe_load(open(sys.argv[1]))['apps']))" "$V" 2>/dev/null)" +n_apps="$(python3 -c "import yaml,sys; print(len(yaml.safe_load(open(sys.argv[1]))['apps']))" "$V4" 2>/dev/null)" t_eq "T2 derive defaults to the 11 BUILT apps (vault/designate excluded)" "$n_apps" "11" -# ---- the central assertion --------------------------------------------------- -if python3 "$R" check "$V" --against "$LIVE" >/dev/null 2>&1; then - ok "T3 render reproduces overlays/vr1-dc1-vips.yaml BYTE-FOR-BYTE" +# ---- the central assertion, on BOTH shapes ----------------------------------- +if python3 "$R" check "$V4" --against "$FIX" >/dev/null 2>&1; then + ok "T3 render reproduces the FROZEN v4 fixture BYTE-FOR-BYTE" else - bad "T3 render does NOT reproduce the committed overlay" + bad "T3 render does NOT reproduce the frozen v4 fixture" +fi +if python3 "$R" check "$V" --against "$LIVE" >/dev/null 2>&1; then + ok "T3b render reproduces the LIVE dual-family overlay BYTE-FOR-BYTE" +else + bad "T3b render does NOT reproduce the live dual-family overlay" fi # T4 the same bytes as the FROZEN fixture, which is the durable target: the live # overlay is about to be rewritten by the R2/R11 reconciliation, the fixture is not. -got="$(python3 "$R" render "$V" 2>/dev/null | sha256sum | cut -d' ' -f1)" +got="$(python3 "$R" render "$V4" 2>/dev/null | sha256sum | cut -d' ' -f1)" want="$(sha256sum "$FIX" | cut -d' ' -f1)" t_eq "T4 rendered output matches the frozen render-baseline fixture hash" "$got" "$want" # ---- proven able to FAIL (three seeded faults) -------------------------------- -sed 's/octet: 50/octet: 51/' "$V" > "$TMP/f1.yaml" -python3 "$R" check "$TMP/f1.yaml" --against "$LIVE" >/dev/null 2>&1 +sed 's/octet: 50/octet: 51/' "$V4" > "$TMP/f1.yaml" +python3 "$R" check "$TMP/f1.yaml" --against "$FIX" >/dev/null 2>&1 t_eq "T5 a wrong octet FAILS the byte-compare" "$?" "1" -python3 - "$V" "$TMP/f2.yaml" <<'PY' +python3 - "$V4" "$TMP/f2.yaml" <<'PY' import sys, yaml v = yaml.safe_load(open(sys.argv[1])); v.pop("header", None) yaml.safe_dump(v, open(sys.argv[2], "w"), sort_keys=False, width=4096) PY -python3 "$R" check "$TMP/f2.yaml" --against "$LIVE" >/dev/null 2>&1 +python3 "$R" check "$TMP/f2.yaml" --against "$FIX" >/dev/null 2>&1 t_eq "T6 a dropped comment header FAILS the byte-compare" "$?" "1" -python3 "$R" derive --dc vr1-dc0 --from-overlay "$LIVE" -o "$TMP/f3.yaml" 2>/dev/null -python3 "$R" check "$TMP/f3.yaml" --against "$LIVE" >/dev/null 2>&1 +python3 "$R" derive --dc vr1-dc0 --from-overlay "$FIX" -o "$TMP/f3.yaml" 2>/dev/null +python3 "$R" check "$TMP/f3.yaml" --against "$FIX" >/dev/null 2>&1 t_eq "T7 dc0 prefixes against the dc1 artifact FAIL (cross-DC)" "$?" "1" # ---- determinism + purity ---------------------------------------------------- -a="$(python3 "$R" render "$V" 2>/dev/null | sha256sum)" -b="$(python3 "$R" render "$V" 2>/dev/null | sha256sum)" +a="$(python3 "$R" render "$V4" 2>/dev/null | sha256sum)" +b="$(python3 "$R" render "$V4" 2>/dev/null | sha256sum)" t_eq "T8 render is deterministic across runs" "$a" "$b" # T9 render is PURE -- no apex, no network. A v4 values file must render with the # apex record deliberately unreachable, or the renderer is not offline-testable # and forward item F2's frozen interface is violated. -out="$(APEX_RECORD=/nonexistent/apex.json python3 "$R" render "$V" 2>/dev/null | sha256sum | cut -d' ' -f1)" +out="$(APEX_RECORD=/nonexistent/apex.json python3 "$R" render "$V4" 2>/dev/null | sha256sum | cut -d' ' -f1)" t_eq "T9 render needs no apex (pure, offline)" "$out" "$want" # T10 ...but DERIVING dual-family does, and must REFUSE rather than invent one. @@ -89,13 +99,11 @@ # ---- ordering ---------------------------------------------------------------- # T11 emission order is ASCENDING OCTET, not alphabetical. Sorting by name # reproduces nothing: keystone(.50) must precede barbican(.51). -first="$(python3 "$R" render "$V" 2>/dev/null | grep -E '^ [a-z]' | head -1 | tr -d ' :')" +first="$(python3 "$R" render "$V4" 2>/dev/null | grep -E '^ [a-z]' | head -1 | tr -d ' :')" t_eq "T11 apps emit in ascending-octet order, not alphabetical" "$first" "keystone" # ---- dual-family (R2) -------------------------------------------------------- -DV="$TMP/dual.yaml" -python3 "$R" derive --dc vr1-dc1 --dual-family --include-unbuilt \ - --from-overlay "$LIVE" -o "$DV" >/dev/null 2>&1 +DV="$V" n2="$(python3 -c "import yaml,sys; print(len(yaml.safe_load(open(sys.argv[1]))['apps']))" "$DV" 2>/dev/null)" t_eq "T12 --include-unbuilt adds R11's vault .61 + designate .62" "$n2" "13"