| 2026-07-31 |

Build the D-135 amendment: jammy-backports at source, T10 re-pointed
...
scripts/dc-mirror.sh's generated sync script gains jammy-backports in --dist=,
with a comment at the generator recording why it is not optional: the MAAS
jammy image ships backports in its sources.list and juju's apt-mirror
model-config rewrites every suite to this mirror, so a mirror without it 404s
and every charm install hook fails. That cost 22 units today.
FIXED AT SOURCE, not only on the live rack -- a live-only edit would leave the
generator regenerating the old scope at the next install or at any future DC
standup.
tests/dc-mirror T10 asserted the old dist string verbatim. RE-POINTED to the
new invariant with the reason recorded in-file, never deleted to go green.
Mutation-proven: reverting the generator's dist line turns T10 red (23/1);
restored byte-identical, 24/24.
Gauntlet ALL GREEN (93); repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Defer Tailscale decisions to end of step; measure jammy-backports at 1.0 GiB
...
OPERATOR DIRECTION, verbatim: "To make sure we don't muddle the in process
deployment, queue all the tailscale decisions to the end of the step. When you
bring them back up give me decisions choices with your recommendation for
each." All four sub-decisions in gap register item 21 are deferred; when
re-raised they come back as choices, each with a recommendation.
MEASURED, at the exact scope dc-mirror.sh:177-181 uses (main,restricted,
universe,multiverse / amd64 / nosource), summing Size: across each component's
binary-amd64 Packages.gz:
main 859,001,998 338 pkgs
universe 211,685,404 123 pkgs
restricted 0 (empty in backports)
multiverse 0 (empty in backports)
TOTAL 1,070,687,402 = ~1.00 GiB, 461 packages
Context: the existing dc0 mirror is 951 GB with 1.8 T free, so backports is
~0.1% of it -- capacity is a non-issue.
Sanity-checked rather than trusted: the five largest entries were listed to
confirm the Size: field was read correctly, and a separate binary-all index
probes 404, so arch:all is already inside binary-amd64 (301 of main's 338) and
is neither double-counted nor missed.
Worth knowing before choosing: ~462 MB of main's 859 MB is LibreOffice -- about
46% of the suite is a desktop package set these nodes will never install.
Transfer estimate stated as the weak measurement it is: one single-stream fetch
measured 932 KB/s, putting 1.07 GB at roughly 19 minutes. Treat as an upper
bound; debmirror parallelises.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Deploy blocker: mirror lacks jammy-backports. Queue per-DC Tailscale (item 21)
...
BLOCKER, logged not fixed (D-135 scope is a ruled surface). 22 of 33 units in
error, all hook failed: "install", all one cause:
E: The repository 'http://10.12.8.4/ubuntu jammy-backports Release'
does not have a Release file.
Measured: the mirror serves jammy/jammy-security/jammy-updates 200 and
jammy-backports 404 -- exactly D-135's "jammy triple" scope -- while the node's
sources.list carries jammy-backports, because the Step-3.5 apt-mirror
model-config rewrites EVERY suite to the DC mirror. Machines are all
started/running; this is purely the artifact layer and units retry.
dc-mirror.sh check dc0 PASSES while this is true -- it verifies sync STATUS,
not suite COVERAGE against what the deployed image asks for.
QUEUED BY OPERATOR DIRECTION, not built: a per-DC Tailscale subnet router is
now a standing DC-standup requirement, dc0 first. Home of record is the tooling
gap register item 21 (new), with a per-DC definition-of-done, the
vendor-documented build constraints, and four sub-decisions needing GA-R5
rulings first: the utility-band OCTET (D-134's map is a standing cross-DC
standard), the admin-reachability model (star vs mesh -- the real architecture
question at region-region scale), HA count, SNAT on/off. This is EXECUTION of
the already-ruled D-129(iii) shape, not a new decision.
Measured while scoping: SEC-010 does NOT need relaxing (the forwarding to
permit is on the router VM, not the transit leg); voffice1 holds no route to
10.12.x at all, so the tailnet stops at Office1 by construction; the existing
Office1 node is UNTAGGED and carries a 180-day key-expiry clock on the
operator's only tailnet path; and D-129(iii) cites D-107 as governing Office1
while D-107 rules nothing about Tailscale -- the second instance of the F7
miscitation class, this time inside a ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

>>> BUNDLE DEPLOY SUCCEEDED: "Deploy of bundle completed." exit 0 <<<
...
Option D half 2. The 23 application definitions were removed INDIVIDUALLY,
never batched (hard rule 3), each read back, with the precondition re-verified
immediately before the first removal and the model read back empty afterwards.
Step-3.5 state survived the clear -- apt-mirror still set, all six spaces still
bound with both address families, checked not assumed.
MEASURED IMMEDIATELY AFTER: juju 56 applications / 9 machines / 33 units,
machines pending+allocating; MAAS 10 machines -- 9 Deploying, 1 Deployed (the
controller).
Three defects closed in the order they were hit: attempt 1's unknown option
prefer-ipv6 (D-101 ruling note); attempt 2a's missing policies/overrides.zip on
the client host; attempt 2b's barbican 265-vs-261 downgrade refusal (explicit
base on all 56 AND a clean model).
Operational note, because the first removal read as a failure: juju
remove-application PROMPTS by default and aborts on non-interactive stdin.
--no-prompt is required.
WHAT IS NOT CLAIMED: this means juju ACCEPTED and QUEUED the bundle. It does
NOT mean the cloud is up. 9 machines are allocating, 33 units pending; the
settle takes hours and nothing here asserts unit health or any service verdict.
G17's dc0 half is now genuinely armable -- the existing capture was taken on
the CONTROLLER VM and says so. The nine role nodes are booting for the first
time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Option D half 1: explicit base on all 56 apps, gated by new invariant 12
...
PROVEN AGAINST THE REAL MODEL, NOT INFERRED. The earlier one-app test only
showed the error MOVING to the next app. The fully-based bundle was staged as a
throwaway on the rack and --dry-run against the LIVE 23-application model --
the same command that errored on barbican 265-vs-261 -- exits 0.
Subordinates are in scope, measured: mysql-router is 1154 at 22.04 and 1178 at
24.04. hacluster sharing rev 166 across both bases is not a property to depend
on.
TWO SILENT UNDER-MATCHES, both caught by cross-checking the edit against the
parsed file rather than by review:
(1) a first pass keyed on the block form inserted 44 of 56 -- the twelve
-hacluster apps are single-line FLOW MAPPINGS. The inserter asserts its
app list equals what yaml.safe_load sees, and refuses otherwise.
(2) overlays/dc-ha-scaleup.yaml DEFINES vault-hacluster, an application that
exists nowhere in bundle.yaml, and it had no base. That overlay is a LATER
deploy step, so this would have re-opened the trap AFTER the deploy
succeeded. Found by the new gate on its FIRST run, in a file the fix was
not looking at -- the same later-step-overlay class as the ceph-mon miss
earlier today.
INVARIANT 12 (NEW): every application carries an explicit base equal to the
bundle's OWN default-base -- keyed on that value, not a literal. A bundle with
no default-base REFUSES rather than passing vacuously. Without this, deleting
the 56 lines is a byte-identical PASS and the trap returns at the next partial
failure, at any DC.
Five cases, each mutation-proven individually (harness 50 -> 55/55): neutering
nobase kills T49/T50/T52, wrongbase kills T51, the refuse branch kills T53.
Gauntlet ALL GREEN (93); repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

RULED 2026-07-31 (GA-R5): option D -- fix the bundle AND clear the model
...
Operator utterance, verbatim: "I want to do option D: Fix the bundle by
defining the base 22.04 on each app and then clearing the modeling and
deploying from a clear model. It is very cheap to clear and redeploy."
I offered three options; the operator composed a fourth that is strictly
stronger than any of them. The durable fix lands (explicit per-app base, so
the bundle survives a partial failure), AND the deploy runs the path that is
MEASURED to work -- a clean deploy onto an empty model, exactly what run 2a
did before it hit the missing resource file -- rather than juju's incremental
re-run path, which is the one carrying the defect.
OPS under GA-R3, no D-number: an explicit base per application states what
bundle.yaml:85's default-base already means. No ruled surface changes.
The invariant it establishes is skill material at stage close: a bundle whose
applications rely solely on default-base is deployable exactly ONCE, and a
production bundle that cannot survive a partial failure is a trap.
Committed and pushed BEFORE the dependent work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Deploy attempt 2: prefer-ipv6 fixed, two new defects, model partially populated
...
The prefer-ipv6 failure did NOT recur. Two new ones did, and for the first time
the model is not empty: MEASURED 23 applications, 0 machines, 0 units.
Attempt 1 aborted during VALIDATION; this one reached EXECUTION, which is why
it left residue. NOTHING IS PROVISIONED -- no MAAS machine left Ready, no disk
written.
DEFECT 1: the rack's ~/repo-stage is a PARTIAL COPY of the repo with no
policies/, and bundle.yaml:214 is the only local-file reference in the deploy
input. juju resolves it relative to the bundle, so it must exist ON THE HOST
THAT DEPLOYS. Nothing caught it: --dry-run does not upload resources (the
SECOND --dry-run blind spot in two days), attempt 1 aborted before resource
upload, and preflight runs on voffice1 where policies/ DOES exist -- the same
wrong-host instrument class already recorded twice, and the second instance
this session. Fixed forward, both files sha256-verified.
DEFECT 2: a bundle relying on default-base is deployable exactly ONCE. The
re-run failed on barbican 265-vs-261. Measured: 265 is the ubuntu@22.04
revision, 261 the ubuntu@24.04 one. The 23 existing apps are all 22.04, so run
2a honoured bundle.yaml:85 -- but on a re-run, for an app that ALREADY EXISTS,
juju resolves without that default. A model-level default-base has NO EFFECT
(tested, then RESET). An explicit per-application base WORKS (tested read-only
on a throwaway; the error moved to the next app).
OWNED: two of those read-only tests were staged under /tmp and failed with
"no charm was found" -- the juju snap's PRIVATE /tmp, a trap this repo has
already recorded in CURRENT-STATE and the phase-4 runbook.
The rollback decision tree is written for tofu apply and defaults to
fix-forward, which is measured not to work here. A partial juju deploy is a
failure mode the repo does not cover. Recovery is an operator decision;
options are enumerated in the capture and the model residue is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

RULED 2026-07-31 (GA-R5): the five new P5 findings accepted; dc0 deploy is clear
...
Operator utterance, verbatim: "Accept the five and proceed to the dc0 deploy".
The 2026-07-30 acceptance covered SIX findings and says it covers "these six,
enumerated, and nothing else". This preflight run reports eleven: all six still
present, plus FIVE new -- maas-region-db-password, maas-region-admin-password,
maas-region-api-key.txt (SEC-027) and maas-juju-api-key.txt,
maas-juju-user-password (SEC-028), all vr1-dc1. Every one is the D-137 forward
register correctly reporting that dc1's MAAS region is authored but not applied.
Carried as known, by-design absences on their existing SEC rows. preflight keeps
exiting FAIL on P5 for the rest of this stage; that RED is ruled-accepted and
must NOT be made green by deleting or weakening a matrix row. Like its
predecessor this covers these FIVE, enumerated, and nothing else -- total
accepted at P5 is now ELEVEN across the two rulings.
Also corrects this changelog's header, which claimed the session was read-only
throughout. True for items 1-5; after the ruling landed it edited the renderer,
the gate, both overlays and both values files. No cloud state changed at any
point.
Committed and pushed BEFORE the deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Pre-deploy: rack input refreshed + hash-verified, preflight captured, dry-run green
...
(a) The dc0 rack's ~/repo-stage is a COPY, not a clone -- nothing updates it, and
it still held the pre-ruling vips overlay. Exactly one of four files differed;
copied, then ALL FOUR hash-compared against repo HEAD. The gitignored
octavia-pki overlay was deliberately not touched (SEC-029) and verifies
byte-identical at 5fc117f1, still 0600. A sweep is evidence about the deploy
only if the swept bytes ARE the deployed bytes.
(b) Preflight captured (242 lines, exit 1), run with
MAAS_PROFILE=vr1-dc0-region -- without it preflight is REGION-BLIND and emits 19
false negatives. Instrument proven current first: maas-profile-assert.sh
vr1-dc0-region hot-kid exits 0. P1-P4 and P7 PASS, incl. aligned VIPs 13 OK / 0
bad against the RE-RENDERED overlay. P5 FAIL, 11 findings.
(c) The P5 delta is ENUMERATED. The 2026-07-30 acceptance covered six findings
and says it covers "these six, enumerated, and nothing else". All six are still
present; FIVE NEW are all vr1-dc1 S2 rows (SEC-027/-028) -- the D-137 forward
register correctly reporting that dc1's region is not built. Diff appended to
the capture. Needs its own GA-R5 exchange before the deploy.
(d) Step 4.2 --dry-run against the fixed input: exit 0, 56 apps / 108 relations
/ 33 unit placements. The ceph-osd tags=openstack exposure is now MEASURED, not
reasoned: the plan places ceph-osd/0..3 on machines 5-8 by explicit id, so the
absent tag never has to match. The residual (a later UNPLACED add-unit) stands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Fix a defect in my own build: invariant 9a was VIP-scoped and the charm list was short
...
Found by a repo-wide grep, not by any gate. Two faults, one cause -- I
measured and gated the population the QUESTION named (the 13 VIP charms)
rather than the population the INVARIANT covers (every application juju
validates).
(1) PREFER_IPV6_CHARMS was WRONG. Re-measured across ALL 33 charms in
bundle.yaml: TWELVE declare the option, not seven. The five missing are
ceph-mon 491, ceph-osd 953 (squid/stable), nova-compute 894, hacluster 166
(2.4/stable) and mysql-innodb-cluster 164 (8.0/stable).
(2) 9a was written INSIDE the VIP loop, so an application with no vip never
reached it -- and the repo ALREADY has that case:
overlays/dc-dc-ipv6-family-matrix.yaml sets prefer-ipv6 on ceph-mon, which
carries no VIP. That overlay is a LATER deploy step, so the miss would have
passed attempt 2 cleanly and surfaced at the step after, looking like a new
fault.
9a now runs over EVERY application, before the VIP loop; the VIP loop skips an
app 9a already flagged so the diagnosis is not doubled. ceph-mon's own in-file
claim of the option is now INDEPENDENTLY VERIFIED from the artifact rather
than taken from the comment.
Harness 48 -> 50/50. T47 (non-VIP app whose charm lacks the option FAILS) and
T48 (ceph-mon PASSES) both mutation-proven: re-scoping 9a back to the VIP loop
kills T47; dropping ceph-mon from the list kills T48.
Gauntlet ALL GREEN (93); repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Sweep both deploy inputs for option NAMES and VALUE TYPES: clean, and proven able to fail
...
The two extensions owed after the ruling, now run:
dc0 75 assignments / 20 schemas / 0 unknown name / 0 type mismatch / 0 note
dc1 75 assignments / 20 schemas / 0 unknown name / 0 type mismatch / 0 note
Both EXIT 0. dc1 introduces no new charm@channel pair -- checked, not assumed.
PROOF OF TEETH, because a clean sweep across two inputs is exactly the
implausibly-uniform result this repo has been burned by. Three defects planted
in a THROWAWAY copy, all three caught, EXIT 1:
[FAIL name] barbican does not declare option 'prefer-ipv6' (attempt-1 defect)
[FAIL type] keystone 'prefer-ipv6': declared boolean, got str ('true')
[note] glance 'vip': declared string, value is unquoted float
Type judgement is deliberately conservative: a mismatch is reported only where
juju's own coercion cannot save it. A scalar where a STRING is declared is a
NOTE, not a failure -- but it is still surfaced, because an unquoted value is
how a version string silently becomes a float. An UNRECOGNISED declared type
REFUSES rather than passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Build the 2026-07-31 ruling: renderer + invariant 9 replaced, overlays re-rendered
...
Three parts, all mutation-proven.
(i) provider-bundle-check.py gains PREFER_IPV6_CHARMS -- the measured authority,
keyed by CHARM name, carrying the revisions and a re-measure warning -- and
invariant 9 is REPLACED, never deleted:
9a prefer-ipv6 on a charm that does NOT declare it FAILS. Asserted on
PRESENCE, not truthiness: 'prefer-ipv6: false' is the same fatal
'unknown option' to juju as true.
9b on a DECLARING charm it still travels with the v6 legs -- the L3-9
protection is retained unchanged for those seven.
9c arity.
The old text's mechanism claim ("prefer-ipv6 makes HAProxy bind :::port") is
refuted by this session's measurement and is recorded as such in-file.
(ii) render-dc-overlays.py emits the option only for a declaring charm, READING
the set from provider-bundle-check with ast rather than restating it (the same
rule APP_OCTET already uses). render() keeps its purity property: the set is a
PARAMETER and the default REFUSES rather than defaulting to empty, because an
empty set renders a plausible artifact with the option nowhere. Both overlays
re-rendered: exactly SIX lines removed per DC and nothing else -- every vip
string byte-identical, so all v6 legs are retained.
(iii) tests/provider-bundle-check 44 -> 48/48. T43 reproduces the attempt-1
defect; T44 proves presence-not-truthiness; T45 is the positive control; T46
asserts the DIAGNOSIS.
MUTATION PASS -- six mutations, every one killed tests. And one NEW assertion
was found to be DECORATION and was REPLACED rather than kept: T46's first form
could not fail under any mutation, because a non-declaring charm with no option
reaches neither branch. Re-written to catch the fold-9a-into-9b simplification
and re-proven.
Gauntlet ALL GREEN (93); repo-lint 0 fail. The fixed dc0 deploy input now
sweeps CLEAN on option names: 75 assignments, 20 schemas, 0 unknown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

prefer-ipv6 research: size option (b) and scope the sweep claim
...
Two corrections to my own framing, both measured:
(1) Option (b) is NOT a values-file edit. 'prefer-ipv6: true' is INJECTED
UNCONDITIONALLY by scripts/render-dc-overlays.py:243-246 whenever fam ==
"dual", and 'family: dual' is a single TOP-LEVEL key in
render/values/vr1-dc{0,1}-vips.yaml:3 -- not per-app data. Implementing (b)
means teaching the renderer which charms declare the option, re-pointing
tests/render-drift, and replacing invariant 9: a code change with a harness.
Recorded before the fork is put to the operator so the ruling is taken on the
real cost.
(2) The option-name sweep's claim is scoped inline. It checked NAMES, dc0
only. Option VALUE TYPES (the 'type:' field in every schema already
downloaded) and the dc1 input are both owed before the redeploy, and each
could cost an attempt the same way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

prefer-ipv6 research DONE: the option never drove the v6 HAProxy bind
...
Answers the 2026-07-31 ruled question ("Research what those 6 charms do with
v6 VIPs first"). Measured by downloading each charm at its pinned channel for
amd64/ubuntu@22.04 and reading its templates and code -- not Charmhub's
config-yaml, which answers only whether the knob is declared.
FINDING: in BOTH template families the ':::port' frontend bind is gated on
ipv6_enabled = not is_ipv6_disabled(), a read of the kernel
net.ipv6.conf.all.disable_ipv6 sysctl -- MEASURED 0 on a live MAAS-deployed
jammy node in this DC. The v6 VIP is assigned by pacemaker on per-address
family detection. vault ships no haproxy template at all; its listener is the
hardcoded literal [::]:8200. prefer-ipv6 is a UNIT ADDRESS-FAMILY switch, and
the one value it sets that sounds like a bind address (haproxy_host) is
consumed by no template in any charm downloaded.
So option (b) -- drop the option from the six, keep their v6 VIP legs -- is
what the measurement supports. NOT TAKEN: the ruling is the operator's, the
overlay is untouched and R2 is unamended.
Also recorded: provider-bundle-check invariant 9's mechanism claim is wrong
and must be REPLACED (never deleted) when a ruling lands; a new and separate
question about the SEVEN charms that do declare the option; a full
option-name sweep of the dc0 deploy input (56 apps / 81 assignments / 20
schemas / 0 unresolvable) showing prefer-ipv6 on the six is the ONLY unknown
option, so nothing else is waiting for attempt 2; and a runbook defect --
the dc0 deploy block omits vr1-dc0-machines.yaml, which is NOT a no-op (it
carries ovn-chassis bridge-interface-mappings).
Read-only session: no cloud mutation, no overlay edit, no ruling adopted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|