#!/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, NO L3, UNTAGGED 1_provider (D-057) -- ovn-chassis MAC-enslaves it
# into OVS br-ex at deploy; the uplink carries NO
# host static (a static here forced a Linux bridge
# that starved br-ex). MAAS must leave enp1s0 RAW.
# enp1s0.104 --> br-prov-api (standard bridge) + STATIC 10.12.8.N (provider-vip; D-057)
# tagged secondary; public API VIP plane + container
# 'public' attach. Mirrors the metal-internal stack.
# enp7s0 --> br-metal (standard bridge) + STATIC 10.12.12.N (metal-admin)
# br-metal.103 (vlan, VID 103)
# --> br-internal (standard bridge) + STATIC 10.12.16.N (metal-internal)
# enp8s0 raw + STATIC 10.12.20.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.12.0/22"; C_INT="$METAL_INTERNAL_CIDR" # 10.12.16.0/22
C_PVIP="$PROVIDER_VIP_CIDR" # 10.12.8.0/22 (D-057 provider-vip; tagged VID 104)
C_DATA="10.12.20.0/22"; C_STOR="10.12.32.0/22"; C_REPL="10.12.36.0/22"
# assert all planes resolve, and the tagged planes are really VID 103 / 104
for c in "$C_PROV" "$C_METAL" "$C_INT" "$C_PVIP" "$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; }
gotpvipvid="$(vlanvid_of "$C_PVIP")"
[ "$gotpvipvid" = "$PROVIDER_VIP_VID" ] || { fail "provider-vip $C_PVIP is VID '$gotpvipvid', expected $PROVIDER_VIP_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"
local out
if ! out="$(maas "$MAAS_PROFILE" "$@" 2>&1)"; then
fail "$desc"
echo " MAAS said: $(printf '%s' "$out" | grep -viE '^(Success|Machine-readable)' | head -3 | tr '\n' ' ')" >&2
return 1
fi
else
echo " WOULD: $desc"
echo " maas $MAAS_PROFILE $*"
fi
}
# release_self_indexed <ip> <subid>: release a DISCOVERED (alloc_type 6) record on
# <ip> so a STATIC can take it. <ip> is THIS host's architecturally-indexed metal
# address (10.12.12.<octet> from HOST_OCTET), so a discovered observation on it is this
# host's own commissioning ghost. SAFETY: refuse if ANY source positively identifies a
# DIFFERENT owner -- the StaticIPAddress node_summary.system_id (when present) must equal
# this host's SID, and the discoveries-table MAC (when present) must equal this host's
# boot MAC. Absent positive foreign identification, releasing a DISCOVERED (advisory)
# observation on this host's own indexed IP is safe.
# (node_summary.system_id is often EMPTY on a fresh discovered record -- that is why the
# earlier system_id-only gate silently no-op'd and the metal static had to be released by
# hand. See troubleshooting appendix / changelog.)
release_self_indexed() {
local ip="$1" subid="$2" rec sid_owner disc_mac basis
rec="$(maas_q subnet ip-addresses "$subid" 2>/dev/null \
| jq -c --arg ip "$ip" '.[]|select(.ip==$ip and .alloc_type==6)' | head -1)"
[ -z "$rec" ] && return 0 # no discovered record -> nothing to do
sid_owner="$(printf '%s' "$rec" | jq -r '.node_summary.system_id // empty')"
if [ -n "$sid_owner" ] && [ "$sid_owner" != "$SID" ]; then
fail "$ip is DISCOVERED by a different node ($sid_owner), not $HN -- refusing (real conflict)"; return 1
fi
disc_mac="$(maas_q discoveries read 2>/dev/null | jq -r --arg ip "$ip" '.[]?|select(.ip==$ip)|.mac_address' | head -1)"
if [ -n "$disc_mac" ] && [ "$disc_mac" != "${HOST_BOOT_MAC[$HN]}" ]; then
fail "$ip observed from MAC $disc_mac, not $HN boot MAC ${HOST_BOOT_MAC[$HN]} -- refusing (real conflict)"; return 1
fi
basis="this host's indexed metal IP"
[ "$sid_owner" = "$SID" ] && basis="system_id match ($SID)"
[ -n "$disc_mac" ] && [ "$disc_mac" = "${HOST_BOOT_MAC[$HN]}" ] && basis="boot-MAC match ($disc_mac)"
note "releasing DISCOVERED $ip (basis: $basis)"
emit "release DISCOVERED $ip" ipaddresses release ip="$ip" force=true discovered=true
}
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 " prov-vip %s sub=%s vlan=%s (vid %s)\n" "$C_PVIP" "$(subid_of "$C_PVIP")" "$(vlanid_of "$C_PVIP")" "$gotpvipvid"
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"
}
# D-057 helper: make a NIC raw + L3-LESS on its plane's UNTAGGED vlan (no subnet link),
# so ovn-chassis can MAC-enslave it into OVS br-ex at deploy. Idempotent.
ensure_raw_unlinked() {
local nic="$1" cidr="$2" id provvlan curvlan lid
id="$(ifid_of "$nic")"; [ -n "$id" ] || { fail "$nic not found on $HN"; return 1; }
provvlan="$(vlanid_of "$cidr")"
# unlink any subnet link(s) first so the uplink carries no L3
for lid in $(maas_q interfaces read "$SID" | jq -r --arg n "$nic" '.[]|select(.name==$n)|.links[]?|select(.subnet!=null)|.id'); do
emit "unlink $nic(id=$id) link id=$lid (L3-less for OVS br-ex)" interface unlink-subnet "$SID" "$id" id="$lid"
done
# ensure it sits on the UNTAGGED vlan of $cidr -- only if not already there (idempotent)
curvlan="$(maas_q interfaces read "$SID" | jq -r --arg n "$nic" '.[]|select(.name==$n)|(.vlan.id // .vlan // empty)' | head -1)"
if [ "$curvlan" != "$provvlan" ]; then
emit "$nic(id=$id) -> VLAN $provvlan ($cidr untagged, no L3)" interface update "$SID" "$id" vlan="$provvlan"
else
note "$nic already on VLAN $provvlan -- SKIP vlan set"
fi
}
hdr "provider plane (enp1s0 RAW + L3-LESS -- OVS br-ex uplink; D-057)"
# D-057: the OVS provider uplink must carry NO L3. ovn-chassis MAC-enslaves enp1s0 into
# br-ex at deploy; the old host static 10.12.4.N is REMOVED here (it forced a Linux bridge
# br-enp1s0 that captured enp1s0 and starved br-ex). Leave enp1s0 raw + unlinked.
ensure_raw_unlinked enp1s0 "$C_PROV"
hdr "provider-vip plane (enp1s0.$PROVIDER_VIP_VID -> br-prov-api -> static; D-057)"
# Tagged secondary on the provider NIC, mirroring metal-internal (br-metal.103 -> br-internal).
# The bundle binds the API charms' 'public' endpoint to the provider-vip space, so containers
# attach HERE (not the untagged uplink). The host provider-plane static MOVES here from enp1s0.
PEID="$(ifid_of enp1s0)"; [ -n "$PEID" ] || fail "enp1s0 not found"
# 1) enp1s0.104 (VLAN, VID 104) on enp1s0
if [ -z "$(ifid_of "enp1s0.$PROVIDER_VIP_VID")" ]; then
emit "create enp1s0.$PROVIDER_VIP_VID (VID $PROVIDER_VIP_VID, vlan obj $(vlanid_of "$C_PVIP")) parent=enp1s0(id=$PEID)" \
interfaces create-vlan "$SID" vlan="$(vlanid_of "$C_PVIP")" parent="$PEID"
else note "enp1s0.$PROVIDER_VIP_VID exists -- SKIP create"; fi
[ "$MODE" = apply ] && PVID="$(ifid_of "enp1s0.$PROVIDER_VIP_VID")" || PVID="<enp1s0.$PROVIDER_VIP_VID-id>"
# 2) br-prov-api (standard) on enp1s0.104
if [ -z "$(ifid_of br-prov-api)" ]; then
emit "create br-prov-api (standard) parent=enp1s0.$PROVIDER_VIP_VID(id=$PVID)" \
interfaces create-bridge "$SID" name=br-prov-api bridge_type=standard parent="$PVID"
else note "br-prov-api exists -- SKIP create"; fi
[ "$MODE" = apply ] && BPID="$(ifid_of br-prov-api)" || BPID="<br-prov-api-id>"
# 3) STATIC on br-prov-api (host provider-plane presence; OVS-free so no br-ex conflict)
if ! linked_to br-prov-api "$C_PVIP"; then
emit "br-prov-api(id=$BPID) -> STATIC 10.12.8.$OCTET on subnet $(subid_of "$C_PVIP")" \
interface link-subnet "$SID" "$BPID" mode=STATIC subnet="$(subid_of "$C_PVIP")" ip_address="10.12.8.$OCTET"
else note "br-prov-api already on $C_PVIP -- SKIP"; fi
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
release_self_indexed "10.12.12.$OCTET" "$(subid_of "$C_METAL")" || true
emit "br-metal(id=$BMID) -> STATIC 10.12.12.$OCTET on subnet $(subid_of "$C_METAL")" \
interface link-subnet "$SID" "$BMID" mode=STATIC subnet="$(subid_of "$C_METAL")" ip_address="10.12.12.$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.16.$OCTET on subnet $(subid_of "$C_INT")" \
interface link-subnet "$SID" "$BIID" mode=STATIC subnet="$(subid_of "$C_INT")" ip_address="10.12.16.$OCTET"
else note "br-internal already on $C_INT -- SKIP"; fi
hdr "data / storage / replication (raw + static)"
carve_raw enp8s0 "$C_DATA" "10.12.20.$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