diff --git a/clientdocs/sweep-receipt.txt b/clientdocs/sweep-receipt.txt index a439312..6786fbb 100644 --- a/clientdocs/sweep-receipt.txt +++ b/clientdocs/sweep-receipt.txt @@ -35,7 +35,7 @@ 30919b61b2b79b33a853a29bc814558d7e0e940a3040b2f7710ea9a14333b71a runbooks/appendix-C-identity-rbac.md 8510402e5375a6d42cc47f28eec9698a23d88e04b22ef70ef6e129e1179f3d32 runbooks/appendix-D-magnum-trust-model.md 927aa7261aa2d9fabe2cb7342499e6c0d0840a93ffbb323ee7003ac8eefe14a7 runbooks/tenant-onboarding-v2-DRAFT.md -b8ad0768a1cad3e9a3e329f3cda39e6a151b80c0df5f0402efb65cb290b6b0b3 scripts/tenant-acceptance.sh +80269942634809f903c4a7349fe97a9609cc84280b09af9813d7a068936f66cd scripts/tenant-acceptance.sh ad00be489480069d52df1b978be0360d7fb04dbf954a8cf487162872f2da0593 scripts/tenant-assert.sh -b7022866fc0fac036716c1a45e575279558593992d47631a00d3509d2cb4fdcd scripts/tenant-offboard.sh +7d203df79e920ac35adedfde634cb1c6ccd5d79f560b7c1b72e1a9aa20ea5654 scripts/tenant-offboard.sh d221f6ed0aabaf7faab12e16ccbc655a324d3ea873c8ed183cbcd9353e24973a scripts/tenant-onboard.sh diff --git a/docs/session-ledger.md b/docs/session-ledger.md index f05efc0..ec8f138 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -440,10 +440,23 @@ merged stderr could false-trip exit 21 on an already-clean tenant. Harness un- excludes coe from warnstderr + new coe-stderr-not-argv assertion (22/22). Gauntlet 38/38, lint 0 fail, L7 re-recorded. Committed + pushed. -- **NEXT in batch (pure DRY, low-risk):** S7 (dead code d011-03/04), S4 - (trust_filter.py extraction x3), S8 (lib-net.sh literal centralization). Plan: - one combined commit DOCFIX-132..134. Then batch complete except the HELD - lib-validate adoption (S1/S2/S3/S5). +- **DONE -- S7 (DOCFIX-132) + S4 (DOCFIX-133), addendum 50.** S7: removed dead + `CF` (d011-04) + subsumed `vr_is_ipv4` guard (d011-03). S4: three inline trust + filters -> one fixture-tested scripts/trust_filter.py (+tests/trust_filter/ 9/9); + SDIR added to offboard + acceptance. Gauntlet 39 ALL GREEN. Committed + pushed. +- **S8 RE-SCOPED (bigger than its "S" estimate; operator decision needed before I + wire it):** the KEYSTONE_VIP literal (10.12.4.50) lives in 6 files, and 3 of + them (tenant-onboard/offboard/acceptance) do NOT source lib-net.sh -- so + centralizing means adding `. lib-net.sh` sourcing into client-critical scripts I + just modified (DOCFIX-131 + S4), which stacks risk. Breakdown: + - S8a (FIP pool FIP_START/END -> lib-net.sh): LOW risk, both phase-04 consumers + already source lib-net. Safe to do. + - S8b (KEYSTONE_VIP -> lib-net.sh): needs sourcing plumbing into 6 scripts incl. + the 3 tenant scripts. Medium; HOLD recommended. + - S8c (provider-bundle-check.py restates plane CIDRs): genuine design change + (python can't source bash -- needs a generated/parsed form). Separate item. + Recommendation logged for operator: do S8a now, hold S8b/S8c. lib-validate + adoption (S1/S2/S3/S5) still HELD until DOCFIX-126/127 live-verify. ## REMAINING OPEN WORK (single list for session resume; 2026-07-07, updated post-devteam-window) diff --git a/docs/v1-redeploy-changelog.md b/docs/v1-redeploy-changelog.md index e2f0995..237e718 100644 --- a/docs/v1-redeploy-changelog.md +++ b/docs/v1-redeploy-changelog.md @@ -3284,4 +3284,36 @@ - Numbers consumed: DOCFIX-131. REVERT: git checkout HEAD~ -- scripts/tenant-offboard.sh tests/tenant-offboard/run-tests.sh clientdocs/sweep-receipt.txt docs/v1-redeploy-changelog.md docs/session-ledger.md; then bash scripts/repo-lint.sh --record-clientdocs-sweep. -Next-free after this push (per scan, keep this line unwrapped): D-076, DOCFIX-132, BUNDLEFIX-012. +### 2026-07-08 (addendum 50, jumphost stream) -- DOCFIX-132: S7 dead-code removal (validate checks d011-03/04) + +Operator-greenlit S-class item S7 (docs/script-quality-findings-20260707.md). Pure +deletion, no behavior change: +- checks/d011-04-octavia-lb.sh: `CF` was assigned from a cred-file probe loop and + never read (0 uses); removed the 2 dead lines. +- checks/d011-03-vip-tenant.sh: the guard `vr_is_ipv4 "$VHOST" || [ -n "$VHOST" ] || + fail` fires only when VHOST is empty, which `[ -n "$VHOST" ]` alone catches -- the + vr_is_ipv4 leg was subsumed (any valid IPv4 is non-empty). Dropped it; identical. +- Gauntlet green (validate harness). +- Numbers consumed: DOCFIX-132. +REVERT: git checkout HEAD~ -- scripts/checks/d011-03-vip-tenant.sh scripts/checks/d011-04-octavia-lb.sh docs/v1-redeploy-changelog.md docs/session-ledger.md. + +### 2026-07-08 (addendum 50, jumphost stream) -- DOCFIX-133: S4 trust-list filter consolidated to scripts/trust_filter.py + +Operator-greenlit S-class item S4. Three near-identical inline python-in-bash trust +filters (tenant-offboard sweep-preamble + audit inventory; tenant-acceptance trustee +lookup) -- inline python beyond the house one-liner scale -- replaced by one +fixture-tested scripts/trust_filter.py parameterized for the three shapes: +- `--fields {ID,TRUSTOR,TRUSTEE}` (space-joined), `--trustor-in` (ws/comma uid set, + default keep-all), `--first`, `--error-token` (default 'trust-parse-error'; '' = silent). +- Preserves each caller's exact contract: offboard fail-closed grep on + 'trust-parse-error' (exit 20); acceptance empty-on-error + first-trustee. +- SDIR added to tenant-offboard.sh + tenant-acceptance.sh (matches the onboard + net_overlap.py invocation pattern). tenant-onboard was not a trust-filter site. +- New tests/trust_filter/run-tests.sh (9/9: 3 shapes, comma set, hit/miss, parse-error + token + silent, empty set, bad-fields rc 2). Gauntlet 39 harnesses ALL GREEN (new + harness auto-discovered); offboard 22/22, acceptance 16/16. L7 receipt re-recorded + (both tenant scripts are pinned tenant-facing files). +- Numbers consumed: DOCFIX-133. +REVERT: git checkout HEAD~ -- scripts/trust_filter.py tests/trust_filter/run-tests.sh scripts/tenant-offboard.sh scripts/tenant-acceptance.sh clientdocs/sweep-receipt.txt docs/v1-redeploy-changelog.md docs/session-ledger.md. + +Next-free after this push (per scan, keep this line unwrapped): D-076, DOCFIX-134, BUNDLEFIX-012. diff --git a/scripts/checks/d011-03-vip-tenant.sh b/scripts/checks/d011-03-vip-tenant.sh index 60d47a0..e439e57 100644 --- a/scripts/checks/d011-03-vip-tenant.sh +++ b/scripts/checks/d011-03-vip-tenant.sh @@ -42,7 +42,7 @@ port=p.port or (443 if p.scheme=="https" else 80) print("%s %d" % (p.hostname or "", port))' "$AUTHURL" 2>/dev/null)" VHOST="${VIP%% *}"; VPORT="${VIP##* }" -vr_is_ipv4 "$VHOST" 2>/dev/null || [ -n "$VHOST" ] || { emit "$ID" "$VR_HOLD" "could not derive keystone VIP from auth_url"; exit "$VR_HOLD"; } +[ -n "$VHOST" ] || { emit "$ID" "$VR_HOLD" "could not derive keystone VIP from auth_url"; exit "$VR_HOLD"; } [ -n "$VPORT" ] || { emit "$ID" "$VR_HOLD" "could not derive keystone port"; exit "$VR_HOLD"; } echo " target keystone VIP: $VHOST:$VPORT (from $CLIENT auth_url)" diff --git a/scripts/checks/d011-04-octavia-lb.sh b/scripts/checks/d011-04-octavia-lb.sh index 68c84ac..564ca19 100644 --- a/scripts/checks/d011-04-octavia-lb.sh +++ b/scripts/checks/d011-04-octavia-lb.sh @@ -33,8 +33,6 @@ fi # --- tenant kubeconfig --- -CF="" -for c in "$HOME/tenant-${CLIENT}/${CLIENT}-cluster-cred.txt"; do [ -s "$c" ] && CF="$c"; done KCFG="$HOME/tenant-${CLIENT}/kube/config" [ -s "$KCFG" ] || { emit "$ID" "$VR_HOLD" "no tenant kubeconfig at $KCFG (run tenant-acceptance/d011-03 first)"; exit "$VR_HOLD"; } export KUBECONFIG="$KCFG" diff --git a/scripts/tenant-acceptance.sh b/scripts/tenant-acceptance.sh index 34eb557..ec9106f 100644 --- a/scripts/tenant-acceptance.sh +++ b/scripts/tenant-acceptance.sh @@ -14,6 +14,7 @@ # 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 -uo pipefail +SDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # for helper .py (trust_filter.py) CLIENT="${1:-}" [ -n "$CLIENT" ] || { echo "usage: tenant-acceptance.sh [foil-appcred-file]"; exit 14; } CF="$HOME/tenant-${CLIENT}/${CLIENT}-cluster-cred.txt" @@ -45,12 +46,7 @@ # used to land at char 0, kill the JSON parse, and silently downgrade the trustee # checks to "WARN could not resolve". Stderr goes to a tempfile, surfaced below. TRERR=$(mktemp) -TRUSTEE=$( (admin_env; openstack trust list -f json "$TRERR") | python3 -c " -import sys,json -try: - ts=[t for t in json.load(sys.stdin) if t.get('Trustor User ID')=='$CUID'] - print(ts[0]['Trustee User ID'] if ts else '') -except Exception: print('')" ) +TRUSTEE=$( (admin_env; openstack trust list -f json "$TRERR") | python3 "$SDIR/trust_filter.py" --fields TRUSTEE --trustor-in "$CUID" --first --error-token '' ) [ -s "$TRERR" ] && echo " NOTE: trust list stderr: $(head -1 "$TRERR")" rm -f "$TRERR" if [ -n "$TRUSTEE" ]; then diff --git a/scripts/tenant-offboard.sh b/scripts/tenant-offboard.sh index 9be710e..5a558b8 100644 --- a/scripts/tenant-offboard.sh +++ b/scripts/tenant-offboard.sh @@ -19,6 +19,7 @@ # Sweep phases are best-effort continue-and-report: failures are printed, counted, and the # script exits nonzero at the end (22 sweep / 23 identity) rather than stranding silently. set -uo pipefail +SDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # for helper .py (trust_filter.py) SWEEP_FAIL=0; ID_FAIL=0 run(){ # run : capture-then-test; count failures loudly local _c="$1"; shift; local out @@ -89,12 +90,7 @@ grep -q "^$MADM " <<<"$MUSERS" || { echo "PRECOND: magnum-domain user list did not include magnum_domain_admin -- listing untrusted (raw: $(head -1 <<<"$MUSERS"))"; exit 20; } # DOCFIX-094 pattern: stdout-only structured capture (merged stderr kills the parse) TRERR=$(mktemp) - STR=$(openstack trust list -f json "$TRERR" | python3 -c " -import sys,json -try: - for t in json.load(sys.stdin): - print(t.get('ID'), t.get('Trustee User ID')) -except Exception as e: print('trust-parse-error', e)") || true + STR=$(openstack trust list -f json "$TRERR" | python3 "$SDIR/trust_filter.py" --fields ID,TRUSTEE) || true [ -s "$TRERR" ] && echo " NOTE: trust list stderr: $(head -1 "$TRERR")" rm -f "$TRERR" grep -q 'trust-parse-error' <<<"${STR:-}" && { echo "PRECOND: trust list unparseable -- sweep would be blind on trusts"; exit 20; } @@ -201,14 +197,7 @@ # DOCFIX-094: stdout-only structured capture -- merged stderr (deprecation notices) # used to land at char 0, kill the JSON parse, and silently EMPTY the trust sweep. TRERR=$(mktemp) -TRUSTS=$( openstack trust list -f json "$TRERR" | python3 -c " -import sys,json -uids=set('''$UIDLIST'''.split()) -try: - for t in json.load(sys.stdin): - if t.get('Trustor User ID') in uids: - print(t.get('ID'), t.get('Trustor User ID'), t.get('Trustee User ID')) -except Exception as e: print('trust-parse-error', e)" ) || true +TRUSTS=$( openstack trust list -f json "$TRERR" | python3 "$SDIR/trust_filter.py" --fields ID,TRUSTOR,TRUSTEE --trustor-in "$UIDLIST" ) || true [ -s "$TRERR" ] && echo " NOTE: trust list stderr: $(head -1 "$TRERR")" rm -f "$TRERR" grep -q 'trust-parse-error' <<<"${TRUSTS:-}" && { echo "PRECOND: trust list unparseable -- audit/sweep would be blind; fix before offboarding"; exit 20; } diff --git a/scripts/trust_filter.py b/scripts/trust_filter.py new file mode 100644 index 0000000..e408ffc --- /dev/null +++ b/scripts/trust_filter.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Filter an `openstack trust list -f json` payload read from stdin. + +S4 consolidation: replaces three near-identical inline python-in-bash blocks +(tenant-offboard sweep-preamble + audit inventory, tenant-acceptance trustee +lookup) with one fixture-tested tool. Keeps the DOCFIX-094 posture: the caller +captures stdout only (stderr of the openstack call goes to its own tempfile), +so a benign CLI warning cannot corrupt the parse. + +Usage: + trust_filter.py --fields ID,TRUSTOR,TRUSTEE [--trustor-in "u1 u2,u3"] \ + [--first] [--error-token TOKEN] + + --fields comma list from {ID,TRUSTOR,TRUSTEE}, printed space-joined, + one line per kept trust. + --trustor-in whitespace/comma-separated Trustor User IDs to keep + (default: keep every trust). + --first emit only the first kept trust. + --error-token printed to stdout on a JSON parse failure so the caller can + detect it (default 'trust-parse-error'); pass '' to print + nothing on error. + +Exit: 0 always on well-formed invocation (parse errors are reported in-band via +--error-token, matching the callers' fail-closed grep); 2 on a bad --fields. +""" +import sys +import json +import argparse +import re + +FIELD_KEYS = {"ID": "ID", "TRUSTOR": "Trustor User ID", "TRUSTEE": "Trustee User ID"} + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--fields", required=True) + ap.add_argument("--trustor-in", default=None) + ap.add_argument("--first", action="store_true") + ap.add_argument("--error-token", default="trust-parse-error") + a = ap.parse_args() + + fields = [f.strip().upper() for f in a.fields.split(",") if f.strip()] + bad = [f for f in fields if f not in FIELD_KEYS] + if not fields or bad: + sys.stderr.write("trust_filter: bad --fields %r\n" % a.fields) + return 2 + + uids = None + if a.trustor_in is not None: + uids = set(re.split(r"[\s,]+", a.trustor_in.strip())) - {""} + + try: + trusts = json.load(sys.stdin) + except Exception as e: # noqa: BLE001 -- report in-band, never traceback + if a.error_token: + print(a.error_token, e) + return 0 + + for t in trusts: + if uids is not None and t.get("Trustor User ID") not in uids: + continue + print(" ".join(str(t.get(FIELD_KEYS[f], "")) for f in fields)) + if a.first: + break + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/trust_filter/run-tests.sh b/tests/trust_filter/run-tests.sh new file mode 100644 index 0000000..f7bd5a9 --- /dev/null +++ b/tests/trust_filter/run-tests.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# tests/trust_filter/run-tests.sh -- fixtures for scripts/trust_filter.py (S4). +# Covers the three real call shapes it consolidates + parse-error + bad-fields. +# No cloud access. +set -uo pipefail +SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; PY="$SD/../../scripts/trust_filter.py" +P=0; F=0 +ck(){ local name="$1" want="$2" got="$3"; if [ "$got" = "$want" ]; then echo "PASS: $name"; P=$((P+1)); else echo "FAIL: $name"; echo " want: [$want]"; echo " got: [$got]"; F=$((F+1)); fi; } + +# fixture: three trusts; trustors u1, u2, u3 +J='[{"ID":"t1","Trustor User ID":"u1","Trustee User ID":"e1"}, + {"ID":"t2","Trustor User ID":"u2","Trustee User ID":"e2"}, + {"ID":"t3","Trustor User ID":"u3","Trustee User ID":"e3"}]' + +# 1) offboard sweep: all trusts, ID + Trustee, no filter +ck all-id-trustee "$(printf 't1 e1\nt2 e2\nt3 e3')" "$(printf '%s' "$J" | python3 "$PY" --fields ID,TRUSTEE)" + +# 2) offboard audit: filter by a uid set (space-separated), 3 fields +ck filter-set "$(printf 't1 u1 e1\nt3 u3 e3')" "$(printf '%s' "$J" | python3 "$PY" --fields ID,TRUSTOR,TRUSTEE --trustor-in "u1 u3")" + +# 2b) comma-separated uid set also accepted +ck filter-set-comma "t2 u2 e2" "$(printf '%s' "$J" | python3 "$PY" --fields ID,TRUSTOR,TRUSTEE --trustor-in "u2,uX")" + +# 3) acceptance: single trustor, first trustee only, empty-on-error +ck accept-hit "e2" "$(printf '%s' "$J" | python3 "$PY" --fields TRUSTEE --trustor-in "u2" --first --error-token '')" +ck accept-miss "" "$(printf '%s' "$J" | python3 "$PY" --fields TRUSTEE --trustor-in "nope" --first --error-token '')" + +# 4) parse error: default token surfaces (grep-detectable), empty token stays silent +ERR="$(printf 'not json' | python3 "$PY" --fields ID,TRUSTEE)" +case "$ERR" in trust-parse-error*) echo "PASS: parse-error-token"; P=$((P+1));; *) echo "FAIL: parse-error-token (got [$ERR])"; F=$((F+1));; esac +ck parse-error-silent "" "$(printf 'not json' | python3 "$PY" --fields TRUSTEE --error-token '')" + +# 5) empty uid set -> no matches (empty tenant, no users) +ck empty-set "" "$(printf '%s' "$J" | python3 "$PY" --fields ID --trustor-in '')" + +# 6) bad --fields -> exit 2 +printf '%s' "$J" | python3 "$PY" --fields ZZZ >/dev/null 2>&1; ck bad-fields-rc 2 $? + +echo "RESULT: PASS=$P FAIL=$F" +[ "$F" = 0 ] && echo "ALL PASS" || { echo "FAILURES"; exit 1; }