diff --git a/.claude/skills/openstack-cloud-ops/references/platform-traps.md b/.claude/skills/openstack-cloud-ops/references/platform-traps.md index 8d97a81..b2dc533 100644 --- a/.claude/skills/openstack-cloud-ops/references/platform-traps.md +++ b/.claude/skills/openstack-cloud-ops/references/platform-traps.md @@ -550,6 +550,43 @@ session: a tool that REFUSES on "found nothing" surfaces this immediately, where one that treats zero matches as "nothing to do" would have written nothing and reported success. +## Juju -- `deploy --dry-run` on a bundle shows FAR less than a plan reader expects + +**MEASURED 2026-08-02, juju 3.6.27, a 56-application bundle plus three overlays** +(`docs/audit/stage5-dryrun-dc0-20260802.txt`). This generalizes past any one gate: if you +are writing an assertion against a bundle dry-run ANYWHERE in the chain, check first that +the property is even printed. + +`juju deploy --overlay ... --dry-run` prints ONLY: + +- `Located charm "" in charm-hub, channel ` +- `- upload charm ... / - deploy application ...` +- `- add new machine N` -- **the BARE id, with no constraints** +- `- add unit /N to ` / `- add lxd container ...` +- `- add relation a:x - b:y` + +It does **NOT** print application options, VIPs, or machine constraints. Adding `--debug` +adds exactly ONE thing: the per-machine constraint lines (`bundlehandler.go:1035`, +`with constraints "arch=amd64 tags=..."`). **Option values are absent at every +verbosity** -- greps for `bridge-interface-mappings`, `physnet1`, `openstack-origin`, +`cloud-archive`, `prefer-chassis` all returned 0 on the `--debug` capture. Beware two +decoys when grepping: the controller's own API addresses look like VIPs from the DC's +band, and `--debug` echoes the full command line, so the overlay FILENAMES appear as hits. + +**Consequences to carry:** + +1. **A dry-run cannot verify an overlay's `options` merged.** Whether juju merges an + `options` map key-by-key or REPLACES it is invisible here -- and no offline merger can + settle it either, because a hand-written deep-merge merges key-by-key by construction + and so cannot distinguish the two behaviours. **The only instrument is `juju config + ` on the deployed model.** Run it the moment `deploy` returns, before convergence, + while a `juju config` correction is still cheap. +2. **If your gate reads tags/constraints, `--debug` is load-bearing, not noise.** A gate + naming a bare `--dry-run` while asserting on tags cannot fail. +3. This is how three assertions in one repo gate came to be ungradable -- one for months, + two written and removed within the same hour. Assert what the command PRINTS; measure + the print, do not assume it from the command's name. + ## Verbatim error -> cause index (grep this first) | Exact string you see | Cause | Where | diff --git a/docs/changelog-20260802-deploy-input.md b/docs/changelog-20260802-deploy-input.md index 14b73a2..63f5d8f 100644 --- a/docs/changelog-20260802-deploy-input.md +++ b/docs/changelog-20260802-deploy-input.md @@ -194,6 +194,38 @@ Both recorded in `docs/CURRENT-STATE.md`, neither actioned. +## Item 5 -- the instrument finding GRADUATES out of the changelog + +`references/platform-traps.md` gains a Juju section: what a bundle +`--dry-run` prints, what it does not print at any verbosity, the two grep decoys +(the controller's own API addresses look like VIPs from the DC's band; `--debug` +echoes the command line so overlay FILENAMES hit), and the three consequences -- +a dry-run cannot verify an options merge, `--debug` is load-bearing for any +tags/constraints assertion, and `juju config` on the deployed model is the only +instrument that settles the merge question. It generalizes past this repo's +phase-4 gate to any bundle deploy in the chain, so a changelog was the wrong +home for it (standing rule: durable observations graduate to their repo surface). + +- **Revert:** drop the section. Not recommended -- it is the reusable half. + +## Item 6 -- Step 4.4's own read hardened, and its unproven status stated + +Two corrections to the block written in item 3, both before it ever runs: + +- The `--format=json` read indexed `["settings"][key]["value"]` directly. A + missing key raises `KeyError`, and **a traceback printed into a results column + reads like output** -- this repo has a logged instance of an assertion being + satisfied by a traceback. Now `.get(...)` with a visible + `<>` sentinel, documented as a FAILURE rather than a blank. +- **Step 4.4 is AUTHORED, NOT EXERCISED.** It is written against a measured + instrument but has never run -- the model was empty when it was written, so its + first execution is at deploy time. Stated in the runbook itself, with the + instruction to report a shape correction rather than work around one, and a + note that `designate` appears in its loop while an unresolved contradiction + says the bundle should not carry designate at this stage. + +- **Revert:** restore the direct index. Do not. + --- **Gates at delivery:** `bash scripts/repo-lint.sh` -- 0 fail, 1 warn (the legacy diff --git a/runbooks/dc-dc-phase4-juju-bundle-per-dc.md b/runbooks/dc-dc-phase4-juju-bundle-per-dc.md index ef0c018..ea6d522 100644 --- a/runbooks/dc-dc-phase4-juju-bundle-per-dc.md +++ b/runbooks/dc-dc-phase4-juju-bundle-per-dc.md @@ -722,14 +722,33 @@ for app in barbican cinder designate glance keystone magnum neutron-api \ nova-cloud-controller nova-compute octavia openstack-dashboard placement; do printf '%-24s %s\n' "$app" "$(juju config "$app" -m "${DC_MODEL}" --format=json \ - | python3 -c 'import json,sys; print(json.load(sys.stdin)["settings"]["openstack-origin"].get("value"))')" + | python3 -c 'import json,sys; s=json.load(sys.stdin).get("settings",{}); print(s.get("openstack-origin",{}).get("value","<>"))' \ + 2>&1 | tail -1)" done for app in ceph-mon ceph-osd ceph-radosgw; do printf '%-24s %s\n' "$app" "$(juju config "$app" -m "${DC_MODEL}" --format=json \ - | python3 -c 'import json,sys; print(json.load(sys.stdin)["settings"]["source"].get("value"))')" + | python3 -c 'import json,sys; s=json.load(sys.stdin).get("settings",{}); print(s.get("source",{}).get("value","<>"))' \ + 2>&1 | tail -1)" done ``` +> **Why `.get(...)` and a sentinel rather than a direct index.** A missing key +> would raise a `KeyError`, and a traceback printed into a results column reads +> like output -- this repo has a logged instance of an assertion being satisfied +> by a traceback. `<>` is a VISIBLE failure. Treat it as a +> failure, not as a blank: it means either the charm never got the option or the +> application is not in this model. +> +> **THIS BLOCK IS AUTHORED, NOT YET EXERCISED (2026-08-02).** It is written +> against a measured instrument -- `juju config` is what holds the merged +> configuration -- but it has never run, because the model was empty when the +> gate was written. Its FIRST execution is at this deploy. Expect to correct its +> shape, not its intent; report a correction rather than working around it. Note +> in particular that `designate` appears in the loop while a logged, unresolved +> contradiction says the bundle should not carry designate at this stage -- if +> that resolves the other way, drop it from the list rather than reading the +> sentinel as a fault. + **GATE:** 1. **`ovn-chassis` carries all THREE options.** `bridge-interface-mappings` (from `overlays/${DC}-machines.yaml`) **alongside** `ovn-bridge-mappings: