#!/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)."