Backfill NetBox reality from pdbmux
Automate the PuppetDB reality import (issue #1) by querying pdbmux — the PuppetDB multiplexer whose HTTPS gateway is reachable from CI/workstations, unlike raw PuppetDB — instead of PuppetDB directly, and shaping the result to the NetBox reality side the devices module reconciles. - Add tools/backfill (Go): query pdbmux /pdb/query/v4/facts for the 13 existing physicals and emit per-host reality YAML — serial/model/UUID, every recordable interface (real NICs plus overlay/loopback/kube-lb) with MAC and CIDR IPs, and CPU/RAM/disk inventory. Filter ephemeral Calico veths and Ceph RBD volumes; take interface names from Facter, never assume them. - Emit deterministic, idempotent, yamllint-clean output into config/au/syd1/reality/<host>.yaml, generated for prodnxsr0001-0013. - Extend modules/infra with a reality variable and reality.tf creating netbox_device_interface/netbox_mac_address/netbox_ip_address/ netbox_inventory_item and device serial; wire reality only for hosts that also have an intent device. - Load reality/*.yaml in the terragrunt env; add `make backfill`; add a go vet/test woodpecker job; drop the in-cluster-only Python script. Closes #1 Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
This commit is contained in:
@@ -54,6 +54,9 @@ resource "netbox_device" "this" {
|
||||
role_id = netbox_device_role.this[each.value.role].id
|
||||
site_id = tonumber(data.netbox_site.this[each.value.site].id)
|
||||
status = each.value.pxe ? "staged" : "active"
|
||||
|
||||
# Serial is hardware reality; take it from the backfill when present.
|
||||
serial = try(var.reality[each.key].serial, null)
|
||||
}
|
||||
|
||||
# Transitional: bootapi keys PXE on MAC and no discovery image exists yet, so seed a
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
locals {
|
||||
# Reality is wired only for hosts that also carry an intent device (the 13
|
||||
# existing physicals get their intent seeded in issue #2). Until then a host's
|
||||
# reality/*.yaml is reviewed and committed but produces no NetBox resources.
|
||||
reality_devices = { for k, r in var.reality : k => r if contains(keys(var.devices), k) }
|
||||
|
||||
# "<device>:<iface>" -> real interface. Names come from Facter, never assumed.
|
||||
reality_interfaces = merge([
|
||||
for dname, r in local.reality_devices : {
|
||||
for i in coalesce(r.interfaces, []) : "${dname}:${i.name}" => {
|
||||
device = dname
|
||||
name = i.name
|
||||
mac = i.mac
|
||||
mtu = i.mtu
|
||||
physical = coalesce(i.physical, false)
|
||||
}
|
||||
}
|
||||
]...)
|
||||
|
||||
reality_macs = { for k, i in local.reality_interfaces : k => i if i.mac != null && i.mac != "" }
|
||||
|
||||
# "<device>:<iface>:<ip>" -> address on that interface (CIDR form).
|
||||
reality_ips = merge([
|
||||
for dname, r in local.reality_devices : {
|
||||
for pair in flatten([
|
||||
for i in coalesce(r.interfaces, []) : [
|
||||
for ip in coalesce(i.ips, []) : { device = dname, iface = i.name, ip = ip }
|
||||
]
|
||||
]) : "${pair.device}:${pair.iface}:${pair.ip}" => pair
|
||||
}
|
||||
]...)
|
||||
|
||||
# "<device>:<key>" -> one NetBox inventory item (CPU, memory, per-disk).
|
||||
reality_inventory = merge([
|
||||
for dname, r in local.reality_devices : {
|
||||
for item in concat(
|
||||
try(r.inventory.cpu, null) == null ? [] : [{
|
||||
key = "cpu"
|
||||
name = "CPU"
|
||||
description = trimspace(format("%s x%d", try(r.inventory.cpu.model, ""), try(r.inventory.cpu.count, 0)))
|
||||
part_id = try(r.inventory.cpu.model, null)
|
||||
serial = null
|
||||
}],
|
||||
try(r.inventory.memory_bytes, null) == null ? [] : [{
|
||||
key = "memory"
|
||||
name = "Memory"
|
||||
description = format("%d bytes", r.inventory.memory_bytes)
|
||||
part_id = null
|
||||
serial = null
|
||||
}],
|
||||
[for d in try(r.inventory.disks, []) : {
|
||||
key = "disk-${d.name}"
|
||||
name = "Disk ${d.name}"
|
||||
description = trimspace(format("%s %d bytes", try(d.model, ""), try(d.size_bytes, 0)))
|
||||
part_id = try(d.model, null)
|
||||
serial = try(d.serial, null)
|
||||
}]
|
||||
) : "${dname}:${item.key}" => merge(item, { device = dname })
|
||||
}
|
||||
]...)
|
||||
}
|
||||
|
||||
# Real NICs and virtual interfaces (overlay/loopback/kube-lb) the host reports.
|
||||
resource "netbox_device_interface" "reality" {
|
||||
for_each = local.reality_interfaces
|
||||
|
||||
device_id = netbox_device.this[each.value.device].id
|
||||
name = each.value.name
|
||||
type = each.value.physical ? "1000base-t" : "virtual"
|
||||
mtu = each.value.mtu
|
||||
}
|
||||
|
||||
resource "netbox_mac_address" "reality" {
|
||||
for_each = local.reality_macs
|
||||
|
||||
mac_address = each.value.mac
|
||||
device_interface_id = netbox_device_interface.reality[each.key].id
|
||||
}
|
||||
|
||||
# NetBox stays IP-authoritative; these record the addresses (incl. overlay /
|
||||
# loopback / kube-lb VIPs) a running host actually carries.
|
||||
resource "netbox_ip_address" "reality" {
|
||||
for_each = local.reality_ips
|
||||
|
||||
ip_address = each.value.ip
|
||||
status = "active"
|
||||
description = each.value.device
|
||||
device_interface_id = netbox_device_interface.reality["${each.value.device}:${each.value.iface}"].id
|
||||
}
|
||||
|
||||
resource "netbox_inventory_item" "reality" {
|
||||
for_each = local.reality_inventory
|
||||
|
||||
device_id = netbox_device.this[each.value.device].id
|
||||
name = each.value.name
|
||||
description = each.value.description
|
||||
part_id = each.value.part_id
|
||||
serial = each.value.serial
|
||||
discovered = true
|
||||
}
|
||||
@@ -66,6 +66,46 @@ variable "managed_ips" {
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "reality" {
|
||||
description = <<-EOT
|
||||
Per-host hardware reality keyed by device name, generated from pdbmux by
|
||||
tools/backfill (see `make backfill`) — never hand-authored. It carries the
|
||||
facts NetBox cannot learn from intent: serial/model/UUID, every recordable
|
||||
interface (real NICs plus overlay/loopback/kube-lb) with MAC and CIDR IPs,
|
||||
and CPU/RAM/disk inventory. Reality is wired into NetBox only for hosts that
|
||||
also have an intent `devices/<host>.yaml`; until that intent lands, a host's
|
||||
reality sits reviewed-but-inert.
|
||||
EOT
|
||||
type = map(object({
|
||||
device = optional(string)
|
||||
serial = optional(string)
|
||||
model = optional(string)
|
||||
uuid = optional(string)
|
||||
interfaces = optional(list(object({
|
||||
name = string
|
||||
mac = optional(string)
|
||||
mtu = optional(number)
|
||||
physical = optional(bool, false)
|
||||
ips = optional(list(string), [])
|
||||
})), [])
|
||||
inventory = optional(object({
|
||||
cpu = optional(object({
|
||||
model = optional(string)
|
||||
cores = optional(number)
|
||||
count = optional(number)
|
||||
}))
|
||||
memory_bytes = optional(number)
|
||||
disks = optional(list(object({
|
||||
name = string
|
||||
model = optional(string)
|
||||
serial = optional(string)
|
||||
size_bytes = optional(number)
|
||||
})), [])
|
||||
}))
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "vault_address" {
|
||||
description = "Vault server address for the token data source."
|
||||
type = string
|
||||
|
||||
Reference in New Issue
Block a user