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.
This commit is contained in:
2026-08-25 13:09:33 +02:00
parent 7912dacf95
commit 43cab4e7d8

View File

@@ -162,6 +162,88 @@ def load_config(path: str | None = None) -> dict[str, Any]:
return merged 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: def main() -> int:
"""Entry point for the Ansible dynamic inventory protocol.""" """Entry point for the Ansible dynamic inventory protocol."""
parser = argparse.ArgumentParser(description="Ansible dynamic inventory backed by NetBox.") parser = argparse.ArgumentParser(description="Ansible dynamic inventory backed by NetBox.")
@@ -174,8 +256,11 @@ def main() -> int:
print(json.dumps({})) print(json.dumps({}))
return 0 return 0
try: try:
load_config() config = load_config()
print(json.dumps({GROUP_NAME: {"hosts": []}, "_meta": {"hostvars": {}}}, indent=2)) 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: except InventoryError as exc:
print(f"netbox-inventory: {exc}", file=sys.stderr) print(f"netbox-inventory: {exc}", file=sys.stderr)
return 1 return 1