Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
190 commits
Select commit Hold shift + click to select a range
7f6365c
feat(oob-sync): OOB-controller sync + shared device-import base
marcinpsk Jun 18, 2026
074650a
test(oob-sync): rename OOB-sentinel test to its unit under test
marcinpsk Jun 18, 2026
120dba6
fix(oob-sync): address CodeRabbit #113 findings (family guard, OOB-IP…
marcinpsk Jun 18, 2026
ed2e1c9
refactor(oob-sync): centralize librenms_id coercion + extract serial-…
marcinpsk Jun 18, 2026
56bd201
test(oob-sync): convert TestOOBDetection to real-DB Device rows
marcinpsk Jun 18, 2026
ee6d645
test(oob-sync): convert TestValidateDeviceForImport to real-DB rows
marcinpsk Jun 18, 2026
0ae63ae
test(oob-sync): convert validate_device_for_import edge-case classes …
marcinpsk Jun 18, 2026
dc426ba
test(oob-sync): convert DeviceConflictActionView core + link tests to…
marcinpsk Jun 18, 2026
ebe4c8a
test(oob-sync): convert DeviceConflict update + sync_serial actions t…
marcinpsk Jun 18, 2026
5ef96c9
test(oob-sync): real-DB migrate action + drive fetch via API boundary
marcinpsk Jun 18, 2026
755dbd8
test(oob-sync): real-DB import_single_device (incl. the missing creat…
marcinpsk Jun 18, 2026
260916e
test(oob-sync): real-DB device field-update views (name/serial/type/p…
marcinpsk Jun 18, 2026
e7ac28e
test(oob-sync): real-DB for librenms_id CF-mutation views (remove-map…
marcinpsk Jun 18, 2026
94dadcb
test(oob-sync): real-DB CreateAndAssignPlatform + AssignVCSerial pers…
marcinpsk Jun 18, 2026
c522699
test(sync-interfaces): real-DB interface attribute sync + creation
marcinpsk Jun 18, 2026
e247d60
test(sync-views): real-DB VLAN + IP sync persistence
marcinpsk Jun 18, 2026
89ed8bb
test(sync-views): real-DB Cable creation in SyncCablesView success path
marcinpsk Jun 18, 2026
9fb490f
test(base/sync-views): convert ORM-touching tests to real DB
marcinpsk Jun 18, 2026
b820d34
test(librenms-id): convert librenms_id helper tests to real DB
marcinpsk Jun 18, 2026
0d17277
test(base-views2): convert cable/IP-view ORM tests to real DB
marcinpsk Jun 18, 2026
86b39b0
test(utils): convert get_librenms_sync_device VC-selection tests to r…
marcinpsk Jun 18, 2026
b5d36d6
test(sync-modules): convert _install_single Module-creation tests to …
marcinpsk Jun 18, 2026
2b487e7
test(modules-view): convert bay-matching algorithm tests to real DB
marcinpsk Jun 18, 2026
bfa8664
test(oob-sync): real-DB rollback assertion for failed OOB attach + ti…
marcinpsk Jun 19, 2026
c140738
fix(oob-sync): readable "Two NetBox devices" merge badge (text-dark)
marcinpsk Jun 19, 2026
78d158d
fix(oob-sync): readable Conflict/Platform badges + project-wide badge…
marcinpsk Jun 19, 2026
e71c0a8
test(import): regression for stale importable state cleared on merge
marcinpsk Jun 19, 2026
40d1200
fix(cables): name fallback matches either ifName or ifDescr
marcinpsk Jun 19, 2026
b72e610
fix(import): correct OOB-already-linked action + keep merge rows blocked
marcinpsk Jun 19, 2026
97c85c9
fix(cables): dual-name fallback in verify flow + OOB-already-linked b…
marcinpsk Jun 19, 2026
cbe1908
fix(cables/utils): capture message-only fetch errors, guard VC enumer…
marcinpsk Jun 20, 2026
24cabec
fix(templates): accurate link-state text, migrated move warning, chec…
marcinpsk Jun 20, 2026
d68e074
fix(device-status/tests): require valid paired OOB id; real iterable …
marcinpsk Jun 20, 2026
879d20d
fix(import): fail closed when a duplicate hostname/serial match is no…
marcinpsk Jun 20, 2026
aa77418
fix(tests/js): real VLAN redirect route, path-sensitive dup tests, ne…
marcinpsk Jun 20, 2026
6525c64
fix(ip/templates): drop ambiguous librenms_id; hide sync-only control…
marcinpsk Jun 20, 2026
94c4cee
docs(tests): condense pr/oob-sync test docstrings to the one-line con…
marcinpsk Jun 20, 2026
93f0048
docs: align pr/oob-sync production docstrings to Google-style (batch 1)
marcinpsk Jun 20, 2026
9f9e501
docs: align production docstrings to Google-style (batch 2)
marcinpsk Jun 20, 2026
ddd3484
docs: align production docstrings to Google-style (batch 3)
marcinpsk Jun 20, 2026
15f5eb7
docs: align production docstrings to Google-style (batch 4)
marcinpsk Jun 20, 2026
edf9fb3
fix(import): run duplicate-hostname fail-closed outside the serial-on…
marcinpsk Jun 20, 2026
03068f0
fix(modules): normalize main inventory indices to int before caching
marcinpsk Jun 20, 2026
a080174
fix(modules): never install OOB-controller inventory onto the host
marcinpsk Jun 20, 2026
0fb1561
fix(import): lock librenms-id conflict check + type-check OOB-IP assi…
marcinpsk Jun 20, 2026
677cd06
test(tables): assert the generic fallback button state for a malforme…
marcinpsk Jun 20, 2026
3b24097
fix(import): block link actions on non-unique hostname/serial matches
marcinpsk Jun 20, 2026
93ab9a2
test(oob-sync): pin the open-redirect barrier input in the failure-re…
marcinpsk Jun 20, 2026
d5f33a6
refactor(oob-sync): consistent server_key var in interfaces/modules r…
marcinpsk Jun 21, 2026
c5ccc50
fix(interfaces): treat non-string ifPhysAddress as absent in shared-L…
marcinpsk Jun 21, 2026
7756dd2
fix(import): detect OOB candidate from oob_ip match without an os typ…
marcinpsk Jun 21, 2026
efd111a
fix(import): restore oob_ip fallback in refresh-time IP matching
marcinpsk Jun 21, 2026
13bc610
fix(ip-sync): fail closed on unhashable port_id in fresh-refresh row …
marcinpsk Jun 21, 2026
a41a47f
fix(interfaces): ignore placeholder MACs in shared-LOM dedup; keep OO…
marcinpsk Jun 21, 2026
90d174f
test(import): exercise save() IntegrityError path; complete truncated…
marcinpsk Jun 21, 2026
c7152e1
test(interfaces): assert OOB-id omission on the message string, not t…
marcinpsk Jun 21, 2026
64ce758
fix(import): match the full duplicate-net_host IP set in OOB-IP detec…
marcinpsk Jun 21, 2026
1533946
fix(interfaces): fail closed on a malformed cached port snapshot
marcinpsk Jun 21, 2026
72a4f8f
fix(cables): don't cache a partial cable snapshot after a host/OOB fe…
marcinpsk Jun 21, 2026
1a94074
fix(import): scan every matching IP for an interface assignment, not …
marcinpsk Jun 22, 2026
cf0d496
fix(import): scan every duplicate net_host row for an assignment in v…
marcinpsk Jun 22, 2026
01ae697
fix(cables): skip host LibreNMS fetches for OOB-only devices (no host…
marcinpsk Jun 22, 2026
5cc13a2
fix(import): exact cross-model librenms_id match wins over name fallback
marcinpsk Jun 22, 2026
b201f2c
test(cables): key OOB-only get_links_data test on device id, not call…
marcinpsk Jun 22, 2026
b27e5e8
fix(import): fail closed when a primary IP resolves to multiple NetBo…
marcinpsk Jun 22, 2026
bf291c2
fix(import): fail closed on serial/IP refresh ambiguity; harden 3 tests
marcinpsk Jun 22, 2026
b312fe0
fix(oob-sync): no-host guard before ports cache; clear stale serial/I…
marcinpsk Jun 23, 2026
43ef2cc
fix(ipam): purge the stale IP cache before failing a malformed refresh
marcinpsk Jun 23, 2026
7363b56
fix(cables): never sync OOB-controller rows onto the host device
marcinpsk Jun 23, 2026
1deec88
fix(cables): exclude the OOB-controller path when resolving a remote …
marcinpsk Jun 23, 2026
3e1a748
refactor(oob): read the OOB sub-object via get_librenms_oob accessor
marcinpsk Jun 23, 2026
668c740
refactor(cables): hoist the raw-link key set to one _RAW_LINK_KEYS co…
marcinpsk Jun 23, 2026
30118dc
refactor(import): extract resolve_device_by_host_ip shared by both IP…
marcinpsk Jun 23, 2026
4ac8eab
fix(import): clear hostname/serial ambiguity blocker on refresh too
marcinpsk Jun 23, 2026
a587c75
fix(import): make primary-IP collisions a clearable terminal ambiguity
marcinpsk Jun 23, 2026
c4449af
fix(cables): drop the stale links cache on a partial fresh fetch
marcinpsk Jun 23, 2026
c150c3e
fix(interfaces): coerce the resolved librenms_id before trusting it
marcinpsk Jun 24, 2026
2719292
test(librenms-api): pin legacy missing-config ValueError message
marcinpsk Jun 24, 2026
956ec27
fix(oob-sync): keep context-only OOB rows non-actionable in cable status
marcinpsk Jun 24, 2026
da51e61
fix(oob-sync): address code-review correctness findings
marcinpsk Jun 24, 2026
8eeebf4
fix(oob-sync): review round 2 — perms, OOB-mapping visibility, query/…
marcinpsk Jun 24, 2026
dea8a6f
refactor(modules): drop phantom string-index normalization
marcinpsk Jun 24, 2026
b19bd0c
fix(views): scope interfaces/cables/VLAN GET render to the request se…
marcinpsk Jun 24, 2026
52df16d
fix(views): render empty for an unresolved GET server_key, not defaul…
marcinpsk Jun 24, 2026
c6d9a3b
fix(imports): conflict on an OOB IP already held by another device's FK
marcinpsk Jun 24, 2026
203725a
fix(imports): validate platform/device_type manufacturer on update_fi…
marcinpsk Jun 24, 2026
dcbc47d
fix(import): don't treat a missing incoming device_id as "linked else…
marcinpsk Jun 24, 2026
c63238e
fix(tables): don't render a Conflict button for an OOB-already-linked…
marcinpsk Jun 24, 2026
2f58a75
fix(ip): resolve mgmt_ip on a pre-upgrade cached snapshot
marcinpsk Jun 24, 2026
b048e47
fix(imports): reject non-positive host ids in _build_id_server_info
marcinpsk Jun 24, 2026
e7b6c53
perf(imports): dedup interface + duplicate-detection queries
marcinpsk Jun 24, 2026
ca33998
fix(imports): surface OOB-only links in the import-action server list
marcinpsk Jun 24, 2026
f2642a3
test: update mock fixtures for the review-fix behaviour changes
marcinpsk Jun 24, 2026
3d7574f
fix(views): scope IP sync tab GET render to request server_key
marcinpsk Jun 25, 2026
e2253bd
fix(cables): guard cache.ttl() for non-Redis backends
marcinpsk Jun 25, 2026
f90cb32
perf(bulk-import): drop redundant find_by_librenms_id re-query in ref…
marcinpsk Jun 25, 2026
eae1c99
perf(import): one-query mapping ambiguity check; real superuser test …
marcinpsk Jun 25, 2026
e3782f2
fix(sync): don't rebuild the default client in _sync_url after fail-c…
marcinpsk Jun 25, 2026
2b6945f
refactor(import): extract fail-closed rebind into _rebind_or_htmx_err…
marcinpsk Jun 25, 2026
57f43b9
test(redirects): exercise real redirect builders instead of grepping …
marcinpsk Jun 25, 2026
8476c25
test(import): bind Full Sync Page server_key assertion to its own anchor
marcinpsk Jun 25, 2026
031599a
fix(oob-sync): scope GET render without re-entering the lazy API prop…
marcinpsk Jun 25, 2026
6ac9d7e
fix(ip): fail closed on a malformed cached IP snapshot
marcinpsk Jun 25, 2026
ebccac3
fix(ip): validate nested cached IP snapshot fields before enrichment
marcinpsk Jun 26, 2026
57744bb
fix(cables): coerce the resolved librenms_id before fetching links
marcinpsk Jun 26, 2026
9dd3da8
fix(import): clear the provisional match when a row becomes ambiguous
marcinpsk Jun 26, 2026
f993ff7
test(base-views): align cable/ip cached-view mocks with the new coerc…
marcinpsk Jun 26, 2026
df309b3
fix(ip): coerce librenms_id before the device-ip fetch
marcinpsk Jun 29, 2026
c762bc6
fix(cables): normalize a missing local-port name in the linked render…
marcinpsk Jun 29, 2026
a91208a
fix(import): keep hostname/serial ambiguity terminal before the prima…
marcinpsk Jun 29, 2026
10602e8
fix(import): block create on terminal hostname/serial ambiguity in im…
marcinpsk Jun 29, 2026
da8f09f
fix(import): treat serial-match + new hostname with no OOB signal as …
marcinpsk Jun 29, 2026
729a11a
fix(oob): coerce the OOB controller id before fetching links/ports
marcinpsk Jun 29, 2026
b1a58f2
fix(import): scope the validation-details modal to the import's server
marcinpsk Jun 29, 2026
a1a7c47
fix(modules): key the GET cache read on the resolved scoped server, n…
marcinpsk Jun 29, 2026
5e168a9
fix(cables): don't render the literal "None" local-port label in the …
marcinpsk Jun 29, 2026
269a1a4
fix(import): pre-select cimc-named OOB interfaces; derive suggester f…
marcinpsk Jun 29, 2026
860ad74
fix(sync): rebind the sync-page header to ?server_key so it matches t…
marcinpsk Jun 29, 2026
aa98c4a
perf(import): prefetch assigned_object in resolve_device_by_host_ip
marcinpsk Jun 29, 2026
72fa01c
test(import): set _librenms_api in TestGetTable for the server_key-sc…
marcinpsk Jun 29, 2026
56f01f6
refactor(cables): extract get_links_data helpers to clear C901 (ours,…
marcinpsk Jun 29, 2026
3b41a08
test(oob): de-mock TestMissingOOBIpPermissions to exercise the real n…
marcinpsk Jun 29, 2026
359761f
test(import): drop the mock-ORM serial-reinstall duplicate for the re…
marcinpsk Jun 29, 2026
2c9fdb9
refactor(librenms-id): single source of truth for the librenms_id JSO…
marcinpsk Jun 29, 2026
3070d60
refactor(views): share one redirect_with_server_key helper for the sy…
marcinpsk Jun 29, 2026
f610b21
fix(import): fail closed when a GET ?server_key no longer resolves
marcinpsk Jun 29, 2026
df48b9d
fix(import): stage same-name OOB controllers as oob_candidate
marcinpsk Jun 29, 2026
d18a460
fix(interfaces): mark snapshot oob_incomplete on a corrupt OOB id
marcinpsk Jun 29, 2026
01d970b
test(cables): assert no device-scoped call for an invalid OOB-only id
marcinpsk Jun 29, 2026
91037cb
test(actions): use a real request in DeviceValidationDetailsView GET …
marcinpsk Jun 30, 2026
8ffe0dc
fix(import): name management-IP ambiguity in the import-blocked error
marcinpsk Jun 30, 2026
7c97dce
fix(sync): align VC/cross-device interface resolution with the render…
marcinpsk Jun 30, 2026
22fdb0c
fix(interfaces): never match an OOB-controller row against a host int…
marcinpsk Jun 30, 2026
c6bc008
fix(modules): coerce the OOB id and fail closed on non-int OOB indices
marcinpsk Jun 30, 2026
b0f2e7e
fix(import): anchor the OOB interface-name suggester on word boundaries
marcinpsk Jun 30, 2026
c2745be
refactor(import): centralize the LibreNMS link-note phrasing in one h…
marcinpsk Jun 30, 2026
5f5c21e
fix(cables): give get_ports_data a defensive librenms_id default
marcinpsk Jun 30, 2026
b8e158c
test(sync): update VC shared-name maps assertion for viewed-member-wins
marcinpsk Jun 30, 2026
73f4954
refactor(sync): use coerce_librenms_id in _build_all_server_mappings
marcinpsk Jun 30, 2026
7ed4818
test(modules): make OOB-id post() tests robust to the render_sync_par…
marcinpsk Jun 30, 2026
cee5723
fix(oob-sync): resolve max-effort review findings
marcinpsk Jun 30, 2026
011e082
fix(import): fail open on a cross-model name match in the refresh re-…
marcinpsk Jun 30, 2026
0231609
test(cables): make the remote-id resolution test independent of the h…
marcinpsk Jun 30, 2026
c50b7f0
fix(oob-sync): route SaveVlanGroupOverrides ttl through cache_remaini…
marcinpsk Jul 1, 2026
c30cfd8
fix(oob-sync): scope module verify port_id to the active server
marcinpsk Jul 1, 2026
e7fc148
fix(oob-sync): sync IP row by name when its port id is ambiguous
marcinpsk Jul 1, 2026
1b15954
fix(oob-sync): compare existing OOB IP with same_host, not raw !=
marcinpsk Jul 1, 2026
09e5266
fix(oob-sync): use is_legacy_librenms_id in migrate action, not inlin…
marcinpsk Jul 1, 2026
8bd81be
refactor(oob-sync): delegate ConvertLegacyLibreNMSId redirect to shar…
marcinpsk Jul 1, 2026
40b9d0e
test(oob-sync): construct LibreNMSAPI with explicit key in scope test
marcinpsk Jul 1, 2026
ee2accd
fix(oob-sync): per-server scoping + rack-fit hardening from max review
marcinpsk Jul 1, 2026
2ccc6c8
fix(oob-sync): fail closed on invalid librenms_id predicates and surf…
marcinpsk Jul 1, 2026
89fea3f
fix(oob-sync): make forward-declared-URL guards robust when rebased u…
marcinpsk Jul 1, 2026
d810fdf
refactor(migrate): extract shared move-to-winner button include; asse…
marcinpsk Jul 1, 2026
18e7846
fix(oob-ip): scope OOB IP lookup to the global table (vrf__isnull)
marcinpsk Jul 2, 2026
9470a07
fix(multi-server): carry server_key on every sync-page POST form
marcinpsk Jul 2, 2026
e450f4f
fix(vc-sync): resolve the VC sync device unconditionally in get_cache…
marcinpsk Jul 2, 2026
2c069a3
fix(import-actions): mirror Device.clean()'s 0U/child rack rules on u…
marcinpsk Jul 2, 2026
d4bf9c7
fix(import): pin Role/Cluster/Rack row updates to the import page's s…
marcinpsk Jul 2, 2026
f97a553
fix(modules): don't install into an arbitrarily-picked bay on duplica…
marcinpsk Jul 2, 2026
7b1356a
fix(ip-sync): allow the primary IP on a sibling VC member's interface
marcinpsk Jul 2, 2026
73f7c2a
fix(interfaces-table): keep OOB rows unmatched on row-level re-render
marcinpsk Jul 2, 2026
fe73a67
fix(modules): fail closed on a corrupt stored OOB controller id
marcinpsk Jul 2, 2026
73d5768
fix(cables,vlans): honor the unresolved server flag; scope cable pagi…
marcinpsk Jul 2, 2026
7020804
fix(sync-view): don't re-enter the lazy client after a failed default…
marcinpsk Jul 2, 2026
0cbe0ea
fix(import-modal): actionable fallback Host pane for promote_to_host …
marcinpsk Jul 2, 2026
575a756
fix(interfaces): render the fragment (not a redirect) on a failed rebind
marcinpsk Jul 2, 2026
00840da
fix(oob-attach): re-verify the legacy-ID gate on the locked row (TOCTOU)
marcinpsk Jul 2, 2026
425764c
fix(bulk-import): tell HTMX users about the background→synchronous fa…
marcinpsk Jul 2, 2026
a4bcaa1
test(sync): align remaining tests and template comments with the rebi…
marcinpsk Jul 2, 2026
5c27706
test: make the fallback-pane and failed-rebind tests restack-robust
marcinpsk Jul 2, 2026
0cf5f76
fix(sync-view): scope the degraded render to the blank/absent-key case
marcinpsk Jul 2, 2026
75c74e7
fix(import): re-attach construction-time sorting stranded by the hx-v…
marcinpsk Jul 2, 2026
86b46c3
test(validation-details): drop the vacuous 'action' fallback assertion
marcinpsk Jul 2, 2026
dc96e44
refactor(templates): share the sync-page hidden server_key input via …
marcinpsk Jul 2, 2026
8707577
test(interfaces): rebuild get_context_data coverage on real DB objects
marcinpsk Jul 2, 2026
2dc657f
fix(librenms-api): don't offer or auto-select unusable server configs
marcinpsk Jul 3, 2026
78b8756
fix(sync): scope VC sync-device resolution, fail-closed VC status, de…
marcinpsk Jul 3, 2026
d019d7d
fix(oob): store OOB link on the VC sync device, not the matched member
marcinpsk Jul 3, 2026
340a4d0
test(interfaces): extract shared move-URL reverse patch helper
marcinpsk Jul 3, 2026
6218883
refactor(oob-sync): address CodeRabbit review findings
marcinpsk Jul 3, 2026
2fc8ab9
test(oob-sync): dissolve the review-fix grab-bag files into their top…
marcinpsk Jul 3, 2026
5062337
fix(import): surface cross-model name-collision warning even without …
marcinpsk Jul 4, 2026
fe78d87
refactor(import): share the partial-validation issue prefix as a cons…
marcinpsk Jul 4, 2026
e8996c4
test(import): pin the both-models-match row as importable-as-new (not…
marcinpsk Jul 4, 2026
cb553d5
fix(sync): escape VC member names in the interface table; guard modul…
marcinpsk Jul 4, 2026
852862d
fix(rebase): reconcile pr/oob-sync onto new develop (two-oracle union)
marcinpsk Jul 16, 2026
e6e33ad
style: ruff format 4 files left unformatted by the rebase
marcinpsk Jul 16, 2026
8cdda12
docs(oob): add screenshots of the OOB Detected import panel and inter…
marcinpsk Jul 17, 2026
1c3c6e1
fix(modules): restore resolve_posted_server_key on all six module ser…
marcinpsk Jul 19, 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
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
Binary file added docs/img/oob/oob-create-new-interface.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/oob/oob-detected-validation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
86 changes: 86 additions & 0 deletions docs/usage_tips/oob_management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# 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 (`idrac`, `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.

![OOB Detected validation panel, showing the OOB attach effect and the Add-as-OOB action](../img/oob/oob-detected-validation.png)

!!! tip "Not seeing the panel?"
The panel only appears when **both** conditions hold: the incoming LibreNMS device's `os`/`hardware` (or hostname) matches an OOB pattern (`idrac`, `ilo`, `ipmi`, `bmc`, `drac`, `cimc`), **and** it matches an existing NetBox device by unique **serial** or by **management IP**. If the incoming hostname already matches a NetBox device name it takes the plain hostname-match path instead, and if the device is already linked to LibreNMS no OOB action is offered. (Device identifiers are blurred in these screenshots.)

### 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.

![OOB IP interface picker with "+ Create new interface" selected and a suggested name](../img/oob/oob-create-new-interface.png)

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
47 changes: 47 additions & 0 deletions netbox_librenms_plugin/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,53 @@
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")

# Shared "From OOB controller" badge markup (the bare <span>; callers add any leading space).
# Centralised so a restyle (color/title/text) happens in one place instead of drifting across the
# cable/module/interface tables and the cable-verify render that each hand-copied it.
OOB_BADGE_HTML = '<span class="badge bg-purple text-white ms-1" title="From OOB controller">OOB</span>'


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

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.

Args:
os_str (str): LibreNMS ``os`` field for the device.
hardware_str (str): LibreNMS ``hardware`` field for the device.

Returns:
str | None: The canonical lowercase token (one of OOB_TYPES), or None if
no token matches.

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
Loading