Provision NetBox devices and virtual machines into OpenNMS via the REST provisioning API.
Project description
netbox-opennms-plugin
Provision NetBox devices and virtual machines into OpenNMS Horizon 36 as requisition nodes — NetBox is the source of truth, OpenNMS monitoring is a derived artifact kept in sync from NetBox intent.
📖 Documentation & contributor guide →
Why
NetBox is your source of truth for what exists; OpenNMS (Horizon 36) monitors it. This plugin closes the gap: you describe monitoring intent in NetBox — which Devices/VMs, which detectors and policies, which services — and the plugin renders and imports the matching OpenNMS requisition over the REST provisioning API. Membership is a live NetBox query, so the monitored set follows your inventory automatically, and every re-sync is idempotent (render-and-replace — a node is never duplicated or implicitly moved).
Features
- Filter-based Requisitions — a Requisition is one user-named OpenNMS Foreign Source whose members are a live NetBox filter (role, tag, site, status, custom field, …) over Devices and/or Virtual Machines.
- Discovery-driven detectors & policies — configured from your live OpenNMS catalog (classes + parameters read via REST, the same API the OpenNMS UI uses), with a curated preset overlay for labels/defaults and a freeform-class escape hatch.
- Per-interface SNMP roles — the management IP is the Primary SNMP interface by default; add more IPs as Primary / Secondary / Not-eligible.
- Asset & metadata enrichment — map NetBox inventory to OpenNMS node asset
fields, and attach
meta-dataat node / interface / service scope. - Conflict safety — an object matched by two Requisitions is a blocking conflict; Sync of every involved Requisition freezes until you resolve it, so a node always lives in exactly one Foreign Source.
- Graded node status — a member with no management IP becomes an inventory-only node with a warning, not a silent skip.
- Dry-run before sync — a per-node diff of exactly what a Sync would add / remove / change against the live OpenNMS state.
- Honest background sync — Sync/Remove run as NetBox Jobs; an OpenNMS
202is reported as submitted for import, never "provisioned"; a drift reconciler cleans up only the requisitions the plugin owns.
Compatibility
| NetBox | 4.6.1+ |
| Python | 3.12+ |
| OpenNMS | Horizon 36 |
| License | MIT |
Quick start (try it in the browser)
Spin up a throwaway NetBox (UI + worker) and a disposable OpenNMS Horizon 36, then click Sync to OpenNMS and watch a node appear:
cd quickstart
docker compose --profile opennms up -d
The quickstart/ stack is seeded with example Devices, VMs, and
Requisitions, so every path is clickable from the first boot. Full walkthrough —
seeding, dry-run, and Sync — in the
Quickstart guide.
Installation
Install into the same Python environment as NetBox. A PyPI release is a fast-follow; until then, install from the tagged Git ref:
pip install "git+https://github.com/no42-org/netbox-opennms-plugin@v0.0.2"
Enable the plugin in NetBox's configuration.py:
PLUGINS = ["netbox_opennms"]
Apply migrations and restart NetBox and its RQ worker:
python manage.py migrate
Then set the OpenNMS connection — see Configuration.
Kubernetes (Helm chart)
The netbox-community/netbox
chart enables plugins that are already installed in the image — it has no
runtime pip step — so bake the plugin into a custom image, then point the chart at
it and enable it. (This is also netbox-docker's own current guidance.) Installing a
plugin at runtime by mounting it into the container is not a supported path:
netbox-docker declined to add runtime-mounted plugins
(PR #1071) in favour
of build-time images, because a container should be fully defined at build time and
plugins that ship static assets need collectstatic to run during the build. (The
plugin itself is pure-Python, so it is not the constraint — this is about the chart
and netbox-docker, not the package.)
-
Build an image with the plugin. Match the base image to a NetBox version the plugin supports (4.6.1+) and to the chart's
appVersion:# Dockerfile FROM netboxcommunity/netbox:v4.6 # NetBox 4.6.1+; keep in step with the chart appVersion RUN /opt/netbox/venv/bin/pip install \ "git+https://github.com/no42-org/netbox-opennms-plugin@v0.0.2"
The tagged Git ref is the install source available today. PyPI is a fast-follow (then this becomes
pip install netbox-opennms-plugin==<version>); for an air-gapped build,make builda wheel andCOPYit in instead.docker build -t registry.example.org/netbox-opennms:v4.6 . docker push registry.example.org/netbox-opennms:v4.6
-
In
values.yaml, point the chart at that image, enable the plugin, and enable the worker:image: repository: registry.example.org/netbox-opennms tag: v4.6 plugins: - netbox_opennms # the package must already be in the image (step 1) pluginsConfig: netbox_opennms: opennms_url: "https://opennms.example.org/opennms" opennms_username: "provision-svc" opennms_password: "********" # lab only — in production source from a Secret via extraConfig default_location: "" import_mode: "false" reconcile_orphans: "true" worker: enabled: true
The worker must run the same plugin-bearing image. The plugin's Sync and drift-reconciler jobs run in the RQ worker; the chart's worker uses
imageby default, so leave it unset. If the worker runs an image without the plugin, the deploy succeeds and the UI works, but background jobs fail at dequeue with an import error. -
Install or upgrade. The image entrypoint applies migrations on boot (no separate
manage.py migrate), so just confirm the plugin's migrations ran:helm repo add netbox https://netbox-community.github.io/netbox-chart/ helm upgrade --install netbox netbox/netbox -f values.yaml kubectl logs deploy/netbox | grep netbox_opennms # → Applying netbox_opennms... OK
Keep opennms_password out of plaintext values — load the plugin config from a
Secret via the chart's extraConfig. On upgrades, rebuild the image with the
new plugin version, bump image.tag, and helm upgrade.
Configuration
Configure the OpenNMS connection and behaviour in PLUGINS_CONFIG. Credentials
are read at runtime and are never stored on a NetBox model.
PLUGINS_CONFIG = {
"netbox_opennms": {
# Base URL of the OpenNMS instance, including the context path.
"opennms_url": "https://opennms.example.org/opennms",
# A provisioning/REST role account (NOT stored on any NetBox model).
"opennms_username": "provision-svc",
"opennms_password": "********", # use your secrets mechanism
# Default OpenNMS monitoring location for requisitions that don't set one.
# Empty means OpenNMS's built-in "Default" location.
"default_location": "",
# rescanExisting value passed to the import step: one of
# "true" | "false" | "dbonly".
"import_mode": "false",
# Periodic drift reconciler (hourly): clears OpenNMS Foreign Sources the
# plugin has pushed but NetBox no longer monitors — when a Requisition is
# renamed or deleted, or its last member leaves. Ownership is tracked per
# pushed Foreign Source, so it only ever touches requisitions the plugin
# created, never a foreign one. "true" / "false"; needs an RQ worker.
"reconcile_orphans": "true",
},
}
You can verify the configured connection from the UI at Plugins → NetBox OpenNMS → Connect OpenNMS (permission-gated; it tests the configured connection and never persists credentials).
import_mode values
| Value | Effect on import |
|---|---|
false (default) |
Import without rescanning nodes already known to OpenNMS. |
true |
Import and rescan existing nodes (re-run detectors/policies). |
dbonly |
Update the OpenNMS database only; do not schedule a scan. |
Running the sync worker
Sync, Remove, and Move run as NetBox background Jobs — they never block the
request, and a bare OpenNMS 202 ACCEPTED is reported honestly as submitted for
import, never "provisioned". A NetBox RQ worker must be running for those
jobs to execute:
python manage.py rqworker
If no worker is running, the Requisition and Sync pages show a warning and jobs stay pending until one starts. Each object's last-sync state (submitted / succeeded-accepted / removed / failed, with the triggering user, time, and any error) is shown on the Device/VM detail page, backed by the NetBox Job log as the audit trail.
How it works
You author a Requisition — one user-named OpenNMS Foreign Source — that owns
its OpenNMS detectors and policies (discovered live from your OpenNMS
instance, with a curated preset overlay for labels/defaults, or a freeform class),
a set of declared services (e.g. ICMP, SNMP), and a live
NetBox filter that selects its member Devices/VMs (by role, tag, site, status,
custom field, …). Every member is monitored: its management IP is its primary
IP unless overridden, OpenNMS auto-discovers services via the detectors, and the
declared services are the guaranteed-present floor. A per-object Monitoring
Override is the escape hatch (exclude an object, pin a different management IP,
add extra interfaces — each with an SNMP role of Primary / Secondary /
Not-eligible (snmp-primary P/S/N; at most one Primary per node) — add/suppress
a service, or change its location).
Requisition filters must be disjoint: an object matched by more than one Requisition's filter is a conflict — Sync of every involved Requisition is blocked (their OpenNMS state stays untouched) until you resolve the overlap, so a node always lives in exactly one Foreign Source and nothing ever moves or disappears implicitly. A dry-run shows, per node, exactly what a Sync would add / remove / change against the live OpenNMS state before you commit.
Per-node status is graded: a Critical (red) — a filter conflict — blocks Sync; a Warning (yellow) is advisory and does not. A member with no management IP is a Warning: rather than silently skipping it, the plugin provisions an inventory-only node with no IP interface (it will not be actively monitored) and surfaces the warning in the Sync preview, the dry-run, and the Device/VM page — exclude the object if you don't want it in OpenNMS at all.
Sync renders the complete OpenNMS foreign-source definition + requisition and imports it. Membership is a live NetBox query, so adding/removing a Device or changing an attribute the filter matches simply re-resolves the Requisition; render-and-replace makes every re-sync idempotent and never duplicates a node.
OpenNMS-side setup
The plugin writes requisitions; it does not configure OpenNMS polling. For monitoring to actually happen:
- Provisioning account —
opennms_usernameneeds a role that can read/write requisitions and trigger imports (e.g. the OpenNMS provisioning/REST role). - Detectors → poller packages — the requisition's detectors tell OpenNMS which
services to auto-discover, but OpenNMS only polls a discovered service if a
matching poller package exists for it. The plugin cannot create poller
packages — ensure your
poller-configuration.xmlcovers the services your detectors discover (and the requisition's declared services). - Detector/policy discovery — the detector and policy editors are populated
live from your instance (
GET /rest/foreignSourcesConfig/{detectors,policies}, the same API the OpenNMS UI uses), so the available classes and their parameters reflect what that OpenNMS actually has (including plugin-provided detectors). The built-in preset registry is only a curation overlay (friendly labels, sensible defaults, a shortlist). If OpenNMS is unreachable while editing, the editor degrades to the curated presets and notes it — you can still save (freeform class entry always works). Discovered results are cached briefly and refreshed at Sync. - Asset & metadata enrichment — a Requisition can carry NetBox inventory into
OpenNMS through two channels. Asset mappings map a NetBox attribute (serial,
model, site, …) to a fixed OpenNMS node asset field (the
OnmsAssetRecordset, discovered from/foreignSourcesConfig/assets; validated at save). Metadata entries attach an arbitrarycontext/key/valuetriad at node / interface / service scope (contextdefaults torequisition; a custom context must beX--prefixed) — the open channel for anything without a fixed asset field, and the home for custom fields (cf_<name>). Values resolve per member; an unresolved value is simply omitted. - Minions / monitoring locations — a node assigned to a non-
Defaultmonitoring location is only polled if a Minion is registered at that location. The plugin best-effort warns when a chosen location is unknown to OpenNMS, but cannot create it. The built-inDefaultlocation is polled by the OpenNMS core (no Minion required).
Requisitions, membership, and node identity
A Requisition's name is its OpenNMS Foreign Source name — user-chosen (it
must be Foreign-Source- and URL-path-safe: no whitespace or # % & + ? / \ : * ' "),
not derived. Its membership is a NetBox filter (FilterSet parameters, e.g.
{"role": ["switch"], "tag": ["critical"]}) applied to Devices and/or
VirtualMachines per its object types; you can seed the filter by importing a
NetBox Saved Filter (a one-time copy — no live link). A filter must actually
constrain each selected object type, so a typo or empty value can't silently
become a fleet-wide catch-all.
Filters must be disjoint. When several Requisitions match the same object it
is a conflict: the object is rendered into none of them and Sync of every
involved Requisition is blocked (frozen — the OpenNMS state stays exactly as last
synced) until you resolve it. Resolve by narrowing a filter — typically with a
negated parameter, e.g. {"role": ["switch"], "tag__n": ["critical"]} — or by
excluding the object (an excluded object never conflicts; it is monitored
nowhere). Conflicts are shown on the Requisition page, the Sync preview, the
dry-run, and the affected Device/VM page. The REST API follows the same
save-never-blocks rule but has no warning channel — after automated writes,
check the Sync preview (or the requisition page) for conflicts.
Node identity is the pair (Foreign Source, type-qualified Foreign ID) —
device-{pk} / vm-{pk} — so a re-sync updates a node in place and renaming a
Device only relabels it (never a duplicate). Moving an object between Requisitions
(a filter change) changes its Foreign Source, which OpenNMS treats as a new node;
the per-node dry-run surfaces such moves — and every add / remove / change
against the live OpenNMS state — before you Sync.
Development & contributing
Full project-structure, build, dev-environment, testing, and release docs live in the contributor guide. The short version:
pip install -e . # editable install
make verify # ruff lint + full unit suite (what CI gates on)
make integration # live round-trip against a disposable OpenNMS Horizon 36
Before a PR: make verify green, Conventional Commits,
signed commits (git commit -s + commit.gpgsign), and an SPDX header on every
source file. CI runs the same Makefile targets, so local and CI stay identical.
License
MIT — see LICENSE. Every source file carries an SPDX header.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file netbox_opennms_plugin-0.0.4.tar.gz.
File metadata
- Download URL: netbox_opennms_plugin-0.0.4.tar.gz
- Upload date:
- Size: 80.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e149ef139c77e0e981bbd165b556eb9c589a344ed7cc19a5d67051beb687405
|
|
| MD5 |
f732b0a0cb06a148a907ae973445d527
|
|
| BLAKE2b-256 |
1c469daa8be3afb1457a94d1d01dd31fddba03f8c46ded9faf06873d8f45befd
|
Provenance
The following attestation bundles were made for netbox_opennms_plugin-0.0.4.tar.gz:
Publisher:
release.yml on no42-org/netbox-opennms-plugin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
netbox_opennms_plugin-0.0.4.tar.gz -
Subject digest:
2e149ef139c77e0e981bbd165b556eb9c589a344ed7cc19a5d67051beb687405 - Sigstore transparency entry: 2170217632
- Sigstore integration time:
-
Permalink:
no42-org/netbox-opennms-plugin@dbe16d827c790514b03530c061a752df6b9e8673 -
Branch / Tag:
refs/tags/v0.0.4 - Owner: https://github.com/no42-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dbe16d827c790514b03530c061a752df6b9e8673 -
Trigger Event:
release
-
Statement type:
File details
Details for the file netbox_opennms_plugin-0.0.4-py3-none-any.whl.
File metadata
- Download URL: netbox_opennms_plugin-0.0.4-py3-none-any.whl
- Upload date:
- Size: 96.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9efa640c4d27a791ac3399697ae8b9a0bf736194df6802f47f4793efc39bd723
|
|
| MD5 |
457c1934577b21bd0bb2b5381afc7c05
|
|
| BLAKE2b-256 |
311bfc63806f8f08c29c6b42df76f0f9ec0352797e304fb4080c60dfbf9b2774
|
Provenance
The following attestation bundles were made for netbox_opennms_plugin-0.0.4-py3-none-any.whl:
Publisher:
release.yml on no42-org/netbox-opennms-plugin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
netbox_opennms_plugin-0.0.4-py3-none-any.whl -
Subject digest:
9efa640c4d27a791ac3399697ae8b9a0bf736194df6802f47f4793efc39bd723 - Sigstore transparency entry: 2170217836
- Sigstore integration time:
-
Permalink:
no42-org/netbox-opennms-plugin@dbe16d827c790514b03530c061a752df6b9e8673 -
Branch / Tag:
refs/tags/v0.0.4 - Owner: https://github.com/no42-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dbe16d827c790514b03530c061a752df6b9e8673 -
Trigger Event:
release
-
Statement type: