diff --git a/docs/v1-redeploy-changelog.md b/docs/v1-redeploy-changelog.md index c59e1d5..c4b17b3 100644 --- a/docs/v1-redeploy-changelog.md +++ b/docs/v1-redeploy-changelog.md @@ -855,5 +855,30 @@ - oslo.config writes `key = value` (spaces); a `^key=` grep gives false negatives. Match `key[[:space:]]*=` (the magnum [trust] count of 0 was this false negative, not a real absence). +### Phase-06 START -- entry audit GREEN + 6.0-BOOT scripted (2026-06-27) +Entry audit (read-only) on the fresh redeploy: provider-ext PRESENT +(id bb386c86-d646-4c71-b6b7-550f5c691bfb, matches phase-04 as-built); noble image ABSENT +(6.0-BOOT seeds); domain capi / project capi-mgmt / the three admin roles / all five flavors +ABSENT (expected fresh); capi-mgmt-net ABSENT (expected); magnum [trust] POPULATED. Cleared to 6.0-BOOT. + +scripts/phase-06-bootstrap.sh -- Step 6.0-BOOT as a D-056 script (verify-or-create domain/project/ + roles/flavors + noble image stage-and-verify: download-to-$HOME / sha256-vs-SHA256SUMS / image + create --file --import; image-conversion lands raw, D-021). Idempotent (all [SKIP] on an existing + cloud); env-overridable; preconditions (openstack/jq/curl/wget/sha256sum/awk + scoped token; sources + ~/admin-openrc if OS_AUTH_URL unset); exit 0/1/2. bash -n + shellcheck clean; ASCII + 0 CR. + tests/phase-06-bootstrap/ -- 7 cases ALL PASS (fresh create-all + image active; idempotent skip-all; + staged-present skips download; checksum mismatch abort; no-published-checksum abort; poll-never-active + abort; missing-auth precondition exit 2). + +DOCFIX-053 -- phase-06 prereq text names the trust keys trustee_domain_id / trustee_domain_admin_id, + but the magnum charm populates the NAME variants (trustee_domain_name=magnum, + trustee_domain_admin_name=magnum_domain_admin). The first entry-audit grep keyed on the _id names + (plus a `^key=` no-spaces anchor) and falsely reported 0; the corrected grep confirmed [trust] is + populated. Fix the prereq wording before Roosevelt. + +D-046 reminder (phase-08 entry check, NOT phase-06): the [trust] section references Keystone domain + `magnum` + user `magnum_domain_admin`, which do NOT survive teardown -- the magnum charm + `domain-setup` action must be re-run before workload-cluster mint (magnum reports ready regardless). + ### Next-free numbers -Design decision: D-057. Doc fix: DOCFIX-053. +Design decision: D-057. Doc fix: DOCFIX-054. diff --git a/scripts/phase-06-bootstrap.sh b/scripts/phase-06-bootstrap.sh new file mode 100644 index 0000000..0faf91a --- /dev/null +++ b/scripts/phase-06-bootstrap.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# scripts/phase-06-bootstrap.sh +# +# Phase-06 Step 6.0-BOOT: fresh-deploy tenant bootstrap. Verify-or-create (idempotent; +# all [SKIP] on an existing cloud): +# - domain capi - project capi-mgmt in capi +# - admin roles member + load-balancer_member + reader on capi-mgmt (D-039) +# - five flavors (as-built specs) +# - image ubuntu-24.04-noble via STAGE-AND-VERIFY (FINDING-3): download to $HOME (snap- +# readable; NOT /tmp -- L7) if missing/checksum-stale, sha256 vs published SHA256SUMS, +# then client-safe `image create --file --import` (image-conversion lands it raw, D-021). +# D-056 script: the image stage-and-verify is the amphora-pipeline shape; the rest is +# idempotent verify-or-create. Human-gated by invocation. +# +# Tunables via env: PROJ_DOMAIN PROJECT IMG_NAME IMG_URL SUM_URL IMG_FILE SRC POLL_TRIES POLL_SLEEP +# Requires: jumphost; admin-openrc (sourced or at ~/admin-openrc); openstack jq curl wget sha256sum awk. +# Usage: source ~/admin-openrc && bash scripts/phase-06-bootstrap.sh +# Exit: 0 all present/created + image active | 1 gate/checksum/poll fail | 2 precondition +# ASCII + LF. + +set -euo pipefail +shopt -s inherit_errexit 2>/dev/null || true + +PROJ_DOMAIN="${PROJ_DOMAIN:-capi}" +PROJECT="${PROJECT:-capi-mgmt}" +IMG_NAME="${IMG_NAME:-ubuntu-24.04-noble}" +IMG_URL="${IMG_URL:-https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img}" +SUM_URL="${SUM_URL:-https://cloud-images.ubuntu.com/noble/current/SHA256SUMS}" +IMG_FILE="${IMG_FILE:-noble-server-cloudimg-amd64.img}" +SRC="${SRC:-$HOME/$IMG_FILE}" +POLL_TRIES="${POLL_TRIES:-40}"; POLL_SLEEP="${POLL_SLEEP:-15}" +ROLES="member load-balancer_member reader" +FLAVOR_SPECS=("gp.large 4 16384 80" "gp.mid 2 8192 40" "capi.node 2 4096 40" "gp.small 1 4096 20" "m1.lbtest 1 1024 4") + +# --- preconditions ------------------------------------------------------------------ +for c in openstack jq curl wget sha256sum awk; do + command -v "$c" >/dev/null 2>&1 || { echo "FAIL: $c not found" >&2; exit 2; } +done +if [ -z "${OS_AUTH_URL:-}" ] && [ -f "$HOME/admin-openrc" ]; then + # shellcheck disable=SC1091 + . "$HOME/admin-openrc" +fi +[ -n "${OS_AUTH_URL:-}" ] || { echo "FAIL: OS_AUTH_URL unset and no ~/admin-openrc" >&2; exit 2; } +[ -n "${OS_USERNAME:-}" ] && [ -n "${OS_USER_DOMAIN_NAME:-}" ] || { echo "FAIL: OS_USERNAME/OS_USER_DOMAIN_NAME unset" >&2; exit 2; } +openstack token issue >/dev/null 2>&1 || { echo "FAIL: no scoped token (admin-openrc)" >&2; exit 2; } + +# 1. domain +if openstack domain show "$PROJ_DOMAIN" -f value -c id >/dev/null 2>&1; then + echo "[SKIP] domain $PROJ_DOMAIN exists" +else + openstack domain create --description "CAPI/Magnum workload identity" "$PROJ_DOMAIN" >/dev/null + echo "[OK] domain $PROJ_DOMAIN" +fi + +# 2. project +if openstack project show "$PROJECT" --domain "$PROJ_DOMAIN" -f value -c id >/dev/null 2>&1; then + echo "[SKIP] project $PROJECT exists" +else + openstack project create --domain "$PROJ_DOMAIN" --description "CAPI management project" "$PROJECT" >/dev/null + echo "[OK] project $PROJECT (domain $PROJ_DOMAIN)" +fi + +# 3. roles (D-039: trustor must hold load-balancer_member or CAPO 403s on Octavia at LB provisioning) +for ROLE in $ROLES; do + if openstack role assignment list --user "$OS_USERNAME" --user-domain "$OS_USER_DOMAIN_NAME" \ + --project "$PROJECT" --project-domain "$PROJ_DOMAIN" --role "$ROLE" -f value 2>/dev/null | grep -q .; then + echo "[SKIP] role $ROLE on $PROJECT" + else + openstack role add --user "$OS_USERNAME" --user-domain "$OS_USER_DOMAIN_NAME" \ + --project "$PROJECT" --project-domain "$PROJ_DOMAIN" "$ROLE" + echo "[OK] role $ROLE on $PROJECT" + fi +done + +# 4. flavors (as-built specs; public) +for spec in "${FLAVOR_SPECS[@]}"; do + read -r fname fv fr fd <<<"$spec" + if openstack flavor show "$fname" -f value -c id >/dev/null 2>&1; then + echo "[SKIP] flavor $fname exists" + else + openstack flavor create --vcpus "$fv" --ram "$fr" --disk "$fd" --public "$fname" >/dev/null + echo "[OK] flavor $fname ($fv vcpu / $fr MB / $fd GB)" + fi +done + +# 5. image (stage-and-verify) +if openstack image show "$IMG_NAME" -f value -c id >/dev/null 2>&1; then + echo "[SKIP] image $IMG_NAME exists" +else + EXP=$(curl -fsSL "$SUM_URL" | awk -v f="$IMG_FILE" '$2=="*"f || $2==f {print $1}') + [ -n "$EXP" ] || { echo "GATE FAIL: no published checksum for $IMG_FILE"; exit 1; } + if [ -f "$SRC" ] && [ "$(sha256sum "$SRC" | awk '{print $1}')" = "$EXP" ]; then + echo "[OK] staged $IMG_FILE present + checksum-valid; skipping download" + else + echo "[..] downloading $IMG_FILE to $SRC (snap-readable; NOT /tmp)" + wget -q -O "$SRC" "$IMG_URL" + GOT=$(sha256sum "$SRC" | awk '{print $1}') + [ "$EXP" = "$GOT" ] || { echo "GATE FAIL: checksum mismatch exp=$EXP got=$GOT"; exit 1; } + echo "[OK] checksum verified ($GOT)" + fi + openstack image create "$IMG_NAME" --file "$SRC" --import \ + --container-format bare --disk-format qcow2 --public \ + --property os_distro=ubuntu --property os_version=24.04 >/dev/null + echo "[OK] image $IMG_NAME import submitted" +fi + +# poll to active (import + glance image-conversion to raw) +echo "=== poll $IMG_NAME -> active ===" +ACTIVE=0 +for i in $(seq 1 "$POLL_TRIES"); do + ST=$(openstack image show "$IMG_NAME" -f value -c status 2>/dev/null || echo '?') + echo "[$i] status=$ST" + if [ "$ST" = active ]; then ACTIVE=1; break; fi + sleep "$POLL_SLEEP" +done +[ "$ACTIVE" = 1 ] || { echo "GATE FAIL: $IMG_NAME not active after $POLL_TRIES tries"; exit 1; } + +echo "=== confirm ===" +openstack image show "$IMG_NAME" -f json | jq -r '"image: status=\(.status) disk_format=\(.disk_format) visibility=\(.visibility)"' +echo "Summary: phase-06 bootstrap complete (domain/project/roles/flavors/image)." diff --git a/tests/phase-06-bootstrap/fakebin/curl b/tests/phase-06-bootstrap/fakebin/curl new file mode 100644 index 0000000..91eecd3 --- /dev/null +++ b/tests/phase-06-bootstrap/fakebin/curl @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +# fake SHA256SUMS: emit a line for the noble image (or a different file if NO_PUBLISHED=1) +if [ "${NO_PUBLISHED:-0}" = 1 ]; then echo "deadbeef *some-other-file.img"; else echo "${FAKE_EXP:-aaaa} *noble-server-cloudimg-amd64.img"; fi diff --git a/tests/phase-06-bootstrap/fakebin/openstack b/tests/phase-06-bootstrap/fakebin/openstack new file mode 100644 index 0000000..3d34ad6 --- /dev/null +++ b/tests/phase-06-bootstrap/fakebin/openstack @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +obj="${1:-}"; act="${2:-}"; rest=" $* " +imgpresent() { [ "${IMG_PRESENT:-0}" = 1 ] || [ -f "${MK_IMAGE:-/nonexistent}" ]; } +case "$obj $act" in + "token issue") exit 0 ;; + "domain show") [ "${DOMAIN_PRESENT:-0}" = 1 ] && { echo id; exit 0; }; exit 1 ;; + "domain create") echo id; exit 0 ;; + "project show") [ "${PROJECT_PRESENT:-0}" = 1 ] && { echo id; exit 0; }; exit 1 ;; + "project create") echo id; exit 0 ;; + "role assignment") [ "${ROLES_PRESENT:-0}" = 1 ] && echo "assign-line"; exit 0 ;; + "role add") exit 0 ;; + "flavor show") [ "${FLAVORS_PRESENT:-0}" = 1 ] && { echo id; exit 0; }; exit 1 ;; + "flavor create") exit 0 ;; + "image show") + if imgpresent; then + if printf '%s' "$rest" | grep -q -- '-f json'; then + echo '{"status":"active","disk_format":"raw","visibility":"public"}' + elif printf '%s' "$rest" | grep -q -- '-c status'; then + [ "${POLL_FAIL:-0}" = 1 ] && echo saving || echo active + else + echo img-id + fi + exit 0 + fi + exit 1 ;; + "image create") : > "${MK_IMAGE:?}"; exit 0 ;; +esac +exit 0 diff --git a/tests/phase-06-bootstrap/fakebin/sha256sum b/tests/phase-06-bootstrap/fakebin/sha256sum new file mode 100644 index 0000000..4a4201a --- /dev/null +++ b/tests/phase-06-bootstrap/fakebin/sha256sum @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +echo "${FAKE_SHA:-aaaa} ${1:-file}" diff --git a/tests/phase-06-bootstrap/fakebin/wget b/tests/phase-06-bootstrap/fakebin/wget new file mode 100644 index 0000000..e191c0b --- /dev/null +++ b/tests/phase-06-bootstrap/fakebin/wget @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# wget -q -O TARGET URL -> create TARGET +prev=""; tgt="" +for a in "$@"; do [ "$prev" = "-O" ] && tgt="$a"; prev="$a"; done +[ -n "$tgt" ] && : > "$tgt" +exit 0 diff --git a/tests/phase-06-bootstrap/run-tests.sh b/tests/phase-06-bootstrap/run-tests.sh new file mode 100644 index 0000000..6565ae8 --- /dev/null +++ b/tests/phase-06-bootstrap/run-tests.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# tests/phase-06-bootstrap/run-tests.sh -- offline regression for phase-06-bootstrap.sh. +set -euo pipefail +IFS=$'\n\t' +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS="$(cd "$HERE/../../scripts" && pwd)" +TARGET="$SCRIPTS/phase-06-bootstrap.sh" +BIN="$HERE/fakebin" +command -v jq >/dev/null 2>&1 || { echo "FAIL: jq required" >&2; exit 1; } +[ -f "$TARGET" ] || { echo "FAIL: target missing" >&2; exit 1; } +chmod +x "$BIN"/* 2>/dev/null || true +WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT +rc_all=0 +run() { + local want="$1" re="$2" label="$3"; shift 3 + rm -f "$WORK/img.marker" "$WORK/noble-server-cloudimg-amd64.img" + local rc + set +e + PATH="$BIN:$PATH" HOME="$WORK" OS_AUTH_URL=x OS_USERNAME=admin OS_USER_DOMAIN_NAME=admin_domain \ + MK_IMAGE="$WORK/img.marker" POLL_TRIES=3 POLL_SLEEP=0 FAKE_EXP=aaaa FAKE_SHA=aaaa \ + env "$@" bash "$TARGET" >"$WORK/out" 2>&1 + rc=$?; set -e + if [ "$rc" -eq "$want" ] && grep -qE "$re" "$WORK/out"; then + printf ' [OK] %-40s exit %s\n' "$label" "$rc" + else + printf ' [XX] %-40s exit %s (want %s; /%s/)\n' "$label" "$rc" "$want" "$re" + sed 's/^/ /' "$WORK/out"; rc_all=1 + fi +} +echo "=== phase-06-bootstrap.sh (fake openstack/curl/wget/sha256sum + real jq) ===" +run 0 'bootstrap complete' "fresh: create everything + image active" +run 0 'SKIP. image ubuntu-24.04-noble exists' "idempotent: all present" \ + DOMAIN_PRESENT=1 PROJECT_PRESENT=1 ROLES_PRESENT=1 FLAVORS_PRESENT=1 IMG_PRESENT=1 +run 1 'checksum mismatch' "checksum mismatch -> abort" FAKE_SHA=bbbb +run 1 'no published checksum' "no published checksum -> abort" NO_PUBLISHED=1 +run 1 'not active after' "poll never active -> abort" POLL_FAIL=1 +run 2 'OS_AUTH_URL unset' "precondition: no auth -> exit 2" OS_AUTH_URL= +echo +# PRESTAGE handling: the 'staged valid' case needs the SRC file present BEFORE the run. +# Re-run that one case explicitly with pre-staged file (the generic run() rm's it). +echo "=== explicit: staged-present (SRC pre-created, matching checksum) ===" +rm -f "$WORK/img.marker"; : > "$WORK/noble-server-cloudimg-amd64.img" +set +e +PATH="$BIN:$PATH" HOME="$WORK" OS_AUTH_URL=x OS_USERNAME=admin OS_USER_DOMAIN_NAME=admin_domain \ + MK_IMAGE="$WORK/img.marker" POLL_TRIES=3 POLL_SLEEP=0 FAKE_EXP=aaaa FAKE_SHA=aaaa \ + bash "$TARGET" >"$WORK/out2" 2>&1 +rc=$?; set -e +if [ "$rc" -eq 0 ] && grep -q 'skipping download' "$WORK/out2"; then echo " [OK] staged-present skips download exit 0"; else echo " [XX] staged-present exit $rc"; sed 's/^/ /' "$WORK/out2"; rc_all=1; fi +echo +[ "$rc_all" -eq 0 ] && echo "ALL PASS" || echo "SOME FAILED" +exit "$rc_all"