Dynamic Oxidized Inventory from NetBox
Oxidized is the go-to tool for network configuration backups. It polls your devices over SSH, grabs the running config, and commits it to a Git repo. But out of the box, it expects a static CSV or YAML file listing your devices. Every time you rack a new switch or decommission an old one, someone has to update that file.
I replaced all of that with a NetBox API integration. Now Oxidized dynamically pulls its device inventory from NetBox and backs up configs to a Git repository with automatic pushes to GitLab.
The problem with static inventory
The default Oxidized setup uses a router.db file:
switch01:junos:ssh
switch02:junos:ssh
router01:junos:ssh
This works fine for a handful of devices. But in a growing datacenter environment:
- New devices get deployed and nobody remembers to add them to the backup list
- Decommissioned devices cause failed polls that clutter the logs
- There’s no single source of truth — NetBox says one thing, the backup list says another
- No metadata about device grouping, site, or role
The fix is obvious: NetBox already knows every device in the network. Oxidized should just ask it.
NetBox as the inventory source
Oxidized supports HTTP-based inventory sources natively. The config tells it to query the NetBox API for devices matching specific criteria:
source:
default: http
http:
url: https://netbox.example.com/api/dcim/devices/?has_primary_ip=True&cf_backup_enable=True&limit=0
scheme: https
headers:
Authorization: Token <netbox-api-token>
pagination: false
hosts_location: results
map:
name: name
model: device_type.slug
ip: primary_ip.address
group: site.slug
Let’s break this down.
The query
The URL filters devices to only those that:
has_primary_ip=True— have a management IP assigned. No IP, no SSH, no point backing up.cf_backup_enable=True— have a custom fieldbackup_enableset to true. This is the opt-in flag. Not every device in NetBox needs to be backed up (PDUs, patch panels, etc.).limit=0— return all matching devices in a single response (no pagination).
The field mapping
Oxidized needs to know which JSON fields map to its internal model:
name: name— the device hostname from NetBox becomes the Oxidized node namemodel: device_type.slug— the device type slug (e.g.,qfx5120-48y-afi) is used to select the correct Oxidized modelip: primary_ip.address— the management IP Oxidized connects togroup: site.slug— the site code (e.g.,ams1,bru2) groups configs in subdirectories
Model mapping
NetBox device type slugs don’t match Oxidized model names directly. A model_map translates them:
model_map:
juniper-junos: junos
ex4100-48t: junos
mx204: junos
qfx5120-48y-afi: junos
srx300: junos
srx340: junos
ex3400-24t: junos
ex3400-48p: junos
Every Juniper device type maps to the junos model. If you add Cisco or Arista devices later, you’d add mappings like catalyst-9300: ios or dcs-7050: eos.
Git output with remote push
Configs are stored in a Git repository inside the container, and automatically pushed to GitLab after every successful poll:
output:
default: git
branch: master
git:
user: svc_oxidized
email: noc@example.com
single_repo: true
repo: /home/oxidized/.config/oxidized/repos/configs.git
hooks:
git_push:
type: githubrepo
events: [post_store]
remote_repo: https://gitlab.example.com/networking/config-backups.git
username: oauth2
password: <gitlab-pat>
The post_store hook fires after every config commit. It pushes to the remote GitLab repo, giving you a full audit trail of every config change on every device — who changed what, when, with diffs.
The single_repo: true setting puts all device configs in one repository. The group field from NetBox (the site slug) creates subdirectories:
configs.git/
├── ams1/
│ ├── ams1leaf01.mgmt.example.com
│ ├── ams1leaf02.mgmt.example.com
│ └── ams1router01.mgmt.example.com
├── ams2/
│ ├── ams2leaf01.mgmt.example.com
│ └── ...
└── bru1/
└── ...
Every file is the full device config, versioned by Git. git log on any file shows the complete change history.
Docker Compose setup
The whole thing runs in a single container:
services:
oxidized:
image: oxidized/oxidized:latest
container_name: oxidized
restart: unless-stopped
ports:
- "8888:8888"
volumes:
- ./config:/home/oxidized/.config/oxidized
The config/ directory contains the Oxidized config file with the NetBox source, model map, and Git output settings. The built-in web UI on port 8888 lets you browse nodes, view configs, and trigger manual polls.
The custom field trick
The key to making this work cleanly is the cf_backup_enable custom field in NetBox. It’s a boolean field on the Device model:
- True — device is included in Oxidized’s inventory
- False / unset — device is excluded
This means you can onboard a new device in NetBox, set backup_enable = true, and Oxidized picks it up on the next poll cycle (default: every hour). No config files to edit, no containers to restart.
It also means you can temporarily exclude a device — maybe it’s being reimaged or has known SSH issues — without removing it from NetBox.
The full Oxidized config
Here’s the complete config for reference:
---
interval: 3600
threads: 30
timeout: 20
retries: 2
prompt: !ruby/regexp /^([\w.@()-]+[#>]\s?)$/
username: svc_oxidized
password: <service-account-password>
extensions:
oxidized-web:
load: true
listen: 0.0.0.0
port: 8888
input:
default: ssh
ssh:
secure: false
output:
default: git
branch: master
git:
user: svc_oxidized
email: noc@example.com
single_repo: true
repo: /home/oxidized/.config/oxidized/repos/configs.git
source:
default: http
http:
url: https://netbox.example.com/api/dcim/devices/?has_primary_ip=True&cf_backup_enable=True&limit=0
scheme: https
headers:
Authorization: Token <netbox-api-token>
pagination: false
hosts_location: results
map:
name: name
model: device_type.slug
ip: primary_ip.address
group: site.slug
model_map:
juniper-junos: junos
ex4100-48t: junos
mx204: junos
qfx5120-48y-afi: junos
srx300: junos
srx340: junos
ex3400-24t: junos
ex3400-48p: junos
hooks:
git_push:
type: githubrepo
events: [post_store]
remote_repo: https://gitlab.example.com/networking/config-backups.git
username: oauth2
password: <gitlab-pat>
What I learned
- Let NetBox be the source of truth. Once you accept that NetBox knows your inventory, everything else follows. Oxidized, monitoring, DNS — they should all derive from the same source.
- The
model_mapis essential. NetBox device type slugs are never going to match Oxidized model names. Build the map early and extend it as you add new device types. - Git push hooks are fragile. The
githubrepohook in Oxidized works but fails silently if credentials expire. Monitor your remote repo to make sure pushes are actually landing. - The custom field is the on/off switch. Having
backup_enableas a boolean in NetBox gives you a clean way to include or exclude devices without touching Oxidized’s config at all. secure: falseis intentional. Oxidized’s SSH client will reject connections if host keys aren’t pre-loaded. In a lab or fast-moving environment where devices get reimaged regularly, disabling strict host key checking avoids constant manual intervention. In production, consider pre-populating known hosts instead.
What’s next
- Webhook-triggered polls — have NetBox fire a webhook when a device is created or updated, triggering an immediate Oxidized poll instead of waiting for the next hour
- Config diff notifications — send a Slack/Teams message when a device config changes, with a link to the GitLab diff
- Multi-vendor expansion — the model map currently only covers Juniper. Adding Cisco IOS-XE and Arista EOS is just a matter of extending the map and testing the Oxidized models
The whole setup is a Docker Compose file and a single YAML config. The hardest part was getting the Oxidized HTTP source config right — the field mapping documentation is sparse. Once that clicked, everything else fell into place.