Newer
Older
openstack-caracal-ipv4 / scripts / carve-host-interfaces.sh
#!/usr/bin/env bash
# scripts/carve-host-interfaces.sh <hostname> [--apply]
#
# Strategy-B interface carve for ONE freshly-commissioned host. Reconstructs the
# host network tree that was lost when the machine was decomposed. Default is
# DRY-RUN (resolves every id live and prints each mutation it WOULD run, changes
# nothing). Pass --apply to execute.
#
# Target tree (octet N = .40-.43 by host index; see lib-hosts.sh HOST_OCTET):
#   enp1s0   raw  + STATIC 10.12.4.N    (provider-public; ovn-chassis builds br-ex
#                                        OVS at deploy and enslaves enp1s0 by MAC --
#                                        MAAS must leave enp1s0 RAW)
#   enp7s0 --> br-metal (standard bridge) + STATIC 10.12.8.N  (metal-admin)
#              br-metal.103 (vlan, VID 103)
#                --> br-internal (standard bridge) + STATIC 10.12.12.N (metal-internal)
#   enp8s0   raw  + STATIC 10.12.16.N   (data-tenant)
#   enp9s0   raw  + STATIC 10.12.32.N   (storage;     Juju auto-bridges at deploy)
#   enp10s0  raw  + STATIC 10.12.36.N   (replication; Juju auto-bridges at deploy)
#   enp11s0  idle (ex-lbaas; no link)
#
# All ids resolved live: system_id by hostname, interface id by name, subnet id and
# VLAN object id by CIDR. Idempotent: skips a bridge/vlan/link that already exists.
# Requires the host to be Ready (link-subnet/update are rejected on Deployed).
#
# Exit: 0 ok | 1 fatal | 2 warning

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"
# shellcheck source=scripts/lib-hosts.sh
. "$SCRIPT_DIR/lib-hosts.sh"

MAAS_PROFILE="${MAAS_PROFILE:-admin}"
FATAL=0
fail() { echo "FAIL: $*" >&2; FATAL=$((FATAL+1)); }
note() { echo "NOTE: $*"; }
hdr()  { echo; echo "=== $* ==="; }

usage() { echo "usage: $0 <hostname> [--apply]" >&2; exit 1; }

HN="${1:-}"; [ -n "$HN" ] || usage
MODE="dryrun"; [ "${2:-}" = "--apply" ] && MODE="apply"

# validate hostname is one of ours
ok=0; for h in "${HOSTS[@]}"; do [ "$h" = "$HN" ] && ok=1; done
[ "$ok" = 1 ] || { echo "ERROR: '$HN' is not one of: ${HOSTS[*]}" >&2; exit 1; }

need_jq || exit 1
OCTET="${HOST_OCTET[$HN]}"

# ---- live resolvers (read-only; safe in both modes) -----------------------
maas_q() { maas "$MAAS_PROFILE" "$@"; }

SID="$(host_sysid "$HN" || true)"
[ -n "$SID" ] || { fail "$HN is not enrolled in MAAS"; exit 1; }

STATUS="$(maas_q machine read "$SID" 2>/dev/null | jq -r '.status_name // "?"')"
[ "$STATUS" = "Ready" ] || { fail "$HN ($SID) is '$STATUS', not Ready -- interface edits are rejected unless Ready/Broken"; exit 1; }

# subnet id + vlan object id, resolved BY CIDR (drift-proof)
SUBNETS_JSON="$(maas_q subnets read)"
subid_of()  { printf '%s' "$SUBNETS_JSON" | jq -r --arg c "$1" '.[]|select(.cidr==$c)|.id' | head -1; }
vlanid_of() { printf '%s' "$SUBNETS_JSON" | jq -r --arg c "$1" '.[]|select(.cidr==$c)|(.vlan.id // .vlan)' | head -1; }
vlanvid_of(){ printf '%s' "$SUBNETS_JSON" | jq -r --arg c "$1" '.[]|select(.cidr==$c)|(.vlan.vid // empty)' | head -1; }

# plane CIDRs (verified set; sourced order from lib-net PLANE_CIDRS)
C_PROV="10.12.4.0/22"; C_METAL="10.12.8.0/22"; C_INT="$METAL_INTERNAL_CIDR"  # 10.12.12.0/22
C_DATA="10.12.16.0/22"; C_STOR="10.12.32.0/22"; C_REPL="10.12.36.0/22"

# assert all six planes resolve, and the internal plane is really VID 103
for c in "$C_PROV" "$C_METAL" "$C_INT" "$C_DATA" "$C_STOR" "$C_REPL"; do
  [ -n "$(subid_of "$c")" ] || { fail "no MAAS subnet for $c"; }
  [ -n "$(vlanid_of "$c")" ] || { fail "no VLAN for $c"; }
done
[ "$FATAL" = 0 ] || exit 1
gotvid="$(vlanvid_of "$C_INT")"
[ "$gotvid" = "$METAL_INTERNAL_VID" ] || { fail "metal-internal $C_INT is VID '$gotvid', expected $METAL_INTERNAL_VID"; exit 1; }

# interface id by name (live)
ifid_of() { maas_q interfaces read "$SID" | jq -r --arg n "$1" '.[]|select(.name==$n)|.id' | head -1; }
# is interface (by name) already linked to a given cidr?
linked_to() {
  maas_q interfaces read "$SID" \
    | jq -e --arg n "$1" --arg c "$2" '.[]|select(.name==$n)|.links[]?|select(.subnet.cidr==$c)' >/dev/null 2>&1
}

# ---- mutation emitter ------------------------------------------------------
# emit "<human desc>" maas <args...>   (runs in apply; prints WOULD in dryrun)
emit() {
  local desc="$1"; shift
  if [ "$MODE" = "apply" ]; then
    echo "  DO: $desc"
    if ! maas "$MAAS_PROFILE" "$@" >/dev/null; then fail "$desc"; return 1; fi
  else
    echo "  WOULD: $desc"
    echo "         maas $MAAS_PROFILE $*"
  fi
}

hdr "$HN ($SID)  octet=.$OCTET  mode=$MODE"
echo "resolved subnet/vlan ids (by CIDR):"
printf "  provider  %s sub=%s vlan=%s\n" "$C_PROV"  "$(subid_of "$C_PROV")"  "$(vlanid_of "$C_PROV")"
printf "  metal     %s sub=%s vlan=%s\n" "$C_METAL" "$(subid_of "$C_METAL")" "$(vlanid_of "$C_METAL")"
printf "  internal  %s sub=%s vlan=%s (vid %s)\n" "$C_INT" "$(subid_of "$C_INT")" "$(vlanid_of "$C_INT")" "$gotvid"
printf "  data      %s sub=%s vlan=%s\n" "$C_DATA" "$(subid_of "$C_DATA")" "$(vlanid_of "$C_DATA")"
printf "  storage   %s sub=%s vlan=%s\n" "$C_STOR" "$(subid_of "$C_STOR")" "$(vlanid_of "$C_STOR")"
printf "  replicat  %s sub=%s vlan=%s\n" "$C_REPL" "$(subid_of "$C_REPL")" "$(vlanid_of "$C_REPL")"

# helper: link a RAW physical NIC -> move to plane VLAN, then STATIC link
carve_raw() {
  local nic="$1" cidr="$2" ip="$3"
  local id vlan sub
  id="$(ifid_of "$nic")"
  [ -n "$id" ] || { fail "$nic not found on $HN"; return 1; }
  if linked_to "$nic" "$cidr"; then note "$nic already STATIC on $cidr -- SKIP"; return 0; fi
  vlan="$(vlanid_of "$cidr")"; sub="$(subid_of "$cidr")"
  emit "$nic(id=$id) -> VLAN $vlan ($cidr)" interface update "$SID" "$id" vlan="$vlan"
  emit "$nic(id=$id) -> STATIC $ip on subnet $sub" interface link-subnet "$SID" "$id" mode=STATIC subnet="$sub" ip_address="$ip"
}

hdr "provider plane (enp1s0 raw + static)"
carve_raw enp1s0 "$C_PROV" "10.12.4.$OCTET"

hdr "metal stack (enp7s0 -> br-metal -> br-metal.103 -> br-internal)"
EID="$(ifid_of enp7s0)"; [ -n "$EID" ] || fail "enp7s0 not found"
# 1) clear enp7s0's commissioning link(s) so the IP lands on the bridge, not the member
if maas_q interfaces read "$SID" | jq -e '.[]|select(.name=="enp7s0")|.links[]?|select(.subnet!=null)' >/dev/null 2>&1; then
  for lid in $(maas_q interfaces read "$SID" | jq -r '.[]|select(.name=="enp7s0")|.links[]?|select(.subnet!=null)|.id'); do
    emit "unlink enp7s0(id=$EID) commissioning link id=$lid" interface unlink-subnet "$SID" "$EID" id="$lid"
  done
fi
# 2) br-metal (standard) on enp7s0 -- inherits enp7s0's 2_metal untagged VLAN
if [ -z "$(ifid_of br-metal)" ]; then
  emit "create br-metal (standard) parent=enp7s0(id=$EID)" interfaces create-bridge "$SID" name=br-metal bridge_type=standard parent="$EID"
else note "br-metal exists -- SKIP create"; fi
[ "$MODE" = apply ] && BMID="$(ifid_of br-metal)" || BMID="<br-metal-id>"
if ! linked_to br-metal "$C_METAL"; then
  emit "br-metal(id=$BMID) -> STATIC 10.12.8.$OCTET on subnet $(subid_of "$C_METAL")" \
    interface link-subnet "$SID" "$BMID" mode=STATIC subnet="$(subid_of "$C_METAL")" ip_address="10.12.8.$OCTET"
else note "br-metal already on $C_METAL -- SKIP"; fi
# 3) br-metal.103 (VLAN, VID 103) on br-metal
if [ -z "$(ifid_of br-metal.103)" ]; then
  emit "create br-metal.103 (VID 103, vlan obj $(vlanid_of "$C_INT")) parent=br-metal(id=$BMID)" \
    interfaces create-vlan "$SID" vlan="$(vlanid_of "$C_INT")" parent="$BMID"
else note "br-metal.103 exists -- SKIP create"; fi
[ "$MODE" = apply ] && V103="$(ifid_of br-metal.103)" || V103="<br-metal.103-id>"
# 4) br-internal (standard) on br-metal.103
if [ -z "$(ifid_of br-internal)" ]; then
  emit "create br-internal (standard) parent=br-metal.103(id=$V103)" \
    interfaces create-bridge "$SID" name=br-internal bridge_type=standard parent="$V103"
else note "br-internal exists -- SKIP create"; fi
[ "$MODE" = apply ] && BIID="$(ifid_of br-internal)" || BIID="<br-internal-id>"
if ! linked_to br-internal "$C_INT"; then
  emit "br-internal(id=$BIID) -> STATIC 10.12.12.$OCTET on subnet $(subid_of "$C_INT")" \
    interface link-subnet "$SID" "$BIID" mode=STATIC subnet="$(subid_of "$C_INT")" ip_address="10.12.12.$OCTET"
else note "br-internal already on $C_INT -- SKIP"; fi

hdr "data / storage / replication (raw + static)"
carve_raw enp8s0  "$C_DATA" "10.12.16.$OCTET"
carve_raw enp9s0  "$C_STOR" "10.12.32.$OCTET"
carve_raw enp10s0 "$C_REPL" "10.12.36.$OCTET"

hdr "enp11s0 (ex-lbaas) -- left idle by design (no link)"
note "no action on enp11s0"

# ---- verify (read-only, both modes) ---------------------------------------
hdr "resulting interface tree (live)"
maas_q interfaces read "$SID" | jq -r '
  .[] | "  \(.name)\ttype=\(.type)\tvlan=\(.vlan.fabric):\(.vlan.vid)\tlinks=\([.links[]?|{(.subnet.cidr // "none"):(.ip_address // .mode)}])"' | sort

echo
if [ "$MODE" = dryrun ]; then
  note "DRY-RUN only -- nothing changed. Re-run with --apply to execute."
fi
echo "Summary: ${FATAL} fatal"
[ "$FATAL" -gt 0 ] && exit 1
exit 0