# =====================================================================
# vr1-dc1 INNER SUBSTRATE (G12, authored 2026-07-21) -- D-123 MODEL B, created
# inside vvr1-dc1. MIRROR of opentofu/vr1-dc0-substrate/main.tf (the pattern of
# record: its comments carry the measured traps -- provider dial, known_hosts,
# upload-from-executing-host -- and apply here unchanged). Run this root AFTER:
# (1) the outer opentofu/ root has booted + sized vvr1-dc1, and (2) the bootstrap
# gate (site-headend-install.sh node-host mode) has installed libvirtd + the
# inner pool + kvm nested=1 + the OPNsense base image on vvr1-dc1.
# =====================================================================

provider "libvirt" {
  # qemu+ssh to vvr1-dc1 over the office1<->dc1 transit. Key auth + known_hosts
  # verified out of band (NO no_verify). The IP is MEASURED after the outer apply
  # (hard rule 2). keyfile + sshauth are REQUIRED URI params (dc0, measured
  # 2026-07-20: the provider's Go ssh transport ignores ~/.ssh/config and default
  # identities). known_hosts: ALL key types must be present (dc0's measured
  # "knownhosts: key mismatch" trap; seed via ssh-keyscan, cross-checked).
  uri = "qemu+ssh://${var.vvr1_dc1_ssh_user}@${var.vvr1_dc1_transit_ip}/system?keyfile=${var.vvr1_dc1_ssh_keyfile}&sshauth=privkey"
}

# Inner storage pool (backs the node + edge disks) -- created inside vvr1-dc1 at
# the path the bootstrap step provisioned.
module "inner_storage" {
  source      = "../modules/dc-storage-pool"
  dc_name     = "vr1-dc1-inner"
  target_path = var.inner_pool_path
}

# The six vr1-dc1 planes (D-124 amendment 2026-07-21 layout -- contiguous /22s in
# 10.12.64.0/19, dc0's role ORDER kept) -- isolated-L2 networks INSIDE vvr1-dc1.
# metal-admin is where the rack serves PXE/DHCP to the inner nodes.
module "vr1_dc1_planes" {
  source        = "../modules/dc-planes"
  dc_name       = "vr1-dc1"
  domain_suffix = var.domain_suffix
  mtu           = var.underlay_mtu
  planes        = var.vr1_dc1_planes
}

# D-125 bridge-in (dc0 pattern, PROVEN there end to end): the DC WAN segment is a
# BRIDGE onto vvr1-dc1's br-vr1-dc1-wan (the IP-less uplink NIC on the vcloud ISP
# NAT `vr1-dc1-uplink` = 172.30.3.0/24, declared in the outer root's vvr1-dc1
# netplan). ONE NAT, at vcloud. host_bridge MUST match the netplan bridge name in
# opentofu/main.tf. The D-125 egress isolation gate re-runs at dc1 standup.
module "vr1_dc1_wan" {
  source       = "../modules/wan-bridge"
  network_name = "vr1-dc1-wan"
  host_bridge  = "br-vr1-dc1-wan"
  # mtu defaults to 1500 (ISP-uplink domain; NOT the jumbo planes/mesh).
}

# D-122: the DC edge -- 2-NIC (WAN + LAN), Office1 pattern (2048/2/nano). LAN =
# provider-public (external gateway, D-100; ruled gw 10.12.64.1 per the D-124
# amendment); WAN = the dedicated uplink above (static .2 on 172.30.3.0/24, set
# via the D-113 API path at the standup addressing step, not here).
module "vr1_dc1_opnsense" {
  source           = "../modules/opnsense-edge"
  vm_name          = "vr1-dc1-opnsense"
  autostart        = true # D-127: the DC edge comes up with its containment VM (routing for the site)
  memory_mib       = 2048
  vcpu             = 2
  pool_name        = module.inner_storage.pool_name
  base_volume_path = var.opnsense_base_path
  lan_network_name = module.vr1_dc1_planes.network_names["provider-public"]
  wan_network_name = module.vr1_dc1_wan.network_name
  # D-129: NEW DC edges boot WITH the qga channel so they never need the retrofit
  # (operator-ruled 2026-07-20).
  expose_qga_channel = true
}

# D-121 Option C layout (per-DC): 3 control + 2 compute + 4 storage = 9 nodes.
# Six NICs each (one per plane); metal-admin FIRST = PXE/boot plane (D-052 default
# binding).
locals {
  vr1_dc1_node_nics = [
    module.vr1_dc1_planes.network_names["metal-admin"], # PXE / boot fabric first
    module.vr1_dc1_planes.network_names["provider-public"],
    module.vr1_dc1_planes.network_names["metal-internal"],
    module.vr1_dc1_planes.network_names["data-tenant"],
    module.vr1_dc1_planes.network_names["storage"],
    module.vr1_dc1_planes.network_names["replication"],
  ]

  # macs: PRE-ASSIGNED at authoring (2026-07-21) so the fleet is pinned from the
  # FIRST apply -- the D-124-amendment standup invariant, and the direct lesson of
  # dc0's 2026-07-20 incident (unpinned MACs regenerated by an in-place apply
  # stranded the whole fleet in MAAS; dc0 had to measure-and-pin after the fact).
  # Scheme, deterministic and collision-free by construction:
  #   52:54:01:d1:NN:PP
  #   ^^^^^^^^ locally-administered unicast (bit 0x02 of octet 1), one octet away
  #            from libvirt's auto-generation space 52:54:00:* -- an auto MAC can
  #            never collide with these.
  #        ^^ d1 tags vr1-DC1 (dc0's pins are measured 52:54:00 values, not this
  #           scheme; future DCs take their own tag octet).
  #           NN = node ordinal 01..09 in the map order below;
  #           PP = NIC ordinal 01..06 in vr1_dc1_node_nics order (metal-admin
  #           first). These are IDENTITY values, not tunables -- change one and
  #           MAAS no longer knows the node.
  vr1_dc1_nodes = {
    "vr1-dc1-control-01" = { vcpu = 16, mem = 65536, disk_gib = 150, macs = [
      "52:54:01:d1:01:01", "52:54:01:d1:01:02", "52:54:01:d1:01:03",
      "52:54:01:d1:01:04", "52:54:01:d1:01:05", "52:54:01:d1:01:06",
    ] }
    "vr1-dc1-control-02" = { vcpu = 16, mem = 65536, disk_gib = 150, macs = [
      "52:54:01:d1:02:01", "52:54:01:d1:02:02", "52:54:01:d1:02:03",
      "52:54:01:d1:02:04", "52:54:01:d1:02:05", "52:54:01:d1:02:06",
    ] }
    "vr1-dc1-control-03" = { vcpu = 16, mem = 65536, disk_gib = 150, macs = [
      "52:54:01:d1:03:01", "52:54:01:d1:03:02", "52:54:01:d1:03:03",
      "52:54:01:d1:03:04", "52:54:01:d1:03:05", "52:54:01:d1:03:06",
    ] }
    "vr1-dc1-compute-01" = { vcpu = 12, mem = 49152, disk_gib = 100, macs = [
      "52:54:01:d1:04:01", "52:54:01:d1:04:02", "52:54:01:d1:04:03",
      "52:54:01:d1:04:04", "52:54:01:d1:04:05", "52:54:01:d1:04:06",
    ] }
    "vr1-dc1-compute-02" = { vcpu = 12, mem = 49152, disk_gib = 100, macs = [
      "52:54:01:d1:05:01", "52:54:01:d1:05:02", "52:54:01:d1:05:03",
      "52:54:01:d1:05:04", "52:54:01:d1:05:05", "52:54:01:d1:05:06",
    ] }
    "vr1-dc1-storage-01" = { vcpu = 8, mem = 24576, disk_gib = 550, osd_gib = 500, macs = [
      "52:54:01:d1:06:01", "52:54:01:d1:06:02", "52:54:01:d1:06:03",
      "52:54:01:d1:06:04", "52:54:01:d1:06:05", "52:54:01:d1:06:06",
    ] }
    "vr1-dc1-storage-02" = { vcpu = 8, mem = 24576, disk_gib = 550, osd_gib = 500, macs = [
      "52:54:01:d1:07:01", "52:54:01:d1:07:02", "52:54:01:d1:07:03",
      "52:54:01:d1:07:04", "52:54:01:d1:07:05", "52:54:01:d1:07:06",
    ] }
    "vr1-dc1-storage-03" = { vcpu = 8, mem = 24576, disk_gib = 550, osd_gib = 500, macs = [
      "52:54:01:d1:08:01", "52:54:01:d1:08:02", "52:54:01:d1:08:03",
      "52:54:01:d1:08:04", "52:54:01:d1:08:05", "52:54:01:d1:08:06",
    ] }
    "vr1-dc1-storage-04" = { vcpu = 8, mem = 24576, disk_gib = 550, osd_gib = 500, macs = [ # Option C: 4th OSD host
      "52:54:01:d1:09:01", "52:54:01:d1:09:02", "52:54:01:d1:09:03",
      "52:54:01:d1:09:04", "52:54:01:d1:09:05", "52:54:01:d1:09:06",
    ] }
    # ---- D-104 AMENDMENT (RULED 2026-07-24): the DEDICATED per-DC Juju controller.
    # NOT a D-121 role node -- the 9 OpenStack nodes above are unchanged, so Option C's
    # 3/2/4 shape, the R-3 Ceph rebuild headroom and the control-quorum spread all stand.
    # `for_each` is keyed, so this plans 1 add / 0 change / 0 destroy.
    #
    # SIZING: 8 GiB / 4 vCPU are the values the capacity GATE actually modelled
    # (`scripts/dc-dc-whole-host-budget.py`, 10-node x 2-DC -> RAM 854/1024 = 83 pct, FIT,
    # 170 GiB headroom; capture docs/audit/stage5-controller-capacity-20260724.txt).
    # Disk is the authoring value the amendment left open; 100 GiB matches the smallest
    # existing node class.
    #
    # MACs DELIBERATELY EMPTY. modules/node-vm documents that as correct ONLY before a
    # node is enlisted anywhere -- which is this VM's state. PIN THEM FROM MEASUREMENT
    # after its first apply, exactly as the nine role nodes were (2026-07-21); leaving
    # them unpinned past enlistment is the MAC-regeneration trap that stranded the fleet.
    #
    # Its MAAS tag is `juju-controller-vr1-dc1` and it carries NO role tag, so
    # `juju bootstrap --constraints tags=juju-controller-vr1-dc1` targets it
    # deterministically and no OpenStack role constraint can cross-grab it.
    "vr1-dc1-juju-01" = { vcpu = 4, mem = 8192, disk_gib = 100, macs = [] }

  }
}

module "vr1_dc1_node" {
  for_each = local.vr1_dc1_nodes

  source          = "../modules/node-vm"
  vm_name         = each.key
  autostart       = false # D-127: node VMs are MAAS-power-controlled -- MANUAL, never on host boot
  vcpu            = each.value.vcpu
  memory_mib      = each.value.mem
  disk_size_bytes = each.value.disk_gib * 1024 * 1024 * 1024
  # R1 (D-121 amendment): opt-in OSD data device, storage nodes only.
  # `try(..., 0)` keeps every other node's plan byte-identical.
  osd_disk_size_bytes = try(each.value.osd_gib, 0) * 1024 * 1024 * 1024
  pool_name           = module.inner_storage.pool_name
  network_names       = local.vr1_dc1_node_nics
  interface_macs      = each.value.macs # pinned FROM THE FIRST APPLY -- see the scheme comment
}
