# Ansible NetBox Inventory An Ansible **dynamic inventory script** that sources hosts from NetBox, either from the IP address list or from the virtual machine list, with a single configurable filter option on top. Contract in one sentence: **one source, one optional filter, one value** — set in a config file, overridable per-run through environment variables. --- ## Features - Two host sources: **IP addresses** (`ipam/ip-addresses`) and **virtual machines** (`virtualization/virtual-machines`) - Filter by tenant name, VRF name, cluster name, site name, hostname domain, or restrict IP addresses to the ones that have a DNS name (deduplicated) - Name-based filters are resolved server-side (name → id) so NetBox does the narrowing, not the script - Hosts land in a single `netbox` group with host variables delivered through `_meta`, so Ansible calls the script exactly once - Plain Ansible inventory script protocol (`--list` / `--host`), no plugin installation, no collections ## Requirements - Python 3.10+ - `requests` and `PyYAML` (PyYAML ships with Ansible anyway) - A NetBox API token with read permissions on the objects you inventory ## Installation ```sh git clone https://git.bjphoster.com/source/ansible-netbox-inventory.git cd ansible-netbox-inventory cp config.yaml.example config.yaml $EDITOR config.yaml ``` Point Ansible at the script directly: ```sh ansible-inventory -i netbox_inventory.py --list ansible all -i netbox_inventory.py -m ping ansible-playbook -i netbox_inventory.py site.yml ``` ## Configuration Everything lives in `config.yaml` next to the script (gitignored, see `config.yaml.example`): ```yaml netbox: url: "https://netbox.example.com" token: "0123456789abcdef0123456789abcdef01234567" verify_ssl: true timeout: 30 inventory: source: "ip-addresses" filter: "tenant" filter_value: "customer1" ``` | Key | Default | Meaning | |---|---|---| | `netbox.url` | — (required) | Base URL of the NetBox instance | | `netbox.token` | — (required) | API token, both legacy tokens (sent as `Authorization: Token ...`) and NetBox 4.5+ v2 tokens (`nbt_...`, sent as `Authorization: Bearer ...`) work | | `netbox.verify_ssl` | `true` | Verify the TLS certificate | | `netbox.timeout` | `30` | Per-request timeout in seconds | | `inventory.source` | `ip-addresses` | `ip-addresses` or `virtual-machines` | | `inventory.filter` | none | Filter option, see the table below | | `inventory.filter_value` | none | Value for the filter option | Every relevant key can be overridden through the environment, which is how you run several differently-filtered inventories off one config file: | Variable | Overrides | |---|---| | `NETBOX_INVENTORY_CONFIG` | Path to the config file | | `NETBOX_URL` | `netbox.url` | | `NETBOX_TOKEN` | `netbox.token` | | `NETBOX_INVENTORY_SOURCE` | `inventory.source` | | `NETBOX_INVENTORY_FILTER` | `inventory.filter` | | `NETBOX_INVENTORY_FILTER_VALUE` | `inventory.filter_value` | ## Filters One filter at a time, each takes exactly one value: | Filter | Sources | Value | Behaviour | |---|---|---|---| | `tenant` | both | tenant name | Objects assigned to that tenant | | `domain` | both | domain | Hostname ends with the domain, on a label boundary (`web.example.com` matches `example.com`, `notexample.com` does not) | | `named` | ip-addresses | `true`/`false` (empty = `true`) | Only addresses with a DNS name, deduplicated by hostname (first one wins) | | `vrf` | ip-addresses | VRF name | Addresses in that VRF | | `cluster` | virtual-machines | cluster name | Virtual machines in that cluster | | `site` | virtual-machines | site name | Virtual machines at that site | Examples: ```sh # every IP address of one tenant NETBOX_INVENTORY_FILTER=tenant NETBOX_INVENTORY_FILTER_VALUE=customer1 \ ansible-inventory -i netbox_inventory.py --list # only IPs that resolve to something, one host per DNS name NETBOX_INVENTORY_FILTER=named \ ansible-inventory -i netbox_inventory.py --list # all virtual machines of one cluster NETBOX_INVENTORY_SOURCE=virtual-machines \ NETBOX_INVENTORY_FILTER=cluster NETBOX_INVENTORY_FILTER_VALUE=proxmox01 \ ansible-inventory -i netbox_inventory.py --list # every host in a DNS domain NETBOX_INVENTORY_FILTER=domain NETBOX_INVENTORY_FILTER_VALUE=example.com \ ansible-inventory -i netbox_inventory.py --list ``` ## Behaviour - **IP addresses**: the inventory hostname is the DNS name when set, the bare address otherwise; `ansible_host` is always the bare address (no prefix length). Host variables: `netbox_ip_address`, `netbox_dns_name`, `netbox_tenant`, `netbox_vrf`, `netbox_description`. - **Virtual machines**: the inventory hostname is the VM name; `ansible_host` is set to the primary IP when the VM has one, otherwise Ansible falls back to resolving the name. Host variables: `netbox_status`, `netbox_cluster`, `netbox_site`, `netbox_tenant`. - Duplicate inventory names (two IPs sharing a DNS name, outside the `named` filter) keep both hosts: the second one falls back to its bare address as the inventory name. - Pagination is followed transparently; `config_context` is excluded from the virtual machine query to keep responses small. - Errors (unreachable NetBox, bad token, unknown tenant/cluster/site/VRF name, invalid source/filter combination) print one readable line on stderr and exit non-zero, so `ansible-inventory` fails loudly instead of running against an empty host list. ## Development ```sh make dev-environment # venv + dev dependencies make test # unit tests (mocked API, no NetBox needed) make lint # ruff checks make format # ruff formatter ``` End-to-end tests run against a real NetBox in docker, modelled on the production deployment but exposed straight on `127.0.0.1:8800` with throwaway credentials (see `e2e/env.example`) and no persistence: ```sh make e2e-up # start the stack, first boot takes a few minutes make test-e2e # seed via the API and test every source/filter scenario make e2e-down # tear down and delete all data ``` Seeding creates a read-only `ansible-inventory` user with a self-provisioned v2 token, two tenants, two sites, two clusters, a VRF, three virtual machines and a handful of IP addresses, then the tests run the actual inventory script as an executable against every filter, every error path and the Ansible `--list` / `--host` protocol. Seeding is idempotent, `make test-e2e` can be re-run at will. ## Known limitations - One filter option per run, filters do not combine; run the script twice with different environments if you need an intersection - No caching: every run hits the NetBox API - Devices (`dcim/devices`) are not a source, only IP addresses and virtual machines - Name lookups (tenant, VRF, cluster, site) take the first match when NetBox returns several objects with the same name ## License GPL-2.0-or-later. See [LICENSE](LICENSE) for more information.