diff --git a/README.md b/README.md index b9707b0..649cd7d 100644 --- a/README.md +++ b/README.md @@ -1 +1,163 @@ # 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, sent as `Authorization: Token ...` | +| `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 +``` + +## 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.