diff --git a/docs/v1-redeploy-changelog.md b/docs/v1-redeploy-changelog.md index c4b17b3..b1ed63f 100644 --- a/docs/v1-redeploy-changelog.md +++ b/docs/v1-redeploy-changelog.md @@ -880,5 +880,23 @@ `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). +### Phase-06 6.0-BOOT verified + 6.0/6.1 scripted (2026-06-27) +6.0-BOOT ran clean (as-executed): domain capi / project capi-mgmt / 3 roles / 5 flavors created; +ubuntu-24.04-noble imported + active, disk_format=raw, checksum 5fa5b05e...1a44 (matches the +published noble SHA256SUMS). GATE met. + +scripts/phase-06-net-setup.sh -- Steps 6.0 + 6.1 (keypair + capi-mgmt-sg ingress tcp/22+6443; + network + subnet 10.20.0.0/24 dns 1.1.1.1,1.0.0.1; router + external gateway on provider-ext) as + one D-056 verify-or-create script. Idempotent; preconditions (openstack/jq/scoped-token/EXT-present); + router gate polls ACTIVE + ext-gw (ROUTER_POLL_TRIES/SLEEP); exit 0/1/2. shellcheck clean; ASCII+0CR. + tests/phase-06-net-setup/ -- 5 cases ALL PASS (fresh create-all; idempotent skip-all; router-not- + ACTIVE gate fail; missing-pubkey exit 2; missing-auth exit 2). The SG-rule syntax is the do-doc's + standard openstack-client form (LIVE-REVIEW note resolved: the run's verify print is the confirmation). + +DOCFIX-054 -- do-doc Step 6.2 FIP allocation is NOT idempotent: `openstack floating ip create` runs + unconditionally, so a re-run allocates + attaches a SECOND floating IP to capi-mgmt-v2 (FIP-pool + leak + ambiguous apiserver endpoint). The forthcoming phase-06-mgmt-vm.sh (6.2) reuses the VM's + existing FIP when present (allocate only when absent). Fix the do-doc 6.2 block before Roosevelt. + ### Next-free numbers -Design decision: D-057. Doc fix: DOCFIX-054. +Design decision: D-057. Doc fix: DOCFIX-055. diff --git a/scripts/phase-06-net-setup.sh b/scripts/phase-06-net-setup.sh new file mode 100644 index 0000000..08dc46c --- /dev/null +++ b/scripts/phase-06-net-setup.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# scripts/phase-06-net-setup.sh +# +# Phase-06 Steps 6.0 + 6.1: the capi-mgmt project's "safe/idempotent setup" pair -- +# 6.0 keypair (import jumphost pubkey) + security group capi-mgmt-sg (ingress tcp/22 + tcp/6443) +# 6.1 network capi-mgmt-net + subnet capi-mgmt-subnet (10.20.0.0/24, DNS 1.1.1.1/1.0.0.1, D-019) +# + router capi-mgmt-router with external gateway on provider-ext +# All verify-or-create (idempotent; safe to re-run). D-056 script; human-gated by invocation. +# Step 6.2 (VM + FIP allocation) is a SEPARATE flagged-mutation script. +# +# Tunables via env: PROJ KEYPAIR PUBKEY SG NET SUBNET ROUTER EXT MGMT_CIDR DNS1 DNS2 +# ROUTER_POLL_TRIES ROUTER_POLL_SLEEP +# Requires: jumphost; admin-openrc (sourced or ~/admin-openrc); openstack; jq; the keypair pubkey. +# Usage: source ~/admin-openrc && bash scripts/phase-06-net-setup.sh +# Exit: 0 all present/created + router ACTIVE w/ ext-gw | 1 gate fail | 2 precondition +# ASCII + LF. + +set -euo pipefail +shopt -s inherit_errexit 2>/dev/null || true + +PROJ="${PROJ:-capi-mgmt}" +KEYPAIR="${KEYPAIR:-capi-mgmt-key}" +PUBKEY="${PUBKEY:-$HOME/.ssh/id_ed25519.pub}" +SG="${SG:-capi-mgmt-sg}" +NET="${NET:-capi-mgmt-net}" +SUBNET="${SUBNET:-capi-mgmt-subnet}" +ROUTER="${ROUTER:-capi-mgmt-router}" +EXT="${EXT:-provider-ext}" +MGMT_CIDR="${MGMT_CIDR:-10.20.0.0/24}" +DNS1="${DNS1:-1.1.1.1}"; DNS2="${DNS2:-1.0.0.1}" +ROUTER_POLL_TRIES="${ROUTER_POLL_TRIES:-6}"; ROUTER_POLL_SLEEP="${ROUTER_POLL_SLEEP:-5}" + +# --- preconditions ------------------------------------------------------------------ +for c in openstack jq; 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; } +openstack token issue >/dev/null 2>&1 || { echo "FAIL: no scoped token (admin-openrc)" >&2; exit 2; } +openstack network show "$EXT" -f value -c id >/dev/null 2>&1 || { echo "FAIL: external network $EXT absent (phase-04 prereq)" >&2; exit 2; } + +# --- 6.0 keypair -------------------------------------------------------------------- +if openstack keypair show "$KEYPAIR" >/dev/null 2>&1; then + echo "[SKIP] keypair $KEYPAIR exists" +else + [ -f "$PUBKEY" ] || { echo "FAIL: pubkey $PUBKEY not found" >&2; exit 2; } + openstack keypair create --public-key "$PUBKEY" "$KEYPAIR" >/dev/null + echo "[OK] keypair $KEYPAIR (from $PUBKEY)" +fi + +# --- 6.0 security group + rules ----------------------------------------------------- +if openstack security group show "$SG" >/dev/null 2>&1; then + echo "[SKIP] security group $SG exists" +else + openstack security group create --project "$PROJ" "$SG" >/dev/null + echo "[OK] security group $SG" +fi +SGID=$(openstack security group show "$SG" -f value -c id) +[ -n "$SGID" ] || { echo "FAIL: could not resolve $SG id" >&2; exit 1; } +for port in 22 6443; do + if openstack security group rule list "$SGID" -f value -c "Port Range" 2>/dev/null | grep -qx "${port}:${port}"; then + echo "[SKIP] sg rule tcp/$port" + else + openstack security group rule create --proto tcp --dst-port "$port" "$SGID" >/dev/null + echo "[OK] sg rule tcp/$port (ingress)" + fi +done + +# --- 6.1 network / subnet / router -------------------------------------------------- +if openstack network show "$NET" >/dev/null 2>&1; then + echo "[SKIP] network $NET exists" +else + openstack network create --project "$PROJ" "$NET" >/dev/null + echo "[OK] network $NET" +fi +if openstack subnet show "$SUBNET" >/dev/null 2>&1; then + echo "[SKIP] subnet $SUBNET exists" +else + openstack subnet create --project "$PROJ" --network "$NET" --subnet-range "$MGMT_CIDR" \ + --dns-nameserver "$DNS1" --dns-nameserver "$DNS2" "$SUBNET" >/dev/null + echo "[OK] subnet $SUBNET ($MGMT_CIDR; dns $DNS1,$DNS2)" +fi +if openstack router show "$ROUTER" >/dev/null 2>&1; then + echo "[SKIP] router $ROUTER exists" +else + openstack router create --project "$PROJ" "$ROUTER" >/dev/null + echo "[OK] router $ROUTER" +fi +# external gateway (set is idempotent) + attach subnet (ignore 'already attached') +openstack router set --external-gateway "$EXT" "$ROUTER" +openstack router add subnet "$ROUTER" "$SUBNET" 2>/dev/null || true + +# --- verify / GATE: router ACTIVE with external gateway ----------------------------- +echo "=== verify ===" +openstack security group rule list "$SGID" -f value -c Protocol -c "Port Range" 2>/dev/null || true +OK=0 +for i in $(seq 1 "$ROUTER_POLL_TRIES"); do + ST=$(openstack router show "$ROUTER" -f value -c status 2>/dev/null || echo '?') + GW=$(openstack router show "$ROUTER" -f json 2>/dev/null | jq -r '.external_gateway_info // empty') + echo "[$i] router status=$ST gw=${GW:+set}" + if [ "$ST" = ACTIVE ] && [ -n "$GW" ]; then OK=1; break; fi + sleep "$ROUTER_POLL_SLEEP" +done +[ "$OK" = 1 ] || { echo "GATE FAIL: router $ROUTER not ACTIVE with external gateway"; exit 1; } +echo "Summary: phase-06 net-setup complete (keypair + sg + network/subnet/router)." diff --git a/tests/phase-06-net-setup/fakebin/openstack b/tests/phase-06-net-setup/fakebin/openstack new file mode 100644 index 0000000..58870ab --- /dev/null +++ b/tests/phase-06-net-setup/fakebin/openstack @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +a1="${1:-}"; a2="${2:-}"; a3="${3:-}"; a4="${4:-}"; rest=" $* " +[ "$a1 $a2" = "token issue" ] && exit 0 +case "$a1" in + keypair) + case "$a2" in show) [ "${KEYPAIR_PRESENT:-0}" = 1 ] && exit 0 || exit 1 ;; create) exit 0 ;; esac; exit 0 ;; + network) + case "$a2" in + show) + # external network (provider-ext) always present; capi-mgmt-net per NET_PRESENT/marker + if [ "$a3" = "${EXT_NAME:-provider-ext}" ]; then echo ext-id; exit 0; fi + { [ "${NET_PRESENT:-0}" = 1 ] || [ -f "${MK_NET:-/x}" ]; } && { echo net-id; exit 0; }; exit 1 ;; + create) : > "${MK_NET:-/dev/null}" 2>/dev/null; exit 0 ;; + esac; exit 0 ;; + subnet) + case "$a2" in show) [ "${SUBNET_PRESENT:-0}" = 1 ] && exit 0 || exit 1 ;; create) exit 0 ;; esac; exit 0 ;; + router) + case "$a2" in + show) + if printf '%s' "$rest" | grep -q -- '-f json'; then + { [ "${GW_PRESENT:-0}" = 1 ] || [ -f "${MK_GW:-/x}" ]; } && echo '{"external_gateway_info":{"network_id":"ext"}}' || echo '{"external_gateway_info":null}' + elif printf '%s' "$rest" | grep -q -- '-c status'; then + echo "${ROUTER_STATUS:-ACTIVE}" + else + [ "${ROUTER_PRESENT:-0}" = 1 ] && exit 0 || exit 1 + fi + exit 0 ;; + create) exit 0 ;; + set) : > "${MK_GW:?}"; exit 0 ;; + add) exit 0 ;; + esac; exit 0 ;; + security) + if [ "$a3" = rule ]; then + case "$a4" in list) [ "${RULES_PRESENT:-0}" = 1 ] && printf '22:22\n6443:6443\n'; exit 0 ;; create) exit 0 ;; esac; exit 0 + fi + case "$a3" in + show) { [ "${SG_PRESENT:-0}" = 1 ] || [ -f "${MK_SG:-/x}" ]; } && { echo sg-id; exit 0; }; exit 1 ;; + create) : > "${MK_SG:?}"; echo sg-id; exit 0 ;; + esac; exit 0 ;; +esac +exit 0 diff --git a/tests/phase-06-net-setup/run-tests.sh b/tests/phase-06-net-setup/run-tests.sh new file mode 100644 index 0000000..44aac27 --- /dev/null +++ b/tests/phase-06-net-setup/run-tests.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# tests/phase-06-net-setup/run-tests.sh -- offline regression for phase-06-net-setup.sh. +set -euo pipefail +IFS=$'\n\t' +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS="$(cd "$HERE/../../scripts" && pwd)" +TARGET="$SCRIPTS/phase-06-net-setup.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 +: > "$WORK/key.pub" +rc_all=0 +run() { + local want="$1" re="$2" label="$3"; shift 3 + rm -f "$WORK/sg.marker" "$WORK/gw.marker" "$WORK/net.marker" + local rc + set +e + PATH="$BIN:$PATH" HOME="$WORK" OS_AUTH_URL=x PUBKEY="$WORK/key.pub" \ + MK_SG="$WORK/sg.marker" MK_GW="$WORK/gw.marker" MK_NET="$WORK/net.marker" \ + EXT_NAME=provider-ext ROUTER_POLL_TRIES=2 ROUTER_POLL_SLEEP=0 \ + env "$@" bash "$TARGET" >"$WORK/out" 2>&1 + rc=$?; set -e + if [ "$rc" -eq "$want" ] && grep -qE "$re" "$WORK/out"; then + printf ' [OK] %-42s exit %s\n' "$label" "$rc" + else + printf ' [XX] %-42s exit %s (want %s; /%s/)\n' "$label" "$rc" "$want" "$re" + sed 's/^/ /' "$WORK/out"; rc_all=1 + fi +} +echo "=== phase-06-net-setup.sh (fake openstack + real jq) ===" +run 0 'net-setup complete' "fresh: create keypair/sg/rules/net/subnet/router" +run 0 'SKIP. keypair capi-mgmt-key' "idempotent: all present" \ + KEYPAIR_PRESENT=1 SG_PRESENT=1 RULES_PRESENT=1 NET_PRESENT=1 SUBNET_PRESENT=1 ROUTER_PRESENT=1 GW_PRESENT=1 +run 1 'GATE FAIL: router' "router not ACTIVE -> gate fail" ROUTER_STATUS=BUILD +run 2 'pubkey .* not found' "fresh keypair, missing pubkey -> exit 2" PUBKEY=/nonexistent/x.pub +run 2 'OS_AUTH_URL unset' "precondition: no auth -> exit 2" OS_AUTH_URL= +echo +[ "$rc_all" -eq 0 ] && echo "ALL PASS" || echo "SOME FAILED" +exit "$rc_all"