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)
- Restrict either source to one IP family (
ip_version: v4orv6), combinable with any filter - Name-based filters are resolved server-side (name → id) so NetBox does the narrowing, not the script
- Hosts land in a single
netboxgroup 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+
requestsandPyYAML(PyYAML ships with Ansible anyway)- A NetBox API token with read permissions on the objects you inventory
Installation
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:
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):
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 |
inventory.ip_version |
both |
both, v4 or v6; on IP addresses only that family is fetched, on virtual machines it selects which primary IP becomes ansible_host |
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 |
NETBOX_INVENTORY_IP_VERSION |
inventory.ip_version |
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:
# 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_hostis 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_hostis 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. - IP version: with
ip_version: both(the default) virtual machines use NetBox's combined primary IP, which prefers IPv6 when a VM has both (NetBox'sPREFER_IPV4setting); setv4orv6to pin the family. On the IP address source the family is filtered server-side, and a VM without a primary IP of the requested family simply gets noansible_host. - Duplicate inventory names (two IPs sharing a DNS name, outside the
namedfilter) keep both hosts: the second one falls back to its bare address as the inventory name. - Pagination is followed transparently;
config_contextis 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-inventoryfails loudly instead of running against an empty host list.
Development
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:
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 for more information.