Bryan Joshua Pedini c1f0b4f724 feat: ip_version option to restrict inventories to ipv4 or ipv6
inventory.ip_version (or NETBOX_INVENTORY_IP_VERSION): both (default),
v4 or v6; the ip-addresses source filters by family server-side, the
virtual-machines source picks primary_ip4/primary_ip6 for ansible_host.
2026-08-25 14:53:24 +02:00
2026-08-25 13:08:30 +02:00

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

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

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:

# 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

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.

Description
No description provided
Readme GPL-2.0 101 KiB
Languages
Python 97.4%
Makefile 2.6%