You've already forked ansible-netbox-inventory
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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user