Files
ansible-netbox-inventory/README.md

190 lines
7.5 KiB
Markdown

# 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: v4` or `v6`),
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 `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 |
| `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:
```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`.
- **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's `PREFER_IPV4` setting); set `v4` or `v6` to 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 no `ansible_host`.
- 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.