You've already forked ansible-netbox-inventory
190 lines
7.5 KiB
Markdown
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.
|