Newer
Older
openstack-caracal-ipv4 / scripts / phase-00-maas-carve.sh
#!/usr/bin/env bash
# scripts/phase-00-maas-carve.sh
#
# Phase-00 MAAS VIP/FIP address carve (rebuild foundation; KI-P3-001). Idempotently
# reserves the front-loaded API-VIP /26 on provider + metal-admin and the FIP pool on
# provider, so MAAS auto-static can never land a host/container primary on a configured
# VIP. Encapsulates the do-doc carve AS A SCRIPT (D-056): idempotent + self-gating; the
# human gates by invoking it (and separately for the destructive stale-delete).
#
# Two correctness fixes vs the do-doc block:
#   DOCFIX-047: subnets resolved BY CIDR (lib-net PATTERN-1), never hardcoded subnet=1/2.
#   DOCFIX-048: VIP reserve defaults to .2-.100 (the as-built width), not the do-doc's
#     stale .2-.63. Idempotency is anchored on the START ip, so a pre-existing .2-.63 OR
#     .2-.100 reserve is left untouched (never an overlap-create).
#
# The stale metal .224-.254 reservation (old scheme) is DETECTED + REPORTED; it is
# deleted only with DELETE_STALE=1 (destructive, gated) and only on an exact start/end match.
#
# Tunables via env: PROVIDER_CIDR METAL_CIDR PROVIDER_VIP_END METAL_VIP_END FIP_START FIP_END DELETE_STALE
# Requires: jumphost; jq; the 'admin' MAAS profile (never 'maas list' -- DOCFIX-016).
# Usage:  scripts/phase-00-maas-carve.sh                  (report + create-if-absent)
#         DELETE_STALE=1 scripts/phase-00-maas-carve.sh   (also remove the stale .224-.254)
# Exit:   0 carve present/created | 1 error | 2 precondition
# ASCII + LF.

set -euo pipefail
shopt -s inherit_errexit 2>/dev/null || true
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/lib-net.sh
. "$SCRIPT_DIR/lib-net.sh"

PROVIDER_CIDR="${PROVIDER_CIDR:-10.12.4.0/22}"
METAL_CIDR="${METAL_CIDR:-10.12.8.0/22}"
PROVIDER_VIP_START="10.12.4.2";  PROVIDER_VIP_END="${PROVIDER_VIP_END:-10.12.4.100}"   # DOCFIX-048 (do-doc said .63)
METAL_VIP_START="10.12.8.2";     METAL_VIP_END="${METAL_VIP_END:-10.12.8.100}"
FIP_START="${FIP_START:-10.12.5.0}"; FIP_END="${FIP_END:-10.12.7.254}"
STALE_METAL_START="10.12.8.224"; STALE_METAL_END="10.12.8.254"

need_jq || exit 2
command -v maas >/dev/null 2>&1 || { echo "FAIL: maas client not found" >&2; exit 2; }
SUBNETS="$(maas admin subnets read 2>/dev/null || true)"
printf '%s' "$SUBNETS" | jq -e 'type=="array"' >/dev/null 2>&1 \
  || { echo "FAIL: 'maas admin subnets read' not JSON (profile 'admin' logged in?)" >&2; exit 2; }
RANGES="$(maas admin ipranges read 2>/dev/null || true)"
printf '%s' "$RANGES" | jq -e 'type=="array"' >/dev/null 2>&1 \
  || { echo "FAIL: 'maas admin ipranges read' not JSON" >&2; exit 2; }

FATAL=0
sid_by_cidr() { printf '%s' "$SUBNETS" | jq -r --arg c "$1" '.[] | select(.cidr==$c) | .id' | head -1; }

ensure_reserved() {  # cidr start end comment
  local cidr="$1" start="$2" end="$3" comment="$4" sid existing
  sid="$(sid_by_cidr "$cidr")"
  [ -n "$sid" ] || { echo "FAIL: no MAAS subnet for cidr $cidr"; FATAL=$((FATAL+1)); return; }
  existing="$(printf '%s' "$RANGES" | jq -r --arg s "$sid" --arg a "$start" \
    '.[] | select(.type=="reserved" and (.subnet.id|tostring)==$s and .start_ip==$a) | "\(.start_ip)-\(.end_ip)"' | head -1)"
  if [ -n "$existing" ]; then
    echo "[SKIP] $cidr: reserved range starting $start already present ($existing)"
  else
    echo "[..] $cidr: reserving $start-$end (subnet id=$sid)"
    maas admin ipranges create type=reserved subnet="$sid" start_ip="$start" end_ip="$end" comment="$comment" >/dev/null
    echo "[OK] reserved $start-$end on $cidr"
  fi
}

echo "=== MAAS carve (CIDR-resolved; idempotent) ==="
ensure_reserved "$PROVIDER_CIDR" "$PROVIDER_VIP_START" "$PROVIDER_VIP_END" "OpenStack public API HA VIPs (front-loaded /26)"
ensure_reserved "$METAL_CIDR"    "$METAL_VIP_START"    "$METAL_VIP_END"    "OpenStack internal/admin API HA VIPs (front-loaded /26)"
ensure_reserved "$PROVIDER_CIDR" "$FIP_START"          "$FIP_END"          "OpenStack Neutron external FIP pool (D-003)"

echo "=== stale metal $STALE_METAL_START-$STALE_METAL_END (old scheme) ==="
MSID="$(sid_by_cidr "$METAL_CIDR")"
STALE_ID=""
[ -n "$MSID" ] && STALE_ID="$(printf '%s' "$RANGES" | jq -r --arg s "$MSID" --arg a "$STALE_METAL_START" --arg b "$STALE_METAL_END" \
  '.[] | select(.type=="reserved" and (.subnet.id|tostring)==$s and .start_ip==$a and .end_ip==$b) | .id' | head -1)"
if [ -z "$STALE_ID" ]; then
  echo "[OK] stale range absent -- nothing to delete"
elif [ "${DELETE_STALE:-0}" = "1" ]; then
  echo "[..] deleting stale reserved range id=$STALE_ID ($STALE_METAL_START-$STALE_METAL_END)"
  maas admin iprange delete "$STALE_ID" >/dev/null
  echo "[OK] stale range id=$STALE_ID deleted"
else
  echo "[REPORT] stale range present: id=$STALE_ID $STALE_METAL_START-$STALE_METAL_END"
  echo "         re-run with DELETE_STALE=1 to remove it (destructive; gated separately)"
fi

echo "=== final: reserved ranges on provider + metal ==="
RANGES="$(maas admin ipranges read 2>/dev/null || true)"   # re-read for post-state
for c in "$PROVIDER_CIDR" "$METAL_CIDR"; do
  sid="$(sid_by_cidr "$c")"
  [ -n "$sid" ] || { echo "  $c: subnet not found"; continue; }
  echo "  $c (subnet id=$sid):"
  printf '%s' "$RANGES" | jq -r --arg s "$sid" '.[] | select(.type=="reserved" and (.subnet.id|tostring)==$s) | "    \(.start_ip)-\(.end_ip)  [\(.comment // "")]"'
done

[ "$FATAL" -eq 0 ] || { echo "Summary: ERRORS ($FATAL)"; exit 1; }
echo "Summary: carve complete (idempotent)."