| 2026-08-03 |

clear the deploy stall: cloud-init apt-get update wedged 13h on 3 units
...
Operator approved: 'Kill the stuck apt-get update on the three units'.
Machines all MAAS-Deployed but juju agents pending on machine 5 and containers
0/lxd/10 + 2/lxd/2 (mysql/0 and /2), stalling the cluster and the whole
pre-vault-init settle. Cause: cloud-init modules --mode=final ran apt-get update
at 04:48 and blocked on archive.ubuntu.com (16s CPU over 13h = stuck on I/O);
jujud installs after apt, so the agent never installed. The 12 that came up drew
a healthy path; these 3 hit the flaky-archive-backend class (08-02 F2) and a
blocked process never retries. Both apt proxies tested 200 at fix time -- network
recovered, only the wedged processes held.
Fix: pkill the stuck apt-get update on all three (direct-SSH juju key for the
machine, lxc exec via the host for the containers), clear stale locks. cloud-init
resumed, jujud installed, all three agents came up started; mysql/0+/2 to
maintenance (install), cluster can form.
Logged not fixed: dual apt proxies on the nodes (MAAS + DC); OS cloud-init apt
not pinned to the resilient DC proxy. DC-standup hardening candidates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
v4 VIP revert EXECUTED: keystone Invalid vips cleared, model converging clean
...
Operator: 'Yes, drop the v6 problem legs'. Done via the render pipeline, not a
hand-edit: render values family dual -> v4 (v6 GUA block retained as the D-141
reserved record), re-rendered to a v4-only overlay, gates green, overlay
re-staged. The 11 API charms with a v6 VIP leg each juju config vip=<v4 triple>,
driven from the staged overlay so model and overlay agree; all rc=0, zero apps
still carry a 2602 leg.
keystone moved from 'Invalid vips: [2602...]' to '(config-changed) Incomplete
relations: database' -- the VIP rejection is gone, it is re-converging normally.
Model: 8/9 started, 6 active, ZERO error; all blocked units are the expected
pre-vault-init set. Node planes stay dual-stack; only the container-VIP family
reverted, per D-141.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

D-141 ADOPTED: IPAM allocations are dual-stack, status-distinguished
...
Operator ruling, exact utterance: "Yes, but you should expand on the record. It
should be noted that when creating future ipam allocations the IPv4 and IPv6
blocks must follow this same structure."
Immediate: the 26 GUA v6 VIP addresses D-139 step 6 created stay in the apex at
status reserved (already their measured state, no NetBox mutation) and are not
promoted to active until the v6 VIPs are live and verified.
Standing rule (the architectural half, hence a D-number under GA-R3 A1):
every IPAM allocation is authored dual-stack and the STATUS field carries the
truth -- v4 active (live), GUA v6 reserved (planned/collision-protected),
superseded ULA deprecated (history). The apex records current reality AND future
plan in one place, legible from status alone: apex driving deploy, not
back-filled to it. Four binding rules: author both families together; status
bears truth never prose; promotion gated on a NAMED capability; never delete a
reserved future-family block (it is the conversion input).
Adjacent to G18 but does not answer it -- D-141 governs allocation STRUCTURE, G18
is the separate lb-mgmt-prefix ruling still deferred-until-live. The promotion
gate for the API-charm v6 VIPs is docs/charm-ip-family-compatibility.md (juju
LP#1723240 + the per-charm fixes), which now cites D-141 as its governing
decision. Roosevelt analog: every DC's IPAM authored dual-stack from day one, v6
reserved until capable, so no DC ever needs a v4->v6 re-carve -- only a status
promotion.
CURRENT-STATE updated same commit (GA-R1 C1); next-free D 141 -> 142.
repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

add charm IPv4/IPv6 compatibility reference; confirm IPv4 config intact
...
Operator request: a durable per-charm IPv4/IPv6 compatibility reference, and
confirmation the IPv4 config still exists for the revert.
docs/charm-ip-family-compatibility.md -- all 33 distinct bundle charms, built
from the repo's MEASURED research (prefer-ipv6 charm research 07-31; the
ceph/hacluster/mysql/OVN findings in CURRENT-STATE:2480-2503) with LP citations.
Charms the repo has not measured for v6 are marked NOT ASSESSED rather than
guessed. Frames the two independent v6 blockers: juju container addressing
(LP#1723240, charm-independent, blocks all API-charm v6 VIPs today) and the
per-charm defects (hacluster ip_version ipv4 LP#2111852 fix-committed-unreleased;
ceph-osd LP#2109798+2061836; mysql bare-URI; OVN encap IPv4-only).
IPv4 config CONFIRMED INTACT by measurement: the current VIP overlay is
dual-stack -- every vip line carries the v4 triple AND the v6 triple, and the v4
legs are live and accepted. Revert = drop the v6 legs from render/values and
re-render; also in git at the parent of a9e3258 and reconstructable from
lib-net.sh. The node-plane v6 work stays; only the container-VIP family reverts.
repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

convergence: install blocker GONE; keystone Invalid vips is the known container-v6 gap
...
8/9 machines started, 6 units active, ZERO in error -- the first deploy had nine
at hook failed install from NO_PUBKEY, so the convergence proves packages install
through the proxy end to end.
Remaining blocked states are the expected early set except one real finding:
keystone/0 Invalid vips on the three GUA v6 VIPs. Measured cause: the keystone
LXD container has zero global IPv6 addresses, so the charm cannot place a v6 VIP
on a space the container has no global v6 on. This is the known-shape condition
from finding D3 (07-31): the nodes are dual-stacked, the API-charm containers are
not. Surfaced then as prefer-ipv6 fatals; with D-139 GUA VIPs it now surfaces as
Invalid vips. IPv6/dual-stack buildout, logged not fixed -- needs an operator
decision, not chased mid-convergence. The v4 VIPs in the same config are accepted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

dc0 bundle RE-DEPLOYED on the rebuilt controller with converged config; 4.4 all PASS
...
Deploy of bundle completed EXIT 0, 04:39:32Z -> 04:41:26Z, onto the freshly
bootstrapped controller. Step 4.4 config gate all PASS and clears both failure
modes that blocked the first attempt:
item 1: ovn-chassis carries all three options (bridge-interface-mappings with
exactly two MACs, ovn-bridge-mappings physnet1:br-ex, prefer-chassis-as-gw true)
-- the options map merged key-by-key.
item 2: every origin resolves to cloud:jammy-caracal or the charm default caracal,
NOT a raw deb line -- so NO_PUBKEY 5EDB1B62EC4926EA cannot recur. The cloud: path
installs ubuntu-cloud-keyring as its side effect and the apt-cacher-ng proxy
serves the UCA content. This is the D-135 amendment (b) convergence proving out
end to end.
Convergence in progress: t+0 nine machines pending, 33 units waiting, zero
error/blocked. Watching to the phase-01 pre-vault-init end state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

dc0 controller REBUILT from MAAS via Path C; environment ready for the deploy
...
Operator ruling: remove the machine and rebuild fresh from MAAS rather than
troubleshoot or gracefully repair. Executed as Path C, every gate captured.
C.2b unregister (client-side) then MAAS RELEASE, not delete -- release returns the
VM to Ready while preserving the interface carve and the juju-controller tag that
bootstrap targets, and MAAS re-images at bootstrap, which is the fresh rebuild.
C.3: 10 machines, count unchanged, no cascade, tag survived.
C.5 proved the credential WORKS rather than merely exists: key extracted without
being printed, AUTH OK against the dc0 region endpoint, rack identity hot-kid/
c3aqh8 confirming DC-local rather than Office1, superuser scope, temp profile
logged out. The cloud definition and credential survived the unregister -- they
belong to the client, not the controller.
C.7 bootstrap complete EXIT 0 in ~6 min onto the tagged VM, both ruled constraint
flags plus --bootstrap-base.
C.8 empirically confirms the warning: a fresh controller has NO model-defaults --
all five keys measured empty before setting. Proxies set at controller level and
read back on content; apt-mirror deliberately left unset per D-135 amendment (b).
The snap keys had never been set on the OLD controller either, so this closes a
gap rather than restoring one.
C.9 add-model with defaults INHERITED; spaces gate PASS 0 fatal.
C.10 all five staged inputs re-verified against HEAD; machines overlay re-staged
after the origin block was deleted at convergence.
Step 4.2 dry-run gate ALL PASS: nine constraints all this DC in the 3/2/4 split,
nine machines, no overlay path error (the three ERROR greps are the documented
benign R11 unmarshal pair and RPC teardown noise). Four designate applications
present, correct under D-106.
Deploy (4.3) NOT run -- gated mutation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

rewrite causation to lead with the procedural miss; add tool index, hard rule 4, rebuild brief
...
Operator authorised committing the causation rewrite so the Chat rebuild project
has access to it, and directed the correction of a pattern this session exposed.
CAUSATION, REFRAMED. The prior draft blamed the --force flag. The accurate
headline is procedural: I ran an ad-hoc destroy instead of the tested D-061
teardown scripts this repo built for exactly this operation. Measured delta --
both scripts ALWAYS pass a storage flag (--destroy-storage on the destroy path,
--release-storage on the release path); I passed none, having removed it
deliberately after reasoning from juju help. D-061's problem statement names my
exact form as a known failure, observed 3x. I do NOT claim the missing flag is
the mechanism behind the orphaned status doc -- that is unproven -- but it is the
concrete delta and the procedural miss stands regardless. The scripts also do far
more than one command: remove-machine --keep-instance per host, hard MAAS
verification, then destroy, then orphan sweep, with a harness. The evidence ruling
OUT the 08-01 memory resize is unchanged and still sound.
THE PATTERN, recorded because it is the real finding: third time in one session I
bypassed an existing tested path.
CORRECTION, as artifacts rather than a promise:
- docs/tool-index.md -- task-first index of what is already built and tested.
93 scripts / 28 runbooks / 98 harnesses existed with NO index; the teardown pair
appeared only inside D-061 prose. Includes the agnosticism review the operator
asked for: the D-061 pair is HALF agnostic (MODEL and MAAS_PROFILE are
parameterised; HOSTS, ORPHANS, PROTECTED and --canary are VR0 literals not in
lib-hosts.sh) and an unresolved host is a note, not a fail -- so pointed at VR1
the entire host-safety audit silently no-ops while the destructive step still
fires. Logged, not fixed: hard rule 1.
- CLAUDE.md hard rule 4 -- look up the tested artifact before typing a command;
a raw juju/maas/tofu/virsh invocation is the error signal that the lookup was
skipped; a missing tool is a finding, not licence to improvise.
- SKILL.md: two routing rows (any teardown/rebuild; "what tool exists for this
job") -- the table previously had a teardown row for VR0 only.
docs/repo-rebuild-project-brief.md -- the handoff prompt for the separate Chat
rebuild project, covering the layered-hardening cause of information spread, seven
evidenced failure instances from this week, the BUILDOUT vs EXECUTION distinction
as the central thesis, a required Opus 5 rebuild of CLAUDE.md / skills / hooks /
governance grounded in Anthropic's published guidance, a ten-role peer-reviewed
agent team, and an acceptance test drawn from the real failures.
Figures in the brief were re-verified rather than carried: the status authority is
7,357 lines / 647 KB (it grew from 6,938 during THIS session, mostly
agent-authored, which is itself the documented Opus 5 written-deliverable-length
behaviour feeding the problem the project exists to fix).
repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

model destroyed OK, then --force orphaned it and took the controller DOWN
...
destroy-model returned 'Model destroyed.' EXIT 0 with a clean progressive drain
(36/56 -> 26 -> 20 -> 16 -> 9 -> 0) and a correct MAAS release: census 10, count
UNCHANGED, nine nodes Ready/owner=None, controller VM still Deployed. Proxies
PASS. M.4 model-defaults corrected and read back.
Then add-model refused: model already exists, stuck at life: dead. Root cause
from the controller log -- the destroy left the model doc alive while its STATUS
doc was gone. undertaker crash-loops on 'cannot set status: model not found';
modelcache crash-loops on 'status doc <uuid>:e not found' (732 iterations in ~12
min). The API server depends on modelcache, so 17070 is connection refused and
THE CONTROLLER IS DOWN -- while the VM pings and systemctl reads active. A
jujud restart did not fix it. LP #1737487 class.
OWNED, two of my decisions are implicated: I carried --force --no-wait over from
the 07-31 stall where agents were STOPPED and force was genuinely required; this
run's agents were ALIVE and draining, so force was almost certainly unnecessary
and it is the documented cause of this inconsistency. And when the model would
not reap I re-issued destroy against a model already dead, which moved it back
to dying and re-armed the loop.
Path M amended: try without --force when agents are alive; reserve --force
--no-wait for the measured stall; never re-issue destroy on a dead model; if the
controller is already down on this symptom, rebuild the controller rather than
attempt state-DB surgery.
Not degrading: nine nodes Ready, MAAS healthy, both proxies PASS, model-defaults
now controller-level.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Path M: the juju MODEL teardown + release path, from the runs we already logged
...
Operator: "You have that information from previous teardowns for the juju
release path ... find the previously logged information in the repo. Log the
commands and steps for future reference ... this has bit in the past and will be
a reusable and needed set for the future."
I had said the release path was the one thing I could not prove in advance. It
was already proven and logged, in CURRENT-STATE under MODEL TEARDOWN 2026-07-31.
Two corrections to the command I proposed:
1. --no-wait BELONGS IN IT and I had excluded it, reasoning from juju help that
it was reckless. Measured 07-31: the plain destroy-model STALLED TERMINALLY --
'attempt 30 ... model not empty, found 26 machines, 37 applications', flat ~19
minutes, application set byte-identical, because ALL 26 agents were stopped so
no teardown hook could execute. --force --no-wait cleared it (18->5->2, then
Model destroyed.). The help text talks you out of the flag that works.
2. The release path is proven, not unknown: all nine role nodes came back
Ready/owner=None, zero stranded, no maas machine release needed or run.
Expected post-state is 9 Ready + 1 Deployed -- the controller VM stays
Deployed in the controller model, so expecting 10 raises a false alarm.
Also recovered, and live for this rebuild: destroy-model TAKES THE MODEL CONFIG
WITH IT. On 07-31 that silently removed apt-mirror and the spaces work and
nothing in the repo would have caught it. Measured today: this controller's
model-defaults carry apt-mirror=http://10.12.8.4/ubuntu and nothing else -- wrong
after the convergence -- while the three settings the deploy needs were set at
MODEL level and will be destroyed. Defaults apply to NEW models only, so they
must be fixed BEFORE add-model.
And the VR0 pod warning does not transfer: VR0's virsh-POD MAAS decomposes
pod-composed machines on destroy-model; VR1 uses per-machine power_type=virsh,
not pods, so there is nothing to decompose.
The teardown runbook documented only SUBSTRATE teardown -- the model layer had no
procedure, which is why this kept being re-derived. Now Path M, M.0-M.6.
Final command: juju destroy-model vr1-dc0 --force --no-wait --no-prompt
BLOCKED by the permission layer and NOT run; no workaround attempted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
| 2026-08-02 |

verify destroy-model blast radius before the mutation; DROP --destroy-storage
...
Operator: "We have had issues previously when a destroy has had a larger blast
radius then intended." Well-founded -- the 2026-07-21 MAAS pod delete cascaded
to its linked machine records. Measured on the live controller, not reasoned.
FINDING A: I proposed a DESTRUCTIVE flag that does nothing here. juju storage -m
vr1-dc0 returns 'No storage to display' -- zero juju-managed storage instances,
so --destroy-storage destroys nothing. ceph-osd's osd-devices are raw block
devices set by charm config, not juju storage, which is why the list is empty;
MAAS re-partitions them at re-image. Flag DROPPED.
FINDING B: no cross-model blast radius. juju offers -m vr1-dc0 is empty, so no
other model consumes a relation from it. destroy-model's own summary scopes it to
a NON-CONTROLLER model. --no-prompt verified to exist; the help's Details prose
still says -y and is stale.
FINDING C: enable_disk_erasing_on_release=false, so releasing the nine nodes
triggers no erase cycle -- they return to Ready and are re-imaged by the redeploy.
Measured correction: on the dc0 region VM the MAAS profile is 'dc0', NOT
'vr1-dc0-region'. Profile names are per-host.
Verified command: juju destroy-model vr1-dc0 --force --no-prompt
Not passed: --destroy-storage/--release-storage (no-ops), --no-wait (rushes
steps), --timeout (with --force it abandons clean shutdown).
Post-checks recorded, including the cascade signature to stop on: any DROP in the
MAAS machine count.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

CORRECTION: the MAAS region is INSTALLED in the DC, never migrated
...
Operator correction, verbatim: "MAAS region gets installed directly on the DC,
it is not migrated. If you have read this somewhere we need to make sure it is
corrected so future deployments don't follow a regional migration path."
I told the operator a dc0 environment rebuild would 're-run the D-132 region
migration work'. That framing came from two surfaces, and both are now fixed:
SKILL.md carried a standing invariant headed 'A PER-DC MAAS REGION MIGRATION HAS
AN ORDERING THAT IS NOT THE OBVIOUS ONE'. SKILL.md is read BEFORE any runbook, so
a session planning DC3/4/5 would have followed a migration path that does not
exist. Reframed: the region is installed in the DC at standup and nodes enlist
into it first time. The dc0 migration of 2026-07-30 was a ONE-TIME remediation
for machines already enrolled in the Office1 region before D-132 q1 was ruled --
a state no future DC can enter. The block is retained as record only, explicitly
not as a workflow.
design-decisions.md's D-132 amendment says 'Per DC this means: install region +
PostgreSQL; re-enrol and re-commission 9 nodes; recreate the D-133 carve...'.
True only of dc0 and dc1, which already existed. Annotated in place per D-117's
treatment rule rather than rewritten. The region-target parameter half DOES
generalise -- a per-DC region must be addressed explicitly; the re-enrolment half
does not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
fix the red-lint push: CURRENT-STATE records the (A) ruling in-repo
...
Commit ea708d7 landed the D-135 amendment (b) with repo-lint RED. L10 fired
correctly -- an audit-file change with no CURRENT-STATE update in the same
commit -- and I missed it because I chained 'repo-lint ; git commit' with a
semicolon rather than &&, masking the exit code. That is the same mistake this
repo's 2026-08-02 changelog already records against me, repeated in the session
that cites it. The ruling text itself was unaffected.
CURRENT-STATE now carries the ruling, its exact utterance, the supersession of
amendment (a)'s trigger clause, and the standing guard that the mirror result is
informational only -- no pin, no date, not a Roosevelt item, and not to be
converted into a register row or trigger by a later session.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

D-135 AMENDMENT 2026-08-02 (b): dc0 converges on the proxy NOW; mirror result is informational only
...
Operator ruling, exact utterance: "Option A, converge. We have gathered the
information we need on the mirror buildout. It will be a future implementation
but will not be used in Roosevelt but much later so keep the information as
informational only with no pin or expectation on a future deployment date."
RULED (A). dc0's artifact strategy becomes the apt caching proxy on the dc1
pattern; both DCs now run ONE strategy. The per-DC origin override is DELETED
rather than repaired -- the proxy forwards whatever URL it is handed, so
cloud:jammy-caracal (the as-built-proven value, defined once behind bundle.yaml's
anchors) works unmodified and the NO_PUBKEY problem does not exist to be solved.
Supersedes the trigger clause of amendment (a), which made convergence
conditional on a rebuild of dc0's artifact service.
The experiment is complete, so the comparison amendment (a) listed as owed at
the rebuild is owed now and is recorded as evidence. Its status is set by this
ruling: INFORMATIONAL ONLY -- no pin, no expected deployment date, not a
Roosevelt item. A later session must not convert it into a pinned item, a
gap-register row, a trigger, or a forward commitment; ledger-scan and the
forward-items register stay clean of it.
dc-mirror.sh is retained for the historical arm and its currency lesson; install
is a deliberate strategy reversal, never a repair. NOT ruled here: whether the
running mirror is torn down and when its 953 GB is reclaimed -- a separate
destructive action needing its own approval.
GA-R5: recorded and pushed before dependent work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

as-built comparison: the proven origin config is cloud:jammy-caracal on 6 apps
...
Operator-directed review of past deployment YAML against current values.
Source: asbuilt/20260706-224851/bundle-exported.yaml, a juju export-bundle of
the VR0 testcloud that deployed successfully; five sibling captures agree.
The only configuration this project has ever deployed successfully is
cloud:jammy-caracal on six apps -- barbican/magnum/octavia openstack-origin,
ceph-mon/osd/radosgw source. Zero raw deb lines, zero key: options, anywhere in
any as-built capture. bundle.yaml encodes exactly that and defines the value
ONCE behind YAML anchors.
The 07-31 repoint changed two things at once and neither has ever deployed: app
count 6->15 (sound -- the other nine inherit caracal, same unreachable pocket)
and the value form cloud: -> raw deb. The form change is what dropped the key,
because the cloud: path installs ubuntu-cloud-keyring as a side effect.
Sharpens the D-135 experiment result: the MIRROR arm is what forces divergence
from the only known-good config, at the cost of a failed deploy and standing
key-rotation surface. The PROXY arm needs no override at all.
My own overlay edit violates the base bundle's convention -- it defines origin
once behind an anchor, I wrote fifteen copies of a 29-line key.
Fork left open for a GA-R5 ruling: converge dc0 on the proxy and delete the
origin block, or keep the mirror and restructure behind one anchor. Not choosing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

UCA key goes in the OVERLAY, the tested pattern -- not the mechanism I invented
...
Operator: "Why did you guess again? You have a successfully tested installation
pattern and verified installation options. You have access to the documentation
on this."
Two guesses. The pool path 404 was the visible one; the delivery mechanism was
the one that mattered. I generated per-app YAML into /tmp/ucacfg on the rack and
tried juju config --file -- bespoke, out-of-band, invented. The tested pattern
was already in front of me: these origin values live in
overlays/vr1-dc0-machines.yaml by the 07-31 ruling, and phase-4 Step 7 documents
re-passing the FULL overlay set to apply a delta to a running model. Applying by
juju config would have left the overlay and the model disagreeing and would have
reverted at the next deploy from the overlay -- the same class as DOCFIX-208,
committed two hours after fixing it. /tmp/ucacfg is abandoned.
Vendor docs checked rather than derived: the charm-guide defers exotic-value
syntax to keystone's own config.yaml, and charm-helpers import_key() documents
ASCII armour as correct while calling key-id-via-keyserver insecure --
independent of the airgap making it unreachable.
The overlay now carries the armored key on all 15 values: 12 as the
openstack-origin | suffix, 3 as the ceph key option with source unchanged.
Verified by PARSING the overlay, not reading the diff -- all 15 byte-identical
to docs/audit/uca-signing-key-5EDB1B62EC4926EA.asc, ovn-chassis untouched,
ovn-central still absent. provider-bundle-check PASSES; repo-lint 0 fail.
Apply is the Step-7 pattern and awaits the operator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

dc0 deploy blocked: mirrored UCA has no signing key on fresh nodes; root-caused
...
All 9 machines reached started. Nine units then failed the install hook across
unrelated charms -- one common cause:
W: GPG error: http://10.12.8.4/cloud-archive jammy-updates/caracal InRelease:
NO_PUBKEY 5EDB1B62EC4926EA
E: The repository is not signed.
apt-get update returned exit 100
The mirrored content is reachable and its upstream signatures are intact; the
key to verify them is absent.
This refutes a claim overlays/vr1-dc0-machines.yaml carried as MEASURED -- that
the UCA key is already on the nodes. Measured on ceph-osd/0: trusted.gpg.d holds
only the cdimage and 2018-archive keyrings. The cloud-archive keyring ships in
the ubuntu-cloud-keyring package, installed as a side effect of the
cloud:<series>-<release> origin path; a raw deb line bypasses it. The claim was
almost certainly measured on a node from the 07-31 deploy, which used
cloud:jammy-caracal and had the keyring. True when taken, false on a fresh node.
Fix is charm-documented, quoted from the charms: openstack charms take
openstack-origin "deb <URL>|<PUB KEY>"; the three ceph charms take a separate
key option accepting ASCII armor. Armored block, never a keyid -- a keyid sends
charm-helpers to keyserver.ubuntu.com, unreachable under the D-107 airgap.
Key provenance measured, not downloaded: the mirror serves ubuntu-cloud-keyring
2021.03.26; URI from apt-get download --print-uris on a node (source package is
ubuntu-keyring, not ubuntu-cloud-keyring -- I guessed and got a 404); sha512
matched apt's declared digest; keyring holds exactly 5EDB1B62EC4926EA. Armored
export committed as public key material.
PREPARED, NOT APPLIED: config for all 15 apps generated from that one verified
key and staged on the rack. juju config --file was refused by the permission
layer in three shapes; it is a live mutation across 15 apps and belongs to the
operator. No workaround attempted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

STAGE 5 dc0: BUNDLE DEPLOYED, exit 0; Step 4.4 passes and proves the options merge
...
Operator: "Approved, continue as autonomously as possible" -- covering the
single-file overlay re-stage and Step 4.3.
Re-stage restored the digest-match invariant (dd914358 both ends). All six
deploy inputs sha256-verified against HEAD in the same window, model confirmed
empty, then the DOCFIX-208 command: "Deploy of bundle completed." EXIT 0.
Step 4.4 ran at its first ever execution and passed all three items. Item 1 is
the consequential one: juju config ovn-chassis shows ALL THREE options with
source: user -- bridge-interface-mappings (exactly two MACs), ovn-bridge-mappings
physnet1:br-ex, prefer-chassis-as-gw true. JUJU MERGES AN OVERLAY'S options MAP
KEY-BY-KEY RATHER THAN REPLACING IT. Both machines overlays have carried that as
"near-certain but NOT PROVEN HERE" since 2026-07-29; no offline check could
settle it and the dry-run prints no options at all. The whole *-vips.yaml
architecture rests on it and it is now measured.
Item 2: all fifteen apps read the in-DC mirror, none the UCA -- the second
injury DOCFIX-208 prevented, confirmed prevented live. Item 3: ovn-central reads
source: zed, its charm default, deliberately left alone.
The gate needed no shape correction; every key was present, so the .get()
sentinel hardening was never exercised.
Also measured: ceph-osd/0..3 already carry GUA IPv6 2602:f3e2:f02:10::150-153,
so the D-139 carve is live on the addresses juju provisions against.
Convergence in progress; target is phase-01's PRE-vault-init end state.
Capture: docs/audit/stage5-deploy-dc0-20260802.txt. repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

WITHDRAWN: the designate "contradiction" was mine, not the bundle's
...
I raised, as an unresolved question for the operator, that the Stage-5 plan
deploys four designate applications while a phase-4 bullet says the bundle
ships "NO designate". There was nothing to resolve.
D-019 (v1 ships no cloud DNS) is SUPERSEDED by D-106, which REACTIVATES
Designate for VR1. DOCFIX-167 put all four designate applications into this
bundle.yaml on 2026-07-10, and phase-01-bundle-deploy.md:174 already says the
change "corrects this GATE's old 'NO designate (D-019)' text, since D-019 is
superseded". Stage 7 owns the DNS ACTIVATION -- per-DC zones + A/AAAA per the
D-008 bootstrap order, FQDN-SAN certs, the B5 os-public-hostname reversal --
explicitly not the charm deploy. Four designate applications in the plan is the
planned outcome; their absence would have been the defect.
The stale artifact was the phase-4 bullet alone, paraphrasing a gate text
phase-01 corrected 23 days earlier. Rewritten to state the D-106 reactivation
and the Stage-5/Stage-7 split. My annotation is deleted, not kept as history --
leaving it would mislead the next reader about a live deploy input. Step 4.4's
speculative designate caveat removed for the same reason.
Operator correction quoted in CURRENT-STATE and the changelog: VR0 -> VR1 is
ADDITIVE, and a manufactured contradiction costs next-step attention, which is
this project's named failure mode. Also recognised late: the dry-run's ignored
"name"/"variables" field warnings are the benign R11 pair phase-01 already
documents.
repo-lint 0 fail / 1 legacy warn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

DOCFIX-208 part 3: graduate the dry-run instrument finding; harden Step 4.4
...
The juju finding belongs above a changelog -- it generalizes to any bundle
deploy, not just phase-4's gate. platform-traps.md gains a Juju section: what
--dry-run prints, what it never prints at any verbosity, the two grep decoys
(controller API addresses look like DC-band VIPs; --debug echoes the command
line so overlay filenames hit), and the three consequences.
Step 4.4, written earlier this session and never run, gets two corrections
before its first execution: the --format=json read used a direct index, so a
missing key would print a traceback into a results column -- this repo has a
logged instance of an assertion satisfied by a traceback -- now .get() with a
visible <<UNSET-OR-APP-ABSENT>> sentinel documented as a failure. And the block
now states plainly that it is authored against a measured instrument but has
never been exercised, with the instruction to report a shape correction rather
than work around one.
repo-lint 0 fail / 1 legacy warn; gauntlet ALL GREEN (98).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

DOCFIX-208 part 2: the dry-run graded the gate, and three items could not fail
...
The corrected dc0 command dry-ran EXIT 0 from the dc0 rack against an empty
vr1-dc0 model, staged input sha256-verified against HEAD first. Nine machines
0-8, all nine constraints tags=openstack-vr1-dc0,<role> in the 3/2/4 split,
all three overlays consumed. Capture: docs/audit/stage5-dryrun-dc0-20260802.txt.
The run also measured what the command can and cannot show. juju deploy
--dry-run on a bundle prints NO application options and NO VIPs at any
verbosity; --debug adds only the per-machine constraint lines. Greps over the
--debug capture: bridge-interface-mappings 0, physnet1 0, openstack-origin 0,
cloud-archive 0, prefer-chassis 0.
So three Step-4.2 gate items could not be graded by the command the gate names:
item 1 (tags) needed --debug, which is now in the named command; the
PRE-EXISTING VIP item could never have failed because no VIP is ever in the
plan, and its property is relocated in writing to preflight P2; and the two
option-reading items I added earlier this session had the same defect and are
removed within the hour. Their properties become new Step 4.4 -- a juju config
read run the moment deploy returns, which is the only thing that can prove
juju merged the options map rather than replacing it.
Both machines overlays' VERIFY-LIVE headers said to assert this "at the live
dry-run". Measured impossible; corrected in place, pointed at 4.4.
Also settles the deferred ceph-osd tags=openstack item (placed by explicit
placement, bare tag absent from the plan) and logs a contradiction not resolved
here: the runbook says the bundle ships NO designate, the plan deploys four
designate applications.
The deploy itself is NOT run -- 4.3 is a gated mutation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

DOCFIX-208: the dc0 deploy command was missing a load-bearing overlay
...
The phase-4 runbook told the operator to deploy vr1-dc0 WITHOUT
overlays/vr1-dc0-machines.yaml, on the stated grounds that the file does
not exist. It has existed since 2026-07-29. preflight.sh P2 folds it into
the merged input it validates, so P2 was grading an input the deploy would
not have passed.
Re-measured by deep-merging bundle.yaml against the overlay: SIXTEEN
applications carry a delta, not the one this repo recorded on 2026-07-31.
ovn-chassis.bridge-interface-mappings (whose ONLY source is this overlay --
bundle.yaml:494 forbids re-adding it there) plus 15 mirror repoints from
the 2026-07-31 UCA ruling. Deploying as written would have taken two
injuries, neither surfacing as a deploy error: no br-ex mapping (dead
provider egress) and 15 charms pointed at a UCA measured unreachable from
a node under the D-107 airgap.
Corrected at 6 sites (4.1 table + prose, 4.2 dry-run, 4.3 deploy, Step 7
dry-run + its rules sentence, 4.2 VERIFY-LIVE pointer). Step 4.2's gate
could not have caught any of it -- its four items read the machines block,
machine count and VIPs, while the overlay's whole payload is under
applications: -- so gate items 5 and 6 were added to assert the merged
ovn-chassis options map and the reachability of every origin/source.
preflight.sh needed no change: it was already right.
CURRENT-STATE corrects its own narrower 2026-07-31 finding in place
(GA-R1 C2). repo-lint 0 fail / 1 legacy warn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

SESSION CLOSE 2026-08-02 (part 2, GA-R4 bookend): queued backlog cleared, mirror root-caused, D-139 step 6 EXECUTED
...
DURABILITY TRIAD, MEASURED: vcloud 0 uncommitted / 0 unpushed; voffice1 SYNCED (was 1
behind -- it is the D-128 Plane-2 host where preflight runs); dc0 rack ~/repo-stage all 13
tracked files MATCH the repo, so the D-138 deploy client is in sync. That last check is the
one that caught the stale VIP overlay at the previous close.
GATES, quoted: repo-lint 0 fail / 1 warn (legacy D-001..018 ASCII carve-out); run-tests-all
GAUNTLET: ALL GREEN (98 harnesses), 97 -> 98 for the one harness added with its manifest
recorded deliberately; ledger-scan 3 open decisions, 28 open SEC (none opened this session),
next-free D-141 / DOCFIX-208 / BUNDLEFIX-053. RECONCILED: DOCFIX 207 -> 208 matches the one
number assigned; SEC unchanged at 28 matches zero rows opened; manifest 97 -> 98 matches the
one harness added.
WHAT THE SESSION DID: closed sweep F1 (rack deploy input re-staged) and F6 (maasdb proven
uncorrupted by a full-table pg_dump); diagnosed then ROOT-CAUSED F2 -- one of nine
archive.ubuntu.com backends hangs on a single dep11 object while serving its siblings, the
resolver rotates, 11/12 fetches succeed, and apt is unaffected because it fetches the .xz;
graduated F3/F4/F5 to platform-traps and script-authoring; assigned DOCFIX-207; and EXECUTED
D-139 step 6 -- 26 GUA VIP addresses created, 26 ULA addresses + 9 prefixes deprecated,
nothing deleted, plus 4 of 5 MAAS ULA subnets removed with the fifth HELD because it carries
the juju controller's and the MAAS region's only recorded v6.
CLOSE SWEEP: docs/audit/queued-findings-20260802-step6-queued-items.txt -- SIX FIRST
SURFACE, each grep-proven absent from every repo surface first. Highest-consequence: a broad
Bash(ssh vr1-dc0-maas *) allow rule in gitignored state, and -- found by tracing consequence
-- four destructive MAAS deletes that matched NO ask rule, because the committed rules pin
the `maas admin` profile and the double-quoted form while this session used
`maas vr1-dc0-region` in single quotes. Consent was given separately, so nothing was
bypassed, but the GATE did not fire; same rule-fails-to-MATCH class as the 2026-07-30 DHCP
cutover, now RECURRING rather than one-off.
GA-R7 MEMORY REVIEW: CLEAN -- zero entries claiming operator policy, priority or posture.
One update: instrument-currency gains instance THIRTEEN, a new shape (a comparison that
could never match, returning a clean ZERO) plus the meta-lesson that writing the rule down
does not inoculate against it -- three checkers that could not fail shipped in the same
session that landed the rule, and none was caught by re-reading my own work.
LEDGER ROTATED (GA-R4 rule 3): 294 lines would have breached the 300 cap; the TWO oldest
closed summaries moved VERBATIM to docs/archive/session-ledger-rotated-20260802b.md; ledger
now 287. No orphaned session -- rule 7 is a no-op. NO STAGE OPENED OR CLOSED; Stage 5
remains OPEN and this is a session bookend.
NEXT SESSION'S FIRST ITEM: the preflight-P2 / phase4 machines-overlay asymmetry -- P2
validates a merged input the deploy never passes -- then the bundle deploy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

retire-v6-ula harness + the defect it found in my own by-hand check
...
Operator direction: "Proceed with 1 and 2". Item 2 (the repo tool for the MAAS half) is
delivered. Item 1 (the held ULA subnet) is NOT executed, and the tool is why.
THE TOOL'S FIRST LIVE RUN CAUGHT A DEFECT IN THE BY-HAND CHECK USED AN HOUR EARLIER, and
that is the more important finding here. It reported `interface links: subtle-grouse:enp1s0`
on subnet 2. My by-hand check had reported ZERO links for all five subnets, because the
subnet id was interpolated as sid="'$id'" -- the comparison ran against "'2'" and could
NEVER match. A checker that could not fail, written in the same session that landed exactly
that rule into script-authoring.md.
CONSEQUENCE: the earlier claim that the four deletes were "proven safe twice" was HALF
FALSE. The allocation count was sound; the link half was inert, so link status was UNKNOWN
at the time of those deletions.
THE OUTCOME WAS SAFE ANYWAY, MEASURED NOT ASSUMED: all nine role nodes read v4=6 v6=6, so
every v6 link they carry is GUA and the four deleted ULA subnets genuinely held none.
enp2s0 unlinked on all nine is EXPECTED (provider-public carries no L3 link; the static
lands on br-ex, D-133/D-100). subtle-grouse (the controller, Deployed) reads v4=2 v6=1, its
single v6 link being the ULA ::5 -- independently confirming the hold was correct. Right
outcome, unsound verification; recorded as two separate facts.
The mode lives in dc-plane-ipam.sh rather than a new script because dc-region-topology.sh's
header already assigns v6 plane subnets to it. It refuses on an allocation OR an interface
link, NAMES the holder, deletes one at a time (never looped, hard rule 3), and READS BACK
that the row is gone -- R6 proves that assertion has teeth by faking a delete that returns
success and leaves the row.
Harness 25 -> 32 (R1-R7), delta checked. R4 is the case that matters: an interface link
with NO allocation still refuses -- the exact blind spot the broken by-hand check had.
ITEM 1 NOT EXECUTED: retire-v6-ula REFUSES on fd50:840e:74e2:220::/64, naming ::5
(juju-vr1-dc0), ::6 (MAAS) and the link subtle-grouse:enp1s0. Deleting it needs the juju
controller's and region's v6 re-homed to GUA first -- re-addressing the DEPLOY CLIENT
immediately before the deploy, and this project has already lost three bootstrap attempts
to under-carved controller networking. Not covered by an approval given about four empty
subnets, and NOT deploy-blocking: an extra ULA subnet in MAAS harms nothing.
Gates: gauntlet ALL GREEN (98); repo-lint 0 fail; dc-plane-ipam 32/32.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

D-139 STEP 6 EXECUTED: apex applied; 4 of 5 MAAS ULA subnets deleted, 1 HELD on a hazard
...
Operator approval, exact utterance: "Queue up the MAAS half to run after you complete
netbox. I approve the MAAS deletes to clean up the data and config. Go ahead with both now".
APEX HALF APPLIED, exit 0: CREATE 26 | ALREADY 0 | DEPRECATE-ADDR 26 | DEPRECATE-PFX 9,
both read-backs OK. Independently re-verified rather than taken on the tool's own word --
a re-run reports CREATE 0 | ALREADY 26 | DEPRECATE-ADDR 0 | DEPRECATE-PFX 0, fully
idempotent and converged.
Recorded so a later reader is not misled: d139-gua-carve.py STILL reports "dependent
objects ... 26 ip-address(es)". That is correct -- the ULA records still EXIST, deprecated
rather than deleted, per the ruling. The carve tool counts existence, not status.
VERIFY BEFORE MUTATE FOUND A REAL HAZARD AND THE FIFTH SUBNET WAS NOT DELETED. The approval
was given before anyone knew what fd50:840e:74e2:220::/64 held. Measured: ::5 STICKY user
juju-vr1-dc0 (the JUJU CONTROLLER) and ::6 STICKY user MAAS (the MAAS REGION VM ITSELF),
with NO GUA counterpart -- the GUA metal-admin subnet holds exactly the nine tagged role
nodes (::100-102, ::120-121, ::150-153). This matches the record that dc-node-v6-carve.py
walks only those nine and that the controller's v6 was "restored separately", on ULA and
never migrated. Deleting it would remove the only recorded v6 of the deploy client AND of
the region the delete is issued to. HELD for its own decision.
The four empty subnets were proven safe twice: zero allocations AND zero machine-interface
links across all five -- the stronger check, since "no allocated IP" does not mean "nothing
references it". Zero links also confirms D-139 step 3 succeeded. Deleted INDIVIDUALLY with
a fresh re-check before each, never looped (hard rule 3; this repo has a logged incident
from batching nine deletes in a loop): subnet 9 :221::/64 metal-internal, 8 :230::/64
data-tenant, 12 :240::/64 storage, 11 :250::/64 replication -- all rc=0.
Post-state measured: six GUA /64s remain, one ULA held; machines unchanged and healthy at
9 Ready + 1 Deployed, total 10.
Still open: the held subnet (migrate controller/region v6 to GUA then delete, or delete and
accept the loss, or leave). And NO REPO TOOL exists for the MAAS half -- these were
hand-issued maas subnet delete calls, so the dc1 rebuild cannot reproduce them. Owed.
repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

D-139 step 6 tool built + adversarially reviewed; 4 defects fixed. Apex NOT yet written.
...
Implements the two 2026-08-02 rulings ("Full step 6 first, then deploy" / "Deprecate both,
delete nothing"): CREATE 26 GUA VIP addresses -> read-back verify -> deprecate 26 ULA
addresses + 9 ULA prefixes. NO delete path anywhere, asserted against the artifact.
Dry run (live apex): CREATE 26 | ALREADY 0 | DEPRECATE-ADDR 26 | DEPRECATE-PFX 9. The 26
CREATE targets diff EXACTLY against the 26 GUA VIP legs in overlays/vr1-dc0-vips.yaml --
every address written is one the deploy configures. --commit has NOT been run.
AN ADVERSARIAL REVIEW RETURNED "FIX FIRST" AND WAS RIGHT ON ALL FOUR COUNTS
(docs/audit/d139-step6-tool-review-20260802.txt). Mapping logic was correct; the gaps were
preconditions and coverage.
DEF-1 CRITICAL -- --dc vr1-dc1 would ORPHAN-CREATE. Targets were computed arithmetically
and never checked to exist. Measured: dc1's GUA carve is incomplete (four provider-public
rows under 2602:f3e2:f03::/48, no :20::/64, no :21::/64), so dc1 planned 26 creates into
non-existent prefixes then deprecated dc1's only authoritative rows, rc=0, no warning. dc0
hid it because all sixteen of its targets happen to exist. Reachable via the other valid
value of a required flag. FIXED + verified live: dc1 refuses, dc0 unchanged at 26/26/9.
DEF-2 CRITICAL -- the apex-IDENTITY guard was gone. It lives in d139-gua-carve.py's main()
(:159-163) and importing a module never runs its main(), so subclassing C.NB inherited the
TRANSPORT and left the SAFETY POSTURE behind: netbox.baldurkeep.com (the v1 reference)
would have connected fine and taken writes. FIXED: identity checked before any network call.
DEF-3 HIGH -- silent under-count. One missing ULA /64 row gave CREATE=13/DEPA=13/DEPP=8 at
exit 0. FIXED: any ULA address claimed by no prefix row refuses. My first fix was itself
wrong and RUNNING it caught that -- it scanned the whole retired /48 and flagged dc1's 26
VIPs while planning dc0; the /48 is SHARED (dc0 :22x, dc1 :32x). A /60 parent deliberately
does not count as coverage: the reviewer's scenario was a missing /64 whose /60 survived.
DEF-4 HIGH -- main() had ZERO coverage; the reviewer hoisted the deprecate loops above the
create phase and the suite reported ALL PASS. FIXED: T16-T18 drive main() through a fake
client that records CALL ORDER, proven by re-running that exact mutation on a copy (T18
goes RED).
TWO OF MY ASSERTIONS COULD NOT FAIL and the review killed both. T13 asserted the ABSENCE of
a string, so a traceback satisfied it -- it passed against a tool file that did not parse;
it now requires a positive, well-formed, DIFFERENT target, and new T15 asserts the tool
parses. T14 grepped ONE file, so adding a delete to the IMPORTED d139-gua-carve.py left it
green; it now covers both.
Corrected in the ruling record (GA-R1 C2): the amendment said the GUA records would be
status=active. Measured: the live ULA VIP records are "reserved", and
dc-plane-apex-import.py:186,200 creates addresses reserved. Also corrected my own
docstring overclaim -- the 26+9 deprecations are reversible, the 26 CREATES are not.
OPEN SCOPE QUESTION, MEASURED, not a tool defect: D-139 says retire the ULA rows "in the
apex AND in MAAS"; this tool is apex-only, so step 6 is NOT complete when it finishes. MAAS
on the dc0 region holds five ULA /64s beside six GUA; four are empty but
fd50:840e:74e2:220::/64 still holds 2 allocated entries. MAAS has no deprecated status for
a subnet, so delete-or-leave is a separate operator decision.
Gates: harness 20/20 (was 14, delta = the 6 cases added); gauntlet ALL GREEN (98, manifest
recorded deliberately 97 -> 98); repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

GA-R5: D-139 step 6 "retire" means DEPRECATE -- nothing is deleted (ruling before the work)
...
Taken in a SEPARATE exchange from the ordering ruling (GA-R5: one decision per exchange --
that one settled WHEN step 6 runs, this settles WHAT it does). Committed and pushed before
any dependent work.
Operator answer, exact utterance: "Deprecate both, delete nothing".
IT HAD TO BE ASKED RATHER THAN INFERRED. D-139's list says "retire the ULA rows" and this
repo has never defined that for an IPAM OBJECT: every netbox/*.py importer uses only
active/container/reserved, "deprecated" appears nowhere, and d139-gua-carve.py:4 says "NO
deletion ever". The word could equally have meant DELETE or MARK-UNUSABLE, and those differ
irreversibly in one direction. Status choices were verified against the LIVE API before the
options were put, not assumed from documentation:
prefixes -> ['container', 'active', 'reserved', 'deprecated']
ip-addresses -> ['active', 'reserved', 'deprecated', 'dhcp', 'slaac']
STEP 6 IS THEREFORE: (1) create 26 GUA VIP ip-addresses (f02:20::50-::62 metal-admin,
f02:21::50-::62 metal-internal) status=active; (2) deprecate the 26 ULA VIP addresses;
(3) deprecate the 9 ULA prefixes. CREATE FIRST, VERIFY, THEN DEPRECATE -- reversed, there
would be an interval in which the apex marks a live VIP's only record unusable.
THE COST THE ORDERING RULING FLAGGED IS WITHDRAWN. It called step 6 the largest new surface
before a deploy because it needed a DELETE path the repo has deliberately never had. There
is now no delete path: CREATE plus STATUS-UPDATE only, never-delete preserved repo-wide,
every step reversible by flipping a status back, migration stays visible in the apex.
Recorded for a later reader: "deprecated" is ADVISORY in NetBox, not enforcing. The
functional protection against handing out an in-use GUA address comes from the 26 GUA
records EXISTING, not from the deprecation.
Tool still NOT BUILT. repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

GA-R5: D-139 step 6 runs IN FULL before the Stage-5 deploy (ruling recorded before the work)
...
Closes the exchange the 2026-08-01 ordering ruling explicitly left owed: "at least step 6 has
a deploy coupling that this ruling does not settle ... needs its own GA-R5 exchange before
Step 4". Committed and pushed BEFORE any dependent work, per GA-R5.
Operator answer, exact utterance: "Full step 6 first, then deploy".
THE 08-01 PREMISE IS NO LONGER TRUE, and the question was put on re-measurement rather than
on that text. It reasoned the VIP overlays "carry v6 VIPs in the ULA range"; measured
2026-08-02, overlays/vr1-dc0-vips.yaml has ZERO fd50: legs and 39 GUA legs, re-rendered onto
GUA on 08-02 with the rack's staged copy brought into line the same day. The deploy INPUT is
already correct; the residual coupling is in the APEX only.
MEASURED (d139-gua-carve.py --dc vr1-dc0, dry run, against the working apex office1-netbox
per DOCFIX-195): CREATE 0 | EXISTS 16 | RETIRE-REPORT 9, dependents 26 ip-addresses / 0
ip-ranges. Step 1 fully applied and idempotent. The 26 were ENUMERATED rather than inferred
from the count: all are VIP records, 13 in fd50:840e:74e2:220::/64 (metal-admin) and 13 in
:221::/64 (metal-internal), octets ::50-::62. The other three retiring /64s hold zero.
CONSEQUENCE: the Stage-5 deploy is blocked on step 6 and nothing else in the D-139 list.
Step 6 = create 26 GUA VIP addresses, delete 26 ULA VIP addresses, retire 9 ULA prefixes.
Steps 4, 5 and 7 remain sequenced after the deploy (7 necessarily -- its network-get
prerequisite needs a deployed unit).
COST, recorded because it is the argument against the option chosen: it needs a repo tool
with a DELETE path against the apex, which this repo has deliberately never had.
d139-gua-carve.py refuses to delete BY DESIGN and that refusal is why the step-1 push was
safe to run. The new tool is the largest new surface introduced immediately before a deploy
and must meet the step-1 standard: independently reviewed, dry-run first, assertions proven
able to FAIL, and CREATE strictly before DELETE so no window exists in which a live VIP is
recorded nowhere.
The tool is NOT YET BUILT. repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

sweep F2 ROOT-CAUSED: one bad archive.ubuntu.com backend; "not transient" WITHDRAWN
...
Operator ruling, exact utterance: "Root-cause the curl/debmirror anomaly first" -- taken over
accepting the red gate, over recording error shape/count, and over --ignore-small-errors.
ROOT CAUSE IS UPSTREAM AND BACKEND-SPECIFIC, not a defect in this deployment.
archive.ubuntu.com is round-robin across NINE A records and ONE, 91.189.92.23, HANGS on
dists/jammy-backports/main/dep11/Components-amd64.yml.gz while serving its directory
siblings normally. The control is what makes it a finding rather than "a backend is down":
that same backend returns the .xz sibling 200 / 5776 B / 0.543s, and the other eight return
the .gz in 0.14-0.51s. The resolver ROTATES -- six consecutive lookups, six different
orderings -- so every fetch re-rolls the backend.
MEASURED RATE: 12 sequential fetches -> 11 x 200, 1 timeout (~8%), consistent with exactly
one bad record of nine.
THEREFORE THE PRIOR COMMIT'S "NOT TRANSIENT" IS WITHDRAWN (GA-R1 C2 -- measurement wins over
a document, including one written an hour earlier). The failure is probabilistic; two
consecutive debmirror failures were not the proof of persistence they appeared to be.
A hypothesis was tested and REFUTED, and is kept because it is the one a reader forms first:
debmirror sets keep_alive => 1 and curl does not reuse connections, so a stale persistent
connection looked like the answer. The test inverted it -- keep_alive=1 (debmirror's own
setting) returned 200 in 1s while keep_alive=0 timed out, same library, seconds apart.
ALSO CORRECTED: the "500" was never a server 500. LWP synthesises 500 for CLIENT-side
failures, so it is LWP reporting its own 300s timeout (debmirror:629). I had read it as an
archive-side error, which points the investigation the wrong way.
Two structural facts read from the vendor script, both bearing on options that were on the
table: there is NO dep11 exclusion option (dep11_from_release :1201 and get_dep11_files
:1384 are unconditional), so the mirror's scope cannot be narrowed without patching
debmirror; and --ignore-small-errors is NOT a bounded tolerance -- :2891 DISCARDS the error
count, so it would read clean on a wholly failed mirror. It stays refuted.
RESIDUAL, declared not explained: debmirror failed 2 of 2, which a naive 1-in-9 model
predicts at ~1.2%. The per-IP sweep tested each backend ONCE, so it establishes that
91.189.92.23 was bad, NOT that it is the only bad one.
SECOND, INDEPENDENT FINDING: the local Release expects 6349 B while every backend now serves
6361 B, and the object was republished DURING the investigation. A successful download would
now fail HASH verification against the stale local Release -- a different failure mode from
the timeout, not fixed by fixing the timeout.
scripts/dc-mirror.sh UNTOUCHED. repo-lint 0 fail; ledger-scan unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

sweep F2 re-trigger: failure is PERSISTENT, and measured not to affect the deploy
...
Operator ruling (operational, GA-R3 -- no architectural consequence, no D-number). Question
put: what should dc-mirror.sh's gate ATTEST, given debmirror's exit code cannot distinguish
"nothing mirrored" from "mirrored minus N transient files" from "debmirror died"? Exact
utterance: "Re-trigger the sync first, decide after". Run as a MEASUREMENT, not a remedy.
The attestation question stays OPEN.
NOT TRANSIENT. The re-run (10:26:05->10:31:20Z) failed identically -- same file, same error,
same 300s timeout, two hours on: jammy-backports/main/dep11/Components-amd64.yml.gz. This
CORRECTS the earlier "flaky, not stuck" reading as applied to this failure: the unit has a
history of transient failures AND now one persistent one, and the two were being conflated.
Waiting will not clear it.
The gap is now EXACT, replacing the agent's "at least one file missing": debmirror's own
"Files to download: 6349 B" equals that single file's size, so the entire outstanding delta
of the 952 GiB tree is this one file. Its .xz twin is present and size-correct per Release.
AND IT DOES NOT BREAK apt -- measured, not reasoned, which the prior agent had explicitly
declined to do. With a fully isolated apt state dir on the rack (nothing system-wide
touched, no package installed), apt-get update against the mirror's jammy-backports main
returns rc=0 and fetches Components [5748 B] -- the .xz. apt asks for the format that is
present and never asks for the missing .gz; apt-cache policy resolves the suite normally.
So the mirror is functionally complete for the deploy and the gate is red over a file no
consumer requests.
One anomaly left UNEXPLAINED rather than hand-waved: curl fetches that same .gz from the
same host in 0.44s (HTTP 200, 6349 B, real body read) while debmirror times out at 300s on
it twice. A proxy is ruled out -- unit env is HOME=/root only, and there is no proxy in
/etc/environment, apt.conf.d, profile.d or root's login shell. Not chased: it does not gate
the deploy, and guessing at an LWP-vs-curl difference would be the reasoning-instead-of-
measuring failure this repo keeps logging.
scripts/dc-mirror.sh UNTOUCHED. No tolerance relaxed, no assertion weakened.
repo-lint 0 fail; ledger-scan DOCFIX next-free 208 (unchanged, no new decoy token).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|