Newer
Older
openstack-caracal-ipv4 / scripts / phase-04-network-verify.sh
#!/usr/bin/env bash
# scripts/phase-04-network-verify.sh
#
# Read-only verify for phase-04 (Neutron external provider network + FIP subnet).
# Two gates, both safe to re-run; mutates NOTHING:
#
#   PRE  (verify-before-mutate for Step 4.1): the MAAS provider subnet is discovered
#        BY CIDR (lib-net PATTERN-1 -- subnet IDs drift across cutovers, so the do-doc's
#        'maas admin subnet read 1' is a post-D-052 landmine; this resolves by the
#        provider CIDR), its gateway matches the pinned provider gateway, and the FIP
#        pool 10.12.5.0-10.12.7.254 is a RESERVED iprange on that subnet (so neutron can
#        own the pool without colliding with a MAAS auto-static primary -- KI-P3-001).
#
#   POST (auto-detected only if 'provider-ext' exists): the phase-04 EXIT GATE asserts --
#        network external=true / type=flat / physnet1 / NOT shared (Option B isolation);
#        subnet cidr / gateway / no-dhcp / FIP allocation pool.
#
# Requires: jumphost; jq; admin-openrc sourced (OS_AUTH_URL set); the 'admin' MAAS profile.
#           NEVER runs 'maas list' (it prints the API key -- DOCFIX-016).
#
# Usage:  source ~/admin-openrc && scripts/phase-04-network-verify.sh
# Exit:   0 PROCEED (pre clear, network not yet made) or PASS (post clear)
#         1 HOLD / FAIL (an assertion failed)
#         2 precondition (jq/openstack/maas missing, openrc not sourced, MAAS not logged in)
#
# Resolves dynamically; the only literals are the D-003 design values (provider CIDR,
# FIP pool, neutron object names), each carrying provenance. Read-only. ASCII + LF.

set -euo pipefail
shopt -s inherit_errexit 2>/dev/null || true
IFS=$'\n\t'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/lib-net.sh
. "$SCRIPT_DIR/lib-net.sh"

# --- phase-04 design literals (D-003; TAG: confirm per site on rebuild) --------------
PROVIDER_CIDR="10.12.4.0/22"                  # provider-public plane (lib-net PLANE_CIDRS[0])
GW_EXPECT="${PLANE_GW[$PROVIDER_CIDR]}"        # 10.12.4.1 -- pinned; asserted against live MAAS
FIP_START="10.12.5.0"
FIP_END="10.12.7.254"
EXT_NET="provider-ext"
EXT_SUBNET="provider-ext-fip"
PHYSNET="physnet1"

FATAL=0
fail() { echo "FAIL: $*" >&2; FATAL=$((FATAL + 1)); }
pass() { echo "PASS: $*"; }

# --- preconditions ------------------------------------------------------------------
need_jq || exit 2
command -v openstack >/dev/null 2>&1 || { echo "FAIL: openstack client not found" >&2; exit 2; }
command -v maas      >/dev/null 2>&1 || { echo "FAIL: maas client not found" >&2; exit 2; }
[ -n "${OS_AUTH_URL:-}" ] || { echo "FAIL: OS_AUTH_URL unset -- 'source ~/admin-openrc' first" >&2; exit 2; }

# MAAS 'admin' profile must be usable (read-only; NOT 'maas list' -- DOCFIX-016).
SUBNETS_JSON="$(maas admin subnets read 2>/dev/null || true)"
printf '%s' "$SUBNETS_JSON" | jq -e 'type=="array"' >/dev/null 2>&1 \
  || { echo "FAIL: 'maas admin subnets read' did not return JSON (profile 'admin' logged in?)" >&2; exit 2; }

echo "=== phase-04 network-carve verify (read-only) ==="
echo

# --- PRE A: discover the provider subnet BY CIDR (PATTERN-1) ------------------------
echo "--- PRE: MAAS provider subnet (by CIDR $PROVIDER_CIDR; never by hardcoded id) ---"
mapfile -t SROW < <(printf '%s' "$SUBNETS_JSON" \
  | jq -r --arg c "$PROVIDER_CIDR" '.[] | select(.cidr==$c) | "\(.id)\t\(.gateway_ip)"')
if [ "${#SROW[@]}" -eq 0 ]; then
  fail "no MAAS subnet with cidr $PROVIDER_CIDR (provider plane missing?)"
  echo "Summary: HOLD (provider subnet not found)"; exit 1
elif [ "${#SROW[@]}" -gt 1 ]; then
  fail "multiple MAAS subnets match cidr $PROVIDER_CIDR (ambiguous): ${SROW[*]}"
  echo "Summary: HOLD (ambiguous provider subnet)"; exit 1
fi
SID="${SROW[0]%%$'\t'*}"
GW_LIVE="${SROW[0]##*$'\t'}"
echo "  provider subnet id=$SID (discovered by CIDR) gateway=$GW_LIVE"

# --- PRE B: gateway assertion -------------------------------------------------------
if [ "$GW_LIVE" = "$GW_EXPECT" ]; then
  pass "provider gateway $GW_LIVE matches pinned $GW_EXPECT"
else
  fail "provider gateway $GW_LIVE != pinned $GW_EXPECT (lib-net PLANE_GW)"
fi

# --- PRE C: FIP pool reserved on the provider subnet --------------------------------
echo "--- PRE: FIP pool $FIP_START-$FIP_END must be a RESERVED iprange on subnet $SID ---"
IPR_JSON="$(maas admin ipranges read 2>/dev/null || true)"
if ! printf '%s' "$IPR_JSON" | jq -e 'type=="array"' >/dev/null 2>&1; then
  fail "'maas admin ipranges read' did not return JSON"
else
  echo "  reserved ranges on subnet $SID:"
  printf '%s' "$IPR_JSON" | jq -r --argjson s "$SID" \
    '.[] | select(.type=="reserved" and .subnet.id==$s) | "    \(.start_ip)-\(.end_ip)  [\(.comment // "")]"' \
    || true
  if printf '%s' "$IPR_JSON" | jq -e --argjson s "$SID" --arg a "$FIP_START" --arg b "$FIP_END" \
       'any(.[]; .type=="reserved" and .subnet.id==$s and .start_ip==$a and .end_ip==$b)' >/dev/null 2>&1; then
    pass "FIP pool $FIP_START-$FIP_END is RESERVED on subnet $SID (neutron can own it)"
  else
    fail "FIP pool $FIP_START-$FIP_END is NOT a reserved iprange on subnet $SID (phase-00 carve missing -- KI-P3-001 risk)"
  fi
fi
echo

# --- POST: phase-04 EXIT GATE (only if the neutron network already exists) ----------
echo "--- POST: neutron provider network (asserts only if it exists) ---"
POST_PRESENT=0
if NET_JSON="$(openstack network show "$EXT_NET" -f json 2>/dev/null)"; then
  POST_PRESENT=1
  ext="$(printf '%s' "$NET_JSON"   | jq -r '."router:external"')"
  ntype="$(printf '%s' "$NET_JSON" | jq -r '."provider:network_type"')"
  pnet="$(printf '%s' "$NET_JSON"  | jq -r '."provider:physical_network"')"
  shared="$(printf '%s' "$NET_JSON" | jq -r '.shared')"
  echo "  network $EXT_NET: external=$ext type=$ntype physnet=$pnet shared=$shared"
  [ "$ext" = "true" ]      || fail "$EXT_NET external != true (got '$ext')"
  [ "$ntype" = "flat" ]    || fail "$EXT_NET type != flat (got '$ntype')"
  [ "$pnet" = "$PHYSNET" ] || fail "$EXT_NET physnet != $PHYSNET (got '$pnet')"
  [ "$shared" = "false" ]  || fail "$EXT_NET shared != false (Option B isolation; got '$shared')"

  if SUB_JSON="$(openstack subnet show "$EXT_SUBNET" -f json 2>/dev/null)"; then
    cidr="$(printf '%s' "$SUB_JSON" | jq -r '.cidr')"
    sgw="$(printf '%s' "$SUB_JSON"  | jq -r '.gateway_ip')"
    dhcp="$(printf '%s' "$SUB_JSON" | jq -r '.enable_dhcp')"
    # allocation_pools shape varies by client version: list of {start,end},
    # list of "start-end" strings, or a single string. Match all three.
    poolmatch="$(printf '%s' "$SUB_JSON" | jq -r --arg a "$FIP_START" --arg b "$FIP_END" '
      (.allocation_pools // empty) as $p
      | if ($p|type)=="array"
          then any($p[];
                 (type=="object" and .start==$a and .end==$b)
                 or (type=="string" and contains($a) and contains($b)))
        elif ($p|type)=="string" then ($p|contains($a) and contains($b))
        else false end')"
    echo "  subnet $EXT_SUBNET: cidr=$cidr gateway=$sgw dhcp=$dhcp fip-pool-match=$poolmatch"
    [ "$cidr" = "$PROVIDER_CIDR" ] || fail "$EXT_SUBNET cidr != $PROVIDER_CIDR (got '$cidr')"
    [ "$sgw" = "$GW_EXPECT" ]      || fail "$EXT_SUBNET gateway != $GW_EXPECT (got '$sgw')"
    [ "$dhcp" = "false" ]          || fail "$EXT_SUBNET enable_dhcp != false (got '$dhcp')"
    [ "$poolmatch" = "true" ]      || fail "$EXT_SUBNET allocation_pool != $FIP_START-$FIP_END"
  else
    fail "network $EXT_NET exists but subnet $EXT_SUBNET is MISSING"
  fi
else
  echo "  $EXT_NET not created yet -- PRE gate is the operative check (run Step 4.1 to create)."
fi
echo

# --- verdict ------------------------------------------------------------------------
if [ "$FATAL" -ne 0 ]; then
  echo "Summary: HOLD/FAIL -- $FATAL assertion(s) failed above."
  exit 1
fi
if [ "$POST_PRESENT" -eq 1 ]; then
  echo "Summary: PASS -- phase-04 EXIT GATE met (provider-ext + provider-ext-fip correct)."
else
  echo "Summary: PROCEED -- PRE gate clear (FIP pool reserved, provider gateway pinned)."
  echo "  Next: run phase-04 Step 4.1 (create provider-ext + provider-ext-fip), then re-run this."
fi
exit 0