Previously I used to use PHP-IPAM, but then Netbox is out there and it is also nice.
But adding all the stuff is annoying. Even more because Netbox doesn't have ascanners or any sort of helper.
So lets build one shall we?
Building a netbox-agent
A multi-platform inventory agent that collects system and network data and upserts it into NetBox DCIM/IPAM. Runs as a daemon or one-shot job. Supports Linux, FreeBSD, TrueNAS, pfSense, macOS, and Windows.
Two implementations exist:
| Implementation | Location | Status |
|---|---|---|
| Go (recommended) | golang/ |
Active — full feature set |
| Ruby | ruby/ |
Legacy — local-host only |
Modes of operation
The Go binary is a single executable with multiple modes, selected by flag:
| Flag | What it does |
|---|---|
| (default) | Collect local host info (hardware, OS, interfaces, IPs) and sync to NetBox as a device or VM |
--cisco |
SSH into Cisco switches, collect show command output, sync to NetBox |
--unifi |
SSH into UniFi APs and Cloud Keys, sync to NetBox |
--scan |
nmap -sn a subnet and upsert all live IPs into NetBox IPAM |
--metallb |
Run as a Kubernetes pod — discover MetalLB pools and sync to NetBox IPAM |
--sysinfo |
Print collected local info to stdout; does not write to NetBox |
All write operations support --dry-run (logs payloads, no POST/PATCH).
Quick start
# Build
cd golang && go mod download && make build
# Install (binary + config + systemd unit)
sudo make install
# Edit config
sudo nano /etc/netbox-agent/config.yml
# Start as daemon
sudo systemctl enable --now netbox-agent
# Or run once
netbox-agent --onceConfiguration
/etc/netbox-agent/config.yml (default path; override with NETBOX_AGENT_CONFIG):
netbox:
url: "https://netbox.example.com/api/"
token: "Bearer nbt_XXXX.YYYYYYYYYYYYYYYYYY"
site: "Main DC" # created in NetBox if absent
cluster: "KVM Cluster" # VMs only
cluster_type: "KVM" # VMs only
insecure_tls: false # skip TLS verify (self-signed certs)
device:
# physical: true # omit to auto-detect via systemd-detect-virt
role: "Server"
# manufacturer: "Raspberry Pi Foundation" # override if detection fails
# model: "Raspberry Pi 4 Model B 8GB"
u_height: 1 # rack units in NetBox device type
agent:
interval_seconds: 3600 # re-sync every hour
# Cisco switches (--cisco mode)
cisco:
platform: ios # global default: ios | iosxe | nxos | sx220
user: admin
password: secret
timeout_seconds: 30
switches:
- host: 192.168.1.1
site: "Main DC"
role: "Network Switch"
cdp: true # collect CDP neighbors
- host: 192.168.1.2
platform: nxos
# UniFi APs and Cloud Keys (--unifi mode)
unifi:
user: ubnt
password: secret
timeout_seconds: 30
aps:
- host: 192.168.1.10
site: "Main DC"
location: "Server Room"Config is hot-reloaded on every interval — no restart required.
What it collects
Local host (default mode)
| Category | Details |
|---|---|
| Identity | Hostname, OS name + version |
| Hardware | Manufacturer, model, serial, asset tag |
| CPU | Model, core count, socket count |
| Memory | Total RAM in MB |
| Disks | Name and size of all physical block devices |
| Interfaces | Name, MAC, MTU, speed, NetBox type |
| IP Addresses | All non-link-local IPv4/IPv6 per interface, with reverse DNS |
| Primary IP | Detected from default-route interface, wired to device/VM record |
Physical vs virtual is auto-detected via systemd-detect-virt and DMI product name. Override with device.physical: true/false.
Cisco switches (--cisco)
Collected via SSH using show commands:
- Hostname, model, serial, IOS version (via
show version/show inventory) - All interfaces: name, type, MAC, speed, MTU, admin/line status, IP
- VLANs: ID and name (via
show vlan brief) - CDP neighbors and cable creation in NetBox (optional,
cdp: true)
Platforms: ios, iosxe, nxos, sx220. Platform is auto-detected from show version if not set.
UniFi devices (--unifi)
Device class is auto-detected:
| Class | Detection | Collection method |
|---|---|---|
| AP (UAP-, U6-, BusyBox/MIPS) | Default | mca-dump JSON + info command |
| Cloud Key (UCK-G2, UCK-G2-Plus, Debian/aarch64) | uname -a contains "cloud-key" or arch=aarch64+Debian |
/proc/*, ip addr, lsblk, df |
Pushed to NetBox:
- Manufacturer: Ubiquiti; device type per model
- Wired interfaces (from
if_table) - Wireless interfaces: one per radio + one per SSID/VAP
- Management IP set as primary IPv4
- Cloud Key: CPU cores, storage, HDD, battery state
- AP: radio bands/channels, SSIDs
MetalLB (--metallb)
Run as a Kubernetes pod. Discovers pools via:
- Kubernetes API (
metallb.io/v1beta1/ipaddresspools) — primary - MetalLB speaker Prometheus metrics scrape — fallback
Pushes to NetBox IPAM:
- CIDR pools →
ipam/prefixes/(taggedmetallb) - Range pools (e.g.
10.0.0.100-10.0.0.150) →ipam/ip-ranges/ - LoadBalancer service IPs →
ipam/ip-addresses/(dns_name =<svc>.<ns>.svc)
Subnet scanner (--scan)
netbox-agent --scan --scan-subnet 192.168.1.0/24 \
--netbox-url https://netbox.example.com/api/ \
--netbox-token "Bearer nbt_XXX.YYY"- Ensures prefix exists in
ipam/prefixes/ - Runs
nmap -sn <subnet>to find live hosts - Reverse-DNS each host
- Upserts each IP into
ipam/ip-addresses/
Requires nmap installed on the host.
Hardware detection
The agent tries five sources in priority order, stopping at the first that works:
| Priority | Source | Platforms |
|---|---|---|
| 1 | dmidecode (DMI types 1/2/3) |
x86 Linux (requires root) |
| 2 | kenv smbios.* |
FreeBSD, pfSense, TrueNAS CORE |
| 3 | /proc/device-tree/model |
Raspberry Pi, ARM SBCs |
| 4 | /proc/cpuinfo Hardware/Revision |
Older RPi kernels |
| 5 | dmesg "Machine model:" / "Board:" |
Embedded fallback |
Dell servers: system serial = service tag → stored as asset_tag; baseboard serial → stored as serial. All other vendors: system serial → serial.
Raspberry Pi: full revision table from Pi 1 Model B through Pi 5, all Compute Modules and Zero variants.
Manufacturer inference: when maker field is blank, derived from model string (Raspberry Pi Foundation, Xunlong, Radxa, Sinovoip, Hardkernel, BeagleBoard.org, Pine64).
Override in config with device.manufacturer and device.model if detection fails.
Interface filtering
Reported (kept):
- Physical Ethernet:
eth*,eno*,ens*,enp*,enx*,em*,igb*,ix*,bge* - WiFi:
wl*,ath* - Bonds/LAG:
bond*,lagg* - Bridges:
vmbr*,bridge*,ovs* - VLANs:
vlan*,*.{digits} - VPN tunnels:
ovpn*,tun*,wg*,pppoe*
Skipped:
- Loopback (
lo,lo0) - Container plumbing:
veth*,docker*,podman*,br-*,virbr* - Proxmox firewall internals:
fwbr*,fwln*,fwpr* - pfSense/OpenBSD internals:
pflog*,pfsync*,enc* - Kernel dummies:
dummy*,ifb*,sit*,ip6tnl* - QEMU taps:
tap* - Calico CNI:
cali*
Upsert behaviour
Every object (device/VM, interface, IP, prefix, etc.) is looked up by natural key before writing:
- Exists →
PATCHchanged fields only - Absent →
POSTto create - Nothing is ever deleted
The comments field on every device/VM is kept as structured data with a Last seen: timestamp so each run is traceable without polluting audit logs.
Reverse DNS resolution
Three sources, tried in order:
dig +short -x <ip>(respects configured DNS)/etc/hosts(works offline)getent hosts <ip>(covers NSS: avahi/mDNS, LDAP, etc.)
Platform support
Linux (standard)
- Hardware:
dmidecode(root) or device-tree - Interfaces:
/sys/class/net - IPs:
ip -o addr show - Routing:
ip route show default - OS:
/etc/os-release
FreeBSD / pfSense / TrueNAS CORE
- Hardware:
kenv smbios.* - Interfaces:
ifconfig -a(hex netmask conversion) - Routing:
route -n get defaultornetstat -rn - OS:
/etc/version(pfSense),/etc/version.freenas(TrueNAS CORE),uname -srfallback
TrueNAS SCALE (Debian 12)
Use linux-amd64 binary. Root filesystem is read-only and reset on upgrades — store binary and config in a ZFS dataset (/mnt/<pool>/). See golang/README-truenas.md for full deployment instructions.
macOS
Hardware detection falls back to "Generic" (no /proc or dmidecode). Useful for local testing.
Windows
Limited info (no /proc, dmidecode, or ip). Provided for completeness.
Building
cd golang
# Current machine
make build
# All targets (dist/)
make all-targets
# Specific targets
make linux-amd64
make linux-arm64
make linux-armv6 # RPi 1/2/Zero
make linux-freebsd # TrueNAS CORE / pfSense
make darwin-arm64 # Apple Silicon
make windows-amd64Cross-compilation reference:
| Target | Command | Binary |
|---|---|---|
| Linux x86-64 | make linux-amd64 |
dist/netbox-agent-linux-amd64 |
| Linux ARM64 (RPi 3/4/5) | make linux-arm64 |
dist/netbox-agent-linux-arm64 |
| Linux ARMv6 (RPi 1/2/Zero) | make linux-armv6 |
dist/netbox-agent-linux-armv6 |
| FreeBSD / pfSense / TrueNAS CORE | make linux-freebsd |
dist/netbox-agent-freebsd-amd64 |
| macOS Intel | make darwin-amd64 |
dist/netbox-agent-darwin-amd64 |
| macOS Apple Silicon | make darwin-arm64 |
dist/netbox-agent-darwin-arm64 |
| Windows x86-64 | make windows-amd64 |
dist/netbox-agent-windows-amd64.exe |
Installation
Manual
cd golang
sudo make install
# Installs binary, config, and both systemd units; does not overwrite existing config.Or manually:
sudo install -m 0755 dist/netbox-agent-linux-amd64 /usr/local/bin/netbox-agent
sudo mkdir -p /etc/netbox-agent
sudo install -m 0600 config.yml /etc/netbox-agent/config.yml
sudo install -m 0644 netbox-agent.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now netbox-agentSystemd modes
| Unit | Behaviour |
|---|---|
netbox-agent.service |
Long-running daemon, re-syncs on interval |
netbox-agent-oneshot.service |
Runs once at boot, exits |
Ansible
See ansible/README.md. The role downloads the correct binary from the GitLab package registry, writes the config, and installs the appropriate systemd unit.
- role: netbox_agent
vars:
netbox_agent_netbox_url: "https://netbox.example.com/api/"
netbox_agent_netbox_token: "Bearer nbt_XXXX.YYYY"
netbox_agent_site: "Main DC"
netbox_agent_device_role: "Server"
netbox_agent_service_mode: "daemon" # or "oneshot" or "push"
netbox_agent_version: "latest"Key Ansible variables:
| Variable | Default | Description |
|---|---|---|
netbox_agent_version |
"latest" |
Tag to install; pin for reproducibility |
netbox_agent_service_mode |
"daemon" |
daemon / oneshot / push |
netbox_agent_physical |
null (auto) |
Force physical/virtual detection |
netbox_agent_interval_seconds |
3600 |
Sync interval (daemon mode) |
netbox_agent_insecure_tls |
false |
Skip TLS verification |
netbox_agent_push_cleanup |
true |
Remove binary after push-mode run |
MetalLB container
cd golang
make linux-amd64
docker build -f Dockerfile.metallb -t netbox-agent-metallb .The image is based on scratch with only the static binary and Mozilla CA bundle. Default command: --metallb --once.
Required RBAC: get/list on metallb.io/ipaddresspools and v1/services.
CLI flags reference
netbox-agent [flags]
Global:
--config PATH Config file (default: /etc/netbox-agent/config.yml; or NETBOX_AGENT_CONFIG env)
--once Run once and exit
--dry-run Collect but do NOT write to NetBox
--verbose Log full HTTP request/response bodies
--sysinfo Print local system info and exit
--version Print version and exit
NetBox overrides:
--netbox-url URL Override config netbox.url
--netbox-token TOK Override config netbox.token
--site NAME Override config netbox.site
Cisco (--cisco):
--cisco-host IP Single switch (skips config switch list)
--cisco-user USER
--cisco-password PW
--cisco-platform ios | iosxe | nxos | sx220 (auto-detected if omitted)
--cisco-enable Send 'enable' after login
--cisco-cdp Collect CDP neighbors, create cables
--cisco-site NAME
--cisco-role NAME Default: "Network Switch"
--cisco-dry-run
UniFi (--unifi):
--unifi-host IP Single device (AP or Cloud Key, auto-detected)
--unifi-user USER Default: ubnt
--unifi-password PW
--unifi-site NAME
--unifi-location NAME NetBox location (room/area)
--unifi-dry-run
Scan (--scan):
--scan-subnet CIDR e.g. 192.168.1.0/24
--scan-dry-runProject layout
netbox-agent/
├── golang/ # Go implementation (main)
│ ├── main.go # Entry point, flag parsing, run loop
│ ├── collector/
│ │ ├── sysinfo.go # Hardware, CPU, RAM, disk, interface collectors
│ │ ├── agent.go # NetBox upsert orchestration (device/VM)
│ │ ├── cisco.go # Cisco SSH entry point + fact gathering
│ │ ├── cisco_ssh.go # SSH client (prompt detection, paging)
│ │ ├── cisco_parser.go # show command parsers
│ │ ├── cisco_push.go # Cisco NetBox push logic
│ │ ├── unifi.go # UniFi AP + Cloud Key collection and push
│ │ └── metallb.go # MetalLB pool discovery and IPAM push
│ ├── netbox/
│ │ ├── client.go # NetBox HTTP API client (GET/POST/PATCH/Find)
│ │ └── comment.go # Structured comment helpers
│ ├── config/
│ │ └── config.go # YAML config loader + structs
│ ├── scanner/
│ │ └── scanner.go # nmap subnet scan + IPAM upsert
│ ├── Makefile # Build, cross-compile, install, upload targets
│ ├── Dockerfile.metallb # Scratch image for MetalLB pod
│ ├── netbox-agent.service # systemd daemon unit
│ ├── netbox-agent-oneshot.service
│ ├── config.yml # Example configuration
│ ├── go.mod
│ ├── README.md # Go implementation docs
│ └── README-truenas.md # TrueNAS-specific deployment notes
├── ruby/ # Legacy Ruby implementation
│ ├── netbox_agent.rb
│ ├── netbox-agent.config.yml
│ ├── netbox-agent.service
│ └── README.md
├── ansible/ # Ansible role
│ ├── README.md
│ └── roles/netbox_agent/
│ ├── defaults/main.yml
│ ├── tasks/main.yml
│ ├── handlers/main.yml
│ └── templates/
│ ├── config.yml.j2
│ ├── netbox-agent.service.j2
│ └── netbox-agent-oneshot.service.j2
└── support-files/
└── RELEASE.md # Release notes templateRequirements
Go implementation
| Dependency | Notes |
|---|---|
| Go 1.21+ | Build-time only; binary is fully static |
dmidecode |
Optional. Hardware info on x86. Requires root. |
kenv |
Optional. Hardware info on FreeBSD. No root required. |
dig |
Optional. Reverse DNS. (dnsutils / bind-utils) |
ip |
Interface/IP enumeration. Standard (iproute2). |
nmap |
Required for --scan mode only. |
| NetBox 4.x | API token in Bearer nbt_... format. |
The compiled binary has no runtime dependencies — copy to any Linux/BSD machine and run.
Ruby implementation
Ruby 2.7+, standard library only (no gems). Same optional system tools as above.
Monitoring the service
# Live logs
journalctl -u netbox-agent -f
# Status
systemctl status netbox-agent
# Trigger immediate re-sync
systemctl restart netbox-agent
# Stop
systemctl stop netbox-agentnetbox-agent Ansible Role
---
# example-playbook.yml
# Run with:
# ansible-playbook -i inventory example-playbook.yml \
# -e netbox_agent_gitlab_token=YOUR_TOKEN
- name: Deploy netbox-agent to virtual machines
hosts: vms
become: true
roles:
- role: netbox_agent
vars:
netbox_agent_netbox_url: "https://netbox.science.net/api/"
netbox_agent_netbox_token: "Bearer nbt_XXXX.YYYYYYYYYYYYYY"
netbox_agent_insecure_tls: true
netbox_agent_site: "HeadQuarters"
netbox_agent_cluster: "KVM Cluster"
netbox_agent_cluster_type: "KVM"
netbox_agent_device_role: "Virtual Machine"
netbox_agent_service_mode: "oneshot" # run once at boot for VMs
netbox_agent_version: "latest"
- name: Deploy netbox-agent to physical servers (daemon mode)
hosts: servers
become: true
roles:
- role: netbox_agent
vars:
netbox_agent_netbox_url: "https://netbox.science.net/api/"
netbox_agent_netbox_token: "Bearer nbt_XXXX.YYYYYYYYYYYYYY"
netbox_agent_insecure_tls: true
netbox_agent_site: "HeadQuarters"
netbox_agent_device_role: "Server"
netbox_agent_physical: true
netbox_agent_service_mode: "daemon"
netbox_agent_interval_seconds: 3600
netbox_agent_version: "latest"TrueNAS Deployment Notes
TrueNAS exists in two very different variants, each requiring a different binary
and deployment approach.
TrueNAS CORE (FreeBSD-based)
TrueNAS CORE runs on FreeBSD 13.x. Use the freebsd-amd64 binary.
Hardware detection
kenv smbios.*is used instead of dmidecode — works without root on FreeBSD.- Interface collection uses
ifconfig -aparsing (no/sys/class/net). - Set
device.manufactureranddevice.modelin config if kenv returns nothing useful. - Serial number is read from
kenv smbios.system.serial.
Installation
TrueNAS CORE allows persistent storage in jails or in /data and /root.
Do not install to /usr/local/bin directly — it will be wiped on major upgrades.
Recommended approach — run from /data:
# Copy binary
cp netbox-agent-freebsd-amd64 /data/netbox-agent
chmod 755 /data/netbox-agent
# Create config
mkdir -p /data/netbox-agent-config
cat > /data/netbox-agent-config/config.yml <<EOF
netbox:
url: "https://netbox.example.com/api/"
token: "Bearer nbt_XXXX.YYYY"
site: "Main DC"
insecure_tls: false
device:
physical: true
role: "NAS"
agent:
interval_seconds: 3600
EOF
chmod 600 /data/netbox-agent-config/config.yml
# Run once to verify
/data/netbox-agent --once --config /data/netbox-agent-config/config.yml --sysinfoScheduling on TrueNAS CORE
Use the built-in Tasks → Cron Jobs UI, or add to /etc/crontab:
0 * * * * root /data/netbox-agent --once --config /data/netbox-agent-config/config.ymlOr use an rc.d script for daemon mode — place in /usr/local/etc/rc.d/netbox_agent:
#!/bin/sh
# PROVIDE: netbox_agent
# REQUIRE: NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name="netbox_agent"
rcvar="netbox_agent_enable"
command="/data/netbox-agent"
command_args="--config /data/netbox-agent-config/config.yml"
pidfile="/var/run/netbox_agent.pid"
load_rc_config $name
: ${netbox_agent_enable:="NO"}
run_rc_command "$1"Then enable it:
echo 'netbox_agent_enable="YES"' >> /etc/rc.conf.local
service netbox_agent startTrueNAS SCALE (Linux-based, Debian 12)
TrueNAS SCALE runs a customised Debian 12 (Bookworm) kernel. Use the linux-amd64 binary.
Important constraints
TrueNAS SCALE mounts the root filesystem read-only and resets /usr, /bin, /opt
on upgrades. Safe persistent locations are:
/mnt/<pool>/— your ZFS pool mount (survives upgrades)- Custom Apps (Docker/Kubernetes) — recommended for long-running agents
Option A — Run from a ZFS dataset (simplest)
# Create a dataset for tooling
zfs create tank/tools
# Install binary and config
cp netbox-agent-linux-amd64 /mnt/tank/tools/netbox-agent
chmod 755 /mnt/tank/tools/netbox-agent
mkdir -p /mnt/tank/tools/netbox-agent-config
cat > /mnt/tank/tools/netbox-agent-config/config.yml <<EOF
netbox:
url: "https://netbox.example.com/api/"
token: "Bearer nbt_XXXX.YYYY"
site: "Main DC"
device:
physical: true
role: "NAS"
agent:
interval_seconds: 3600
EOF
chmod 600 /mnt/tank/tools/netbox-agent-config/config.ymlSchedule via System → Advanced → Cron Jobs in the TrueNAS UI, or create
a systemd override that survives across the TrueNAS-managed systemd:
mkdir -p /etc/systemd/system/netbox-agent.service.d
cat > /etc/systemd/system/netbox-agent.service <<EOF
[Unit]
Description=NetBox Agent
After=network-online.target zfs-mount.service
Wants=network-online.target
[Service]
Type=simple
ExecStart=/mnt/tank/tools/netbox-agent --config /mnt/tank/tools/netbox-agent-config/config.yml
Restart=on-failure
RestartSec=60
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now netbox-agentNote: TrueNAS SCALE may reset
/etc/systemd/system/on major version upgrades.
Check after each upgrade and re-enable if needed.
Option B — Custom App (Docker, recommended for production)
Deploy as a Custom App in Apps → Discover Apps → Custom App:
image:
repository: your-registry/netbox-agent
tag: latest
command: ["--once"] # or remove for daemon mode
env:
- name: NETBOX_AGENT_CONFIG
value: /etc/netbox-agent/config.yml
volumeMounts:
- mountPath: /etc/netbox-agent
name: config
volumes:
- name: config
hostPath:
path: /mnt/tank/tools/netbox-agent-configHardware detection on TrueNAS
dmidecodeis available on TrueNAS SCALE and CORE — the agent will use it.- It will correctly detect the host's manufacturer, model, and serial number.
- Set
device.physical: truein config — TrueNAS does not runsystemd-detect-virt.
What the agent reports for TrueNAS
| Field | Source |
|---|---|
| Hostname | hostname -f |
| OS | /etc/version.freenas (CORE) or /etc/truenas_version (SCALE) |
| Manufacturer / Model / Serial | dmidecode (SCALE) or kenv (CORE) |
| Interfaces | All physical NICs, VLANs, bonds — not the internal TrueNAS bridge |
| IP Addresses | All non-link-local addresses per interface |
| Disks | Physical block devices (note: ZFS pool members may show as individual disks) |




