# scripts/lib-hosts.sh
#
# Single source of truth for the four OpenStack KVM host VMs (VR0 / Baldurkeep):
# their enrollment identity and virsh power parameters. SOURCED, not executed.
# ASCII + LF. Contains constants + read-only helpers ONLY; no mutations.
#
# WHY hostname-keyed (NOT system_id-keyed): MAAS system_ids are minted fresh on
# every (re-)enrollment. The old 4na83t/qdbqd6/h8frng/tmsafc died when the hosts
# were decomposed on 2026-06-26 and re-enrollment assigned new random ids. The
# stable identities are the hostname and the libvirt domain name, so every map
# here keys on hostname and the live system_id is resolved AT RUNTIME via
# host_sysid(). This SUPERSEDES the SYSID-keyed maps in lib-net.sh (DOCFIX-040).
#
# NO SECRET lives here. The libvirt SSH password is read interactively at run time
# (reenroll-hosts.sh) and is never written to a file, a command line, or the repo.

# shellcheck shell=bash
# shellcheck disable=SC2034  # constants are consumed by sourcing scripts

# Guard: sourced only.
if [ "${BASH_SOURCE[0]:-}" = "${0}" ]; then
  echo "lib-hosts.sh is a sourced library; do not run it directly." >&2
  exit 2
fi

# The four OpenStack KVM hosts. libvirt domain name == MAAS hostname == power_id.
HOSTS=( openstack0 openstack1 openstack2 openstack3 )

# Auxiliary (non-Juju-managed) VMs that dc-node-carve.sh may carve via an
# EXPLICIT --host, but which MUST NOT enter HOSTS: every HOSTS consumer
# (maas-role-tags, pre-flight-checks ${#HOSTS[@]}, phase-00-teardown, reenroll,
# dc-node-v6-carve) would otherwise treat them as OpenStack role nodes. Populated
# per-DC below; empty at the flat/VR0 layer. (D-129(iii) amdt 2026-08-07.)
CARVE_AUX_HOSTS=()

# host -> last IPv4 octet on every plane (.40-.43). Stable by design (D-052 index).
declare -A HOST_OCTET=( [openstack0]=40 [openstack1]=41 [openstack2]=42 [openstack3]=43 )

# host -> PXE/boot NIC MAC == the 2_metal interface (libvirt <boot order='1'/>).
# Fixed in the libvirt domain XML; captured 2026-06-26. The boot NIC MUST be the
# metal/PXE plane or commissioning cannot DHCP/PXE.
declare -A HOST_BOOT_MAC=(
  [openstack0]=52:54:00:4f:1c:0b
  [openstack1]=52:54:00:83:25:1f
  [openstack2]=52:54:00:23:bd:72
  [openstack3]=52:54:00:b2:7b:30
)

# virsh power (non-secret). MAAS reaches libvirt over SSH on the OOB host address.
# Mirrors the surviving juju/lxd/tailscale virsh machines' live config (read
# 2026-06-26). power_id is set per host to the hostname by the caller.
VIRSH_POWER_ADDRESS="qemu+ssh://logxen@10.12.64.1/system"
# VR1 NODE NIC -> PLANE ORDER (MEASURED 2026-07-30; D-133 flat per-NIC carve).
# The substrate roots pin six MACs per node, and the module attaches them in list
# order, so macs[i] IS enp<i+1>s0. Verified end-to-end on vr1-dc0-control-01:
#   main.tf macs[0..5] = be:69:c5, 29:e5:2b, 05:98:c4, 96:e8:36, 5a:dc:91, 35:cc:01
#   live carve         = enp1s0 be:69:c5 metal-admin      (fabric-4, PXE)
#                        enp2s0 29:e5:2b provider-public  (br-ex inherits this MAC)
#                        enp3s0 05:98:c4 metal-internal
#                        enp4s0 96:e8:36 data-tenant
#                        enp5s0 5a:dc:91 storage
#                        enp6s0 35:cc:01 replication
#
# !! THIS IS NOT lib-net.sh's PLANE_CIDRS ORDER. !! PLANE_CIDRS starts with
# provider-public; the NIC order starts with METAL-ADMIN. Anything that walks
# PLANE_CIDRS positionally to assign NICs swaps enp1s0 and enp2s0 and puts the
# PXE/metal-admin leg on provider-public -- which would strand commissioning on a
# plane with no DHCP. Iterate THIS array for NIC placement, never PLANE_CIDRS.
NIC_PLANE_ORDER=( metal-admin provider-public metal-internal data-tenant storage replication )
# The provider-public NIC is the OVS br-ex parent (Pattern A): the node's static
# lands on br-ex, and the member NIC itself carries no L3 link.
BREX_PARENT_NIC="enp2s0"

# THE CARVE IS NOT UNIFORM ACROSS ROLES -- measured 2026-07-30 from the
# pre-migration capture, and assuming uniformity would mis-carve the controller.
#   ROLE NODES (the 9 openstack-<site> machines): ALL SIX planes carved, plus an
#     OVS br-ex bridge parented on enp2s0 carrying the provider-public static.
#     enp2s0 itself holds NO L3 link.
#   JUJU CONTROLLER (<dc>-juju-01): only TWO planes -- enp1s0 metal-admin and
#     enp2s0 provider-public (raw NIC, WITH the gateway). NO br-ex, and
#     enp3s0..enp6s0 are deliberately LEFT on their auto-created VLANs. It runs
#     no workloads, so it needs no tenant/storage/replication legs; it needs a
#     default route, which is what the provider-public leg with a gateway gives
#     it (the under-carve that cost three bootstrap attempts on 2026-07-30).
# Evidence: capture rows for 7n87bt (controller, enp3-6 on vlans 5200-5203 =
# auto) versus 8mtpxq (storage-02, enp3-6 on 5190-5193 = named planes + br-ex).
NODE_CARVE_ROLE_PLANES=6      # role nodes: every plane
NODE_CARVE_JUJU_PLANES=2      # controller: metal-admin + provider-public only
JUJU_HOST_SUFFIX="juju-01"    # how the controller is identified in HOSTS
# The per-DC Tailscale subnet router carves the SAME shape as the controller --
# metal-admin + provider-public, NO OVS br-ex -- just at the utility .7 octet
# (D-129(iii) amdt 2026-08-07; provider-public egress reaches Headscale).
NODE_CARVE_TAILSCALE_PLANES=2
TAILSCALE_HOST_SUFFIX="tailscale-01"  # how the aux Tailscale VM is identified
# The per-DC MAAS region VM (.6) carves the SAME 2-plane shape (metal-admin +
# provider-public, no br-ex) -- metal-admin is its service address (nodes reach
# MAAS at :5240), provider-public is egress for image sync (D-134 octet .6). It
# is bootstrapped BY Office1 (deployed there, then MAAS installed on it), so its
# carve runs against --profile admin with all Office1 racks in --expect-rack.
REGION_HOST_SUFFIX="maas-01"

# The per-region forms exist only for the VR1 DCs (D-132 q1). Declared EMPTY at
# the flat/VR0 layer so a shell that never selects a VR1 DC cannot pick up a
# neighbouring DC's address -- an empty value fails loud; a stale one powers off
# the wrong rack's machines.
VIRSH_POWER_ADDRESS_FROM_OFFICE1=""
VIRSH_POWER_ADDRESS_FROM_DCREGION=""
HOST_ARCH="amd64"

# MAAS tag the deploy bundle places units against (constraint tags=openstack).
# Must be (re-)applied to every host post-commission or the bundle cannot bind.
HOST_TAG="openstack"

# host_sysid <hostname>: resolve the LIVE MAAS system_id by hostname (never
# hardcode it). Echoes the system_id, or empty if the host is not enrolled.
# VR0 ONLY: VR0 hostnames are stable (hostname == domain name == power_id).
# For VR1 DC nodes MAAS ASSIGNS a RANDOM name, RE-MINTED on every (re-)enlistment
# (measured: the 2026-07-21 pod-delete recovery renamed all 9) -- so RESOLUTION
# must use host_sysid_by_bootmac() here, never the hostname. BUT the operator
# convention (D-134 amendment 2026-08-07) is to SET the hostname back to
# vr1-<dc>-<role>-NN (== domain == power_id) after commission/deploy for
# operability; "random by default" is not "leave it random". Standup DoD: zero
# non-vr1-<dc>-* hostnames in the DC's region.
host_sysid() {
  local hn="$1"
  maas "${MAAS_PROFILE:-admin}" machines read 2>/dev/null \
    | jq -r --arg h "$hn" '.[] | select(.hostname==$h) | .system_id' | head -1
}

# host_sysid_by_bootmac <mac>: resolve the LIVE MAAS system_id by boot-NIC MAC.
# THE VR1 resolver: VR1 node identity is the tofu node name -> its config-PINNED
# boot MAC (opentofu/vr1-dcN-substrate; pinned 2026-07-21/22); the MAC survives
# re-enlistment, the random hostname does not.
host_sysid_by_bootmac() {
  local mac="$1"
  maas "${MAAS_PROFILE:-admin}" machines read 2>/dev/null \
    | jq -r --arg m "$mac" '.[] | select(.boot_interface.mac_address==$m) | .system_id' | head -1
}

# --- $DC selector convention (2026-07-09, tooling gap register #1) ---
#
# Mirrors lib-net.sh's lib_net_select_dc -- SAME region-qualified selectors
# (D-119). The VR1 arms were POPULATED 2026-07-23 (Stage 4 carve session) from
# MEASURED enrollment: both fleets commissioned Ready, identities cross-checked
# tofu-name -> pinned boot MAC -> live system_id 9/9 per DC
# (docs/audit/stage4-carve-verify-20260723.txt). Until then both VR1 arms
# failed loud by design; that era's asymmetry note is history (git).
#
# VR1 IDENTITY MODEL (differs from VR0 -- read before consuming these maps):
#   - HOSTS entries are the TOFU NODE NAMES (config-of-record, stable), NOT
#     MAAS hostnames -- MAAS names are random and re-minted on re-enlistment.
#   - Resolve a live system_id via host_sysid_by_bootmac "${HOST_BOOT_MAC[$h]}"
#     (never via host_sysid; never cache system_ids).
#   - HOST_OCTET carries the D-134-AMENDED per-role bands (control .100-.102,
#     compute .120-.121, storage .150-.153), same octet on all six planes
#     (D-133 flat carve -- applied + verified live, capture above).
#   - VIRSH_POWER_ADDRESS is the per-DC rack transit URI (ruled step-D shape,
#     per-machine virsh power -- D-103/D-123 amendments; SEC-012/SEC-016 keys).
#   - THE POWER ADDRESS DEPENDS ON WHICH MAAS REGION DIALS IT (measured
#     2026-07-30, D-132 q1 per-DC regions). MAAS's own power ops originate from
#     the REGION, so the reachable rack address differs by region:
#       VIRSH_POWER_ADDRESS_FROM_OFFICE1  -- the rack's TRANSIT /30 leg. What the
#         Office1 region has always used. MEASURED CLOSED from a DC-local region.
#       VIRSH_POWER_ADDRESS_FROM_DCREGION -- the rack's METAL-ADMIN leg. What a
#         DC-local MAAS region (D-132 q1, at utility .6) must use. MEASURED from
#         vr1-dc0-maas-01: 172.31.0.2:22 CLOSED, 10.12.8.2:22 OPEN; its only
#         routes are 10.12.4.0/22, 10.12.8.0/22 and a default via 10.12.4.1.
#     VIRSH_POWER_ADDRESS remains an ALIAS OF THE OFFICE1 FORM so existing
#     callers (reenroll-hosts.sh, the teardown runbook's virsh probes) are
#     bit-for-bit unchanged. Flipping the default to the DC-region form is OWED
#     once BOTH DCs have migrated -- doing it now would silently change the
#     teardown path while Office1 still owns dc1's nodes.
#   - HOST_TAG is the region-qualified per-DC placement tag (ruled 2026-07-23).
# ONE SELECTION PER SHELL: cross-DC re-selection is REFUSED (stale-value
# guard, lib-net precedent); same-DC re-selection is a no-op.
_LIB_HOSTS_SELECTED=""
lib_hosts_select_dc() {
  local dc="${1:?usage: lib_hosts_select_dc <vr0-dc0|vr1-dc0|vr1-dc1>}"
  if [ -n "$_LIB_HOSTS_SELECTED" ] && [ "$_LIB_HOSTS_SELECTED" != "$dc" ]; then
    echo "FAIL: lib-hosts already selected '$_LIB_HOSTS_SELECTED' in this shell -- start a fresh shell to select '$dc' (stale cross-DC host values are the D-117 off-by-one class)" >&2
    return 1
  fi
  case "$dc" in
    vr0-dc0)
      : # already the sourced defaults above -- explicit no-op, not a guess.
      ;;
    vr1-dc0)
      # MEASURED 2026-07-23 (Stage 4): 9/9 Ready; MACs are the config pins
      # (opentofu/vr1-dc0-substrate, pinned from virsh domiflist 2026-07-21);
      # octets applied live per D-134 amendment; tag applied 9/9 same day.
      HOSTS=( vr1-dc0-control-01 vr1-dc0-control-02 vr1-dc0-control-03
              vr1-dc0-compute-01 vr1-dc0-compute-02
              vr1-dc0-storage-01 vr1-dc0-storage-02 vr1-dc0-storage-03 vr1-dc0-storage-04 
              vr1-dc0-juju-01 )
      HOST_OCTET=( [vr1-dc0-control-01]=100 [vr1-dc0-control-02]=101 [vr1-dc0-control-03]=102
                   [vr1-dc0-compute-01]=120 [vr1-dc0-compute-02]=121
                   [vr1-dc0-storage-01]=150 [vr1-dc0-storage-02]=151
                   [vr1-dc0-storage-03]=152 [vr1-dc0-storage-04]=153
                   [vr1-dc0-juju-01]=5 [vr1-dc0-tailscale-01]=7 [vr1-dc0-maas-01]=6 )
      HOST_BOOT_MAC=( [vr1-dc0-control-01]=52:54:00:be:69:c5
                      [vr1-dc0-control-02]=52:54:00:02:ff:57
                      [vr1-dc0-control-03]=52:54:00:4f:de:a9
                      [vr1-dc0-compute-01]=52:54:00:1b:19:e6
                      [vr1-dc0-compute-02]=52:54:00:18:ab:b4
                      [vr1-dc0-storage-01]=52:54:00:5f:8d:42
                      [vr1-dc0-storage-02]=52:54:00:48:86:2c
                      [vr1-dc0-storage-03]=52:54:00:b1:94:d1
                      [vr1-dc0-storage-04]=52:54:00:2b:ed:ab
                      [vr1-dc0-juju-01]=52:54:00:48:e7:1e
                      [vr1-dc0-tailscale-01]=52:54:00:54:d5:5e
                      [vr1-dc0-maas-01]=52:54:00:dc:5c:0b )
      VIRSH_POWER_ADDRESS_FROM_OFFICE1="qemu+ssh://jessea123@172.31.0.2/system"
      VIRSH_POWER_ADDRESS_FROM_DCREGION="qemu+ssh://jessea123@10.12.8.2/system"
      VIRSH_POWER_ADDRESS="$VIRSH_POWER_ADDRESS_FROM_OFFICE1"
      HOST_TAG="openstack-vr1-dc0"
      # The per-DC Tailscale subnet router (.7): an AUX carve target reached via
      # --host, NOT an OpenStack role node (stays out of HOSTS). Boot MAC + octet
      # above pin its identity; the substrate pins the domain (main.tf:243).
      CARVE_AUX_HOSTS=( vr1-dc0-tailscale-01 vr1-dc0-maas-01 )
      ;;
    vr1-dc1)
      # MEASURED 2026-07-23 (Stage 4): 9/9 Ready; MACs are the config pins
      # (opentofu/vr1-dc1-substrate, 52:54:01:d1:NN:01 encodes the node index);
      # octets applied live per D-134 amendment; tag applied 9/9 same day.
      HOSTS=( vr1-dc1-control-01 vr1-dc1-control-02 vr1-dc1-control-03
              vr1-dc1-compute-01 vr1-dc1-compute-02
              vr1-dc1-storage-01 vr1-dc1-storage-02 vr1-dc1-storage-03 vr1-dc1-storage-04 
              vr1-dc1-juju-01 )
      HOST_OCTET=( [vr1-dc1-control-01]=100 [vr1-dc1-control-02]=101 [vr1-dc1-control-03]=102
                   [vr1-dc1-compute-01]=120 [vr1-dc1-compute-02]=121
                   [vr1-dc1-storage-01]=150 [vr1-dc1-storage-02]=151
                   [vr1-dc1-storage-03]=152 [vr1-dc1-storage-04]=153
                   [vr1-dc1-juju-01]=5 [vr1-dc1-tailscale-01]=7 [vr1-dc1-maas-01]=6 )
      HOST_BOOT_MAC=( [vr1-dc1-control-01]=52:54:01:d1:01:01
                      [vr1-dc1-control-02]=52:54:01:d1:02:01
                      [vr1-dc1-control-03]=52:54:01:d1:03:01
                      [vr1-dc1-compute-01]=52:54:01:d1:04:01
                      [vr1-dc1-compute-02]=52:54:01:d1:05:01
                      [vr1-dc1-storage-01]=52:54:01:d1:06:01
                      [vr1-dc1-storage-02]=52:54:01:d1:07:01
                      [vr1-dc1-storage-03]=52:54:01:d1:08:01
                      [vr1-dc1-storage-04]=52:54:01:d1:09:01
                      [vr1-dc1-juju-01]=52:54:00:53:12:70
                      [vr1-dc1-tailscale-01]=52:54:00:f9:d6:df
                      [vr1-dc1-maas-01]=52:54:00:ff:5c:02 )
      VIRSH_POWER_ADDRESS_FROM_OFFICE1="qemu+ssh://jessea123@172.31.0.6/system"
      # 10.12.68.2 MEASURED on the dc1 rack 2026-07-30 (`ip -4 -o addr`: virbr6
      # inet 10.12.68.2/22), corroborated by dc-rack-net.sh's dc1 LEGS block.
      # NOT inferred from dc0's .2 by symmetry.
      VIRSH_POWER_ADDRESS_FROM_DCREGION="qemu+ssh://jessea123@10.12.68.2/system"
      VIRSH_POWER_ADDRESS="$VIRSH_POWER_ADDRESS_FROM_OFFICE1"
      HOST_TAG="openstack-vr1-dc1"
      # The per-DC Tailscale subnet router (.7): an AUX carve target reached via
      # --host, NOT an OpenStack role node (stays out of HOSTS). MAC pinned from
      # opentofu/vr1-dc1-substrate/main.tf:207. dc1 carve is gated on the
      # dedicated vr1-dc1 region being built first (operator ruling 2026-08-07).
      CARVE_AUX_HOSTS=( vr1-dc1-tailscale-01 vr1-dc1-maas-01 )
      ;;
    dc0|dc1|dc2)
      echo "FAIL: bare '$dc' is RETIRED (D-119). It was AMBIGUOUS ACROSS REGIONS: 'dc0' meant VR0's live DC0 here, but VR1's FIRST DC in the NetBox importer. Use the region-qualified selector: vr0-dc0 | vr1-dc0 | vr1-dc1" >&2
      return 1
      ;;
    *)
      echo "FAIL: unknown DC '$dc' (expected vr0-dc0, vr1-dc0, or vr1-dc1)" >&2
      return 1
      ;;
  esac
  _LIB_HOSTS_SELECTED="$dc"
}
