diff --git a/.woodpecker/apply.yaml b/.woodpecker/apply.yaml index 63b028b..5855b1c 100644 --- a/.woodpecker/apply.yaml +++ b/.woodpecker/apply.yaml @@ -13,7 +13,7 @@ steps: - make apply backend_options: kubernetes: - serviceAccountName: terraform-ipam + serviceAccountName: terraform-infra resources: requests: memory: 512Mi diff --git a/.woodpecker/plan.yaml b/.woodpecker/plan.yaml index 70cf11c..2ada2d2 100644 --- a/.woodpecker/plan.yaml +++ b/.woodpecker/plan.yaml @@ -11,7 +11,7 @@ steps: - make plan backend_options: kubernetes: - serviceAccountName: terraform-ipam + serviceAccountName: terraform-infra resources: requests: memory: 512Mi diff --git a/Makefile b/Makefile index 33d97ba..fa3600f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: init plan apply format pre-commit VAULT_AUTH_METHOD ?= approle -VAULT_K8S_ROLE ?= woodpecker_terraform_ipam +VAULT_K8S_ROLE ?= woodpecker_terraform_infra VAULT_K8S_MOUNT ?= auth/k8s/au/syd1 VAULT_K8S_JWT_PATH ?= /var/run/secrets/kubernetes.io/serviceaccount/token @@ -12,7 +12,7 @@ define vault_env else \ export VAULT_TOKEN=$$(vault write -field=token auth/approle/login role_id=$$VAULT_ROLEID); \ fi && \ - export CONSUL_HTTP_TOKEN=$$(vault read -field=token consul_root/au/syd1/creds/terraform-ipam) + export CONSUL_HTTP_TOKEN=$$(vault read -field=token consul_root/au/syd1/creds/terraform-infra) endef init: diff --git a/README.md b/README.md index 8be57ba..293d72f 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,103 @@ -# terraform-ipam +# terraform-infra -YAML-driven Terraform/Terragrunt for IP address management: it manages NetBox -IPAM prefixes and Kea DHCP scopes together from a single subnet definition. +YAML-driven Terraform/Terragrunt for infrastructure state that belongs in NetBox + +Kea: subnets (NetBox prefixes + Kea DHCP scopes), logical networks, and physical +devices. **NetBox is authoritative for all IP/interface data**; this repo declares +intent and lets discovery/PuppetDB fill hardware reality. -Defining a subnet creates the NetBox prefix, optionally a DHCP scope in Kea (via -the `kea` provider against the KeaAPI), marks the DHCP range in NetBox, and -records the gateway. +> Renamed from `terraform-ipam` — scope now spans devices and provisioning, not just +> IPAM. The old repo name is archived by the terraform-git rename. ## Layout ``` -config///subnets/.yaml # one file per subnet, filename = subnet name +config///subnets/.yaml # one file per subnet (prefix + DHCP scope) +config///networks/.yaml # logical networks devices join +config///devices/.yaml # one file per device (intent only) config///managed-ips.yaml # extra manually-managed IPs environments///terragrunt.hcl # one Terragrunt env per region/dc -modules/ipam/ # the subnet module +modules/infra/ # the module +ci/puppetdb_backfill.py # reality backfill generator (local/in-cluster) ``` -`make plan` / `make apply` (via Terragrunt) reconcile the YAML against NetBox + -Kea. State lives in Consul under `infra/terraform/ipam///state`. +`make plan` / `make apply` reconcile the YAML against NetBox + Kea. State lives in +Consul under `infra/terraform/infra///state`. -## Subnet schema (`config///subnets/.yaml`) +## Subnet schema (`subnets/.yaml`) ```yaml prefix: 198.18.15.0/24 description: syd1 production -site: syd1 # NetBox site slug (optional; resolved to site_id) -dhcp: # omit the whole block for a NetBox-only prefix +site: syd1 # NetBox site slug (optional) +dhcp: # omit the block for a NetBox-only prefix enabled: true - start: 200 # host-octet int; pool/range start - stop: 220 # host-octet int; pool/range stop -router: 254 # host-octet int; recorded as the gateway IP -dns: [198.18.200.7] + start: 200 # host-octet int + stop: 220 +router: 254 # host-octet int -> gateway IP +dns: [198.18.200.7] # DHCP-scope DNS next_server: 198.18.19.19 domain: main.unkin.net ``` +Creates `netbox_prefix` (+ `netbox_ip_range` role `dhcp` + `kea_subnet` when `dhcp` +set, + gateway `netbox_ip_address`). A prefix backing a network is tagged `net:`. -Resource mapping per subnet: +## Network schema (`networks/.yaml`) -| YAML | Resource | -|---------------------|-----------------------------------------------------------------| -| `prefix` | `netbox_prefix` (`status=active`, `description`, `site_id`) | -| `dhcp.start/stop` | `netbox_ip_range` (role `dhcp`) + `kea_subnet` pool | -| `router` | `netbox_ip_address` `/mask` description `gateway` | -| `dns/next_server/domain` | `kea_subnet` dns_servers / next_server / domain_name | +Per-network provisioning constants — hoisted here, never repeated per device. -Host-octet ints are expanded against the prefix with `cidrhost()`. A subnet with -no `dhcp` block is NetBox-only (e.g. `local.yaml` = 198.18.25.0/24 — excluded -from Kea). The `dhcp` role is a single shared `netbox_ipam_role`. PXE boot files -are handled by Kea cluster client-classes, not here. +```yaml +subnet: net-198-18-15 # which subnet's prefix backs this network +gateway: 198.18.15.254 +dns: [198.18.19.16] +search: main.unkin.net +``` -## Managed IPs (`config///managed-ips.yaml`) +## Device schema (`devices/.yaml`) — intent only -A list of `{ip, description}` (ip in full CIDR form) rendered as `netbox_ip_address`. +Declare only human intent. Serial, real interface names (which vary by model — +OptiPlex 3060 = `enp1s0`, 3050/7080 = `enp2s0`), MACs and inventory are owned by the +first-boot discovery image / PuppetDB, never hardcoded here. + +```yaml +site: syd1 +role: roles::base # puppet mgmt_class / ENC role -> NetBox device role +model_hint: optiplex-3070 # NetBox device type; discovery confirms via dmidecode +provision: + profile: almalinux9-dell_3070 # cobbler profile / kickstart template + platform: almalinux9 +networks: + mgmt: 198.18.15.14/24 # requested IP; "" = next-available from the prefix +pxe: true +bootstrap_mac: a4:bb:6d:xx:xx:xx # TRANSITIONAL — see below +``` + +Mapping: `netbox_device` (+ `netbox_manufacturer`/`netbox_device_type`/ +`netbox_device_role`), `netbox_ip_address` (static) or `netbox_available_ip_address` +(next-available, `ignore_changes` so a machine is never re-IPed). `bootstrap_mac` is +**transitional**: it seeds one placeholder `netbox_device_interface` + `netbox_mac_address` +so bootapi can key the PXE boot on MAC until the discovery image exists — remove it +once discovery/backfill populates the real NICs. + +## PuppetDB reality backfill + +`ci/puppetdb_backfill.py` sweeps already-provisioned hosts and emits their hardware +reality (serial/model/UUID + every interface's MAC/IPs, incl. overlay/loopback +addresses Cobbler lacks) as reviewable YAML — the NetBox *reality* side, not the +intent above. PuppetDB needs no auth but is **not reachable from CI**; run it +in-cluster/from puppet infra. Wiring its output into NetBox is a follow-up (issue). ## Providers | Provider | Source | Notes | |----------|--------|-------| -| netbox | `e-breuninger/netbox` 4.3.0 | community provider from the OpenTofu registry | -| kea | `artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/kea` 0.0.1 | KeaAPI; `cluster_ref=kea` | -| vault | `hashicorp/vault` 5.6.0 | reads the KeaAPI + NetBox tokens | +| netbox | `e-breuninger/netbox` 4.3.0 | OpenTofu registry | +| kea | `artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/kea` 0.0.1 | `cluster_ref=kea` | +| vault | `hashicorp/vault` 5.6.0 | reads NetBox + Kea tokens | -Endpoints (module variable defaults): NetBox `https://netbox.k8s.syd1.au.unkin.net`, -KeaAPI `http://kea-api.dhcp-system.svc:8080` (ClusterIP, in-cluster only). +Tokens: Vault KV v2 `kv/service/terraform/infra` fields `netbox_token` + `kea_token`. -Tokens are read at plan time from Vault KV v2 `kv/service/terraform/ipam` with -fields `netbox_token` and `kea_token` (the vault provider authenticates with the -CI `VAULT_TOKEN`). +## Blockers -## Deployment status / blockers - -- **NetBox is not deployed** on the cluster yet (only on the unmerged - `benvin/netbox` argocd-apps branch; host `netbox.k8s.syd1.au.unkin.net`). Until - it is live and reachable, `plan`/`apply` against NetBox will fail. -- **KeaAPI is not deployed** yet (only on the unmerged `benvin/kea-deployment` - argocd-apps branch; `KeaAPI` CR `kea-api`, ns `dhcp-system`, ClusterIP `:8080`). -- **Seed `kv/service/terraform/ipam`** in Vault with `netbox_token` + - `kea_token` before the pipeline can authenticate to either API. -- **Confirm the `kea` provider 0.0.1** is published to the `terraform-unkin` - ArtifactAPI registry (release the provider repo if not). +- **NetBox not deployed** yet (unmerged `benvin/netbox` argocd branch); NetBox sites + must pre-exist. **KeaAPI not deployed** yet (argocd-apps PR #333). Seed + `kv/service/terraform/infra` before the pipeline can auth. diff --git a/ci/puppetdb_backfill.py b/ci/puppetdb_backfill.py new file mode 100755 index 0000000..f67e9d0 --- /dev/null +++ b/ci/puppetdb_backfill.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Backfill NetBox device *reality* from PuppetDB for already-provisioned hosts. + +This emits the hardware-owned facts a running host reports (serial, model, UUID, +and every interface's name/MAC/IPs including the overlay/loopback addresses +Cobbler never had) as reviewable per-host YAML. It targets the *reality* side of +NetBox (interfaces + inventory) for the ~13 existing physicals; it is NOT the +intent `config/.../devices/*.yaml` (a human writes those). + +PuppetDB needs no auth (TLS terminates at the gateway; upstream is plain HTTP). +It is NOT reachable from CI, so run this in-cluster or from puppet infra: + + ./puppetdb_backfill.py --out ../reality \ + --url http://puppetdb.puppet.svc.cluster.local:8080/pdb/query/v4/facts \ + prodnxsr0001 prodnxsr0002 ... + +Fact paths follow Facter conventions (networking.*, dmi.*); verify against a live +factset (`GET /pdb/query/v4/factsets`) before trusting output on a new estate. +""" +import argparse +import json +import os +import sys +import urllib.parse +import urllib.request + +DEFAULT_URL = "http://puppetdb.puppet.svc.cluster.local:8080/pdb/query/v4/facts" + + +def query_facts(url, certname): + ast = json.dumps(["=", "certname", certname]) + full = url + "?" + urllib.parse.urlencode({"query": ast}) + with urllib.request.urlopen(full, timeout=30) as resp: + rows = json.load(resp) + return {row["name"]: row["value"] for row in rows} + + +def device_reality(certname, facts): + networking = facts.get("networking", {}) or {} + ifaces = networking.get("interfaces", {}) or {} + dmi = facts.get("dmi", {}) or {} + product = dmi.get("product", {}) or {} + + interfaces = [] + for name, data in sorted(ifaces.items()): + if name == "lo": + continue + ips = [] + for b in (data.get("bindings", []) or []): + if b.get("address"): + ips.append(b["address"]) + if data.get("ip") and data["ip"] not in ips: + ips.append(data["ip"]) + interfaces.append({ + "name": name, + "mac": data.get("mac"), + "ips": ips, + }) + + return { + "device": certname.split(".")[0], + "serial": product.get("serial_number") or facts.get("serialnumber"), + "model": product.get("name") or facts.get("productname"), + "uuid": product.get("uuid") or facts.get("uuid"), + "interfaces": interfaces, + } + + +def to_yaml(d): + # tiny dependency-free YAML emitter for this fixed shape + out = [ + f"device: {d['device']}", + f"serial: {d['serial'] or ''}", + f"model: {d['model'] or ''}", + f"uuid: {d['uuid'] or ''}", + "interfaces:", + ] + for i in d["interfaces"]: + out.append(f" - name: {i['name']}") + out.append(f" mac: {i['mac'] or ''}") + out.append(" ips: [%s]" % ", ".join(i["ips"])) + return "\n".join(out) + "\n" + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("hosts", nargs="+", help="certnames or short hostnames") + ap.add_argument("--url", default=os.environ.get("PUPPETDB_URL", DEFAULT_URL)) + ap.add_argument("--out", help="write .yaml here instead of stdout") + args = ap.parse_args() + + for host in args.hosts: + certname = host if "." in host else f"{host}.main.unkin.net" + try: + facts = query_facts(args.url, certname) + except Exception as e: # noqa: BLE001 + print(f"# {certname}: query failed: {e}", file=sys.stderr) + continue + if not facts: + print(f"# {certname}: no facts (not in PuppetDB)", file=sys.stderr) + continue + text = to_yaml(device_reality(certname, facts)) + if args.out: + os.makedirs(args.out, exist_ok=True) + path = os.path.join(args.out, f"{certname.split('.')[0]}.yaml") + with open(path, "w") as fh: + fh.write(text) + print(f"wrote {path}", file=sys.stderr) + else: + print(text) + + +if __name__ == "__main__": + main() diff --git a/config/au/syd1/devices/prodnxsr0014.yaml b/config/au/syd1/devices/prodnxsr0014.yaml new file mode 100644 index 0000000..7f59500 --- /dev/null +++ b/config/au/syd1/devices/prodnxsr0014.yaml @@ -0,0 +1,11 @@ +site: syd1 +role: roles::base +model_hint: optiplex-3070 +provision: + profile: almalinux9-dell_3070 + platform: almalinux9 +networks: + mgmt: 198.18.15.14/24 +pxe: true +# transitional: removed once the discovery image / puppetdb backfill populates real NIC MACs +bootstrap_mac: a4:bb:6d:a4:e5:c1 diff --git a/config/au/syd1/devices/prodnxsr0015.yaml b/config/au/syd1/devices/prodnxsr0015.yaml new file mode 100644 index 0000000..34cedba --- /dev/null +++ b/config/au/syd1/devices/prodnxsr0015.yaml @@ -0,0 +1,11 @@ +site: syd1 +role: roles::base +model_hint: optiplex-3070 +provision: + profile: almalinux9-dell_3070 + platform: almalinux9 +networks: + mgmt: 198.18.15.15/24 +pxe: true +# transitional: removed once the discovery image / puppetdb backfill populates real NIC MACs +bootstrap_mac: a4:bb:6d:a6:30:c4 diff --git a/config/au/syd1/devices/prodnxsr0016.yaml b/config/au/syd1/devices/prodnxsr0016.yaml new file mode 100644 index 0000000..0776b3f --- /dev/null +++ b/config/au/syd1/devices/prodnxsr0016.yaml @@ -0,0 +1,11 @@ +site: syd1 +role: roles::base +model_hint: optiplex-3070 +provision: + profile: almalinux9-dell_3070 + platform: almalinux9 +networks: + mgmt: 198.18.15.16/24 +pxe: true +# transitional: removed once the discovery image / puppetdb backfill populates real NIC MACs +bootstrap_mac: a4:bb:6d:9f:22:13 diff --git a/config/au/syd1/devices/prodnxsr0017.yaml b/config/au/syd1/devices/prodnxsr0017.yaml new file mode 100644 index 0000000..3b6f192 --- /dev/null +++ b/config/au/syd1/devices/prodnxsr0017.yaml @@ -0,0 +1,11 @@ +site: syd1 +role: roles::base +model_hint: optiplex-3070 +provision: + profile: almalinux9-dell_3070 + platform: almalinux9 +networks: + mgmt: 198.18.15.17/24 +pxe: true +# transitional: removed once the discovery image / puppetdb backfill populates real NIC MACs +bootstrap_mac: 8c:04:ba:9c:b6:08 diff --git a/config/au/syd1/devices/prodnxsr0018.yaml b/config/au/syd1/devices/prodnxsr0018.yaml new file mode 100644 index 0000000..e716382 --- /dev/null +++ b/config/au/syd1/devices/prodnxsr0018.yaml @@ -0,0 +1,11 @@ +site: syd1 +role: roles::base +model_hint: optiplex-3070 +provision: + profile: almalinux9-dell_3070 + platform: almalinux9 +networks: + mgmt: 198.18.15.18/24 +pxe: true +# transitional: removed once the discovery image / puppetdb backfill populates real NIC MACs +bootstrap_mac: a4:bb:6d:a4:db:94 diff --git a/config/au/syd1/devices/prodnxsr0019.yaml b/config/au/syd1/devices/prodnxsr0019.yaml new file mode 100644 index 0000000..e5d19f5 --- /dev/null +++ b/config/au/syd1/devices/prodnxsr0019.yaml @@ -0,0 +1,11 @@ +site: syd1 +role: roles::base +model_hint: optiplex-3070 +provision: + profile: almalinux9-dell_3070 + platform: almalinux9 +networks: + mgmt: 198.18.15.19/24 +pxe: true +# transitional: removed once the discovery image / puppetdb backfill populates real NIC MACs +bootstrap_mac: a4:bb:6d:a4:56:11 diff --git a/config/au/syd1/networks/mgmt.yaml b/config/au/syd1/networks/mgmt.yaml new file mode 100644 index 0000000..a8dc884 --- /dev/null +++ b/config/au/syd1/networks/mgmt.yaml @@ -0,0 +1,4 @@ +subnet: net-198-18-15 +gateway: 198.18.15.254 +dns: [198.18.19.16] +search: main.unkin.net diff --git a/environments/au/syd1/terragrunt.hcl b/environments/au/syd1/terragrunt.hcl index d82a13e..9977bd2 100644 --- a/environments/au/syd1/terragrunt.hcl +++ b/environments/au/syd1/terragrunt.hcl @@ -15,15 +15,29 @@ locals { trimsuffix(basename(f), ".yaml") => yamldecode(file("${local.cfg_dir}/${f}")) } + network_files = fileset(local.cfg_dir, "networks/*.yaml") + networks = { + for f in local.network_files : + trimsuffix(basename(f), ".yaml") => yamldecode(file("${local.cfg_dir}/${f}")) + } + + device_files = fileset(local.cfg_dir, "devices/*.yaml") + devices = { + for f in local.device_files : + trimsuffix(basename(f), ".yaml") => yamldecode(file("${local.cfg_dir}/${f}")) + } + managed_ips_file = "${local.cfg_dir}/managed-ips.yaml" managed_ips = fileexists(local.managed_ips_file) ? yamldecode(file(local.managed_ips_file)) : [] } terraform { - source = "${get_repo_root()}/modules/ipam" + source = "${get_repo_root()}/modules/infra" } inputs = { subnets = local.subnets + networks = local.networks + devices = local.devices managed_ips = local.managed_ips } diff --git a/environments/root.hcl b/environments/root.hcl index fe3d9ad..150beec 100644 --- a/environments/root.hcl +++ b/environments/root.hcl @@ -5,7 +5,7 @@ generate "backend" { terraform { backend "consul" { address = "https://consul.service.consul" - path = "infra/terraform/ipam/${path_relative_to_include()}/state" + path = "infra/terraform/infra/${path_relative_to_include()}/state" scheme = "https" lock = true ca_file = "/etc/pki/tls/certs/ca-bundle.crt" diff --git a/modules/infra/devices.tf b/modules/infra/devices.tf new file mode 100644 index 0000000..8b6ade0 --- /dev/null +++ b/modules/infra/devices.tf @@ -0,0 +1,99 @@ +locals { + # network name -> backing prefix id (from the subnets above). + network_prefix_id = { for nname, n in var.networks : nname => netbox_prefix.this[n.subnet].id } + + # Distinct device inventory objects to create in NetBox. + device_models = toset([for d in var.devices : d.model_hint if d.model_hint != null]) + device_roles = toset([for d in var.devices : d.role]) + + # Devices that carry a transitional bootstrap MAC (placeholder PXE interface). + bootstrap_devices = { for k, d in var.devices : k => d if d.bootstrap_mac != null } + + # Flatten device network memberships: ":" -> {device, network, ip}. + device_networks = merge([ + for dname, d in var.devices : { + for nname, ip in d.networks : "${dname}:${nname}" => { + device = dname + network = nname + ip = ip + } + } + ]...) + + static_device_ips = { for k, dn in local.device_networks : k => dn if dn.ip != "" } + auto_device_ips = { for k, dn in local.device_networks : k => dn if dn.ip == "" } +} + +resource "netbox_manufacturer" "dell" { + count = length(local.device_models) > 0 ? 1 : 0 + name = "Dell" + slug = "dell" +} + +resource "netbox_device_type" "this" { + for_each = local.device_models + + manufacturer_id = netbox_manufacturer.dell[0].id + model = title(replace(each.value, "-", " ")) + slug = each.value +} + +resource "netbox_device_role" "this" { + for_each = local.device_roles + + name = each.value + slug = lower(replace(each.value, "::", "-")) + color_hex = "9e9e9e" +} + +resource "netbox_device" "this" { + for_each = var.devices + + name = each.key + device_type_id = netbox_device_type.this[each.value.model_hint].id + 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" +} + +# Transitional: bootapi keys PXE on MAC and no discovery image exists yet, so seed a +# single placeholder interface carrying bootstrap_mac. Discovery replaces it with the +# real (model-specific) NICs later; the interface name is a neutral label, not a +# hardware assumption. +resource "netbox_device_interface" "bootstrap" { + for_each = local.bootstrap_devices + + device_id = netbox_device.this[each.key].id + name = "bootstrap" + type = "1000base-t" +} + +resource "netbox_mac_address" "bootstrap" { + for_each = local.bootstrap_devices + + mac_address = each.value.bootstrap_mac + device_interface_id = netbox_device_interface.bootstrap[each.key].id +} + +resource "netbox_ip_address" "device" { + for_each = local.static_device_ips + + ip_address = each.value.ip + status = "active" + description = each.value.device + device_interface_id = try(netbox_device_interface.bootstrap[each.value.device].id, null) +} + +resource "netbox_available_ip_address" "device" { + for_each = local.auto_device_ips + + prefix_id = local.network_prefix_id[each.value.network] + status = "active" + description = each.value.device + device_interface_id = try(netbox_device_interface.bootstrap[each.value.device].id, null) + + # Machines are never re-IPed (a replacement is a new machine); keep the allocation sticky. + lifecycle { + ignore_changes = [prefix_id] + } +} diff --git a/modules/ipam/main.tf b/modules/infra/main.tf similarity index 74% rename from modules/ipam/main.tf rename to modules/infra/main.tf index 770feeb..3c024a4 100644 --- a/modules/ipam/main.tf +++ b/modules/infra/main.tf @@ -8,8 +8,18 @@ locals { # Subnets that declare a gateway. gateways = { for k, v in var.subnets : k => v if v.router != null } - # Distinct NetBox site slugs referenced by any subnet. - sites = toset([for v in var.subnets : v.site if v.site != null]) + # Distinct NetBox site slugs referenced by any subnet or device. + sites = toset(concat( + [for v in var.subnets : v.site if v.site != null], + [for d in var.devices : d.site], + )) + + # net: tags to apply to each subnet's prefix (a prefix may back many networks). + prefix_net_tags = { + for sk in keys(var.subnets) : sk => [ + for nname, n in var.networks : "net:${nname}" if n.subnet == sk + ] + } } data "netbox_site" "this" { @@ -17,6 +27,14 @@ data "netbox_site" "this" { slug = each.value } +# One tag per network so prefixes can be discovered by network membership. +resource "netbox_tag" "network" { + for_each = var.networks + + name = "net:${each.key}" + slug = "net-${each.key}" +} + resource "netbox_prefix" "this" { for_each = var.subnets @@ -24,6 +42,9 @@ resource "netbox_prefix" "this" { status = "active" description = each.value.description site_id = each.value.site != null ? tonumber(data.netbox_site.this[each.value.site].id) : null + tags = [for t in local.prefix_net_tags[each.key] : t] + + depends_on = [netbox_tag.network] } # Role tagging a range as DHCP-managed; created once and shared by every range. diff --git a/modules/ipam/providers.tf b/modules/infra/providers.tf similarity index 100% rename from modules/ipam/providers.tf rename to modules/infra/providers.tf diff --git a/modules/infra/variables.tf b/modules/infra/variables.tf new file mode 100644 index 0000000..b6099cf --- /dev/null +++ b/modules/infra/variables.tf @@ -0,0 +1,85 @@ +variable "subnets" { + description = "Map of subnets keyed by name (config file basename)." + type = map(object({ + prefix = string + description = optional(string, "") + site = optional(string) + router = optional(number) + dns = optional(list(string), []) + next_server = optional(string) + domain = optional(string) + dhcp = optional(object({ + enabled = optional(bool, true) + start = number + stop = number + })) + })) + default = {} +} + +variable "networks" { + description = <<-EOT + Named logical networks devices join, keyed by network name. `subnet` binds the + network to a subnet (its prefix is tagged net: and used for device IP + allocation). gateway/dns/search are per-network provisioning constants (carried + for downstream consumers; not per-device). + EOT + type = map(object({ + subnet = string + gateway = optional(string) + dns = optional(list(string), []) + search = optional(string) + })) + default = {} +} + +variable "devices" { + description = <<-EOT + Intent-only device declarations keyed by device name. Reality (serial, real + interface names, MACs) is owned by discovery/PuppetDB, not this file. + `networks` maps a network name to a requested IP in CIDR form, or "" to + allocate the next-available from the network's prefix. `bootstrap_mac` is + transitional: it seeds a placeholder interface so bootapi can key the PXE boot + on MAC until the discovery image exists. + EOT + type = map(object({ + site = string + role = string + model_hint = optional(string) + provision = optional(object({ + profile = optional(string) + platform = optional(string) + })) + networks = optional(map(string), {}) + pxe = optional(bool, false) + bootstrap_mac = optional(string) + })) + default = {} +} + +variable "managed_ips" { + description = "Manually managed extra IP addresses (full CIDR form, e.g. 198.18.15.5/24)." + type = list(object({ + ip = string + description = optional(string, "") + })) + default = [] +} + +variable "vault_address" { + description = "Vault server address for the token data source." + type = string + default = "https://vault.service.consul:8200" +} + +variable "netbox_server_url" { + description = "NetBox server base URL." + type = string + default = "https://netbox.k8s.syd1.au.unkin.net" +} + +variable "kea_endpoint" { + description = "KeaAPI base URL (in-cluster ClusterIP service)." + type = string + default = "http://kea-api.dhcp-system.svc:8080" +} diff --git a/modules/ipam/versions.tf b/modules/infra/versions.tf similarity index 100% rename from modules/ipam/versions.tf rename to modules/infra/versions.tf diff --git a/modules/ipam/variables.tf b/modules/ipam/variables.tf deleted file mode 100644 index b9db439..0000000 --- a/modules/ipam/variables.tf +++ /dev/null @@ -1,45 +0,0 @@ -variable "subnets" { - description = "Map of subnets keyed by name (config file basename)." - type = map(object({ - prefix = string - description = optional(string, "") - site = optional(string) - router = optional(number) - dns = optional(list(string), []) - next_server = optional(string) - domain = optional(string) - dhcp = optional(object({ - enabled = optional(bool, true) - start = number - stop = number - })) - })) - default = {} -} - -variable "managed_ips" { - description = "Manually managed extra IP addresses (full CIDR form, e.g. 198.18.15.5/24)." - type = list(object({ - ip = string - description = optional(string, "") - })) - default = [] -} - -variable "vault_address" { - description = "Vault server address for the token data source." - type = string - default = "https://vault.service.consul:8200" -} - -variable "netbox_server_url" { - description = "NetBox server base URL." - type = string - default = "https://netbox.k8s.syd1.au.unkin.net" -} - -variable "kea_endpoint" { - description = "KeaAPI base URL (in-cluster ClusterIP service)." - type = string - default = "http://kea-api.dhcp-system.svc:8080" -}