diff --git a/.claude/hooks/guard-destructive.py b/.claude/hooks/guard-destructive.py index 4b66798..400fef1 100644 --- a/.claude/hooks/guard-destructive.py +++ b/.claude/hooks/guard-destructive.py @@ -24,8 +24,22 @@ "prints the MAAS API key (DOCFIX-016); use 'maas admin ...' directly"), (r"git\s+push\s+(--force|-f)\b", "force-push is banned on this repo"), - (r"(cat|less|more|head|tail|cp|scp|base64|xxd|od|strings)\b[^|;&]*" - r"(vault-init/|as-executed/|-cred\.txt|appcred)", + # strong readers: match through quotes (a quoted path is still a file read). + # Paths: secret dirs, home-anchored tenant handover dirs, cred/appcred + # files, and private-key .pem files. + (r"(cat|less|more|head|tail|cp|scp|base64|xxd|od|strings|python3|openssl)\b[^|;&]*" + r"(vault-init/|as-executed/" + r"|(?:~|\$HOME|\$\{HOME\}|/home/[\w.-]+)/tenant-[\w.-]+/" + r"|-cred\.txt|appcred|\.pem\b)", + "secret-adjacent file: never read key/cred material into context (whitelist-print rule)"), + # text tools (grep/awk/sed): block BARE-path reads of the same secret set, + # but not quoted mentions -- searching source/prose for a string that merely + # names a .pem or secret dir must stay allowed (the token must start at a + # word boundary after whitespace/=, which a quote character breaks). + (r"(grep|awk|sed)\b[^|;&]*(^|[\s=])[\w./~${}-]*" + r"(vault-init/|as-executed/" + r"|(?:~|\$HOME|\$\{HOME\}|/home/[\w.-]+)/tenant-[\w.-]+/" + r"|-cred\.txt|appcred|\.pem\b)", "secret-adjacent file: never read key/cred material into context (whitelist-print rule)"), (r"rm\s+-rf\s+(/|~)\s*$", "catastrophic rm"), diff --git a/clientdocs/sweep-receipt.txt b/clientdocs/sweep-receipt.txt index 15d3985..6363c45 100644 --- a/clientdocs/sweep-receipt.txt +++ b/clientdocs/sweep-receipt.txt @@ -34,7 +34,7 @@ 30919b61b2b79b33a853a29bc814558d7e0e940a3040b2f7710ea9a14333b71a runbooks/appendix-C-identity-rbac.md 8510402e5375a6d42cc47f28eec9698a23d88e04b22ef70ef6e129e1179f3d32 runbooks/appendix-D-magnum-trust-model.md 927aa7261aa2d9fabe2cb7342499e6c0d0840a93ffbb323ee7003ac8eefe14a7 runbooks/tenant-onboarding-v2-DRAFT.md -a6080e4d4207a0a47d0ce9e9f54f5b7b3235e0dbf96b00eb2fbc2238df9895ff scripts/tenant-acceptance.sh +b8ad0768a1cad3e9a3e329f3cda39e6a151b80c0df5f0402efb65cb290b6b0b3 scripts/tenant-acceptance.sh ad00be489480069d52df1b978be0360d7fb04dbf954a8cf487162872f2da0593 scripts/tenant-assert.sh -0b09bbc3de1927e299eb8650ea901c9f1939abc097e717aa70a049079b409edd scripts/tenant-offboard.sh -3f5a3656ef8ebfc0ef9370b1ffd416ee98e40640c803b8bad8492e80ee289051 scripts/tenant-onboard.sh +45f338d88c816d01a4f0b33f2e3ede1f4b081e701efad1508e9d8e3d102e0485 scripts/tenant-offboard.sh +08f9540cc57d325ebe9d62a44eab3aaf27fb327e06008a1edb28505a14961af7 scripts/tenant-onboard.sh diff --git a/docs/script-quality-findings-20260707.md b/docs/script-quality-findings-20260707.md new file mode 100644 index 0000000..bcbc347 --- /dev/null +++ b/docs/script-quality-findings-20260707.md @@ -0,0 +1,141 @@ +# Script-quality findings register -- read-only sweep, 2026-07-07 + +**What this is.** The deferred-findings register from the 2026-07-07 read-only +script-quality sweep, recorded so the items survive session compaction. Nothing +in this file has been executed; every fix listed here is a DOCFIX **candidate** +to be delivered later under the standard change-delivery loop (harness green, +repo-lint clean, changelog entry with revert). No identifier numbers are +assigned in this register -- the batch that fixes an item assigns its number +via the next-free rule at delivery time. + +**Sweep coverage.** 110 files reviewed (scripts/, tests/, .claude/hooks/). +Tools available on the review host: bash and python3 only; shellcheck and +pyflakes were absent, so findings come from manual read plus targeted fixture +probes, not static analysis. + +**Batch note.** The sweep's E/R-class items approved for immediate fix were +implemented in the fix batch of this date and are NOT listed here, except the +two previously-known items marked FIXED-THIS-BATCH below. + +## Deferred findings (R-class; fixes are DOCFIX candidates) + +### R5 -- lib-validate.sh: vr_json tempfile leak +`vr_json` (scripts/lib-validate.sh, ~line 52) creates `_VR_ERR="$(mktemp)"` on +every call and never removes it. `vr_err_tail` legitimately needs the file +after return, but nothing cleans it up afterwards: one leaked tempfile per +`vr_json` call, per check, per validation run. Fix shape: a single per-process +stderr file reused across calls, or an EXIT trap in the library's sourcing +contract. + +### R6 -- `| head -1` SIGPIPE / extract-then-check class in phase-05/06 +Live-pipe `openstack ... | head -1` captures that use the fragment without +whole-output validation: +- scripts/phase-05-amphora-pipeline.sh:62 (`image list --tag ... | head -1`) +- scripts/phase-05-amphora-pipeline.sh:66 (`image list --name ... | head -1`) +- scripts/phase-06-mgmt-vm.sh:106 (`port list --server ... | head -1`) +- scripts/phase-06-mgmt-vm.sh:108 (`floating ip list --port ... | head -1`) +Same class the house style forbids (SIGPIPE race on the producer; a partial +failure yields a plausible-looking fragment). Fix shape: capture whole output, +validate shape (uuid regex), then select the first row. + +### R7 -- phase-07-conductor-graft.sh: remote tempdir leak +The helm-install payload run on the conductor (~line 153) does +`D=$(mktemp -d); cd "$D"` and never removes the directory: one leaked tempdir +(plus tarball) on the remote unit per graft run. Fix shape: trap cleanup inside +the remote payload. + +### R9 -- cloud-assert.sh --capture: `--long` + silent empty images.json +Line ~184: `openstack image list --long -f json "$DIR/images.json" +2>/dev/null || true`. Two defects: (a) the deprecated `--long` flag only adds +stderr noise (and `2>/dev/null` then masks REAL errors, against the +stderr-separation rule); (b) a failed list silently commits an empty/missing +images.json into the captured BOM. Fix shape: drop `--long`, stderr to a +tempfile surfaced on failure, fail the capture loudly if the JSON is empty or +unparseable. + +### R11 -- harness tempfile leaks +Harnesses that mktemp without a trap cleanup (leak per run): +- tests/carve-host-interfaces/run-tests.sh +- tests/lib-validate/run-tests.sh (also amplifies R5: every vr_json case leaks) +- tests/phase-00-maas-standup/run-tests.sh +Fix shape: the standard `W=$(mktemp -d); trap 'rm -rf "$W"' EXIT` pattern the +other 34 harnesses already use. + +## Previously-known items -- FIXED-THIS-BATCH + +- **tenant-offboard.sh Phase B counter (FIXED-THIS-BATCH).** Logged in the + changelog (2026-07-06 addendum 28) as FOUND-NOT-FIXED: the residual-trust + sweep incremented SWEEP_FAIL inside a `printf | while` pipeline subshell, so + failed trust deletes could never raise exit 22. Fixed in this date's batch + (here-string loop; regression case in tests/tenant-offboard). +- **tenant-offboard.sh hardcoded auth URL (FIXED-THIS-BATCH).** The known + hardcoded-OS_AUTH_URL class (house rule 3): tenant_env now reads `auth_url=` + from the cred file with the literal demoted to a KEYSTONE_VIP-overridable + fallback. NOTE: the tenant-acceptance.sh foil_env sibling of this class + (addendum 28 FOUND-NOT-FIXED list) remains OPEN and is NOT covered by this + batch. + +## Consolidation proposals (S-class; S1-S8) + +Transferred verbatim from the sweep report at integration (2026-07-07); +effort estimates are the sweep's (S/M/L). Operator has NOT ruled on S1-S5/ +S7/S8 -- proposals only. + +- **S1 (M)** The unset-OS_*/source-env dance is hand-rolled ~15x across + tenant-onboard (8 subshells), tenant-offboard (2), tenant-acceptance (3), + d063-apply (1), tenant-assert (2) -- while scripts/lib-validate.sh already + ships tested vr_scrub_os / vr_admin_env / vr_tenant_env. Proposal: source + lib-validate.sh from the tenant-* family + d063-apply; add one + `vr_appcred_env ` for the app-cred variant. (The offboard tenant_env + auth-url bug was fixed narrowly in this batch; this proposal remains the + wider consolidation.) +- **S2 (S)** run() capture-then-test helper duplicated with eval-counter + variants in tenant-offboard.sh and d063-apply.sh vs the rc-returning + lib-validate run(). Consolidate; count failures at call sites. +- **S3 (S)** is_id() defined in 4 scripts (tenant-onboard, tenant-offboard, + d063-apply, tenant-assert); lib-validate already has vr_is_hex32. Ride + along with S1. +- **S4 (M)** The trust-list stdout-only capture + inline-python filter block + is near-identical 3x (tenant-offboard x2, tenant-acceptance x1) and is + inline python-in-bash beyond one-liner scale (house rule: .py files). One + fixture-tested scripts/trust_filter.py would serve all three. +- **S5 (S)** Kube-image UUID resolution block (json capture + jq kube filter + + tempfile + 36-hex validation) appears 3x inside tenant-onboard.sh. One + local function. +- **S6 (no action)** cap()/oerr() triplicated across clientdocs/scripts/ + {smoke-test,acceptance-run,ci-cleanup-sweep} -- CORRECT as-is: handover + artifacts must be standalone off-jumphost. Worth a header comment noting + the duplication is deliberate. +- **S7 (S)** Dead code: checks/d011-04 CF assigned never used; checks/d011-03 + vr_is_ipv4-or-VHOST first test subsumed by second. (The other two S7 items + -- offboard no-op is_id, validate.sh lost line-rewrite -- were FIXED in + this batch.) +- **S8 (S)** Move the KEYSTONE_VIP default and the D-003 FIP pool bounds into + lib-net.sh as tagged constants (the literal is duplicated in 4 files: + tenant-onboard, phase-03-admin-openrc, phase-06-kubeconfig-gate, + phase-06-k8s-bootstrap; FIP pool literals duplicated across the two + phase-04-network scripts; provider-bundle-check.py re-states plane CIDRs + as a second source of truth -- python cannot source bash, needs a + generated or parsed form). + +## Found during the fix batch (this date; logged, NOT fixed -- out of scope) + +- **tenant-offboard.sh Phase A cluster captures merge stderr.** The + tenant-scope `coe cluster list` captures (inventory ~line 160; delete loop + and wait loop in Phase A) still use `2>&1`: a stderr warning line would + word-split into `coe cluster delete` argv and keep the wait loop from ever + seeing empty (budget exit 21). Same class as the Phase C fix; was not in the + approved batch scope. The offboard harness deliberately excludes coe + commands from its stderr-noise scenario until this is fixed. +- **Merged-stderr `domain show`/`token issue` captures (fail-closed class).** + tenant-offboard.sh (~line 133, sweep-mode preamble) and tenant-onboard.sh + stage subshells validate `$(... 2>&1)` captures by shape/equality. Failure + direction is CLOSED (safe), but a benign stderr warning on a noisy CLI would + abort with a false precondition/auth failure. Cosmetic-risk cleanup + candidate, same stdout-only pattern. +- **tenant-offboard.sh Phase E0 app-cred inventory merges stderr.** Line ~267 + tests rc (failures route correctly), but a success-with-warning would + word-split the warning into the display-only cascade-inventory lines. + Display pollution only; no delete argv exposure. + +ASCII + LF. diff --git a/docs/session-ledger.md b/docs/session-ledger.md index 69df189..0224291 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -21,7 +21,7 @@ ## Machine-derived (re-seed from `scripts/ledger-scan.sh`; do not hand-edit) -_As of 2026-07-06 (post-addendum-27, d011-batch3 window close; re-run the scan to refresh):_ +_As of 2026-07-07 (post-addendum-33; re-run the scan to refresh):_ - **PROPOSED / OPEN decisions:** D-068 (Vault substrate hardening, Roosevelt -- 1.16 ruled out per amendment; off-EOL path OPEN), D-071 (routine update cadence + @@ -31,8 +31,8 @@ - **OPEN security rows:** SEC-001 (rotate at v1 close, re-ruled 2026-07-06), SEC-003 (custodians + second-person unseal -- DEFERRED; keeps d011-06 MANUAL), SEC-004 (flip repo to private at v1 close -- deferral reaffirmed). -- **Next-free numbers:** D = 075, DOCFIX = 112, BUNDLEFIX = 012. - (Scan and the addendum-32 changelog next-free pointer agree.) +- **Next-free numbers:** D = 075, DOCFIX = 118, BUNDLEFIX = 012. + (Scan and the addendum-33 changelog next-free pointer agree.) --- @@ -271,6 +271,42 @@ - **Open (small):** Horizon manual checks on foil1 (operator, browser, non-blocking); capi-test-1 CREATE_FAILED leftover (orphan-sweep candidate); beta-cluster lbtest leftover; repo-lint worktree-exclusion DOCFIX candidate. +- **SCRIPT-QUALITY BATCH LANDED (addendum 33, 2026-07-07):** repo-wide sweep + (110 scripts) -> operator-ruled fix scope shipped as DOCFIX-112..117: + stage2 anti-escalation guard fail-closed (SECURITY -- was fail-open both + ways); tenant-offboard hardening cluster (stderr-as-argv, Phase B counter + CLOSED after 2 days logged, LB-wait fails loud, cred-file auth_url); + validate.sh nonstd-exit HOLD; juju-spaces-check macaroon-vs-absent; pipefail + sweep + vault-kv-health rc-test; guard-hook blocklist extension. Gauntlet + 38/38. Deferred S-class + minor R items in + docs/script-quality-findings-20260707.md (operator has NOT ruled S1-S5/S7/S8). + +## REMAINING OPEN WORK (single list for session resume; 2026-07-07) + +1. **Devteam onboarding window (PRIMARY NEXT TASK, operator-gated, own + session):** name devteam (pinned); TENANT_CIDR = dev team's choice or + 10.100.0.0/24 fallback (ruled); guard is the D-074 rewrite; snapshot + + 1.1GB controller backup gates SATISFIED (asbuilt/20260706-224851). + Sequence: run-logged window -> stages 0-4 -> tenant-assert -> stage7 + canary -> instantiate the RELEASED clientdocs pack ({{PLACEHOLDER}} fill, + strip TEMPLATE NOTE) -> collect the 7 Jenkins intake answers (DOCFIX-101 + entry) -> deliver. Doubles as first client-facing workflow execution. +2. **Operator manual item:** foil1 Horizon checks (contract 5.1/5.3) -- + domain-admin login + GUI probe-user create/delete; non-blocking. +3. **Operator ruling available anytime:** capi-test-1 CREATE_FAILED cluster + record delete (one gated command; next orphan-audit then reaps its + trustee). +4. **S-class consolidation + minor R items:** proposals in + docs/script-quality-findings-20260707.md -- present options to operator + before building (S1-S5/S7/S8 unruled; S6 ruled no-action). +5. **Pinned decisions (do NOT re-ask):** D-071 ratification (pre-DC-DC HA/ + backup session); D-068 off-EOL path (Roosevelt-scale); SEC-001/003/004 + at v1 close; d011-06 stays MANUAL until SEC-003 rehearsal (gates D-011 + FULL close; v1-close checklist QUEUED per handoff section 6). +6. **Cross-stream:** main-chat to reconcile its ledger section against + addenda 27-33 (its backlog items 6/7/8 delivered here; its Verify-live + queue fully cleared; D-050/D-073/D-074 all resolved). + - **CLIENT PACKAGE RELEASED (addendum 31, 2026-07-07):** operator-requested polish sweep integrated (DOCFIX-110) + banner ruling applied (DRAFT removed; the three per-client templates keep TEMPLATE; README says diff --git a/docs/v1-redeploy-changelog.md b/docs/v1-redeploy-changelog.md index a9d1846..37511c8 100644 --- a/docs/v1-redeploy-changelog.md +++ b/docs/v1-redeploy-changelog.md @@ -2609,3 +2609,81 @@ a re-audit would classify that trustee for reaping. Next-free after this push (per scan, keep this line unwrapped): D-075, DOCFIX-112, BUNDLEFIX-012. + +### 2026-07-07 (addendum 33, jumphost stream) -- script-quality fix batch: DOCFIX-112..117 (sweep E/R findings, operator-ruled scope) + +A read-only repo-wide quality sweep (110 scripts; bash -n and py_compile +all-clean; shellcheck/pyflakes absent on the jumphost, manual passes for +their high-value classes) surfaced 4 runtime errors + robustness items. +Operator ruled the fix scope: errors + coupled robustness now; S-class +consolidation deferred. All fixes harness-proven with discriminating +regression cases (each proven failing against the pre-fix code where a +deterministic mock can express it). + +- **DOCFIX-112 -- tenant-onboard.sh stage2 anti-escalation guard fail-closed + (SECURITY).** The `role assignment list | grep -qw admin` live pipe under + pipefail failed OPEN both ways: SIGPIPE on the very match it exists to + catch, or a failed list, both printed "anti-escalation OK". Now + capture-then-test, stdout-only, STOP on unverifiable (the same form the + file already used 30 lines up). Harness +3 stage2 cases; fail-closed case + proven discriminating vs pre-fix. REVERT: git checkout HEAD~ -- + scripts/tenant-onboard.sh tests/tenant-onboard/run-tests.sh. +- **DOCFIX-113 -- tenant-offboard.sh hardening cluster.** (a) stdout-only + inventory captures via new inv() helper (stderr to tempfile, surfaced as + NOTE); (b) every Phase C/D/E row shape-validated (row_ok/is_id) before + becoming delete argv -- a merged deprecation warning previously word-split + into deletes (mock replay: 70 bogus delete calls pre-fix); invalid rows + SKIP + count; (c) Phase B residual-trust loop printf|while subshell -> + here-string, so SWEEP_FAIL survives (failed trust delete now exits 22; + found-not-fixed since addendum 28, now CLOSED); (d) LB-gone wait counts + stdout only and fails LOUDLY on budget exhaustion or list error (was a + silent fall-through into FIP/server deletes); (e) discarded is_id no-op + enforced; (f) tenant_env reads auth_url= from the cred file, hardcoded VIP + demoted to KEYSTONE_VIP-overridable fallback. Harness 14->20, mock made + CLI-truthful (UUID rows, stderr warnings); all six new cases fail pre-fix. + REVERT: git checkout HEAD~ -- scripts/tenant-offboard.sh + tests/tenant-offboard/run-tests.sh. +- **DOCFIX-114 -- validate.sh non-contract exit codes count as HOLD.** A + check dying 127/141/139 previously yielded OVERALL PASS exit 0 (note_worst + matched nothing; the nonstd annotation was dead code ordered after + REPORT+=). Default arm now annotates then forces HOLD. Proven: old rc=0, + new rc=2. REVERT: git checkout HEAD~ -- scripts/validate.sh + tests/validate/run-tests.sh. +- **DOCFIX-115 -- juju-spaces-check.sh model gate capture-then-test.** The + live `juju models | jq | grep -qx` pipe masked juju auth failure (and the + SIGPIPE race) as "model not present"; auth failure now exits 1 with the + existing macaroon hint. NEW harness tests/juju-spaces-check (5 cases; + script previously had none). REVERT: git checkout HEAD~ -- + scripts/juju-spaces-check.sh; git rm -r tests/juju-spaces-check. +- **DOCFIX-116 -- pipefail upgrades + vault-kv-health rc-test.** + tenant-acceptance.sh and vault-kv-health.sh set -u -> set -uo pipefail + (pipelines audited); 7 harnesses upgraded likewise; + vault-kv-inner-probe.sh stays set -u with the paste-block exemption + documented in-line; vault-kv-health C2 no longer misreports a juju + show-unit failure as "no vault_url" FAIL -- rc tested first, juju failure + warns UNVERIFIED on the HOLD path. Harness 8->10. REVERT: git checkout + HEAD~ -- scripts/tenant-acceptance.sh scripts/vault-kv-health.sh + scripts/vault-kv-inner-probe.sh tests/checks/run-tests.sh + tests/d063-apply/run-tests.sh tests/ledger-scan/run-tests.sh + tests/lib-validate/run-tests.sh tests/validate/run-tests.sh + tests/vault-kv-health/run-tests.sh. +- **DOCFIX-117 -- guard-destructive.py secret-read blocklist extension.** + Reader set gains python3/openssl (quote-blind) and grep/awk/sed + (quote-sensitive: bare-path reads block, quoted prose mentions pass); + path set gains home-anchored tenant-*/ and .pem (repo tests/tenant-*/ + stays readable). Precision-tested; harness 13->25. REVERT: git checkout + HEAD~ -- .claude/hooks/guard-destructive.py tests/claude-guard/run-tests.sh. +- **docs/script-quality-findings-20260707.md (rides the batch):** + deferred-findings register -- R5/R6/R7/R9/R11 verified with locations, + S1-S8 consolidation proposals transferred VERBATIM at integration (the + fix agent flagged it could not see the sweep session's text and refused + to fabricate -- correct call), fixed-this-batch markers, plus new + found-while-fixing items (tenant-offboard Phase A coe-capture stderr + class; benign-warning aborts on fail-closed preamble captures; Phase E0 + display pollution). Sweep clean bill recorded: DOCFIX-094 stderr class + fully closed repo-wide, no /tmp-to-snap bugs, libs correctly set -e-free. +- **Gates:** gauntlet ALL GREEN (38 harnesses, 1 new); repo-lint 0 fail / + 1 legacy warn; L7 receipt re-recorded (tenant scripts changed; clientdocs + reviewed -- client-visible contract unchanged by this batch). + +Next-free after this push (per scan, keep this line unwrapped): D-075, DOCFIX-118, BUNDLEFIX-012. diff --git a/scripts/juju-spaces-check.sh b/scripts/juju-spaces-check.sh index a820f4d..1a32281 100644 --- a/scripts/juju-spaces-check.sh +++ b/scripts/juju-spaces-check.sh @@ -34,7 +34,12 @@ echo # Is MODEL present? Strip any owner/ prefix; match the bare model name. -if ! juju models --format json 2>/dev/null | jq -r '.models[]?.name' 2>/dev/null | sed 's#.*/##' | grep -qx "$MODEL"; then +# Capture-then-test (DOCFIX-115): the live-pipe form (juju | jq | grep -qx) +# raced SIGPIPE under pipefail and masked a juju auth failure as "model not +# present". A failed 'juju models' now takes the macaroon-hint path instead. +MJSON="$(juju models --format json)" || { fail "juju models failed (auth/macaroon? re-login or append /dev/null | sed 's#.*/##')" || true +if ! grep -qx "$MODEL" <<<"$MNAMES"; then note "model '$MODEL' not present yet -- run 'juju add-model $MODEL' first (spaces are per-model)" echo echo "Summary: model-absent (run add-model, then re-run this check)" diff --git a/scripts/tenant-acceptance.sh b/scripts/tenant-acceptance.sh index b9b9eaf..34eb557 100644 --- a/scripts/tenant-acceptance.sh +++ b/scripts/tenant-acceptance.sh @@ -13,7 +13,7 @@ # NOTE: P2 leaves the lbtest deployment + LB in place for inspection (cleanup is a separate gate). # Provenance: gate t3-01 of the 2026-07-02 session (D-066/D-067 acceptance); mock-tested 4-branch. # Harness: tests/tenant-acceptance/run-tests.sh (offline; covers the DOCFIX-094 stdout-only trust capture). -set -u +set -uo pipefail CLIENT="${1:-}" [ -n "$CLIENT" ] || { echo "usage: tenant-acceptance.sh [foil-appcred-file]"; exit 14; } CF="$HOME/tenant-${CLIENT}/${CLIENT}-cluster-cred.txt" diff --git a/scripts/tenant-offboard.sh b/scripts/tenant-offboard.sh index bd199e4..75eb550 100644 --- a/scripts/tenant-offboard.sh +++ b/scripts/tenant-offboard.sh @@ -38,6 +38,21 @@ admin_env(){ for v in $(env|awk -F= '/^OS_/{print $1}'); do unset "$v"; done; source "$HOME/admin-openrc"; } is_id(){ [[ "${1:-}" =~ ^[0-9a-f]{32}$ ]]; } +row_ok(){ is_id "${1//-/}"; } # keystone 32-hex OR dashed uuid row +# inv