Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
246 lines
9.7 KiB
Terraform
246 lines
9.7 KiB
Terraform
# NetBox object graph, driven by topology.yml.
|
|
#
|
|
# WHY yamldecode rather than HCL resources per object: topology.yml stays the readable,
|
|
# authoritative artifact (git -> NetBox, see ../CONTEXT.md §4), and Terraform supplies what
|
|
# a plain script could not — state, `plan` as a drift report, and DELETION. Removing an
|
|
# entry from the YAML now removes the object from NetBox, which the previous seed script
|
|
# never did.
|
|
#
|
|
# Ownership boundary, matching ../../../infrastructure/openbao/terraform: Terraform owns API-level
|
|
# configuration. The k8s manifests that RUN NetBox live one level up in ../.
|
|
|
|
locals {
|
|
topo = yamldecode(file("${path.module}/topology.yml"))
|
|
|
|
# --- flattened lookup maps -------------------------------------------------
|
|
# Interfaces are nested under devices/VMs in the YAML; Terraform needs flat maps keyed
|
|
# by a stable string. "<parent>:<iface>" is that key everywhere below.
|
|
device_ifaces = merge([
|
|
for d in local.topo.devices : {
|
|
for i in d.interfaces : "${d.name}:${i.name}" => merge(i, { device = d.name })
|
|
}
|
|
]...)
|
|
|
|
vm_ifaces = merge([
|
|
for v in local.topo.virtual_machines : {
|
|
for i in v.interfaces : "${v.name}:${i.name}" => merge(i, { vm = v.name })
|
|
}
|
|
]...)
|
|
|
|
# Only interfaces that actually carry an address.
|
|
device_ips = { for k, i in local.device_ifaces : k => i if try(i.ip, null) != null }
|
|
vm_ips = { for k, i in local.vm_ifaces : k => i if try(i.ip, null) != null }
|
|
|
|
# The single address that becomes the parent's primary_ip4.
|
|
device_primary = { for k, i in local.device_ips : i.device => k if try(i.primary, false) }
|
|
vm_primary = { for k, i in local.vm_ips : i.vm => k if try(i.primary, false) }
|
|
|
|
device_macs = { for k, i in local.device_ifaces : k => i if try(i.mac, null) != null }
|
|
|
|
inventory_items = merge([
|
|
for d in local.topo.devices : {
|
|
for it in try(d.inventory_items, []) : "${d.name}:${it.name}" => merge(it, { device = d.name })
|
|
}
|
|
]...)
|
|
}
|
|
|
|
# --- site + IPAM ---------------------------------------------------------------
|
|
resource "netbox_site" "this" {
|
|
name = local.topo.site.name
|
|
slug = local.topo.site.slug
|
|
description = local.topo.site.description
|
|
status = "active"
|
|
}
|
|
|
|
resource "netbox_ipam_role" "this" {
|
|
for_each = { for r in local.topo.prefix_roles : r.slug => r }
|
|
name = each.value.name
|
|
slug = each.value.slug
|
|
}
|
|
|
|
resource "netbox_vlan_group" "this" {
|
|
name = local.topo.vlan_group.name
|
|
slug = local.topo.vlan_group.slug
|
|
description = local.topo.vlan_group.description
|
|
# Required by the provider. The SDN zone is a plain VLAN zone on vmbr0, which is
|
|
# bridge-vlan-aware for the full range, so do not narrow this without changing that.
|
|
vid_ranges = [[1, 4094]]
|
|
}
|
|
|
|
resource "netbox_vlan" "this" {
|
|
for_each = { for v in local.topo.vlans : tostring(v.vid) => v }
|
|
vid = each.value.vid
|
|
name = each.value.name
|
|
group_id = netbox_vlan_group.this.id
|
|
site_id = netbox_site.this.id
|
|
status = "active"
|
|
}
|
|
|
|
resource "netbox_prefix" "this" {
|
|
for_each = { for p in local.topo.prefixes : p.prefix => p }
|
|
prefix = each.value.prefix
|
|
status = "active"
|
|
# The provider exposes plain `site_id` and handles NetBox 4.2+'s generic
|
|
# scope_type/scope_id internally — which is exactly the trap that broke the hand-rolled
|
|
# script (posting `site` was silently dropped). Using the provider avoids it.
|
|
site_id = netbox_site.this.id
|
|
role_id = netbox_ipam_role.this[each.value.role].id
|
|
vlan_id = try(netbox_vlan.this[tostring(each.value.vlan)].id, null)
|
|
description = each.value.description
|
|
}
|
|
|
|
resource "netbox_ip_range" "this" {
|
|
for_each = { for r in local.topo.ip_ranges : "${r.start}-${r.end}" => r }
|
|
start_address = each.value.start
|
|
end_address = each.value.end
|
|
status = each.value.status
|
|
mark_utilized = try(each.value.mark_utilized, false)
|
|
description = each.value.description
|
|
}
|
|
|
|
# --- Wi-Fi ---------------------------------------------------------------------
|
|
# ⚠ PARTIAL: the provider can create the SSIDs but has NO attribute for attaching them to
|
|
# a radio interface, and none for `rf_role`. Neither does the netbox.netbox Ansible
|
|
# collection. That last mile is done by ./attach-wireless.py — see ../README.md.
|
|
resource "netbox_wireless_lan" "this" {
|
|
for_each = { for w in local.topo.wireless_lans : w.ssid => w }
|
|
ssid = each.value.ssid
|
|
auth_type = each.value.auth_type
|
|
auth_cipher = each.value.auth_cipher
|
|
description = each.value.description
|
|
# auth_psk deliberately unset: OpenBao is the secrets store, not NetBox.
|
|
}
|
|
|
|
# --- hardware ------------------------------------------------------------------
|
|
resource "netbox_manufacturer" "this" {
|
|
for_each = { for m in local.topo.manufacturers : m.slug => m }
|
|
name = each.value.name
|
|
slug = each.value.slug
|
|
}
|
|
|
|
resource "netbox_device_type" "this" {
|
|
for_each = { for d in local.topo.device_types : d.slug => d }
|
|
model = each.value.model
|
|
slug = each.value.slug
|
|
manufacturer_id = netbox_manufacturer.this[each.value.manufacturer].id
|
|
# Same reason as vm_role above: NetBox's default is true, so pin it or every plan wants
|
|
# to clear it. Meaningless for this hardware (nothing is rack-mounted) but stops churn.
|
|
is_full_depth = true
|
|
}
|
|
|
|
resource "netbox_device_role" "this" {
|
|
for_each = { for r in local.topo.device_roles : r.slug => r }
|
|
name = each.value.name
|
|
slug = each.value.slug
|
|
color_hex = each.value.color
|
|
# NetBox defaults this to true; the provider defaults it to false, so without pinning it
|
|
# every plan shows a spurious vm_role true -> false diff.
|
|
vm_role = true
|
|
}
|
|
|
|
resource "netbox_device" "this" {
|
|
for_each = { for d in local.topo.devices : d.name => d }
|
|
name = each.value.name
|
|
site_id = netbox_site.this.id
|
|
role_id = netbox_device_role.this[each.value.role].id
|
|
device_type_id = netbox_device_type.this[each.value.type].id
|
|
description = each.value.description
|
|
comments = try(each.value.comments, "")
|
|
serial = try(each.value.serial, "")
|
|
status = try(each.value.status, "active")
|
|
}
|
|
|
|
resource "netbox_device_interface" "this" {
|
|
for_each = local.device_ifaces
|
|
device_id = netbox_device.this[each.value.device].id
|
|
name = each.value.name
|
|
type = each.value.type
|
|
description = try(each.value.description, "")
|
|
mtu = try(each.value.mtu, null)
|
|
}
|
|
|
|
resource "netbox_inventory_item" "this" {
|
|
for_each = local.inventory_items
|
|
device_id = netbox_device.this[each.value.device].id
|
|
name = each.value.name
|
|
manufacturer_id = netbox_manufacturer.this[each.value.manufacturer].id
|
|
part_id = try(each.value.part_id, "")
|
|
serial = try(each.value.serial, "")
|
|
description = try(each.value.description, "")
|
|
}
|
|
|
|
# MACs are first-class objects in NetBox 4.2+; `mac_address` on the interface is read-only.
|
|
resource "netbox_mac_address" "this" {
|
|
for_each = local.device_macs
|
|
mac_address = upper(each.value.mac)
|
|
device_interface_id = netbox_device_interface.this[each.key].id
|
|
}
|
|
|
|
resource "netbox_device_interface_primary_mac_address" "this" {
|
|
for_each = local.device_macs
|
|
interface_id = netbox_device_interface.this[each.key].id
|
|
mac_address_id = netbox_mac_address.this[each.key].id
|
|
}
|
|
|
|
# --- virtualization ------------------------------------------------------------
|
|
resource "netbox_cluster_type" "this" {
|
|
for_each = { for c in local.topo.cluster_types : c.slug => c }
|
|
name = each.value.name
|
|
slug = each.value.slug
|
|
}
|
|
|
|
resource "netbox_cluster" "this" {
|
|
for_each = { for c in local.topo.clusters : c.name => c }
|
|
name = each.value.name
|
|
cluster_type_id = netbox_cluster_type.this[each.value.type].id
|
|
description = each.value.description
|
|
site_id = netbox_site.this.id
|
|
}
|
|
|
|
resource "netbox_virtual_machine" "this" {
|
|
for_each = { for v in local.topo.virtual_machines : v.name => v }
|
|
name = each.value.name
|
|
cluster_id = netbox_cluster.this[each.value.cluster].id
|
|
description = each.value.description
|
|
# NetBox DERIVES a VM's site from its cluster. Leaving this unset makes the provider
|
|
# try to clear it on every plan (site_id 1 -> None), so declare it to match.
|
|
site_id = netbox_site.this.id
|
|
}
|
|
|
|
resource "netbox_interface" "this" {
|
|
for_each = local.vm_ifaces
|
|
virtual_machine_id = netbox_virtual_machine.this[each.value.vm].id
|
|
name = each.value.name
|
|
description = try(each.value.description, "")
|
|
}
|
|
|
|
# --- addresses -----------------------------------------------------------------
|
|
resource "netbox_ip_address" "device" {
|
|
for_each = local.device_ips
|
|
ip_address = each.value.ip
|
|
status = "active"
|
|
# No `object_type` here: the provider pairs that with the GENERIC `interface_id`
|
|
# ("all of interface_id,object_type must be specified"). The dedicated
|
|
# *_interface_id attributes are standalone and imply the type.
|
|
device_interface_id = netbox_device_interface.this[each.key].id
|
|
# Native NetBox field. Setting it is INTENT: "this host needs a static A record in AD
|
|
# DNS". Domain-joined hosts self-register and are deliberately absent.
|
|
# ../generate/samba-a-records.py turns these into samba_ad_extra_a_records.
|
|
dns_name = try(each.value.dns_name, "")
|
|
}
|
|
|
|
resource "netbox_ip_address" "vm" {
|
|
for_each = local.vm_ips
|
|
ip_address = each.value.ip
|
|
status = "active"
|
|
virtual_machine_interface_id = netbox_interface.this[each.key].id
|
|
dns_name = try(each.value.dns_name, "")
|
|
}
|
|
|
|
# primary_ip4 lives on the parent, so the provider models it as its own resource.
|
|
resource "netbox_device_primary_ip" "this" {
|
|
for_each = local.device_primary
|
|
device_id = netbox_device.this[each.key].id
|
|
ip_address_id = netbox_ip_address.device[each.value].id
|
|
}
|