Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
18030d0
feat(oob-sync): OOB management controller support, detection, and mig…
marcinpsk May 3, 2026
9812d0c
feat(oob-sync): set OOB IP via an interface-assigned address; drop ge…
marcinpsk May 31, 2026
b863825
fix(oob-sync): PR #79 review hardening, OOB-flow corrections, and docs
marcinpsk May 31, 2026
e8e9feb
fix(oob-sync): address PR #295 upstream review
marcinpsk Jun 3, 2026
dd45377
fix(oob-sync): PR #79 review round — nested-dict mapping shapes & guards
marcinpsk Jun 3, 2026
24f853a
fix(oob-sync): guard self host/OOB id conflict and non-positive mappi…
marcinpsk Jun 3, 2026
44c859a
fix(oob-sync): normalize librenms_id at source; tighten weak test ass…
marcinpsk Jun 3, 2026
81bd2f4
fix(oob-sync): reject ambiguous device-type mappings; render confirm …
marcinpsk Jun 3, 2026
b638b5e
fix(oob-sync): reject concurrent OOB type change on attach
marcinpsk Jun 3, 2026
661755c
fix(ip-sync): re-resolve interface from current NetBox state at sync …
marcinpsk Jun 4, 2026
bdfa9b1
fix(interfaces): clear ports cache at start of refresh so failures sh…
marcinpsk Jun 5, 2026
bb12b26
fix(interfaces): cache host-only snapshot as oob_incomplete instead o…
marcinpsk Jun 5, 2026
d2e48d3
fix(platform): reuse existing platform and add missing mapping on Cre…
marcinpsk Jun 8, 2026
f1ee434
fix(oob-sync): PR #79 CodeRabbit round-13 findings
marcinpsk Jun 8, 2026
496cbee
fix(oob-sync): address CodeRabbit review (platform race reuse + error…
marcinpsk Jun 8, 2026
a1bf890
fix(oob-sync): neutral skip-warning wording; generic platform-create …
marcinpsk Jun 9, 2026
0bc548d
test(migrate): assert HX-Refresh absent on the 409 conflict paths
marcinpsk Jun 9, 2026
2528f1c
fix(oob-sync): clear stale serial flags, keep migrated context, guard…
marcinpsk Jun 9, 2026
ca048a8
fix(oob-sync): don't leak raw save exception in the merge toast
marcinpsk Jun 9, 2026
7be953c
fix(oob-sync): fail-closed librenms_id matching + import/migration ha…
marcinpsk Jun 10, 2026
0edf949
fix(oob-sync): validate server_key before reflecting it in the sync r…
marcinpsk Jun 10, 2026
512b963
fix(oob-sync): re-check PlatformMapping add permission at the write site
marcinpsk Jun 10, 2026
40983a0
fix(oob-sync): surface skipped-mapping warning in the import modal
marcinpsk Jun 10, 2026
4052762
fix(oob-sync): fail closed on duplicate host-only or OOB-only librenm…
marcinpsk Jun 10, 2026
c8846f2
fix(oob-sync): signal ambiguous librenms_id distinctly so callers fai…
marcinpsk Jun 10, 2026
6819ddc
fix(oob-sync): make ambiguous librenms_id a durable import blocker
marcinpsk Jun 10, 2026
292812f
fix(oob-sync): address out-of-diff review findings
marcinpsk Jun 10, 2026
f20738a
fix(oob-sync): guard server_key redirects with explicit allowlist (Co…
marcinpsk Jun 10, 2026
580d045
fix(oob-sync): gate server_key redirects with url_has_allowed_host_an…
marcinpsk Jun 10, 2026
f939b90
fix(oob-sync): populate existing_librenms_link for VM matches; re-sou…
marcinpsk Jun 10, 2026
213ac1a
fix(oob-sync): drop VM binding when cross-model device lookup is ambi…
marcinpsk Jun 10, 2026
3b31f2e
fix(oob-sync): validate PlatformMapping before save; order-independen…
marcinpsk Jun 10, 2026
e42e2ac
fix(oob-sync): re-check serial/IP matches when refreshing a stale-lin…
marcinpsk Jun 11, 2026
90e3744
fix(oob-sync): compose HTMX envelope with format_html; don't double-r…
marcinpsk Jun 11, 2026
0504b0b
test(oob-sync): pin full DB-error sanitization; cover url-barrier rej…
marcinpsk Jun 11, 2026
92f4b80
fix(oob-sync): consistent VM link warning, fail-closed perms, script-…
marcinpsk Jun 11, 2026
a24ea93
fix(oob-sync): don't report failure when a committed import action ca…
marcinpsk Jun 11, 2026
9f75b71
fix(oob-sync): gate server_key failure redirect with open-redirect ba…
marcinpsk Jun 11, 2026
1ded05c
fix(oob-sync): verify PlatformMapping target platform; fall back to a…
marcinpsk Jun 11, 2026
9c09855
test(oob-sync): stub lazy librenms_api in object.__new__ platform views
marcinpsk Jun 11, 2026
5141dfe
fix(oob-sync): fail closed in HTML permission path when request is ab…
marcinpsk Jun 11, 2026
cbeb1b6
test(oob-sync): assert prefix-aware redirect fallback, not hardcoded '/'
marcinpsk Jun 11, 2026
8ef63e9
fix(oob-sync): harden match-drop cleanup, OOB merge, mapping perms, I…
marcinpsk Jun 11, 2026
b16d4a1
fix(oob-sync): validate OOB IP before net_host preflight in _missing_…
marcinpsk Jun 11, 2026
690e9c6
fix(modules): adopt template interfaces even when the port_id bind no…
marcinpsk Jun 11, 2026
602aea9
fix(oob-sync): PR 79 review — interface re-lock type guard, prefix fa…
marcinpsk Jun 11, 2026
287482c
fix(oob-sync): VC sync-device scoping, OOB row read-only, no-interfac…
marcinpsk Jun 11, 2026
99c7b9d
fix(oob-sync): host-pair status ordering + tighten two review tests
marcinpsk Jun 11, 2026
e32a8fd
fix(oob-sync): set IPAddressTable.tab so IP-tab pagination stays on t…
marcinpsk Jun 11, 2026
8923b07
fix(oob-sync): host-id precedence in VC sync-device, generic skip rea…
marcinpsk Jun 11, 2026
f4bcb62
fix(oob): scope cache.ttl/server_key/form markup; warn on existing pl…
marcinpsk Jun 11, 2026
395197b
fix(oob): harden merge/migrate/import edge cases from review
marcinpsk Jun 12, 2026
fbd3d4f
fix(oob): collapse multi-line {# #} template comment to one line
marcinpsk Jun 12, 2026
65d0e60
fix(oob): IP table pagination prefix + dedup bulk-import IDs on parse…
marcinpsk Jun 12, 2026
c54fe0f
fix(oob): OOB-IP perm preflight no-op guard, mapping race warning, HT…
marcinpsk Jun 12, 2026
f2a1dd4
fix(oob): harden multi-server convert, merge/move, and cable verify
marcinpsk Jun 12, 2026
e78ca94
fix(oob): require change Device for move-to-winner endpoints
marcinpsk Jun 12, 2026
7667a3a
fix(oob): clear stale ambiguous-id blocker, drop obsolete migrated ma…
marcinpsk Jun 12, 2026
be603d7
fix(oob): fail closed on misconfigured server in rebind/redirect; dro…
marcinpsk Jun 12, 2026
13741c7
fix(modules): report both bind and adoption in interface-update message
marcinpsk Jun 12, 2026
cc54a1b
fix(tests): isolate rebind_api_for_server in SyncIPAddressesView unit…
marcinpsk Jun 12, 2026
51c0bad
fix(migrate): preserve sync tab + server_key on non-HTMX move redirects
marcinpsk Jun 12, 2026
47a5052
fix(oob): extract OOB iface-picker toggle to a script block; guard me…
marcinpsk Jun 12, 2026
725969c
fix(oob): reject boolean object_id before the falsy check in IP verify
marcinpsk Jun 12, 2026
46af91e
fix(oob): don't close the validation modal on Escape while a nested d…
marcinpsk Jun 12, 2026
ff9c58a
fix(import): fail closed on ambiguous librenms_id, harden id input + …
marcinpsk Jun 13, 2026
785de4f
fix(oob/import): harden migrated-marker, OOB id/module rows, inventor…
marcinpsk Jun 13, 2026
0b6dabd
fix(modules): bulk install matches module-scoped bays for orphan tran…
marcinpsk Jun 13, 2026
33fbf5f
fix(modules): fail closed on malformed inventory elements; preserve b…
marcinpsk Jun 13, 2026
ac1853d
fix(modules): generic OOB toast, fail closed on malformed ports paylo…
marcinpsk Jun 13, 2026
c34061b
fix(modules/ip): fail closed on more malformed LibreNMS payloads; don…
marcinpsk Jun 13, 2026
d952728
fix(modules/migrate/import): server_key in module redirects, lock own…
marcinpsk Jun 13, 2026
6bcd47f
test(migrate): real-DB IP-FK reconcile coverage + fix unique-constrai…
marcinpsk Jun 13, 2026
1aae04a
test(import): convert _refresh_existing_device coverage to real DB
marcinpsk Jun 13, 2026
5e3a4b5
test(migrate): convert marker/winner-resolution coverage to real DB
marcinpsk Jun 13, 2026
9debfcf
test: centralise real-DB object builders in conftest
marcinpsk Jun 14, 2026
7117aea
test(oob): convert OOB interface/IP attach coverage to real DB
marcinpsk Jun 14, 2026
721c429
test(ip-sync): convert interface-resolution coverage to real DB
marcinpsk Jun 14, 2026
ca04009
fix(merge): save donor before winner so oob_ip transfer respects the …
marcinpsk Jun 14, 2026
d0bd644
test(oob): convert AddAsOOBView.post coverage to real DB
marcinpsk Jun 14, 2026
a5b789b
test(modules): real VC member + module for VC-normalization report view
marcinpsk Jun 14, 2026
de05bb3
fix(ip-sync): reject malformed IP rows before enrichment + strengthen…
marcinpsk Jun 14, 2026
05c462b
fix(cables): skip non-dict port rows before dereferencing in get_link…
marcinpsk Jun 14, 2026
3088470
fix(vlan-sync): keep CSRF + hidden inputs out of the migrated-mode div
marcinpsk Jun 14, 2026
d4457be
docs(interfaces): correct get_context_data docstring for OOB-incomple…
marcinpsk Jun 14, 2026
92344e0
chore: stop tracking Playwright MCP session artifacts
marcinpsk Jun 14, 2026
421f33e
fix(import): require a unique serial/hostname peer for merge suggestion
marcinpsk Jun 14, 2026
d9d4285
test(migrate): assert reject paths move/persist nothing before the toast
marcinpsk Jun 14, 2026
917af35
test(device-fields): cover _sync_redirect URL-validation reject branch
marcinpsk Jun 14, 2026
60e848e
fix(interfaces): invalidate ports snapshot before missing-librenms_id…
marcinpsk Jun 14, 2026
cf40551
fix(migrated): keep CSRF token in migrated interface tab; escapejs hx…
marcinpsk Jun 14, 2026
0a72cda
test: harden HTMX reject-path, OOB message-restore, and log assertions
marcinpsk Jun 14, 2026
357f586
fix(views): harden cable/port refresh against missing ids, malformed …
marcinpsk Jun 14, 2026
406afd4
fix(import-ui): close nested modal in the no-Bootstrap fallback path
marcinpsk Jun 14, 2026
395138a
test(device-ops): exercise the [:2] serial-peer slice and assert the …
marcinpsk Jun 14, 2026
4eb7efc
fix(oob): preserve vendor-specific OOB type in name-derived detection
marcinpsk Jun 14, 2026
a8f7d7e
fix(import): skip host drift checks on OOB matches; make ambiguous id…
marcinpsk Jun 14, 2026
1183fa6
fix(cables): return [] for a successful zero-row refresh, None only o…
marcinpsk Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,6 @@ cython_debug/
ca-bundle.crt
*.pem
.github/hooks/

# Playwright MCP session artifacts (page snapshots / console logs)
.playwright-mcp/
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* [Background Jobs & Caching](librenms_import/background_jobs.md)
* [Sync & Configuration](usage_tips/virtual_chassis.md)
* [Virtual Chassis](usage_tips/virtual_chassis.md)
* [Out-of-Band Management](usage_tips/oob_management.md)
* [Interface Mappings](usage_tips/interface_mappings.md)
* [Module Sync](usage_tips/module_sync.md)
* [Mapping Rules](usage_tips/mapping_rules.md)
Expand Down
9 changes: 9 additions & 0 deletions docs/feature_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
* Background job processing for large device sets
* Duplicate detection to prevent re-importing existing devices

### [Out-of-Band (OOB) Management](usage_tips/oob_management.md)

* Detects when a LibreNMS device (iDRAC/iLO/BMC/IPMI/CIMC) is the OOB controller of an existing NetBox device
* **Add as OOB** — link the controller to the host and set `oob_ip` on a chosen (or new) interface
* **Promote to host** — re-point a device currently linked to its OOB controller onto the incoming host device
* **Merge NetBox devices** — reconcile two devices (hostname-matched vs serial-matched) that represent one physical box
* Per-server linkage stored in the `librenms_id` custom field as `{"<server_key>": {"id": N, "oob": {"id": M, "type": "drac"}}}`
* Post-merge **Move to winner** actions to migrate interfaces, IP addresses, and primary/OOB IPs at your own pace

### [Module / Inventory Sync](usage_tips/module_sync.md)

* Compare LibreNMS ENTITY-MIB inventory to NetBox module bays and installed modules
Expand Down
5 changes: 5 additions & 0 deletions docs/librenms_import/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ The plugin checks for existing devices using:

If both a VM and Device with the same hostname exist, the plugin cannot determine which to match and allows import. Set the `librenms_id` custom field on the correct existing object to clarify the match.

## Out-of-Band (OOB) Detection

When an incoming LibreNMS device looks like an out-of-band controller (iDRAC, iLO, BMC, …) and matches an existing NetBox device, the validation details show an **OOB Detected** panel instead of a plain import button. Rather than creating a duplicate device, the plugin offers the appropriate reconciliation action — **Add as OOB**, **Promote to host**, or **Merge NetBox devices**. See [Out-of-Band (OOB) Management](../usage_tips/oob_management.md) for the full flow.

## Next Steps

- [Import Settings](import_settings.md) - Configure device naming and import options
- [Out-of-Band Management](../usage_tips/oob_management.md) - Reconcile OOB controllers with their host devices
7 changes: 7 additions & 0 deletions docs/usage_tips/custom_field.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ If the field was not created automatically (fallback): follow these steps to cre
```json
{"production": 42, "staging": 17}
```
- Out-of-band (OOB) form — when a device is linked to its OOB controller, the per-server value is an object holding the host id and the controller's id/type:

```json
{"production": {"id": 42, "oob": {"id": 99, "type": "drac"}}}
```

This shape is written automatically by the OOB flows — see [Out-of-Band Management](oob_management.md). You don't normally edit it by hand.
- Legacy single-server example (integer) — read-only/deprecated; do not use for new entries:
```
42
Expand Down
79 changes: 79 additions & 0 deletions docs/usage_tips/oob_management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Out-of-Band (OOB) Management

Many servers expose a dedicated **out-of-band management controller** — iDRAC, iLO, BMC, IPMI, CIMC, and similar. LibreNMS usually polls that controller as its **own device**, separate from the host it lives in. NetBox models the same relationship differently: the controller is not a separate Device — its address is the host Device's **OOB IP** (`oob_ip`).

This plugin bridges the two models. During import it detects when an incoming LibreNMS device is really the OOB side of a host you already have, and offers the right action to reconcile them instead of creating a duplicate device.

## How the link is stored

OOB linkage is recorded in the `librenms_id` [custom field](custom_field.md) alongside the host's own LibreNMS ID. The per-server value is promoted from a bare integer to a small object:

```json
{
"production": {
"id": 42,
"oob": { "id": 99, "type": "drac" }
}
}
```

- `id` — the LibreNMS device ID of the **host**.
- `oob.id` — the LibreNMS device ID of the **OOB controller**.
- `oob.type` — a short label for the controller (`drac`, `ilo`, `bmc`, `ipmi`, `cimc`, …), or the generic `oob` when the specific type can't be determined.

Only these identity essentials are stored. The controller's IP and firmware version are intentionally **not** persisted here — the IP's source of truth is the host Device's interface-assigned `oob_ip`, and the version lives in LibreNMS and can be read back any time from `oob.id`.

## OOB detection during import

When a searched LibreNMS device looks like an OOB controller (by its OS/hardware strings, e.g. an iDRAC) and matches an existing NetBox device, the validation details show an **OOB Detected** panel instead of a plain import button. From there one of three resolution flows is offered, depending on what already exists.

### Add as OOB

Use when the existing NetBox device is the **host** and the incoming LibreNMS device is its OOB controller.

The **Add as OOB to *device*** action links the controller's LibreNMS ID into the host's `oob.id` slot. NetBox requires `oob_ip` to be assigned to one of the device's interfaces, so the form includes an **OOB IP interface** picker:

- A sensible interface is **pre-selected** (matched by name — `idrac`/`ilo`/`bmc`-style). Because the OOB IP is frequently *not* physically on that interface, the selection is **overridable**.
- Choose **+ Create new interface…** to create one (default name suggested) to hang the OOB IP on.

The OOB IP is then created (or re-homed) assigned to the chosen interface and set as the device's `oob_ip`. If you make no interface selection, the link is still recorded and the OOB IP is left for you to set later.

!!! note "Permissions"
Setting the OOB IP can create an Interface, create an IPAddress, or re-home an existing one. The action requires the matching NetBox `add`/`change` permissions for those models; if you lack them the link is still recorded and the IP step is skipped with a warning. See [Permissions & Access](permissions.md).

### Promote to host

Use when the existing NetBox device is currently linked to the **OOB controller** (its `librenms_id` points at the controller) and the incoming LibreNMS device is the **host** side.

**Promote to host of *device*** re-points the linkage: the incoming host's LibreNMS ID becomes the device's `id`, and the previously-linked controller ID is demoted into the `oob` slot. No new device is created. A pre-promote modal lets you optionally override the device's **name**, **device type**, and **platform** — all default to **Keep current**, so the original promote behaviour is unchanged unless you explicitly choose **Use new**.

### Merge NetBox devices

Use when **two different NetBox devices** turn out to represent one physical box — typically one created from the LibreNMS hostname and another from the chassis serial, where at least one already carries a LibreNMS link.

The validation modal lists both candidates (hostname-matched and serial-matched) with their current linkage, and you pick which one to **keep** (the *winner*) and which to absorb (the *donor*). Merging consolidates the donor's LibreNMS link state under the active server key into the winner, clears the donor's active link, and writes a `_migrated_to` marker on the donor pointing at the winner. Interfaces, cables, and primary/OOB IPs are **not** moved automatically — you re-home those incrementally (see below).

## Migrating a donor device after a merge

A donor device (one with a `_migrated_to` marker) shows a banner on its LibreNMS sync page with **Move to winner** actions, so you can move resources over at your own pace:

- **Move interface to winner** — reassigns an interface (and the cables, IPs, and MACs that hang off it) to the winner. Fails if the winner already has an interface with the same name — rename or remove that one first.
- **Move IP address to winner** — re-homes an interface-assigned IP to the winner's same-named interface (move the interface first if it doesn't exist on the winner yet).
- **Transfer primary IPv4 / IPv6 / OOB IP** — points the winner's `primary_ip4` / `primary_ip6` / `oob_ip` foreign key at the donor's value and clears it on the donor. Refuses to overwrite a value already set on the winner — clear it there first.

Each action runs under a row lock and verifies the `_migrated_to` marker before touching anything. Once the donor has nothing left to migrate you can delete it.

## Setting Primary and OOB IPs in general

Outside the OOB import flows, both `primary_ip` and `oob_ip` are driven from interface-assigned addresses:

- **Primary IP** is set on the device's **IP Addresses** sync tab: with **Set Primary IP** enabled, a synced IP that matches the LibreNMS management IP and is interface-assigned becomes the device's primary.
- **OOB IP** is set through the **Add as OOB** flow above.

This keeps every IP relationship valid against NetBox's requirement that primary/OOB IPs be assigned to one of the device's own interfaces.

## See also

- [Custom Field Setup](custom_field.md) — the `librenms_id` field that stores the linkage.
- [Validation & Configuration](../librenms_import/validation.md) — where OOB is detected during import.
- [Permissions & Access](permissions.md) — permissions required for the OOB/IP actions.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ nav:
- Background Jobs & Caching: librenms_import/background_jobs.md
- Sync & Configuration:
- Virtual Chassis: usage_tips/virtual_chassis.md
- Out-of-Band Management: usage_tips/oob_management.md
- Interface Mappings: usage_tips/interface_mappings.md
- Module Sync: usage_tips/module_sync.md
- Mapping Rules: usage_tips/mapping_rules.md
Expand Down
36 changes: 36 additions & 0 deletions netbox_librenms_plugin/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
import re

# Plugin permissions (from LibreNMSSettings model)
PERM_VIEW_PLUGIN = "netbox_librenms_plugin.view_librenmssettings"
PERM_CHANGE_PLUGIN = "netbox_librenms_plugin.change_librenmssettings"

# LibreNMS VLAN state values
LIBRENMS_VLAN_STATE_ACTIVE = 1

# OOB management controller detection
# Trailing \d*\b restricts matches to whole tokens (optionally with a numeric suffix like
# iDRAC9 / drac9) so a prefix collision inside an unrelated word — e.g. "dracut", "ipmitool"
# — can't misclassify a normal device as an OOB controller.
OOB_TYPE_PATTERN = re.compile(r"\b(idrac|ilo|ipmi|bmc|drac|cimc|oob)\d*\b", re.IGNORECASE)
OOB_TYPES = ("idrac", "ilo", "ipmi", "bmc", "drac", "cimc", "oob")


def normalize_oob_type(os_str: str, hardware_str: str = "") -> str | None:
"""
Extract and normalize the OOB controller type from LibreNMS os/hardware strings.

Returns the canonical lowercase token (one of OOB_TYPES) or None if no match.

A vendor-specific match (idrac/ilo/ipmi/bmc/drac/cimc) always wins over the
generic ``oob`` token, even when ``oob`` appears earlier in the text, so e.g.
``normalize_oob_type("oob", "iDRAC9")`` resolves to ``"idrac"`` rather than
being masked by the generic token.

Examples:
normalize_oob_type("drac9", "iDRAC9") → "drac"
normalize_oob_type("oob", "iDRAC9") → "idrac"
normalize_oob_type("ilo", "") → "ilo"
normalize_oob_type("ubuntu", "") → None
"""
generic = None
for text in (os_str or "", hardware_str or ""):
for m in OOB_TYPE_PATTERN.finditer(text):
token = m.group(1).lower()
if token != "oob":
return token # vendor-specific match wins immediately
generic = generic or "oob" # remember the generic fallback, keep scanning
return generic
1 change: 1 addition & 0 deletions netbox_librenms_plugin/import_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import_single_device,
validate_device_for_import,
)
from .collisions import detect_bulk_collisions # noqa: F401
from .filters import ( # noqa: F401
_apply_client_filters,
get_device_count_for_filters,
Expand Down
Loading