From 43cab4e7d82f2e2551c3cfe6b1b801939c99aa1f Mon Sep 17 00:00:00 2001 From: Bryan Joshua Pedini Date: Tue, 25 Aug 2026 13:09:33 +0200 Subject: [PATCH] feat: ip address and virtual machine inventory sources hosts land in a single "netbox" group with hostvars under _meta; IP addresses use the DNS name as inventory hostname when set, virtual machines expose their primary IP as ansible_host when they have one. --- netbox_inventory.py | 89 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/netbox_inventory.py b/netbox_inventory.py index 95b2cc3..330f4ce 100755 --- a/netbox_inventory.py +++ b/netbox_inventory.py @@ -162,6 +162,88 @@ def load_config(path: str | None = None) -> dict[str, Any]: return merged +def strip_prefix(address: str) -> str: + """Return a bare IP address without the CIDR prefix length.""" + return address.split("/", 1)[0] + + +def ip_host_entry(ip: dict[str, Any]) -> tuple[str, dict[str, Any]]: + """ + Turn a NetBox IP address record into an inventory (name, hostvars) pair. + + The DNS name is used as the inventory hostname when set, the bare address + otherwise; the bare address always ends up in ansible_host. + """ + address = strip_prefix(ip.get("address") or "") + name = ip.get("dns_name") or address + hostvars = { + "ansible_host": address, + "netbox_ip_address": ip.get("address") or "", + "netbox_dns_name": ip.get("dns_name") or "", + "netbox_tenant": (ip.get("tenant") or {}).get("name") or "", + "netbox_vrf": (ip.get("vrf") or {}).get("name") or "", + "netbox_description": ip.get("description") or "", + } + return name, hostvars + + +def vm_host_entry(vm: dict[str, Any]) -> tuple[str, dict[str, Any]]: + """ + Turn a NetBox virtual machine record into an inventory (name, hostvars) pair. + + ansible_host is only set when the virtual machine has a primary IP, so that + Ansible falls back to resolving the name otherwise. + """ + name = vm.get("name") or "" + hostvars = { + "netbox_status": (vm.get("status") or {}).get("value") or "", + "netbox_cluster": (vm.get("cluster") or {}).get("name") or "", + "netbox_site": (vm.get("site") or {}).get("name") or "", + "netbox_tenant": (vm.get("tenant") or {}).get("name") or "", + } + primary = vm.get("primary_ip") or {} + if primary.get("address"): + hostvars["ansible_host"] = strip_prefix(primary["address"]) + return name, hostvars + + +def fetch_entries(client: NetBoxClient, source: str) -> list[tuple[str, dict[str, Any]]]: + """Fetch all hosts of the configured source from NetBox.""" + if source == SOURCE_IP_ADDRESSES: + records = client.get_all("ipam/ip-addresses") + return [ip_host_entry(record) for record in records if record.get("address")] + records = client.get_all("virtualization/virtual-machines", {"exclude": "config_context"}) + return [vm_host_entry(record) for record in records if record.get("name")] + + +def build_inventory(config: dict[str, Any], client: NetBoxClient) -> dict[str, Any]: + """ + Build the full --list inventory structure for Ansible. + + Raises: + InventoryError: On an unknown source or NetBox API failures. + """ + source = config["source"] + if source not in SOURCES: + raise InventoryError( + f'Unknown source "{source}", valid sources: {", ".join(SOURCES)}.' + ) + entries = fetch_entries(client, source) + hostvars: dict[str, dict[str, Any]] = {} + for name, host in entries: + if name in hostvars: + # duplicate inventory names keep both hosts by falling back to the + # bare address as the name; identical duplicates are dropped + name = host.get("ansible_host") or name + if name in hostvars: + continue + hostvars[name] = host + return { + GROUP_NAME: {"hosts": sorted(hostvars)}, + "_meta": {"hostvars": hostvars}, + } + + def main() -> int: """Entry point for the Ansible dynamic inventory protocol.""" parser = argparse.ArgumentParser(description="Ansible dynamic inventory backed by NetBox.") @@ -174,8 +256,11 @@ def main() -> int: print(json.dumps({})) return 0 try: - load_config() - print(json.dumps({GROUP_NAME: {"hosts": []}, "_meta": {"hostvars": {}}}, indent=2)) + config = load_config() + client = NetBoxClient( + config["url"], config["token"], config["verify_ssl"], config["timeout"] + ) + print(json.dumps(build_inventory(config, client), indent=2, sort_keys=True)) except InventoryError as exc: print(f"netbox-inventory: {exc}", file=sys.stderr) return 1