feat: multi-server librenms_id — track device origin per LibreNMS server - #20
feat: multi-server librenms_id — track device origin per LibreNMS server
#20marcinpsk wants to merge 19 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (3)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds per-server (server_key) LibreNMS ID handling and migration; threads server_key through API, utils, views, cache keys, import/sync flows, templates, and JS; switches cable/interface identifiers to Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client as Client (browser)
participant View as Sync/View
participant API as LibreNMSAPI
participant Utils as Utils
participant Cache as Cache
participant DB as NetBoxDB
Client->>View: GET/POST (may include server_key)
View->>API: ensure LibreNMSAPI instance (server_key)
View->>Cache: get_cache_key(obj, data_type, server_key)
alt cache miss
View->>API: fetch remote data
API-->>View: remote data
View->>Utils: find_by_librenms_id(..., server_key)
Utils->>DB: select_for_update / set_librenms_device_id(...)
DB-->>Utils: ack
View->>Cache: set(cache_key(server_key), data)
else cache hit
Cache-->>View: cached data
end
View->>Client: render UI (per-server mappings, migrate/remove actions)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (1)
604-645: 🛠️ Refactor suggestion | 🟠 MajorUse the standard sync wrapper/content templates for the new Modules tab.
This adds a new sync resource, but it is wired through
inc/_module_sync.htmlinstead of the usual_module_sync.html/_module_sync_content.htmlpair the other tabs follow. Keeping Modules outside that structure will make its refresh flow drift from the rest of the sync UI.As per coding guidelines: Each sync resource has two templates following a naming convention:
_<resource>_sync.html(the tab wrapper, loaded once when the tab is selected) and_<resource>_sync_content.html(the HTMX-swappable inner fragment, refreshed on data changes without a full page reload). When adding a new sync resource, create both the wrapper and content templates following this pattern.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html` around lines 604 - 645, The Modules tab is using the nonstandard inc/_module_sync.html include (id="modules", modules-tab), breaking the shared sync wrapper/content pattern; create a proper wrapper template named _module_sync.html that matches the other tabs (contains the tab-pane wrapper with data-tab-id="modules" and includes the swappable fragment _module_sync_content.html), replace the current include of inc/_module_sync.html with {% include 'netbox_librenms_plugin/_module_sync.html' %}, and ensure you add the inner fragment _module_sync_content.html to be used for HTMX refreshes so Modules follows the same refresh flow as interfaces/cables/ipaddresses.netbox_librenms_plugin/views/sync/interfaces.py (1)
101-110:⚠️ Potential issue | 🔴 CriticalUse the server-aware cache key for ports.
get_cached_ports_data()still reads the unsuffixedportscache entry. In a multi-server setup this can either miss the freshly fetched cache for the active server or, worse, pull another server's port list into this sync run. Passself.librenms_api.server_keyintoget_cache_key()here.🔧 Minimal fix
def get_cached_ports_data(self, request, obj): """Return cached LibreNMS port data for the given object.""" - cached_data = cache.get(self.get_cache_key(obj, "ports")) + cached_data = cache.get(self.get_cache_key(obj, "ports", self.librenms_api.server_key)) if not cached_data: messages.warning( request,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/interfaces.py` around lines 101 - 110, get_cached_ports_data currently retrieves the unsuffixed "ports" cache entry which can return stale or cross-server data; change the cache lookup to use the server-aware cache key by passing self.librenms_api.server_key into get_cache_key (i.e., call self.get_cache_key(obj, "ports", self.librenms_api.server_key)) so the method reads the per-server cached ports list and returns cached_data.get("ports", []) as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/forms.py`:
- Around line 584-585: The cache key write was changed to include the server_key
(cache_key = f"librenms_locations_choices:{api.server_key}") while the reader in
import_utils.py still expects the legacy "librenms_locations_choices" key;
update one side so both match. Fix by either (A) having the writer also populate
the legacy key in addition to the namespaced key, or (B) change the reader in
import_utils.py to look up f"librenms_locations_choices:{api.server_key}" (using
the same LibreNMSAPI.server_key) when resolving location IDs to names; ensure
the code paths around LibreNMSAPI and the cache lookup use the identical key
format.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 379-385: The template expects a root-level existing_id_servers but
DeviceValidationDetailsView.get() (in
netbox_librenms_plugin/views/imports/actions.py) does not set it; update
DeviceValidationDetailsView.get() to add existing_id_servers to the template
context by extracting it from the validation data (e.g., existing_id_servers =
validation.get("existing_id_servers") or building the list from validation)
before rendering so the HTMX fragment and view response remain in sync with the
template's loop over existing_id_servers.
In `@netbox_librenms_plugin/tests/test_librenms_id.py`:
- Around line 71-83: The test should verify the actual predicate passed into
mock_model.objects.filter by inspecting mock_model.objects.filter.call_args and
asserting it includes both the legacy-integer branch and the server-key JSON
branch used by find_by_librenms_id; update the assertions in
test_queries_server_key_and_legacy_integer (and the similar block at 107-118) to
check that the filter call arguments contain the expected clauses (e.g. the
legacy id clause and the server-key clause or the combined Q object) rather than
only asserting filter() was invoked.
In `@netbox_librenms_plugin/tests/test_mixins.py`:
- Around line 111-150: Update the tests to cover the new per-server cache suffix
by calling get_cache_key and get_last_fetched_key with the server_key argument
and asserting the returned keys include that server identifier; specifically
modify/add assertions in tests that reference get_cache_key (e.g.,
test_get_cache_key_format, test_get_cache_key_includes_model_name,
test_get_cache_key_different_data_types) to call mixin.get_cache_key(obj,
"ports", server_key="srv1") and assert "srv1" (or the server_key used) appears
in the key, and update test_get_last_fetched_key_format to call
mixin.get_last_fetched_key(obj, "ports", server_key="srv1") and assert the
server_key is included alongside "last_fetched", model_name and pk.
In `@netbox_librenms_plugin/tests/test_sync_devices.py`:
- Around line 142-145: The test currently only checks that 42 appears in the
positional args for view._librenms_api.update_device_field; change this to
assert the exact call payload so the field and value are validated. Replace the
loose checks with a single assertion like
view._librenms_api.update_device_field.assert_called_once_with(42,
expected_field, expected_value) (or construct expected_payload and assert
call_args[0] == (42, expected_payload)), where expected_field/expected_value
match the intended site/field values being synced, then keep
mock_msg.success.assert_called_once() as-is.
In `@netbox_librenms_plugin/tests/test_sync_interfaces.py`:
- Around line 69-70: The test's current check uses iface.method_calls which
won't detect attribute assignment, so change the test to initialize the
interface with a unique sentinel (use
self._make_device_interface(speed=object()) or similar) before calling
view.update_interface_attributes(iface, librenms_data, "1000base-t", {"speed"},
"ifName") and then assert that iface.speed is still the same sentinel after the
call to prove the excluded "speed" field was not mutated.
In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py`:
- Around line 331-416: Replace the hand-rolled plugin configs and MagicMock
NetBox objects in tests that call
BaseLibreNMSSyncView._build_all_server_mappings with the shared fixtures from
tests/conftest.py (e.g. use mock_multi_server_config or mock_legacy_config for
PLUGINS_CONFIG and the provided mock_netbox_device / mock_netbox_vm / related
NetBox object fixtures instead of _make_obj and inline plugins_cfg), and update
the test setup to inject those fixtures (via patching
django_settings.PLUGINS_CONFIG with the fixture values and passing/using the
mock NetBox object fixtures) so the tests rely on the centralized fixtures for
configuration, API client, and objects rather than ad‑hoc mocks.
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 154-163: The code currently fabricates a legacy "default" server
from plugins_cfg top-level keys even when multi-server mode is active; update
the conditional so the legacy fallback only runs when no servers list is
configured (i.e., plugins_cfg.get("servers") is falsy or empty). Concretely,
change the existing check that uses srv_cfg is None and sk == "default" to also
require that plugins_cfg.get("servers") is missing/empty before assigning
srv_cfg from plugins_cfg["librenms_url"]/["display_name"]; leave is_configured
and other logic unchanged.
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 434-443: The code treats a legacy librenms_url as a configured
server even when a "servers" section exists; update the check so
legacy_url_configured is considered true only when a top-level "librenms_url"
exists AND there is no "servers" section (or it's empty). In practice, change
the condition using plugins_cfg, configured_servers and legacy_url_configured so
the branch reads something like: if server_key in configured_servers or
(legacy_url_configured and not configured_servers and server_key == "default"),
referencing the variables plugins_cfg, configured_servers, legacy_url_configured
and server_key to locate where to modify.
- Around line 405-410: RemoveServerMappingView is currently hard-coded to
Device; update it to handle both Device and VirtualMachine so VM librenms_id
mappings can be removed. Modify the required_object_permissions on
RemoveServerMappingView to include the VirtualMachine change permission (in
addition to Device), and change any internal logic that references Device
directly to derive the model dynamically (e.g., from the request payload or URL
param) and call the shared librenms_id helper functions with that model/class
instead of a Device literal; ensure permission checks, lookups and deletion
paths work for both Device and VirtualMachine and import VirtualMachine where
needed.
In `@netbox_librenms_plugin/views/sync/interfaces.py`:
- Around line 235-238: The current code only calls set_librenms_device_id(...)
when "librenms_id" already exists in interface.cf, which skips initializing the
mapping for new interfaces; change the logic in the block that reads
librenms_interface.get("port_id") so that whenever port_id is not None you call
set_librenms_device_id(interface, port_id, self.librenms_api.server_key)
regardless of whether "librenms_id" is present in interface.cf (so the helper
can create or migrate the value for first-time writes).
---
Outside diff comments:
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`:
- Around line 604-645: The Modules tab is using the nonstandard
inc/_module_sync.html include (id="modules", modules-tab), breaking the shared
sync wrapper/content pattern; create a proper wrapper template named
_module_sync.html that matches the other tabs (contains the tab-pane wrapper
with data-tab-id="modules" and includes the swappable fragment
_module_sync_content.html), replace the current include of inc/_module_sync.html
with {% include 'netbox_librenms_plugin/_module_sync.html' %}, and ensure you
add the inner fragment _module_sync_content.html to be used for HTMX refreshes
so Modules follows the same refresh flow as interfaces/cables/ipaddresses.
In `@netbox_librenms_plugin/views/sync/interfaces.py`:
- Around line 101-110: get_cached_ports_data currently retrieves the unsuffixed
"ports" cache entry which can return stale or cross-server data; change the
cache lookup to use the server-aware cache key by passing
self.librenms_api.server_key into get_cache_key (i.e., call
self.get_cache_key(obj, "ports", self.librenms_api.server_key)) so the method
reads the per-server cached ports list and returns cached_data.get("ports", [])
as before.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1e156d47-708b-463d-bc98-e2272a4fe903
📒 Files selected for processing (21)
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/mixins.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/sync/interfaces.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (13)
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Follow frontend conventions for templates and static files defined in
.github/instructions/frontend.instructions.md
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer for table row updates. Table row updates must return<tr hx-swap-oob="true">. AvoidouterHTMLswaps; use OOB (Out-of-Band) or targetedinnerHTMLswaps to keep table layout intact.
Styling assumes Tabler defaults. Removingtable-responsivewrappers was deliberate to prevent dropdown clipping—do not re-add them.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/{templates,static}/**/*.{html,js}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/{templates,static}/**/*.{html,js}: All HTMX requests andfetch()calls must include a CSRF token. The standard pattern isdocument.querySelector('[name=csrfmiddlewaretoken]').value(from a hidden form input). The import JS also usesgetCookie('csrftoken')as a fallback — prefer the hidden input approach for consistency.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers. Buttons target thehtmx-modal-contentelement and JavaScript inlibrenms_import.htmltoggles the wrapper. Do not reintroducedata-bs-toggleor duplicate modal IDs.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
netbox_librenms_plugin/{templates,tables}/**/*.{html,py}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates live in
templates/netbox_librenms_plugin/; reuse/includes underinc/. Sync pages extendlibrenms_sync_base.html. Tables emit HTMX-enabled columns and buttons (tables/*.py), so prefer updating the table renderer in Python rather than templates when changing row actions.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/tables/device_status.py
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/mixins.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
netbox_librenms_plugin/templates/**/htmx/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments live in
templates/netbox_librenms_plugin/htmx/and include:device_import_row.html(individual import row updates),device_validation_details.html(expandable validation details),device_vc_details.html(virtual chassis member details),bulk_import_confirm.html(import confirmation modal content). Keep server responses and HTMX targets in sync when modifying these fragments.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/tables/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
Table classes in
tables/must useToggleColumn(attrs={'input': {'name': 'select'}})for selection, accept contextual parameters in constructors (e.g.,device,interface_name_field,vlan_groups), setself.tabandself.prefixfor multi-table pagination, includedata-*attributes in row attrs, and VLAN columns must userender_vlans()with hidden inputs and JSON data.
Files:
netbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/tables/device_status.py
netbox_librenms_plugin/views/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/**/*.py: Views must follow a three-layer structure: Base views inviews/base/, Object sync views inviews/object_sync/, and Sync action views inviews/sync/with shared mixins fromviews/mixins.py
New views must extend the closest base class and compose mixins fromviews/mixins.py(LibreNMSPermissionMixin,NetBoxObjectPermissionMixin,LibreNMSAPIMixin,CacheMixin,VlanAssignmentMixin)
All views must inheritLibreNMSPermissionMixinfromviews/mixins.pywithpermission_required = PERM_VIEW_PLUGIN
Declarerequired_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request
Use_get_safe_redirect_url(request)to validate referrer URLs in permission checks to prevent open-redirect attacks
Files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/mixins.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
netbox_librenms_plugin/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/sync/**/*.py: Sync POST handlers must callrequire_all_permissions()(not justrequire_write_permission()) and return early if it returns a response; userequire_all_permissions_json()for AJAX/JSON endpoints
Follow sync conventions defined in.github/instructions/sync.instructions.mdfor sync views, base views, tables, and sync JavaScript
Files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/sync/device_fields.py
**/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
Sync action views must follow the pattern: check permissions with
LibreNMSPermissionMixinandNetBoxObjectPermissionMixin, read selected items fromrequest.POST.getlist('select'), load cached data usingCacheMixin.get_cache_key(), apply changes insidetransaction.atomic(), and redirect to the sync tab with?tab=<resource>.
Files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/sync/device_fields.py
netbox_librenms_plugin/static/**/*import*.js
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/static/**/*import*.js: The import page usesModalManagerclass andfilterModalManagerinstance—always use this reference in fetch callbacks, not undefinedmodalInstancevariables.
The import filter form uses fetch withAccept: application/json, text/html—JSON for background jobs, HTML for synchronous mode.
Import page JavaScript (librenms_import.js) is wrapped in an IIFE withwindow.LibreNMSImportInitializedguard to prevent re-initialization during HTMX swaps.
TheModalManagerclass wraps Bootstrap 5 modal show/hide with fallback.
ThepollJobStatus()function polls/api/core/background-tasks/{jobId}/every 2s, updates progress messages, handles cancel button, and redirects on completion.
ThecaptureSelectionState()andrestoreSelectionState()functions preserve checkbox state across HTMX content swaps.
TheinitializeFilterForm()function intercepts form submit, detects JSON response (background job), and starts polling.
CSRF token extraction should usegetCookie('csrftoken')(cookie-based) as the approach for the import page JavaScript.
Files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
netbox_librenms_plugin/static/**/*.js
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/static/**/*.js: Always checkresponse.okbefore processing fetch responses to catch HTTP errors. In catch blocks, showerror.messagefor debugging rather than generic messages.
ThecreateCacheCountdown()function is a generic countdown timer for cache expiration display.
Files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
**/views/object_sync/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
Object sync view methods must create instances of concrete table views, copy the
requestobject, and callget_context_data(). VMs must skip cables and VLANs by returningNonefrom thoseget_*_context()methods.
Files:
netbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.py
**/views/base/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
**/views/base/**/*.py: Base view classes (BaseLibreNMSSyncView,BaseInterfaceTableView,BaseCableTableView,BaseIPAddressTableView,BaseVLANTableView) must implement the data pipeline pattern: fetch data from LibreNMS API, cache results withCacheMixinkeys likelibrenms_{data_type}_{model_name}_{pk}, compare against NetBox objects, and render a django-tables2 table in a partial template.
Base table view classes must implement resource-specific comparison logic: interface matching by name, IP matching by address/mask, VLAN matching by VID+group, and cables by matching remote devices and checking cable status.
VlanAssignmentMixinmust resolve VLAN group scope in order: Rack → Location → Site → SiteGroup → Region → Global, and must provide auto-selection of the most-specific VLAN group and lookup map building for interface and VLAN sync.
Files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
🧠 Learnings (50)
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,tables}/**/*.{html,py} : Templates live in `templates/netbox_librenms_plugin/`; reuse/includes under `inc/`. Sync pages extend `librenms_sync_base.html`. Tables emit HTMX-enabled columns and buttons (`tables/*.py`), so prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/_*_sync{,_content}.html : Each sync resource has two templates following a naming convention: `_<resource>_sync.html` (the tab wrapper, loaded once when the tab is selected) and `_<resource>_sync_content.html` (the HTMX-swappable inner fragment, refreshed on data changes without a full page reload). Current resources: `_interface_sync`, `_cable_sync`, `_ipaddress_sync`, `_vlan_sync`. When adding a new sync resource, create both the wrapper and content templates following this pattern.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*.html : HTMX fragments live in `templates/netbox_librenms_plugin/htmx/` and include: `device_import_row.html` (individual import row updates), `device_validation_details.html` (expandable validation details), `device_vc_details.html` (virtual chassis member details), `bulk_import_confirm.html` (import confirmation modal content). Keep server responses and HTMX targets in sync when modifying these fragments.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Follow sync conventions defined in `.github/instructions/sync.instructions.md` for sync views, base views, tables, and sync JavaScript
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Sync pipelines must follow the pattern: fetch LibreNMS data via `librenms_api.py`, cache with `CacheMixin`, build comparison tables via `tables/*.py`, and render HTMX fragments from `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/librenms_api.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,static}/**/*.{html,js} : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers. Buttons target the `htmx-modal-content` element and JavaScript in `librenms_import.html` toggles the wrapper. Do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript URL and tab state management must implement `initializeTabs()`, `getDeviceIdFromUrl()`, and `setInterfaceNameFieldFromURL()` to maintain browser state and URL synchronization.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/sync/**/*.py : Sync action views must follow the pattern: check permissions with `LibreNMSPermissionMixin` and `NetBoxObjectPermissionMixin`, read selected items from `request.POST.getlist('select')`, load cached data using `CacheMixin.get_cache_key()`, apply changes inside `transaction.atomic()`, and redirect to the sync tab with `?tab=<resource>`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Views must follow a three-layer structure: Base views in `views/base/`, Object sync views in `views/object_sync/`, and Sync action views in `views/sync/` with shared mixins from `views/mixins.py`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/inc/paginator.html : `inc/paginator.html` is a custom paginator that preserves tab state and `interface_name_field` in pagination URLs. Used across all sync tables.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Hook into NetBox Django 5 plugin APIs via `navigation.py`, `urls.py`, and `api/` modules under `netbox_librenms_plugin/`; respect NetBox plugin conventions
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/urls.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py : Use `LibreNMSAPIMixin` to provide lazy-loaded `LibreNMSAPI` instance via `self.librenms_api` property and `get_server_info()` method for template context.
Applied to files:
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`.
Applied to files:
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/tests/test_mixins.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*import*.html : Device import dropdowns rely on TomSelect decorators set up elsewhere. Keep `<select class="device-role-select">` markup stable to preserve JS hook-up.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer for table row updates. Table row updates must return `<tr hx-swap-oob="true">`. Avoid `outerHTML` swaps; use OOB (Out-of-Band) or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `DeviceValidationDetailsView` (GET) must render expandable validation details via `htmx/device_validation_details.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Removing `table-responsive` wrappers was deliberate to prevent dropdown clipping—do not re-add them.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript verification functions must include `handleInterfaceChange()`, `handleCableChange()`, `handleVRFChange()` that POST to single-item verify endpoints to validate resource changes.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_sync_interfaces.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : The `captureSelectionState()` and `restoreSelectionState()` functions preserve checkbox state across HTMX content swaps.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/settings.html : `settings.html` uses a split-form pattern: two separate Django forms (`ServerConfigForm` + `ImportSettingsForm`) sharing one page, differentiated by a hidden `form_type` field (`"server_config"` or `"import_settings"`). The test-connection button is an HTMX POST to `TestLibreNMSConnectionView`, returning an inline alert fragment.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Per-device dropdown update views (`DeviceRoleUpdateView`, `DeviceClusterUpdateView`, `DeviceRackUpdateView`) must apply selection to validation state and return re-rendered row via `render_device_row()`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `BulkImportConfirmView` (POST) must render confirmation modal with selected device list, returning `htmx/bulk_import_confirm.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Reuse fixtures from `tests/conftest.py` instead of creating ad-hoc mocks: Configuration (`mock_multi_server_config`, `mock_legacy_config`), API client (`mock_librenms_api`), NetBox objects (`mock_netbox_device`, `mock_netbox_vm`, `mock_netbox_site`, `mock_netbox_platform`, `mock_netbox_device_type`, `mock_netbox_device_role`, `mock_netbox_cluster`, `mock_netbox_rack`), HTTP responses (`mock_response_factory`, `mock_success_response`, `mock_device_response`, `mock_error_response`, `mock_auth_error_response`), and Import workflow (`sample_librenms_device`, `sample_librenms_device_minimal`, `sample_validation_state`, `sample_validation_state_vm`).
Applied to files:
netbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_mixins.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/librenms_api.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_netbox_librenms_plugin.py : `test_netbox_librenms_plugin.py` is an empty placeholder — do not add tests there.
Applied to files:
netbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/librenms_api.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module.
Applied to files:
netbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_mixins.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances.
Applied to files:
netbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_sync_devices.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Keep REST endpoint server responses and HTMX targets in sync; implement REST endpoints in `views/imports/actions.py` and HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`)
Applied to files:
netbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py : Base view classes (`BaseLibreNMSSyncView`, `BaseInterfaceTableView`, `BaseCableTableView`, `BaseIPAddressTableView`, `BaseVLANTableView`) must implement the data pipeline pattern: fetch data from LibreNMS API, cache results with `CacheMixin` keys like `librenms_{data_type}_{model_name}_{pk}`, compare against NetBox objects, and render a django-tables2 table in a partial template.
Applied to files:
netbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/object_sync/vms.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py : Base table view classes must implement resource-specific comparison logic: interface matching by name, IP matching by address/mask, VLAN matching by VID+group, and cables by matching remote devices and checking cable status.
Applied to files:
netbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/object_sync/devices.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`
Applied to files:
netbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_mixins.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions in `utils.py` (`find_matching_site`, `match_librenms_hardware_to_device_type`, `find_matching_platform`)
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/librenms_api.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/models.py : Coordinate schema changes through Django migrations in `migrations/` directory; update `models.py`, admin, and Pydantic representations accordingly
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/jobs.py : Follow background job conventions defined in `.github/instructions/background-jobs.instructions.md` for `jobs.py`, import views, and import utilities
Applied to files:
netbox_librenms_plugin/utils.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript VLAN modal functions must implement `openVlanDetailModal()`, `verifyVlanInGroup()`, `verifyVlanSyncGroup()` for per-interface VLAN detail editing.
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/object_sync/**/*.py : Object sync view methods must create instances of concrete table views, copy the `request` object, and call `get_context_data()`. VMs must skip cables and VLANs by returning `None` from those `get_*_context()` methods.
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/views.py : API endpoints must use `LibreNMSPluginPermission` class in `api/views.py` where GET checks `view_librenmssettings` and other methods check `change_librenmssettings`
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Sync POST handlers must call `require_all_permissions()` (not just `require_write_permission()`) and return early if it returns a response; use `require_all_permissions_json()` for AJAX/JSON endpoints
Applied to files:
netbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Test business logic via the utility modules (import_utils.py, import_validation_helpers.py, etc.) they call, not via HTTP requests, for views in `views/sync/`, `views/object_sync/`, and `views/imports/actions.py`.
Applied to files:
netbox_librenms_plugin/tests/test_sync_devices.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : The import page uses `ModalManager` class and `filterModalManager` instance—always use this reference in fetch callbacks, not undefined `modalInstance` variables.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : Import page JavaScript (`librenms_import.js`) is wrapped in an IIFE with `window.LibreNMSImportInitialized` guard to prevent re-initialization during HTMX swaps.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : The `ModalManager` class wraps Bootstrap 5 modal show/hide with fallback.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript TomSelect dropdown initialization must use a `TOMSELECT_INIT_DELAY_MS = 100` constant and implement delayed initialization after HTMX swaps. Required initializer functions: `initializeVCMemberSelect()`, `initializeVRFSelects()`, `initializeVlanGroupSelects()`, `initializeVlanSyncGroupSelects()`.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : CSRF token extraction should use `getCookie('csrftoken')` (cookie-based) as the approach for the import page JavaScript.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py|**/views/sync/**/*.py : Cache keys for sync data must follow the format: `librenms_{data_type}_{model_name}_{pk}` for fetched data and `librenms_{data_type}_last_fetched_{model_name}_{pk}` for fetch timestamps. VLAN group overrides must use `get_vlan_overrides_key(obj)`.
Applied to files:
netbox_librenms_plugin/views/mixins.pynetbox_librenms_plugin/tests/test_mixins.py
7d85b98 to
71ac0b6
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
71ac0b6 to
c56ac4c
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
netbox_librenms_plugin/import_utils.py (2)
1327-1335:⚠️ Potential issue | 🔴 CriticalNew imports are still being stored in the legacy single-int format.
Both creation paths write
custom_field_data={"librenms_id": int(...)}directly. That drops the originatingserver_key, so fresh imports from non-default LibreNMS instances are immediately indistinguishable from old single-server records. Persist the mapping throughset_librenms_device_id(..., server_key)before saving both the Device and the VM.Also applies to: 1766-1774
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils.py` around lines 1327 - 1335, The new-import path is storing librenms_id as a single int in device_data (custom_field_data={"librenms_id": int(device_id)}) which loses the server_key; update the creation flow to persist the mapping via set_librenms_device_id(server_key, int(device_id)) before saving the Device (and likewise for the VM creation path), and remove/replace direct single-int assignments so that both Device and VM use the set_librenms_device_id(...) call to record the id+server_key mapping rather than writing the legacy single-int custom field.
780-806:⚠️ Potential issue | 🔴 CriticalUse the multi-server lookup helper here, not a raw integer CF filter.
Both queries still only match the legacy bare-integer shape (
custom_field_data__librenms_id=int(...)). Once a Device or VM has been migrated to{"server_key": device_id}, this path stops recognizing it as already linked and the import flow can offer a duplicate import. Switch these lookups tofind_by_librenms_id(..., server_key)so legacy ints and per-server mappings are both honored.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils.py` around lines 780 - 806, The existing lookup uses raw integer custom-field filters (VirtualMachine.objects.filter(custom_field_data__librenms_id=int(librenms_id)) and Device.objects.filter(...)) which misses migrated per-server mappings; replace those filters with the multi-server helper find_by_librenms_id(librenms_id, server_key=server_key) (or the project’s equivalent) to return the existing VM/Device, so both legacy ints and {"server_key": id} shapes are honored; remove the int(...) conversion/ValueError handling around those filters and assign the helper result to existing_vm and existing_device respectively, keeping the subsequent result[...] assignments and name-matching logic unchanged.netbox_librenms_plugin/tables/interfaces.py (1)
355-366:⚠️ Potential issue | 🟠 MajorKeep
render_librenms_id()read-only.
get_librenms_device_id()normalizes string-backed values by callingsave()inutils.py, so using it from a table renderer turns a normal GET into a write path for each affected interface row. That can fire change logging/signals during page render; use a non-mutating lookup here and leave normalization to explicit write/migration paths.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tables/interfaces.py` around lines 355 - 366, render_librenms_id currently calls get_librenms_device_id(netbox_interface, self.server_key) which performs a save/normalization and turns a read into a write; change the renderer to use a non-mutating lookup instead (e.g. read the raw attribute like netbox_interface.librenms_id or a precomputed mapping on the table/context) and do not call get_librenms_device_id here; leave normalization to explicit write paths or a dedicated non-saving helper (create/use a get_librenms_device_id_readonly if needed) so rendering does not trigger saves/signals.
♻️ Duplicate comments (3)
netbox_librenms_plugin/tests/test_librenms_id.py (1)
130-141: 🧹 Nitpick | 🔵 TrivialAssert the default-key predicate here, not just the call count.
This still passes if
find_by_librenms_id()stops defaulting to"default"and builds theQfor some other key. Inspect the constructed predicate here the same way astest_queries_server_key_and_legacy_integer()so the fallback behavior is actually covered.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_librenms_id.py` around lines 130 - 141, The test test_default_server_key_is_default currently only asserts that mock_model.objects.filter was called; update it to assert the actual predicate used so the fallback to the "default" server key is verified: after calling find_by_librenms_id(mock_model, 42) inspect mock_model.objects.filter.call_args and assert it was called with a Q that matches the same structure used in test_queries_server_key_and_legacy_integer but using the default key string "default" (i.e., ensure the Q contains the "default" key path and value 42 rather than any other key); use the same technique/assertion used in test_queries_server_key_and_legacy_integer to compare the constructed predicate.netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (1)
74-89:⚠️ Potential issue | 🟠 MajorVM stale mappings still have no cleanup action.
The backend now accepts
object_type="vm", but this branch only renders the remove form for devices. VM sync pages will still show orphaned mappings as “Not configured” with no supported way to delete them; render the same action for virtual machines and include the object type in the POST payload.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html` around lines 74 - 89, The template only renders the "Remove" form when model_name == "device", leaving VM stale mappings without a cleanup action; update the conditional in librenms_sync_base.html to also allow model_name == "vm" (or check inclusion in ["device","vm"]) so the same form/action (the form posting to the 'plugins:netbox_librenms_plugin:remove_server_mapping' view) is rendered for VMs, and add a hidden input named object_type with value="{{ model_name }}" alongside the existing hidden server_key so the POST payload includes the object type for backend handling; keep the existing confirm text and CSRF token.netbox_librenms_plugin/tests/test_sync_view_mismatch.py (1)
331-416: 🧹 Nitpick | 🔵 TrivialReuse the shared config/object fixtures in these mapping tests.
These cases still hand-roll
PLUGINS_CONFIGpayloads and mock NetBox objects, which will drift as the multi-server config shape evolves. Replace_make_obj()and the inline config dicts with the fixtures fromtests/conftest.py.Based on learnings: Reuse fixtures from
tests/conftest.pyinstead of creating ad-hoc mocks: Configuration (mock_multi_server_config,mock_legacy_config) and NetBox objects (mock_netbox_device,mock_netbox_vm, ...).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py` around lines 331 - 416, Tests in test_sync_view_mismatch.py are creating ad-hoc PLUGINS_CONFIG dicts and MagicMock NetBox objects via _make_obj() instead of using shared fixtures; update the tests (functions test_returns_none_for_legacy_int, test_returns_none_for_missing_cf, test_single_configured_server, test_orphaned_server_is_not_configured, test_multiple_servers_sorted_active_first) to use the existing fixtures from tests/conftest.py (e.g., mock_multi_server_config, mock_legacy_config, mock_netbox_device, mock_netbox_vm) by removing _make_obj() usage and replacing inline plugins_cfg mocks with the appropriate fixture injection and patching of django_settings.PLUGINS_CONFIG so the tests rely on the shared fixtures rather than hand-rolled data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 405-427: The validation result from validate_device_for_import()
is missing the librenms_id_needs_migration flag; update
validate_device_for_import to compare the incoming device's LibreNMS identifier
format against the existing_device's stored LibreNMS identifier (e.g., compare
libre_device.device_id / incoming.librenms_id or their normalized forms) and set
validation.librenms_id_needs_migration = True when formats differ (and include
validation.existing_device = existing_device if not already set); ensure the
logic respects serial_confirmed flow so the template's checkbox/button behavior
(migrate-btn and force-migrate-{{ libre_device.device_id }}) works as intended.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`:
- Around line 604-611: The Modules tab currently includes inc/_module_sync.html
directly (tied to the modules-tab button and `#modules` tab pane) which bypasses
the standard two-template HTMX pattern; create two templates named following the
convention (e.g. _module_sync.html and _module_sync_content.html) and update the
tab button and pane to include the wrapper template (the _module_sync.html that
mirrors _resource_sync.html) while the content pane should load/refresh the
_module_sync_content.html via HTMX just like other resources; ensure the tab
id/data-tab "modules" and target "#modules" remain the same so HTMX refresh
behavior matches the existing _resource_sync/_resource_sync_content flow.
In `@netbox_librenms_plugin/utils.py`:
- Around line 476-502: get_librenms_device_id currently performs side-effect
writes (obj.custom_field_data assignment + obj.save()) while doing reads; change
it to avoid automatic persistence by adding an optional parameter (e.g.,
auto_save=True) or by returning a tuple (device_id, needs_save) instead of
writing directly — detect normalized values the same way (string -> int,
dict[server_key] normalization) but if normalization is needed set
needs_save=True and update the in-memory custom_field_data without calling
obj.save(); callers (e.g., the table renderer in tables/interfaces.py) should be
updated to pass auto_save=False or to call obj.save() only when they intend to
persist changes so reads no longer trigger DB writes or signals.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Line 782: The context key "existing_id_servers" is always None because
validate_device_for_import() never sets it; either update
validate_device_for_import() in netbox_librenms_plugin/import_utils.py to
populate validation["existing_id_servers"] (e.g., derive per-server IDs when an
existing_device is found) or, alternatively, compute and set
context["existing_id_servers"] here in imports/actions.py from
validation["existing_device"] before rendering the template so the per-server
mapping badges can render; locate validate_device_for_import(), the validation
dict, and the use of context["existing_id_servers"] to implement the fix.
In `@netbox_librenms_plugin/views/object_sync/devices.py`:
- Around line 82-97: The code threads server_key into
VCInterfaceTable/LibreNMSInterfaceTable but the cache access in
SingleInterfaceVerifyView and SaveVlanGroupOverridesView still uses
get_cache_key(..., "ports") without the server scope, causing cross-server cache
collisions; update those flows to accept and pass the same server_key (or append
it to the cache suffix) when calling get_cache_key so the cache key becomes
server-scoped (e.g., get_cache_key(..., "ports", server_key) or
get_cache_key(..., f"ports:{server_key}") ), and ensure
SingleInterfaceVerifyView and SaveVlanGroupOverridesView propagate server_key
from the request/context into their cache get/set calls to match the table
layer.
In `@netbox_librenms_plugin/views/object_sync/vms.py`:
- Around line 46-50: The get_table method is not forwarding the resolved
interface_name_field into the LibreNMSVMInterfaceTable constructor, causing VM
sync to fall back to the default field; update get_table (method name get_table
in vms.py) to pass the interface_name_field argument through to
LibreNMSVMInterfaceTable so it receives the same interface_name_field used
earlier (LibreNMSVMInterfaceTable inherits row/accessor behavior from
LibreNMSInterfaceTable and needs this parameter to match ifDescr-based
installs).
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 409-425: The POST handler currently checks permissions against
both Device and VirtualMachine; before calling require_all_permissions()
determine the target object_type (e.g., read object_type from the request
payload/POST parameter or URL like how _get_object expects it), then set
self.required_object_permissions['POST'] = [("change", Device)] or [("change",
VirtualMachine)] for that request, and only then call require_all_permissions();
this scopes the required permission to the specific model (use symbols
required_object_permissions, post, _get_object, and require_all_permissions to
locate and implement the change).
---
Outside diff comments:
In `@netbox_librenms_plugin/import_utils.py`:
- Around line 1327-1335: The new-import path is storing librenms_id as a single
int in device_data (custom_field_data={"librenms_id": int(device_id)}) which
loses the server_key; update the creation flow to persist the mapping via
set_librenms_device_id(server_key, int(device_id)) before saving the Device (and
likewise for the VM creation path), and remove/replace direct single-int
assignments so that both Device and VM use the set_librenms_device_id(...) call
to record the id+server_key mapping rather than writing the legacy single-int
custom field.
- Around line 780-806: The existing lookup uses raw integer custom-field filters
(VirtualMachine.objects.filter(custom_field_data__librenms_id=int(librenms_id))
and Device.objects.filter(...)) which misses migrated per-server mappings;
replace those filters with the multi-server helper
find_by_librenms_id(librenms_id, server_key=server_key) (or the project’s
equivalent) to return the existing VM/Device, so both legacy ints and
{"server_key": id} shapes are honored; remove the int(...) conversion/ValueError
handling around those filters and assign the helper result to existing_vm and
existing_device respectively, keeping the subsequent result[...] assignments and
name-matching logic unchanged.
In `@netbox_librenms_plugin/tables/interfaces.py`:
- Around line 355-366: render_librenms_id currently calls
get_librenms_device_id(netbox_interface, self.server_key) which performs a
save/normalization and turns a read into a write; change the renderer to use a
non-mutating lookup instead (e.g. read the raw attribute like
netbox_interface.librenms_id or a precomputed mapping on the table/context) and
do not call get_librenms_device_id here; leave normalization to explicit write
paths or a dedicated non-saving helper (create/use a
get_librenms_device_id_readonly if needed) so rendering does not trigger
saves/signals.
---
Duplicate comments:
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`:
- Around line 74-89: The template only renders the "Remove" form when model_name
== "device", leaving VM stale mappings without a cleanup action; update the
conditional in librenms_sync_base.html to also allow model_name == "vm" (or
check inclusion in ["device","vm"]) so the same form/action (the form posting to
the 'plugins:netbox_librenms_plugin:remove_server_mapping' view) is rendered for
VMs, and add a hidden input named object_type with value="{{ model_name }}"
alongside the existing hidden server_key so the POST payload includes the object
type for backend handling; keep the existing confirm text and CSRF token.
In `@netbox_librenms_plugin/tests/test_librenms_id.py`:
- Around line 130-141: The test test_default_server_key_is_default currently
only asserts that mock_model.objects.filter was called; update it to assert the
actual predicate used so the fallback to the "default" server key is verified:
after calling find_by_librenms_id(mock_model, 42) inspect
mock_model.objects.filter.call_args and assert it was called with a Q that
matches the same structure used in test_queries_server_key_and_legacy_integer
but using the default key string "default" (i.e., ensure the Q contains the
"default" key path and value 42 rather than any other key); use the same
technique/assertion used in test_queries_server_key_and_legacy_integer to
compare the constructed predicate.
In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py`:
- Around line 331-416: Tests in test_sync_view_mismatch.py are creating ad-hoc
PLUGINS_CONFIG dicts and MagicMock NetBox objects via _make_obj() instead of
using shared fixtures; update the tests (functions
test_returns_none_for_legacy_int, test_returns_none_for_missing_cf,
test_single_configured_server, test_orphaned_server_is_not_configured,
test_multiple_servers_sorted_active_first) to use the existing fixtures from
tests/conftest.py (e.g., mock_multi_server_config, mock_legacy_config,
mock_netbox_device, mock_netbox_vm) by removing _make_obj() usage and replacing
inline plugins_cfg mocks with the appropriate fixture injection and patching of
django_settings.PLUGINS_CONFIG so the tests rely on the shared fixtures rather
than hand-rolled data.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: ec9576f6-9b3d-4fec-8a5b-090451baa69a
📒 Files selected for processing (23)
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/import_utils.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/mixins.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/sync/interfaces.py
| {% if module_sync %} | ||
| <li class="nav-item" role="presentation"> | ||
| <button class="nav-link" id="modules-tab" data-tab="modules" data-bs-toggle="tab" | ||
| data-bs-target="#modules" type="button" role="tab" aria-controls="modules"> | ||
| Modules | ||
| </button> | ||
| </li> | ||
| {% endif %} |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Keep the new Modules tab on the standard sync-template pair.
Including inc/_module_sync.html directly bypasses the _resource_sync.html / _resource_sync_content.html pattern the other tabs use for HTMX refreshes. Please give Modules the same wrapper/content split so it behaves like the other sync resources.
As per coding guidelines, "Each sync resource has two templates following a naming convention: _resource_sync.html and _resource_sync_content.html ... When adding a new sync resource, create both templates following this pattern."
Also applies to: 641-645
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`
around lines 604 - 611, The Modules tab currently includes inc/_module_sync.html
directly (tied to the modules-tab button and `#modules` tab pane) which bypasses
the standard two-template HTMX pattern; create two templates named following the
convention (e.g. _module_sync.html and _module_sync_content.html) and update the
tab button and pane to include the wrapper template (the _module_sync.html that
mirrors _resource_sync.html) while the content pane should load/refresh the
_module_sync_content.html via HTMX just like other resources; ensure the tab
id/data-tab "modules" and target "#modules" remain the same so HTMX refresh
behavior matches the existing _resource_sync/_resource_sync_content flow.
c56ac4c to
a9b956e
Compare
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
netbox_librenms_plugin/views/object_sync/devices.py (1)
343-362:⚠️ Potential issue | 🟠 MajorScope VLAN override cache entries by
server_keytoo.
portsis now keyed per LibreNMS server, but the override map is still loaded and saved through the sharedget_vlan_overrides_key(device)key. If the same NetBox device is synced against two servers, saving overrides from one server will leak into the other.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/object_sync/devices.py` around lines 343 - 362, The VLAN override map is currently stored under a device-only key and leaks between LibreNMS servers; update the overrides cache key to include the server_key (use the same per-server scope as ports) by changing get_vlan_overrides_key to accept a server_key parameter (or add a new method) and then call cache.get(self.get_vlan_overrides_key(device, server_key)) and cache.set(self.get_vlan_overrides_key(device, server_key), existing, timeout=ports_ttl) in the block that reads/updates overrides; also update any other callers of get_vlan_overrides_key to pass server_key so overrides are isolated per server.netbox_librenms_plugin/import_utils.py (2)
453-456:⚠️ Potential issue | 🟠 MajorNormalize
server_keyfrom the provided API before building the import cache key.
process_device_filters()passes anapiinstance but no explicitserver_key, so this key becomeslibrenms_devices_import_None_...for every server. That defeats the per-server namespace and can return another server’s device list from cache.🛠️ Suggested fix
if api is None: api = LibreNMSAPI(server_key=server_key) + server_key = server_key or getattr(api, "server_key", "default") # Build LibreNMS API filters using the type/query format # LibreNMS API v0 expects ?type=X&query=Y format, not direct parameters @@ - cache_key = f"librenms_devices_import_{server_key}_{hash(str(api_filters))}_{hash(str(client_filters))}" + cache_key = ( + f"librenms_devices_import_{server_key}_{hash(str(api_filters))}_{hash(str(client_filters))}" + )Also applies to: 552-554
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils.py` around lines 453 - 456, process_device_filters builds the import cache key using the server_key parameter which can be None when an api instance is passed, causing cache collisions; before constructing the cache key, normalize server_key by extracting it from the provided api (e.g., read api.server_key or an accessor on the LibreNMSAPI instance) when api is not None, and use that normalized value in the import cache key construction; apply the same fix in the other occurrence that builds the cache key (the block around the second LibreNMSAPI creation) so both places use the api-derived server_key when api is provided.
1777-1785:⚠️ Potential issue | 🔴 CriticalNew VM imports are still written in the legacy bare-int format.
This path never records which LibreNMS server the VM came from, so two servers that both have
device_id=42will still collide on the same VM link. It also bypasses the migration helpers added in this PR. Please threadserver_key/api.server_keyintocreate_vm_from_librenms()and persist the mapping viaset_librenms_device_id()after creation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils.py` around lines 1777 - 1785, The VM creation currently stores a bare int librenms_id and doesn't record which LibreNMS server it came from; update create_vm_from_librenms to accept a server_key (or thread api.server_key into its call), stop writing raw ints into custom_field_data, and after creating the VirtualMachine (the vm returned from VirtualMachine.objects.create) call set_librenms_device_id(vm, api.server_key, libre_device["device_id"]) to persist the server-scoped mapping using the migration helper so IDs are namespaced by server.
♻️ Duplicate comments (3)
netbox_librenms_plugin/forms.py (1)
582-593: 🧹 Nitpick | 🔵 TrivialConsider lazy server_key resolution to improve resilience.
The current implementation instantiates
LibreNMSAPI()before the cache lookup (line 584). If the selected server is misconfigured, the__init__will raise an exception before checking the cache, preventing use of cached location choices.A more resilient pattern would resolve
server_keycheaply first (e.g., fromLibreNMSSettings.objects.first().selected_server), attempt the cache lookup, and only instantiate the full API client on a cache miss.That said, this is a minor edge case—on API init failure, the form already falls back to default empty choices, so functionality isn't broken.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/forms.py` around lines 582 - 593, The code instantiates LibreNMSAPI() before checking cache, so a misconfigured server can raise in __init__ and prevent using cached choices; instead, first resolve the cheap server_key (e.g., read LibreNMSSettings.objects.first().selected_server or similar) and build the cache_key from that to check cache.get(cache_key) before creating a LibreNMSAPI instance, and only instantiate LibreNMSAPI() and call api.get_locations() on a cache miss; update references to cache_key, LibreNMSAPI, server_key, LibreNMSSettings, get_locations, and self.fields["librenms_location"].choices accordingly.netbox_librenms_plugin/tests/test_sync_view_mismatch.py (1)
331-429: 🧹 Nitpick | 🔵 TrivialReuse the shared config/object fixtures in this mapping test suite.
These cases still hand-roll both
PLUGINS_CONFIGpayloads and NetBox objects even though the suite already provides dedicated multi-server config and object fixtures. Reusing those fixtures will keep these assertions aligned if the server-config shape changes again.Based on learnings: Reuse fixtures from
tests/conftest.pyinstead of creating ad-hoc mocks: Configuration (mock_multi_server_config,mock_legacy_config), API client (mock_librenms_api), NetBox objects (mock_netbox_device,mock_netbox_vm,mock_netbox_site,mock_netbox_platform,mock_netbox_device_type,mock_netbox_device_role,mock_netbox_cluster,mock_netbox_rack), HTTP responses (mock_response_factory,mock_success_response,mock_device_response,mock_error_response,mock_auth_error_response), and Import workflow (sample_librenms_device,sample_librenms_device_minimal,sample_validation_state,sample_validation_state_vm).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py` around lines 331 - 429, The tests in this file manually construct PLUGINS_CONFIG and NetBox mock objects; change each test (e.g., test_single_configured_server, test_orphaned_server_is_not_configured, test_multiple_servers_sorted_active_first) to reuse the shared fixtures from tests/conftest.py instead of hand-rolling values: inject mock_multi_server_config or mock_legacy_config for PLUGINS_CONFIG and use the provided mock_netbox_* fixtures (e.g., mock_netbox_device, mock_netbox_vm, mock_netbox_site, mock_netbox_platform, mock_netbox_device_type, mock_netbox_device_role, mock_netbox_cluster, mock_netbox_rack) for the obj passed to BaseLibreNMSSyncView._build_all_server_mappings; where the tests currently patch django_settings.PLUGINS_CONFIG, replace that patch with the fixture-provided config and adjust assertions to rely on the fixture shapes (keeping server keys like "production" and "mock-dev" consistent with mock_multi_server_config).netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (1)
605-612:⚠️ Potential issue | 🟠 MajorKeep Modules on the standard sync wrapper/content pair.
The Modules tab still includes
inc/_module_sync.htmldirectly, so it bypasses the same HTMX refresh contract the other sync resources use. Please add_module_sync.html/_module_sync_content.htmland include the wrapper here instead.As per coding guidelines, "Each sync resource has two templates following a naming convention:
_resource_sync.htmland_resource_sync_content.html... When adding a new sync resource, create both templates following this pattern."Also applies to: 642-645
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html` around lines 605 - 612, The Modules tab currently includes the content partial directly (inc/_module_sync.html) and bypasses the standard HTMX wrapper/content contract; create two templates named _module_sync.html (wrapper) and _module_sync_content.html (inner content) following the existing _<resource>_sync.html and _<resource>_sync_content.html pattern, move the current inc/_module_sync.html content into the new _module_sync_content.html, and update the tab markup (the block using module_sync and the Modules button) to include the wrapper template (_module_sync.html) instead of including the content directly; also apply the same change to the other occurrence referenced around lines 642-645 so both places use the wrapper/content pair.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils.py`:
- Around line 782-787: The code incorrectly forces server_key = "default" when
api is None, which causes find_by_librenms_id(VirtualMachine, librenms_id,
server_key) to search the wrong server; change the logic so server_key is None
when api is None (e.g. server_key = api.server_key if api is not None else None)
and propagate that None into find_by_librenms_id so callers that skip VC
detection (api=None) do not get remapped to the "default" server; make the same
change for the other occurrence around import_single_device() (lines ~810-812)
to preserve correct server scoping.
In `@netbox_librenms_plugin/tests/test_mixins.py`:
- Around line 155-181: Update the tests to assert exact cache-key strings rather
than substrings: for get_last_fetched_key(obj, "ports") assert it equals
"librenms_ports_last_fetched_device_3" and for the server-scoped call
get_last_fetched_key(obj, "ports", server_key="srv1") assert it equals
"librenms_ports_last_fetched_srv1_device_3"; also add an assertion that
get_vlan_overrides_key(obj) exists and returns
"librenms_vlan_overrides_device_3" to lock the VLAN helper contract; locate and
change assertions around get_last_fetched_key and get_vlan_overrides_key in the
tests referencing those helper functions.
In `@netbox_librenms_plugin/tests/test_sync_interfaces.py`:
- Around line 6-15: The tests currently build ad-hoc mocks via helper functions
_make_view() and _make_device_interface(); instead, refactor those tests to use
the shared fixtures from tests/conftest.py (e.g. mock_librenms_api,
mock_multi_server_config, mock_legacy_config and the NetBox object fixtures) and
wire them into the test functions (inject fixtures into the test signature) so
SyncInterfacesView instances are created by using the fixture-provided
mock_librenms_api and NetBox objects rather than object.__new__ builders; update
any assertions that reference view._librenms_api.server_key or view._lookup_maps
to use the fixture state instead.
In `@netbox_librenms_plugin/utils.py`:
- Around line 483-507: The current custom-field normalization accepts a bare
int/string for librenms_id universally; update the logic in the function around
cf_value handling (referencing cf_value, server_key, obj.custom_field_data,
auto_save and obj.save()) so that the bare-integer fallback (returning cf_value
when isinstance(cf_value, int)) only applies when server_key equals the
default/legacy server identifier, and when reading JSON dicts or performing
lookups (used by find_by_librenms_id) treat both numeric and string forms as
equivalent by attempting int casts and also accepting string matches until
migration completes; ensure you still write back normalized ints into
obj.custom_field_data and save when auto_save is True.
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Line 120: The code passes the original `obj` into `_build_all_server_mappings`
causing badge/mapping mismatch for VC members; instead build
`all_server_mappings` from the same resolved lookup object used when computing
`self.librenms_id` (the object returned by the `get()`/lookup that may resolve
via `librenms_sync_device`) and pass that resolved object into
`_build_all_server_mappings` (replace `obj` with the resolved lookup object used
for `self.librenms_id`, keeping `self.librenms_api.server_key` unchanged).
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1121-1131: The conflict-resolution flow currently reads/writes a
bare integer custom field directly; update the handler so all branches (link,
update, update_serial, and collision checks) use the server-aware accessors:
call self.librenms_api.get_librenms_id(existing_device) (and for candidates) to
read the per-server mapping, and use set_librenms_device_id(existing_device,
librenms_id, self.librenms_api.server_key) when writing instead of assigning
custom_field_data["librenms_id"] directly; also adjust the collision check to
look up devices by get_librenms_id() (or by querying the JSON mapping field for
the server_key) so conflicts on non-default servers are detected.
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 413-427: The view currently treats only "vm" as a VM and falls
back to Device for any other object_type, causing wrong permission checks and
lookups when the template posts "virtualmachine"; update normalization in post
(and where _get_object and _sync_url_name read object_type) to lower-case and
map "virtualmachine" to "vm" (or explicitly validate and reject unknown values)
before branching so target_model is set correctly (use a small conditional or
mapping to convert object_type -> "vm" or "device" and then choose
VirtualMachine vs Device accordingly, and ensure _get_object and _sync_url_name
use the same normalized value).
---
Outside diff comments:
In `@netbox_librenms_plugin/import_utils.py`:
- Around line 453-456: process_device_filters builds the import cache key using
the server_key parameter which can be None when an api instance is passed,
causing cache collisions; before constructing the cache key, normalize
server_key by extracting it from the provided api (e.g., read api.server_key or
an accessor on the LibreNMSAPI instance) when api is not None, and use that
normalized value in the import cache key construction; apply the same fix in the
other occurrence that builds the cache key (the block around the second
LibreNMSAPI creation) so both places use the api-derived server_key when api is
provided.
- Around line 1777-1785: The VM creation currently stores a bare int librenms_id
and doesn't record which LibreNMS server it came from; update
create_vm_from_librenms to accept a server_key (or thread api.server_key into
its call), stop writing raw ints into custom_field_data, and after creating the
VirtualMachine (the vm returned from VirtualMachine.objects.create) call
set_librenms_device_id(vm, api.server_key, libre_device["device_id"]) to persist
the server-scoped mapping using the migration helper so IDs are namespaced by
server.
In `@netbox_librenms_plugin/views/object_sync/devices.py`:
- Around line 343-362: The VLAN override map is currently stored under a
device-only key and leaks between LibreNMS servers; update the overrides cache
key to include the server_key (use the same per-server scope as ports) by
changing get_vlan_overrides_key to accept a server_key parameter (or add a new
method) and then call cache.get(self.get_vlan_overrides_key(device, server_key))
and cache.set(self.get_vlan_overrides_key(device, server_key), existing,
timeout=ports_ttl) in the block that reads/updates overrides; also update any
other callers of get_vlan_overrides_key to pass server_key so overrides are
isolated per server.
---
Duplicate comments:
In `@netbox_librenms_plugin/forms.py`:
- Around line 582-593: The code instantiates LibreNMSAPI() before checking
cache, so a misconfigured server can raise in __init__ and prevent using cached
choices; instead, first resolve the cheap server_key (e.g., read
LibreNMSSettings.objects.first().selected_server or similar) and build the
cache_key from that to check cache.get(cache_key) before creating a LibreNMSAPI
instance, and only instantiate LibreNMSAPI() and call api.get_locations() on a
cache miss; update references to cache_key, LibreNMSAPI, server_key,
LibreNMSSettings, get_locations, and self.fields["librenms_location"].choices
accordingly.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`:
- Around line 605-612: The Modules tab currently includes the content partial
directly (inc/_module_sync.html) and bypasses the standard HTMX wrapper/content
contract; create two templates named _module_sync.html (wrapper) and
_module_sync_content.html (inner content) following the existing
_<resource>_sync.html and _<resource>_sync_content.html pattern, move the
current inc/_module_sync.html content into the new _module_sync_content.html,
and update the tab markup (the block using module_sync and the Modules button)
to include the wrapper template (_module_sync.html) instead of including the
content directly; also apply the same change to the other occurrence referenced
around lines 642-645 so both places use the wrapper/content pair.
In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py`:
- Around line 331-429: The tests in this file manually construct PLUGINS_CONFIG
and NetBox mock objects; change each test (e.g., test_single_configured_server,
test_orphaned_server_is_not_configured,
test_multiple_servers_sorted_active_first) to reuse the shared fixtures from
tests/conftest.py instead of hand-rolling values: inject
mock_multi_server_config or mock_legacy_config for PLUGINS_CONFIG and use the
provided mock_netbox_* fixtures (e.g., mock_netbox_device, mock_netbox_vm,
mock_netbox_site, mock_netbox_platform, mock_netbox_device_type,
mock_netbox_device_role, mock_netbox_cluster, mock_netbox_rack) for the obj
passed to BaseLibreNMSSyncView._build_all_server_mappings; where the tests
currently patch django_settings.PLUGINS_CONFIG, replace that patch with the
fixture-provided config and adjust assertions to rely on the fixture shapes
(keeping server keys like "production" and "mock-dev" consistent with
mock_multi_server_config).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: d9f981b4-e860-4996-8a8c-eddf75538a2a
📒 Files selected for processing (27)
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/import_utils.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/_interface_sync_content.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/base/interfaces_view.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/mixins.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/sync/interfaces.py
| def _make_view(): | ||
| """Return a SyncInterfacesView with a mocked LibreNMS API.""" | ||
| from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView | ||
|
|
||
| view = object.__new__(SyncInterfacesView) | ||
| view._librenms_api = MagicMock() | ||
| view._librenms_api.server_key = "default" | ||
| view.request = MagicMock() | ||
| view._lookup_maps = {} | ||
| return view |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Reuse the shared test fixtures instead of private mock builders.
_make_view() and _make_device_interface() rebuild API/object scaffolding that already lives in tests/conftest.py, which makes these tests easier to drift from the rest of the suite. Prefer wiring mock_librenms_api and the shared NetBox fixtures into the tests directly where possible.
Based on learnings, "Reuse fixtures from tests/conftest.py instead of creating ad-hoc mocks: Configuration (mock_multi_server_config, mock_legacy_config), API client (mock_librenms_api), NetBox objects ..."
Also applies to: 21-44
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/tests/test_sync_interfaces.py` around lines 6 - 15,
The tests currently build ad-hoc mocks via helper functions _make_view() and
_make_device_interface(); instead, refactor those tests to use the shared
fixtures from tests/conftest.py (e.g. mock_librenms_api,
mock_multi_server_config, mock_legacy_config and the NetBox object fixtures) and
wire them into the test functions (inject fixtures into the test signature) so
SyncInterfacesView instances are created by using the fixture-provided
mock_librenms_api and NetBox objects rather than object.__new__ builders; update
any assertions that reference view._librenms_api.server_key or view._lookup_maps
to use the fixture state instead.
a9b956e to
24998bd
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js (1)
783-811:⚠️ Potential issue | 🟠 MajorHandle HTTP failures before parsing the verify-interface response.
handleInterfaceChange()still callsresponse.json()unconditionally and never catches failures. A 403/500 response from the now server-scoped endpoint will reject the promise and leave the row stale with no useful diagnostic.Suggested fix
fetch('/plugins/librenms_plugin/verify-interface/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value }, body: JSON.stringify({ device_id: value, interface_name: select.dataset.interface, interface_name_field: document.querySelector('input[name="interface_name_field"]:checked').value, server_key: document.getElementById('current-server-key')?.value || null }) }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); + }) .then(data => { const row = document.querySelector(`tr[data-interface="${select.dataset.rowId}"]`); if (data.status === 'success' && row) { const formattedRow = data.formatted_row; row.querySelector('td[data-col="name"]').innerHTML = formattedRow.name; @@ row.querySelector('td[data-col="description"]').innerHTML = formattedRow.description; initializeFilters(); } - }); + }) + .catch(error => { + console.error('Interface verification failed:', error.message); + });As per coding guidelines,
netbox_librenms_plugin/static/**/*.js: Always checkresponse.okbefore processing fetch responses to catch HTTP errors. In catch blocks, showerror.messagefor debugging rather than generic messages.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js` around lines 783 - 811, The handleInterfaceChange function currently calls response.json() without checking HTTP status; update handleInterfaceChange to check response.ok after the fetch (when calling the verify-interface endpoint) and handle non-OK responses by reading response.text() or response.json() for diagnostics, then surface the error (use error.message or the response body) and avoid updating the row; keep the success path that reads data.formatted_row and calls initializeFilters() but add a .catch that logs/shows error.message so failures (403/500) don’t silently leave the row stale; reference symbols: handleInterfaceChange, response.ok, response.json(), initializeFilters(), and the verify-interface POST.netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
33-44:⚠️ Potential issue | 🟠 MajorMake the VC lookup server-aware.
This branch still uses
obj.cf.get("librenms_id")as a yes/no check, so a member that only has a mapping for some other server will block fallback to the VC sync device on the current server. The new_librenms_lookup_devicethen carries that wrong choice through the whole page.🔧 Suggested fix
librenms_lookup_device = obj if hasattr(obj, "virtual_chassis") and obj.virtual_chassis: - # Check if this device has its own librenms_id - if not obj.cf.get("librenms_id"): + # Check the mapping for the active server only. + if not self.librenms_api.get_librenms_id(obj): # Use helper function to determine the sync device sync_device = get_librenms_sync_device(obj) if sync_device: librenms_lookup_device = sync_deviceAs per coding guidelines,
Use LibreNMSAPI.get_librenms_id() instead of directly accessing the librenms_id custom field when mapping Devices/VMs to LibreNMS.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/librenms_sync_view.py` around lines 33 - 44, The current VC branch checks obj.cf.get("librenms_id") which is not server-aware and can block falling back to the VC sync device; instead call the API to determine if this device maps for the active server. Replace the obj.cf.get(...) check with a call to self.librenms_api.get_librenms_id(obj) and if that returns falsy, call get_librenms_sync_device(obj) and then call self.librenms_api.get_librenms_id on the sync device; finally assign self.librenms_id from the API result and set self._librenms_lookup_device to whichever device was actually used (obj or sync_device) so subsequent logic uses the server-aware mapping.
♻️ Duplicate comments (8)
netbox_librenms_plugin/utils.py (1)
456-507:⚠️ Potential issue | 🟠 MajorKeep legacy scalar IDs scoped to the default server only.
Bare
librenms_idscalars are still treated as valid for everyserver_key, andfind_by_librenms_id()still ignores string-stored values like"42". In a multi-server install that lets a legacy ID from one server satisfy lookups for another server, while some existing rows remain undiscoverable until an unrelated write normalizes them.Suggested fix
def get_librenms_device_id(obj, server_key: str = "default", *, auto_save: bool = True): @@ - if isinstance(cf_value, int): - return cf_value # backward compat: bare integer from pre-migration + if isinstance(cf_value, int): + return cf_value if server_key == "default" else None if isinstance(cf_value, str): + if server_key != "default": + return None # Someone stored a bare string (e.g., via NetBox UI/API) — normalise to int. try: int_id = int(cf_value) @@ def find_by_librenms_id(model, librenms_id, server_key: str = "default"): @@ - return model.objects.filter( - Q(**{f"custom_field_data__librenms_id__{server_key}": librenms_id}) - | Q(custom_field_data__librenms_id=librenms_id) - ).first() + try: + numeric_id = int(librenms_id) + except (TypeError, ValueError): + return None + + query = ( + Q(**{f"custom_field_data__librenms_id__{server_key}": numeric_id}) + | Q(**{f"custom_field_data__librenms_id__{server_key}": str(numeric_id)}) + ) + if server_key == "default": + query |= ( + Q(custom_field_data__librenms_id=numeric_id) + | Q(custom_field_data__librenms_id=str(numeric_id)) + ) + return model.objects.filter(query).first()Also applies to: 544-563
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/utils.py` around lines 456 - 507, The current get_librenms_device_id treats legacy scalar librenms_id values as valid for any server_key and leaves find_by_librenms_id ignoring string IDs; change get_librenms_device_id to scope bare integer scalars (and bare-string normalisation) to the default server only by returning the scalar only when server_key == "default" (or matches the configured default key), and when cf_value is dict keep per-server behaviour and normalise string entries as already implemented (update obj.custom_field_data and save when auto_save). Also update the corresponding lookup function find_by_librenms_id to respect server scoping (only match scalar IDs for the default server) and to consider string-stored IDs by normalising/handling strings the same way as get_librenms_device_id so rows with "42" are discoverable without requiring a write.netbox_librenms_plugin/views/imports/actions.py (1)
1121-1131:⚠️ Potential issue | 🔴 CriticalFinish the server-aware
librenms_idmigration across the other conflict actions.This new branch uses
set_librenms_device_id(), butlink,update,update_serial, and the collision check immediately above still read/write a bare integer. On non-default servers those paths can overwrite dict-backed mappings or miss conflicts, so the new action only fixes one branch of the flow.Based on learnings: Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS.Suggested fix
- if action in {"link", "update", "update_serial"}: - id_conflict = ( - Device.objects.filter(custom_field_data__librenms_id=int(librenms_id)) - .exclude(pk=existing_device.pk) - .first() - ) + if action in {"link", "update", "update_serial"}: + from netbox_librenms_plugin.utils import find_by_librenms_id, set_librenms_device_id + + id_conflict = find_by_librenms_id(Device, librenms_id, self.librenms_api.server_key) + if id_conflict and id_conflict.pk == existing_device.pk: + id_conflict = None @@ - existing_device.custom_field_data["librenms_id"] = int(librenms_id) + set_librenms_device_id(existing_device, librenms_id, self.librenms_api.server_key) @@ - existing_device.custom_field_data["librenms_id"] = int(librenms_id) + set_librenms_device_id(existing_device, librenms_id, self.librenms_api.server_key) @@ - existing_device.custom_field_data["librenms_id"] = int(librenms_id) + set_librenms_device_id(existing_device, librenms_id, self.librenms_api.server_key)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/imports/actions.py` around lines 1121 - 1131, The other branches ('link', 'update', 'update_serial' and the collision-check) still read/write a bare integer librenms_id; update those paths to use the server-aware accessors: call LibreNMSAPI.get_librenms_id(self.librenms_api, existing_device) when reading the current mapping and use set_librenms_device_id(existing_device, librenms_id, self.librenms_api.server_key) when writing so dict-backed per-server IDs aren't overwritten; also update any conflict-check logic to compare via get_librenms_id and adjust logging/messages to show the per-server dict format consistently and keep the _save_device(existing_device) usage after writes.netbox_librenms_plugin/forms.py (1)
584-585:⚠️ Potential issue | 🟡 MinorCheck the cache before constructing
LibreNMSAPI().A warm
librenms_locations_choices:<server_key>cache is unusable ifLibreNMSAPI()raises during server/config resolution, so the form drops back to the empty default even though cached choices exist. Resolve the active server key cheaply first, then build the API client only on a cache miss.Suggested fix
try: # Use caching to avoid repeated API calls - api = LibreNMSAPI() - cache_key = f"librenms_locations_choices:{api.server_key}" + settings = LibreNMSSettings.objects.first() + server_key = getattr(settings, "selected_server", "default") or "default" + cache_key = f"librenms_locations_choices:{server_key}" cached_choices = cache.get(cache_key) if cached_choices: self.fields["librenms_location"].choices = cached_choices return + api = LibreNMSAPI(server_key=server_key) # Fetch locations from LibreNMS success, locations = api.get_locations()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/forms.py` around lines 584 - 585, The code currently constructs LibreNMSAPI() before checking cache, which can raise and prevent using an existing librenms_locations_choices:<server_key> cache; change the flow to first resolve the active server key cheaply (use whatever helper or config lookup that yields api.server_key without instantiating LibreNMSAPI), build cache_key = f"librenms_locations_choices:{server_key}", check the cache and return cached choices if present, and only instantiate LibreNMSAPI() (and call into it) when the cache misses so that exceptions during API construction don’t discard valid cached choices.netbox_librenms_plugin/import_utils.py (1)
641-641:⚠️ Potential issue | 🔴 CriticalDon't let api-less validation silently fall back to the
"default"server.
server_keystill defaults to"default", so any caller that intentionally passesapi=Nonekeeps resolving existing links against the wrong LibreNMS server. In this module that means non-default imports can miss already-linked Devices/VMs whenever VC detection is skipped, which reopens duplicate-import paths.Pass the active `server_key` explicitly at the local call sites that disable VC detection, rather than relying on the default.Suggested fix
def validate_device_for_import( libre_device: dict, import_as_vm: bool = False, api: "LibreNMSAPI" = None, *, - server_key: str = "default", + server_key: str | None = None, include_vc_detection: bool = True, force_vc_refresh: bool = False, use_sysname: bool = True, strip_domain: bool = False, ) -> dict: @@ - server_key = api.server_key if api is not None else server_key + resolved_server_key = api.server_key if api is not None else server_key @@ - existing_vm = find_by_librenms_id(VirtualMachine, librenms_id, server_key) + existing_vm = find_by_librenms_id(VirtualMachine, librenms_id, resolved_server_key) @@ - existing_device = find_by_librenms_id(Device, librenms_id, server_key) + existing_device = find_by_librenms_id(Device, librenms_id, resolved_server_key)Also applies to: 783-788, 808-812
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils.py` at line 641, A default of server_key: str = "default" causes callers that pass api=None to silently resolve against the wrong LibreNMS server; remove the implicit fallback by making server_key required (or Optional without "default") in the function signature(s) and update each local call site that disables VC detection (the calls that pass api=None) to explicitly forward the active server_key variable (e.g., call foo(..., api=None, server_key=server_key)); ensure you update all similar call sites in this module that disable VC detection so validation uses the correct server_key instead of "default".netbox_librenms_plugin/views/object_sync/devices.py (1)
129-145:⚠️ Potential issue | 🟠 MajorPass the same server-scoped table context into verify-row rendering.
The cache lookup is now keyed by
server_key, but the temporaryVCInterfaceTable/LibreNMSInterfaceTablebuilt here still omits that context. On non-default servers the JSON response can render buttons/data attributes for the wrong server, and if the constructors now requireserver_keythis path will fail outright. Reuse the same table-construction path asDeviceInterfaceTableView.get_table(), or pass the sameserver_keyinputs here as well.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/object_sync/devices.py` around lines 129 - 145, The cached lookup uses server_key but the temporary table here (VCInterfaceTable/LibreNMSInterfaceTable) is constructed without the same server-scoped context, causing wrong rendering or failure; update this branch to build the table the same way as DeviceInterfaceTableView.get_table() or pass server_key into the table constructor so the table instantiation for selected_device includes the server_key (e.g., reuse DeviceInterfaceTableView.get_table(...) or call VCInterfaceTable(..., server_key=server_key) / LibreNMSInterfaceTable(..., server_key=server_key)) before calling format_interface_data(port_data, selected_device).netbox_librenms_plugin/tests/test_sync_interfaces.py (1)
109-115: 🧹 Nitpick | 🔵 TrivialUse a sentinel here too to prove
descriptionstayed untouched.A fresh
MagicMockalready satisfiesiface.description != "eth0", so this test still passes if the implementation writesNone,"", or any other wrong value. Seeddescriptionwith a unique object and assert identity after the update.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_sync_interfaces.py` around lines 109 - 115, Test uses a fresh MagicMock for iface.description so it passes even if code sets None/empty; set iface.description to a unique sentinel object (e.g., object()) before calling view.update_interface_attributes and then assert that iface.description is still that sentinel (identity check) after the call to prove it was untouched; reference the test function using view.update_interface_attributes and the iface.description attribute to locate where to set the sentinel and change the assertion.netbox_librenms_plugin/tests/test_librenms_id.py (1)
97-105: 🧹 Nitpick | 🔵 TrivialAssert that the combined lookup is actually an
OR.These checks still pass if
find_by_librenms_id()regresses toQ(custom_field_data__librenms_id__default=42, custom_field_data__librenms_id=42), which would miss the legacy/per-server fallback behavior. Assertq_arg.connector == "OR"and the child values too, not just the field names.Also applies to: 147-155
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_librenms_id.py` around lines 97 - 105, The combined Q constructed in find_by_librenms_id must be asserted to be an OR and to contain the expected child lookups and values: update the test after retrieving call_args = mock_model.objects.filter.call_args to assert isinstance(q_arg, Q) and add assertions q_arg.connector == "OR" and that each child in q_arg.children is a tuple where the first element equals the expected lookup names ("custom_field_data__librenms_id__default" and "custom_field_data__librenms_id") and the second element equals the expected librenms id value (e.g., 42); apply the same stricter assertions to the second test block around lines 147-155 as well so regressions to a single combined lookup are caught.netbox_librenms_plugin/views/sync/device_fields.py (1)
423-429:⚠️ Potential issue | 🟠 MajorReject unknown
object_typevalues instead of defaulting them toDevice.Any value other than
"vm"/"virtualmachine"currently falls through the device branch. A stale or malformed form can therefore run the device permission check, lookup, and redirect path against the wrong record when PKs overlap. Validate the input against an explicit allowlist before choosing the target model.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/device_fields.py` around lines 423 - 429, The post method currently maps any non-"virtualmachine"/non-"vm" object_type to Device, which can mis-route permissions/lookup; modify post to validate request.POST["object_type"] against an explicit allowlist (e.g., {"device", "vm", "virtualmachine"}) before setting target_model and self.required_object_permissions, and reject unknown values with a clear failure (raise BadRequest or return an HTTP 400) instead of defaulting to Device; update the mapping logic that sets target_model (and the normalization from "virtualmachine" -> "vm") so only allowed names select VirtualMachine or Device and all other inputs are treated as invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils.py`:
- Around line 1368-1372: The code in import_single_device() writes the
librenms_id mapping using the raw server_key arg (or "default") which can be
incorrect because LibreNMSAPI(server_key=server_key) may resolve a different
active server; update the write to use the resolved server key from the API
instance (e.g., use the LibreNMSAPI object's server_key or equivalent resolved
attribute) when calling set_librenms_device_id(device, device_id,
resolved_server_key) before device.save(), so the mapping is persisted under the
actual server used.
In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 1345-1356: The helper _setup_librenms_id_match currently sets
filter.return_value.first.return_value for mock_device and mock_vm so every
Device.objects.filter(...).first() returns existing_device; change it to scope
the mock to only the librenms-id query by replacing the blanket return_value
with a side_effect on the filter call (or filter.return_value) that inspects
positional args for Q objects (the pattern used by find_by_librenms_id) and
returns a Mock whose first.return_value is existing_device only when those
positional Q args are present, otherwise return a Mock whose first.return_value
is None; update both mock_device.objects.filter and mock_vm.objects.filter to
use this scoped side_effect so hostname/serial matching paths are not affected.
In `@netbox_librenms_plugin/utils.py`:
- Around line 511-541: set_librenms_device_id currently treats string values
like "42" as unexpected and resets them, losing legacy mappings; update the
logic in set_librenms_device_id to recognize int-like strings (e.g., str of
digits or convertible to int) when reading cf_value and migrate them into the
dict form (cf_value = {"default": int(cf_value)}) instead of resetting to {};
likewise update migrate_legacy_librenms_id to handle int-like strings the same
way so an explicit migration will recover those values; refer to the cf_value
variable, server_key parameter, and the set_librenms_device_id and
migrate_legacy_librenms_id functions when making the changes.
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 442-445: The membership check treats only dict-backed custom-field
mappings as valid and ignores legacy bare-integer librenms_id values; normalize
cf_value before checks by detecting legacy types (e.g., int or string) and
converting them into the expected dict shape (for example mapping to a 'default'
key) or by calling the existing mapping helper used elsewhere; update the logic
where cf_value is read (the cf_value variable and its membership checks around
server_key in this file, including the shown block and the later block at lines
~473-480) so legacy ints are normalized prior to the isinstance(...) and
server_key membership checks so pre-migration mappings can be found and deleted.
---
Outside diff comments:
In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js`:
- Around line 783-811: The handleInterfaceChange function currently calls
response.json() without checking HTTP status; update handleInterfaceChange to
check response.ok after the fetch (when calling the verify-interface endpoint)
and handle non-OK responses by reading response.text() or response.json() for
diagnostics, then surface the error (use error.message or the response body) and
avoid updating the row; keep the success path that reads data.formatted_row and
calls initializeFilters() but add a .catch that logs/shows error.message so
failures (403/500) don’t silently leave the row stale; reference symbols:
handleInterfaceChange, response.ok, response.json(), initializeFilters(), and
the verify-interface POST.
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 33-44: The current VC branch checks obj.cf.get("librenms_id")
which is not server-aware and can block falling back to the VC sync device;
instead call the API to determine if this device maps for the active server.
Replace the obj.cf.get(...) check with a call to
self.librenms_api.get_librenms_id(obj) and if that returns falsy, call
get_librenms_sync_device(obj) and then call self.librenms_api.get_librenms_id on
the sync device; finally assign self.librenms_id from the API result and set
self._librenms_lookup_device to whichever device was actually used (obj or
sync_device) so subsequent logic uses the server-aware mapping.
---
Duplicate comments:
In `@netbox_librenms_plugin/forms.py`:
- Around line 584-585: The code currently constructs LibreNMSAPI() before
checking cache, which can raise and prevent using an existing
librenms_locations_choices:<server_key> cache; change the flow to first resolve
the active server key cheaply (use whatever helper or config lookup that yields
api.server_key without instantiating LibreNMSAPI), build cache_key =
f"librenms_locations_choices:{server_key}", check the cache and return cached
choices if present, and only instantiate LibreNMSAPI() (and call into it) when
the cache misses so that exceptions during API construction don’t discard valid
cached choices.
In `@netbox_librenms_plugin/import_utils.py`:
- Line 641: A default of server_key: str = "default" causes callers that pass
api=None to silently resolve against the wrong LibreNMS server; remove the
implicit fallback by making server_key required (or Optional without "default")
in the function signature(s) and update each local call site that disables VC
detection (the calls that pass api=None) to explicitly forward the active
server_key variable (e.g., call foo(..., api=None, server_key=server_key));
ensure you update all similar call sites in this module that disable VC
detection so validation uses the correct server_key instead of "default".
In `@netbox_librenms_plugin/tests/test_librenms_id.py`:
- Around line 97-105: The combined Q constructed in find_by_librenms_id must be
asserted to be an OR and to contain the expected child lookups and values:
update the test after retrieving call_args = mock_model.objects.filter.call_args
to assert isinstance(q_arg, Q) and add assertions q_arg.connector == "OR" and
that each child in q_arg.children is a tuple where the first element equals the
expected lookup names ("custom_field_data__librenms_id__default" and
"custom_field_data__librenms_id") and the second element equals the expected
librenms id value (e.g., 42); apply the same stricter assertions to the second
test block around lines 147-155 as well so regressions to a single combined
lookup are caught.
In `@netbox_librenms_plugin/tests/test_sync_interfaces.py`:
- Around line 109-115: Test uses a fresh MagicMock for iface.description so it
passes even if code sets None/empty; set iface.description to a unique sentinel
object (e.g., object()) before calling view.update_interface_attributes and then
assert that iface.description is still that sentinel (identity check) after the
call to prove it was untouched; reference the test function using
view.update_interface_attributes and the iface.description attribute to locate
where to set the sentinel and change the assertion.
In `@netbox_librenms_plugin/utils.py`:
- Around line 456-507: The current get_librenms_device_id treats legacy scalar
librenms_id values as valid for any server_key and leaves find_by_librenms_id
ignoring string IDs; change get_librenms_device_id to scope bare integer scalars
(and bare-string normalisation) to the default server only by returning the
scalar only when server_key == "default" (or matches the configured default
key), and when cf_value is dict keep per-server behaviour and normalise string
entries as already implemented (update obj.custom_field_data and save when
auto_save). Also update the corresponding lookup function find_by_librenms_id to
respect server scoping (only match scalar IDs for the default server) and to
consider string-stored IDs by normalising/handling strings the same way as
get_librenms_device_id so rows with "42" are discoverable without requiring a
write.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1121-1131: The other branches ('link', 'update', 'update_serial'
and the collision-check) still read/write a bare integer librenms_id; update
those paths to use the server-aware accessors: call
LibreNMSAPI.get_librenms_id(self.librenms_api, existing_device) when reading the
current mapping and use set_librenms_device_id(existing_device, librenms_id,
self.librenms_api.server_key) when writing so dict-backed per-server IDs aren't
overwritten; also update any conflict-check logic to compare via get_librenms_id
and adjust logging/messages to show the per-server dict format consistently and
keep the _save_device(existing_device) usage after writes.
In `@netbox_librenms_plugin/views/object_sync/devices.py`:
- Around line 129-145: The cached lookup uses server_key but the temporary table
here (VCInterfaceTable/LibreNMSInterfaceTable) is constructed without the same
server-scoped context, causing wrong rendering or failure; update this branch to
build the table the same way as DeviceInterfaceTableView.get_table() or pass
server_key into the table constructor so the table instantiation for
selected_device includes the server_key (e.g., reuse
DeviceInterfaceTableView.get_table(...) or call VCInterfaceTable(...,
server_key=server_key) / LibreNMSInterfaceTable(..., server_key=server_key))
before calling format_interface_data(port_data, selected_device).
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 423-429: The post method currently maps any
non-"virtualmachine"/non-"vm" object_type to Device, which can mis-route
permissions/lookup; modify post to validate request.POST["object_type"] against
an explicit allowlist (e.g., {"device", "vm", "virtualmachine"}) before setting
target_model and self.required_object_permissions, and reject unknown values
with a clear failure (raise BadRequest or return an HTTP 400) instead of
defaulting to Device; update the mapping logic that sets target_model (and the
normalization from "virtualmachine" -> "vm") so only allowed names select
VirtualMachine or Device and all other inputs are treated as invalid.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: f35ce380-ada1-4809-b593-9803442e6921
📒 Files selected for processing (28)
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/import_utils.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/_interface_sync_content.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/conftest.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/base/interfaces_view.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/mixins.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/sync/interfaces.py
| # Store librenms_id in per-server dict format so the mapping is | ||
| # traceable back to the source server (set_librenms_device_id also | ||
| # migrates any legacy bare-int value on write). | ||
| set_librenms_device_id(device, device_id, server_key or "default") | ||
| device.save() |
There was a problem hiding this comment.
Persist the resolved API server key here, not the raw function argument.
import_single_device() already resolves the active server through LibreNMSAPI(server_key=server_key), but this write still falls back to "default" when the caller omits server_key. That stores the mapping under the wrong server for callers that rely on the selected server in settings.
Suggested fix
- set_librenms_device_id(device, device_id, server_key or "default")
+ set_librenms_device_id(device, device_id, api.server_key)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Store librenms_id in per-server dict format so the mapping is | |
| # traceable back to the source server (set_librenms_device_id also | |
| # migrates any legacy bare-int value on write). | |
| set_librenms_device_id(device, device_id, server_key or "default") | |
| device.save() | |
| # Store librenms_id in per-server dict format so the mapping is | |
| # traceable back to the source server (set_librenms_device_id also | |
| # migrates any legacy bare-int value on write). | |
| set_librenms_device_id(device, device_id, api.server_key) | |
| device.save() |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/import_utils.py` around lines 1368 - 1372, The code in
import_single_device() writes the librenms_id mapping using the raw server_key
arg (or "default") which can be incorrect because
LibreNMSAPI(server_key=server_key) may resolve a different active server; update
the write to use the resolved server key from the API instance (e.g., use the
LibreNMSAPI object's server_key or equivalent resolved attribute) when calling
set_librenms_device_id(device, device_id, resolved_server_key) before
device.save(), so the mapping is persisted under the actual server used.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
netbox_librenms_plugin/import_utils/device_operations.py (1)
160-205:⚠️ Potential issue | 🟡 MinorInitialize
librenms_id_needs_migrationin the base validation result.Right now the key only exists on legacy matches. That makes the return shape inconsistent and forces every consumer to use
.get()or handleKeyError. Seed it withFalseinresultand only flip it toTruein these branches.🔧 Suggested fix
"name_matches": False, # True when existing device name matches LibreNMS sysName "name_sync_available": False, # True when existing device name differs from sysName "suggested_name": None, # sysName to suggest when name_sync_available is True "device_type_mismatch": False, # True when existing device's type differs from LibreNMS + "librenms_id_needs_migration": False, "issues": [], "warnings": [],Also applies to: 247-249, 270-272
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/device_operations.py` around lines 160 - 205, The result dict created in the validation flow must include a default "librenms_id_needs_migration": False key so the return shape is consistent; add this key to the initial result literal (alongside keys like "is_ready", "can_import", "resolved_name") and leave it False by default, then keep the existing code paths that currently set librenms_id_needs_migration = True in the legacy-match branches (the blocks that currently add that key only for legacy matches) so they flip the value to True when appropriate (do not remove those assignments, only ensure the key exists initially).
♻️ Duplicate comments (1)
netbox_librenms_plugin/views/sync/device_fields.py (1)
442-480:⚠️ Potential issue | 🟠 MajorHandle legacy bare-integer
librenms_idvalues in the removal path.Both membership checks only accept dict-backed custom-field data. Objects still carrying the legacy integer format will always hit “No mapping found” here and can never be cleared, even though the rest of this PR still treats that value as a readable
defaultmapping. Route these reads/writes through the sharedlibrenms_idhelpers, or normalize legacy values before both the pre-check and the locked re-check.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/device_fields.py` around lines 442 - 480, The removal path currently assumes obj.custom_field_data.get("librenms_id") is a dict (cf_value) and rejects legacy bare-integer values; update the pre-check and the locked re-check to normalize legacy integers into the dict form (or call the shared librenms_id helper) before performing membership and deletion logic: convert an int cf_value to {"default": int_value} (or use the existing helper that returns a dict) when reading cf_value and when reloading obj_locked.custom_field_data, then perform the server_key membership check and deletion on that normalized dict and write back None when empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Line 65: Create a centralized cache-key formatter (e.g., a module-level
constant like LOCATION_CHOICES_KEY_FMT or a helper function
get_location_choices_cache_key(server_key)) and replace the hardcoded literal
"librenms_locations_choices:{server_key}" (see the use assigning
location_cache_key) with a call/format using that helper; update all
readers/writers in this module to use the new helper so the key format is
defined in one place and cannot drift.
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Line 91: import_single_device currently re-validates objects using the
implicit "default" server when validation is None, causing server-aware lookups
to be bypassed and duplicates created; update the fallback validation path in
import_single_device to pass the active server_key (the server_key parameter)
into the validation/lookup call instead of hardcoding "default" so the lookup is
server-aware, and apply the same change to the other affected validation
fallbacks (the blocks around the regions referenced at 233-238 and 697-704) to
ensure all re-validations use the provided server_key.
---
Outside diff comments:
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 160-205: The result dict created in the validation flow must
include a default "librenms_id_needs_migration": False key so the return shape
is consistent; add this key to the initial result literal (alongside keys like
"is_ready", "can_import", "resolved_name") and leave it False by default, then
keep the existing code paths that currently set librenms_id_needs_migration =
True in the legacy-match branches (the blocks that currently add that key only
for legacy matches) so they flip the value to True when appropriate (do not
remove those assignments, only ensure the key exists initially).
---
Duplicate comments:
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 442-480: The removal path currently assumes
obj.custom_field_data.get("librenms_id") is a dict (cf_value) and rejects legacy
bare-integer values; update the pre-check and the locked re-check to normalize
legacy integers into the dict form (or call the shared librenms_id helper)
before performing membership and deletion logic: convert an int cf_value to
{"default": int_value} (or use the existing helper that returns a dict) when
reading cf_value and when reloading obj_locked.custom_field_data, then perform
the server_key membership check and deletion on that normalized dict and write
back None when empty.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: a07be972-43fc-4d5b-a6d5-2e9467c5b2c1
📒 Files selected for processing (4)
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: test-netbox (3.12)
- GitHub Check: test-netbox (3.14)
🧰 Additional context used
📓 Path-based instructions (4)
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.py
netbox_librenms_plugin/views/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/**/*.py: Views must follow a three-layer structure: Base views inviews/base/, Object sync views inviews/object_sync/, and Sync action views inviews/sync/with shared mixins fromviews/mixins.py
New views must extend the closest base class and compose mixins fromviews/mixins.py(LibreNMSPermissionMixin,NetBoxObjectPermissionMixin,LibreNMSAPIMixin,CacheMixin,VlanAssignmentMixin)
All views must inheritLibreNMSPermissionMixinfromviews/mixins.pywithpermission_required = PERM_VIEW_PLUGIN
Declarerequired_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request
Use_get_safe_redirect_url(request)to validate referrer URLs in permission checks to prevent open-redirect attacks
Files:
netbox_librenms_plugin/views/sync/device_fields.py
netbox_librenms_plugin/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/sync/**/*.py: Sync POST handlers must callrequire_all_permissions()(not justrequire_write_permission()) and return early if it returns a response; userequire_all_permissions_json()for AJAX/JSON endpoints
Follow sync conventions defined in.github/instructions/sync.instructions.mdfor sync views, base views, tables, and sync JavaScript
Files:
netbox_librenms_plugin/views/sync/device_fields.py
**/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
Sync action views must follow the pattern: check permissions with
LibreNMSPermissionMixinandNetBoxObjectPermissionMixin, read selected items fromrequest.POST.getlist('select'), load cached data usingCacheMixin.get_cache_key(), apply changes insidetransaction.atomic(), and redirect to the sync tab with?tab=<resource>.
Files:
netbox_librenms_plugin/views/sync/device_fields.py
🧠 Learnings (36)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`.
Applied to files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py|**/views/sync/**/*.py : Cache keys for sync data must follow the format: `librenms_{data_type}_{model_name}_{pk}` for fetched data and `librenms_{data_type}_last_fetched_{model_name}_{pk}` for fetch timestamps. VLAN group overrides must use `get_vlan_overrides_key(obj)`.
Applied to files:
netbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py,**/jobs.py : Cache key generation must use helper functions: `get_validated_device_cache_key()`, `get_cache_metadata_key()`, `get_active_cached_searches()`, `get_import_device_cache_key()`. Never hardcode cache key formats
Applied to files:
netbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module.
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Reuse fixtures from `tests/conftest.py` instead of creating ad-hoc mocks: Configuration (`mock_multi_server_config`, `mock_legacy_config`), API client (`mock_librenms_api`), NetBox objects (`mock_netbox_device`, `mock_netbox_vm`, `mock_netbox_site`, `mock_netbox_platform`, `mock_netbox_device_type`, `mock_netbox_device_role`, `mock_netbox_cluster`, `mock_netbox_rack`), HTTP responses (`mock_response_factory`, `mock_success_response`, `mock_device_response`, `mock_error_response`, `mock_auth_error_response`), and Import workflow (`sample_librenms_device`, `sample_librenms_device_minimal`, `sample_validation_state`, `sample_validation_state_vm`).
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances.
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_netbox_librenms_plugin.py : `test_netbox_librenms_plugin.py` is an empty placeholder — do not add tests there.
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions in `utils.py` (`find_matching_site`, `match_librenms_hardware_to_device_type`, `find_matching_platform`)
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `process_device_filters(filters, ...)` must fetch and validate devices from LibreNMS, returning a list
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`.
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/sync/**/*.py : Sync action views must follow the pattern: check permissions with `LibreNMSPermissionMixin` and `NetBoxObjectPermissionMixin`, read selected items from `request.POST.getlist('select')`, load cached data using `CacheMixin.get_cache_key()`, apply changes inside `transaction.atomic()`, and redirect to the sync tab with `?tab=<resource>`.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Follow sync conventions defined in `.github/instructions/sync.instructions.md` for sync views, base views, tables, and sync JavaScript
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/models.py : Coordinate schema changes through Django migrations in `migrations/` directory; update `models.py`, admin, and Pydantic representations accordingly
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Views must follow a three-layer structure: Base views in `views/base/`, Object sync views in `views/object_sync/`, and Sync action views in `views/sync/` with shared mixins from `views/mixins.py`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,tables}/**/*.{html,py} : Templates live in `templates/netbox_librenms_plugin/`; reuse/includes under `inc/`. Sync pages extend `librenms_sync_base.html`. Tables emit HTMX-enabled columns and buttons (`tables/*.py`), so prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `BulkImportDevicesView` (POST) must execute import: background mode enqueues `ImportDevicesJob`; sync mode calls `bulk_import_devices()` + `bulk_import_vms()` and returns OOB row swaps with `HX-Trigger: closeModal`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript VLAN modal functions must implement `openVlanDetailModal()`, `verifyVlanInGroup()`, `verifyVlanSyncGroup()` for per-interface VLAN detail editing.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript verification functions must include `handleInterfaceChange()`, `handleCableChange()`, `handleVRFChange()` that POST to single-item verify endpoints to validate resource changes.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Declare `required_object_permissions` as a dict mapping HTTP methods to `[(action, Model)]` tuples for NetBox model operations; some views may set this dynamically per-request
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Sync POST handlers must call `require_all_permissions()` (not just `require_write_permission()`) and return early if it returns a response; use `require_all_permissions_json()` for AJAX/JSON endpoints
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_permissions.py : For `NetBoxObjectPermissionMixin` tests, set `required_object_permissions` on the instance before calling `check_object_permissions()`.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/views.py : API endpoints must use `LibreNMSPluginPermission` class in `api/views.py` where GET checks `view_librenmssettings` and other methods check `change_librenmssettings`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`)
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/object_sync/**/*.py : Object sync view methods must create instances of concrete table views, copy the `request` object, and call `get_context_data()`. VMs must skip cables and VLANs by returning `None` from those `get_*_context()` methods.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Per-device dropdown update views (`DeviceRoleUpdateView`, `DeviceClusterUpdateView`, `DeviceRackUpdateView`) must apply selection to validation state and return re-rendered row via `render_device_row()`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `ImportDevicesJob` background job: imports devices and VMs. Must call `bulk_import_devices_shared()` for devices and `bulk_import_vms()` for VMs. `job.data` keys must include `imported_device_pks`, `imported_vm_pks`, `imported_libre_device_ids`, `imported_libre_vm_ids`, `server_key`, `total`, `success_count`, `failed_count`, `skipped_count`, `virtual_chassis_created`, `errors`, `completed`
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `validate_device_for_import(device, ...)` is the core validation function that produces validation state dict
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_devices_shared(devices, user, ...)` must be the shared implementation between sync and background import
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py : Use `LibreNMSAPIMixin` to provide lazy-loaded `LibreNMSAPI` instance via `self.librenms_api` property and `get_server_info()` method for template context.
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
🔇 Additional comments (8)
netbox_librenms_plugin/tests/test_import_utils.py (6)
1345-1356: The blanketreturn_valueapproach still doesn't scope the mock to librenms-id queries only.The docstring explains the Q-object usage but doesn't address the core concern: with
return_value, everyDevice.objects.filter(...).first()returnsexisting_device, so tests pass even if the code falls through to hostname/serial matching rather than usingfind_by_librenms_id().
1645-1651: LGTM!The
*argsaddition correctly allowsdevice_filterto accept positional Q-object arguments fromfind_by_librenms_id(), while the existing kwargs-based routing (if "serial" in kwargs) handles differentiation between query types.
1836-1843: LGTM!This is the correct approach for scoping mock responses: checking
if args:distinguishes Q-object calls fromfind_by_librenms_id()versus keyword-based queries (serial, hostname), ensuring accurate test behavior.
1876-1886: LGTM!The combined logic correctly handles librenms_id lookups (via
argscheck), serial drift detection, and excludes/filters appropriately. This provides accurate mock behavior for the test scenarios.
1918-1925: LGTM!Consistent with the established pattern for Q-object detection. The mock properly returns
existingonly for librenms_id lookups while returningNonefor other query types.
1961-1968: LGTM!The signature update to
*args, **kwargsensures compatibility with Q-object calls while maintaining the serial-based routing logic.netbox_librenms_plugin/views/sync/device_fields.py (2)
285-294: Atomic platform creation/assignment looks solid.Wrapping both writes in one transaction avoids leaving an orphaned
Platformbehind if the device update fails.
423-429: Scoped per-model permission checks are fixed.Resolving
object_typebeforerequire_all_permissions()keeps the POST permission gate aligned with the object actually being mutated.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
netbox_librenms_plugin/views/sync/device_fields.py (2)
442-480:⚠️ Potential issue | 🟠 MajorNormalize legacy bare-integer mappings before both membership checks.
The rest of the PR still treats a legacy bare integer as the readable
"default"mapping, but Line 442 and Line 473 only accept dict-backed values. An object still storinglibrenms_id = 42will always hit “No mapping found” or “already removed” here and cannot be cleaned up through this endpoint. Normalize the legacy value before both checks, or route deletion through the shared helper layer.🛠️ Minimal fix
- cf_value = obj.custom_field_data.get("librenms_id") + cf_value = obj.custom_field_data.get("librenms_id") + if isinstance(cf_value, int): + cf_value = {"default": cf_value} if not isinstance(cf_value, dict) or server_key not in cf_value: messages.warning(request, f"No mapping found for server '{server_key}'.") return redirect(sync_url, pk=pk) @@ - cf = obj_locked.custom_field_data.get("librenms_id", {}) + cf = obj_locked.custom_field_data.get("librenms_id", {}) + if isinstance(cf, int): + cf = {"default": cf} # Re-check after acquiring lock; mirror the pre-transaction protection logic _is_protected = server_key in configured_servers or ( legacy_url_configured and not configured_servers and server_key == "default" )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/device_fields.py` around lines 442 - 480, The handlers use cf_value = obj.custom_field_data.get("librenms_id") and later cf = obj_locked.custom_field_data.get("librenms_id", {}) but only treat dicts, so legacy bare integers (e.g. 42) are ignored; normalize a legacy integer/string librenms_id into the dict form { "default": <value> } before both the first membership check (where cf_value is inspected for server_key) and before the locked re-check (where cf is read from obj_locked) so the subsequent logic that checks server_key and deletes the mapping works for legacy single-server entries; update the code around cf_value and cf normalization (referencing cf_value, server_key, configured_servers, legacy_url_configured, obj_locked, cf) to perform this conversion.
423-429:⚠️ Potential issue | 🟠 MajorFail closed on unexpected
object_typevalues.This only remaps
"virtualmachine"to"vm"; every other unexpected value still falls through toDevice. A malformed or tampered POST can therefore scope permissions, lookups, and deletion to the wrong model when device and VM PKs overlap. Normalize with.strip().lower()and reject anything outside the allowed set before selectingtarget_model.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/device_fields.py` around lines 423 - 429, Normalize and validate the incoming object_type in the post method: call .strip().lower() on request.POST.get("object_type", "device"), only allow the explicit set {"device","virtualmachine","vm"}, and if the value is not in that set immediately reject the request (e.g., raise PermissionDenied or return HttpResponseBadRequest) instead of falling back to Device; then map "virtualmachine" to "vm" and set target_model = VirtualMachine if object_type == "vm" else Device and keep setting self.required_object_permissions = {"POST": [("change", target_model)]}.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 311-319: The catch-all ValidationError handler for the block
around device.full_clean() incorrectly always reports a "slug collision"
platform error; update the error handling so you distinguish platform creation
failures from device validation failures: either split the try/except so
platform creation (the code that uses platform_name/slug) has its own except
that shows the slug-collision message, and the device.full_clean() call has a
separate except that surfaces the actual ValidationError e (use
logger.error(..., exc_info=True) and messages.error(request, str(e)) or a
user-friendly variant), or keep a single handler but inspect the ValidationError
contents before choosing the platform-slug message; target the ValidationError
handling around device.full_clean(), logger.error, and messages.error to
implement this.
---
Duplicate comments:
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 442-480: The handlers use cf_value =
obj.custom_field_data.get("librenms_id") and later cf =
obj_locked.custom_field_data.get("librenms_id", {}) but only treat dicts, so
legacy bare integers (e.g. 42) are ignored; normalize a legacy integer/string
librenms_id into the dict form { "default": <value> } before both the first
membership check (where cf_value is inspected for server_key) and before the
locked re-check (where cf is read from obj_locked) so the subsequent logic that
checks server_key and deletes the mapping works for legacy single-server
entries; update the code around cf_value and cf normalization (referencing
cf_value, server_key, configured_servers, legacy_url_configured, obj_locked, cf)
to perform this conversion.
- Around line 423-429: Normalize and validate the incoming object_type in the
post method: call .strip().lower() on request.POST.get("object_type", "device"),
only allow the explicit set {"device","virtualmachine","vm"}, and if the value
is not in that set immediately reject the request (e.g., raise PermissionDenied
or return HttpResponseBadRequest) instead of falling back to Device; then map
"virtualmachine" to "vm" and set target_model = VirtualMachine if object_type ==
"vm" else Device and keep setting self.required_object_permissions = {"POST":
[("change", target_model)]}.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4edf8324-bf11-4fe2-98d6-9353a3a7dbc8
📒 Files selected for processing (2)
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: test-netbox (3.14)
🧰 Additional context used
📓 Path-based instructions (7)
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/views/sync/device_fields.py
netbox_librenms_plugin/views/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/**/*.py: Views must follow a three-layer structure: Base views inviews/base/, Object sync views inviews/object_sync/, and Sync action views inviews/sync/with shared mixins fromviews/mixins.py
New views must extend the closest base class and compose mixins fromviews/mixins.py(LibreNMSPermissionMixin,NetBoxObjectPermissionMixin,LibreNMSAPIMixin,CacheMixin,VlanAssignmentMixin)
All views must inheritLibreNMSPermissionMixinfromviews/mixins.pywithpermission_required = PERM_VIEW_PLUGIN
Declarerequired_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request
Use_get_safe_redirect_url(request)to validate referrer URLs in permission checks to prevent open-redirect attacks
Files:
netbox_librenms_plugin/views/sync/device_fields.py
netbox_librenms_plugin/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/sync/**/*.py: Sync POST handlers must callrequire_all_permissions()(not justrequire_write_permission()) and return early if it returns a response; userequire_all_permissions_json()for AJAX/JSON endpoints
Follow sync conventions defined in.github/instructions/sync.instructions.mdfor sync views, base views, tables, and sync JavaScript
Files:
netbox_librenms_plugin/views/sync/device_fields.py
**/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
Sync action views must follow the pattern: check permissions with
LibreNMSPermissionMixinandNetBoxObjectPermissionMixin, read selected items fromrequest.POST.getlist('select'), load cached data usingCacheMixin.get_cache_key(), apply changes insidetransaction.atomic(), and redirect to the sync tab with?tab=<resource>.
Files:
netbox_librenms_plugin/views/sync/device_fields.py
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Follow frontend conventions for templates and static files defined in
.github/instructions/frontend.instructions.md
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer for table row updates. Table row updates must return<tr hx-swap-oob="true">. AvoidouterHTMLswaps; use OOB (Out-of-Band) or targetedinnerHTMLswaps to keep table layout intact.
Styling assumes Tabler defaults. Removingtable-responsivewrappers was deliberate to prevent dropdown clipping—do not re-add them.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/{templates,static}/**/*.{html,js}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/{templates,static}/**/*.{html,js}: All HTMX requests andfetch()calls must include a CSRF token. The standard pattern isdocument.querySelector('[name=csrfmiddlewaretoken]').value(from a hidden form input). The import JS also usesgetCookie('csrftoken')as a fallback — prefer the hidden input approach for consistency.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers. Buttons target thehtmx-modal-contentelement and JavaScript inlibrenms_import.htmltoggles the wrapper. Do not reintroducedata-bs-toggleor duplicate modal IDs.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/{templates,tables}/**/*.{html,py}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates live in
templates/netbox_librenms_plugin/; reuse/includes underinc/. Sync pages extendlibrenms_sync_base.html. Tables emit HTMX-enabled columns and buttons (tables/*.py), so prefer updating the table renderer in Python rather than templates when changing row actions.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
🧠 Learnings (32)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Follow sync conventions defined in `.github/instructions/sync.instructions.md` for sync views, base views, tables, and sync JavaScript
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/sync/**/*.py : Sync action views must follow the pattern: check permissions with `LibreNMSPermissionMixin` and `NetBoxObjectPermissionMixin`, read selected items from `request.POST.getlist('select')`, load cached data using `CacheMixin.get_cache_key()`, apply changes inside `transaction.atomic()`, and redirect to the sync tab with `?tab=<resource>`.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/models.py : Coordinate schema changes through Django migrations in `migrations/` directory; update `models.py`, admin, and Pydantic representations accordingly
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,tables}/**/*.{html,py} : Templates live in `templates/netbox_librenms_plugin/`; reuse/includes under `inc/`. Sync pages extend `librenms_sync_base.html`. Tables emit HTMX-enabled columns and buttons (`tables/*.py`), so prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Views must follow a three-layer structure: Base views in `views/base/`, Object sync views in `views/object_sync/`, and Sync action views in `views/sync/` with shared mixins from `views/mixins.py`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Sync POST handlers must call `require_all_permissions()` (not just `require_write_permission()`) and return early if it returns a response; use `require_all_permissions_json()` for AJAX/JSON endpoints
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript VLAN modal functions must implement `openVlanDetailModal()`, `verifyVlanInGroup()`, `verifyVlanSyncGroup()` for per-interface VLAN detail editing.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions in `utils.py` (`find_matching_site`, `match_librenms_hardware_to_device_type`, `find_matching_platform`)
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript verification functions must include `handleInterfaceChange()`, `handleCableChange()`, `handleVRFChange()` that POST to single-item verify endpoints to validate resource changes.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `BulkImportDevicesView` (POST) must execute import: background mode enqueues `ImportDevicesJob`; sync mode calls `bulk_import_devices()` + `bulk_import_vms()` and returns OOB row swaps with `HX-Trigger: closeModal`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Declare `required_object_permissions` as a dict mapping HTTP methods to `[(action, Model)]` tuples for NetBox model operations; some views may set this dynamically per-request
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_permissions.py : For `NetBoxObjectPermissionMixin` tests, set `required_object_permissions` on the instance before calling `check_object_permissions()`.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/views.py : API endpoints must use `LibreNMSPluginPermission` class in `api/views.py` where GET checks `view_librenmssettings` and other methods check `change_librenmssettings`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`)
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/object_sync/**/*.py : Object sync view methods must create instances of concrete table views, copy the `request` object, and call `get_context_data()`. VMs must skip cables and VLANs by returning `None` from those `get_*_context()` methods.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Per-device dropdown update views (`DeviceRoleUpdateView`, `DeviceClusterUpdateView`, `DeviceRackUpdateView`) must apply selection to validation state and return re-rendered row via `render_device_row()`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/_*_sync{,_content}.html : Each sync resource has two templates following a naming convention: `_<resource>_sync.html` (the tab wrapper, loaded once when the tab is selected) and `_<resource>_sync_content.html` (the HTMX-swappable inner fragment, refreshed on data changes without a full page reload). Current resources: `_interface_sync`, `_cable_sync`, `_ipaddress_sync`, `_vlan_sync`. When adding a new sync resource, create both the wrapper and content templates following this pattern.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*.html : HTMX fragments live in `templates/netbox_librenms_plugin/htmx/` and include: `device_import_row.html` (individual import row updates), `device_validation_details.html` (expandable validation details), `device_vc_details.html` (virtual chassis member details), `bulk_import_confirm.html` (import confirmation modal content). Keep server responses and HTMX targets in sync when modifying these fragments.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Sync pipelines must follow the pattern: fetch LibreNMS data via `librenms_api.py`, cache with `CacheMixin`, build comparison tables via `tables/*.py`, and render HTMX fragments from `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Keep REST endpoint server responses and HTMX targets in sync; implement REST endpoints in `views/imports/actions.py` and HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/settings.html : `settings.html` uses a split-form pattern: two separate Django forms (`ServerConfigForm` + `ImportSettingsForm`) sharing one page, differentiated by a hidden `form_type` field (`"server_config"` or `"import_settings"`). The test-connection button is an HTMX POST to `TestLibreNMSConnectionView`, returning an inline alert fragment.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer for table row updates. Table row updates must return `<tr hx-swap-oob="true">`. Avoid `outerHTML` swaps; use OOB (Out-of-Band) or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,static}/**/*.{html,js} : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers. Buttons target the `htmx-modal-content` element and JavaScript in `librenms_import.html` toggles the wrapper. Do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript URL and tab state management must implement `initializeTabs()`, `getDeviceIdFromUrl()`, and `setInterfaceNameFieldFromURL()` to maintain browser state and URL synchronization.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Removing `table-responsive` wrappers was deliberate to prevent dropdown clipping—do not re-add them.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
🔇 Additional comments (2)
netbox_librenms_plugin/views/sync/device_fields.py (1)
285-294: Good atomic create-and-assign flow.Wrapping
Platformcreation and device assignment in one transaction avoids leaving an orphaned platform behind when the later device validation/save fails.netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (1)
319-330: Good guard for LibreNMS'"-"sysName sentinel.This avoids showing a bogus sync button or success state when LibreNMS returns its placeholder instead of a real hostname.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js (1)
615-632:⚠️ Potential issue | 🟡 MinorUse
error.messagein this catch.This fetch branch still logs the raw error object, while the rest of this file now standardizes on
error.message. Keeping that consistent makes failed override persistence easier to diagnose.As per coding guidelines, "In catch blocks, show
error.messagefor debugging rather than generic messages."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js` around lines 615 - 632, The catch block for the fetch to '/plugins/librenms_plugin/save-vlan-group-overrides/' currently logs the raw error object; change it to log the error.message instead to match the rest of the file. Locate the fetch call that sends device_id/deviceId and vid_group_map/vidGroupMap and replace the catch handler's console.error call so it outputs the error.message (e.g., console.error('Failed to persist VLAN group overrides:', error.message)) rather than the full error object.
♻️ Duplicate comments (3)
netbox_librenms_plugin/views/sync/device_fields.py (2)
312-320:⚠️ Potential issue | 🟡 MinorDon't label every
ValidationErroras a slug collision.This branch also catches
device.full_clean(), so unrelated device validation failures are currently reported as platform-name collisions.🩹 Minimal fix
except ValidationError as e: + error_msg = e.message_dict if hasattr(e, "message_dict") else str(e) logger.error( f"ValidationError assigning platform '{platform_name}' to device pk={pk}: {e}", exc_info=True, ) messages.error( request, - f"Platform '{platform_name}' could not be created (slug collision). Try a different name.", + f"Failed to assign platform '{platform_name}': {error_msg}", ) return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/device_fields.py` around lines 312 - 320, The current except block around ValidationError conflates platform creation errors and device.full_clean() errors; separate the concerns by splitting the try/except: wrap platform creation/assignment (the code that uses platform_name to create or save a Platform) in its own try/except that catches ValidationError and logs/messages the slug-collision message referencing platform_name and pk, and wrap device.full_clean() (or any device validation calls) in a separate try/except that logs/messages a device validation failure (include pk and the ValidationError details). Update the logger.error and messages.error calls accordingly so platform-specific errors only use the "slug collision" message and device.full_clean() errors use a distinct message that reports device validation problems.
445-449:⚠️ Potential issue | 🟠 MajorNormalize bare string legacy IDs before the mapping checks.
The rest of the PR accepts
"42"as a legacylibrenms_id, but this view only wraps bareints into{"default": ...}. Objects still storing an int-like string will fall through to “No mapping found” / “already removed,” so the orphaned mapping remains undeletable.Also applies to: 479-483
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/device_fields.py` around lines 445 - 449, The code that normalizes legacy bare-integer custom field values (the block starting with cf_value = obj.custom_field_data.get("librenms_id") and the subsequent "Normalize legacy bare-integer" logic) only handles int types; update it (and the equivalent second occurrence handling later in the same module) to also detect numeric strings (e.g., isinstance(cf_value, str) and cf_value.isdigit()) and normalize them to the same dict form (e.g., {"default": int(cf_value)}) before the server_key membership check so legacy IDs stored as "42" are treated the same as 42 and mapping cleanup works. Ensure you adjust both places where cf_value is normalized (the first cf_value normalization and the similar block around the later mapping removal logic) and keep server_key usage unchanged.netbox_librenms_plugin/utils.py (1)
456-507:⚠️ Potential issue | 🟠 MajorLegacy fallback is still cross-server and string-blind.
A bare legacy
librenms_idis still returned for everyserver_key, andfind_by_librenms_id()folds that same bare-value branch into every server-specific lookup while ignoring string-backed storage like"42". In a multi-server setup, that lets server A’s legacy42shadow server B’s42, and string-backed rows remain invisible until some unrelated read path normalizes them.Also applies to: 554-573
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/utils.py` around lines 456 - 507, The function get_librenms_device_id (and the related find_by_librenms_id code paths) currently treat a bare integer/string librenms_id as valid for every server_key, which lets a legacy value on one server shadow another and ignores string-backed IDs; change the logic so a bare int or bare string is only accepted as a legacy fallback when server_key == "default" (or whichever canonical legacy key your plugin uses), otherwise return None; also ensure the string branch normalises and saves only for the canonical legacy key and mirror the same server-key-aware behaviour in find_by_librenms_id to avoid cross-server shadowing and to surface string-backed IDs in multi-server setups.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 35-45: The VC fallback isn't server-aware: change the call in
librenms_sync_view (where get_librenms_sync_device(obj) is used when
self.librenms_api.get_librenms_id(obj) is falsy) to pass the active server_key
(e.g., get_librenms_sync_device(obj, server_key)), and update
get_librenms_sync_device to accept that server_key and resolve candidates by
calling LibreNMSAPI.get_librenms_id(candidate, server_key) (do not read raw CFs)
so the selected librenms_lookup_device is chosen for the correct server context
before assigning self.librenms_id and self._librenms_lookup_device.
---
Outside diff comments:
In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js`:
- Around line 615-632: The catch block for the fetch to
'/plugins/librenms_plugin/save-vlan-group-overrides/' currently logs the raw
error object; change it to log the error.message instead to match the rest of
the file. Locate the fetch call that sends device_id/deviceId and
vid_group_map/vidGroupMap and replace the catch handler's console.error call so
it outputs the error.message (e.g., console.error('Failed to persist VLAN group
overrides:', error.message)) rather than the full error object.
---
Duplicate comments:
In `@netbox_librenms_plugin/utils.py`:
- Around line 456-507: The function get_librenms_device_id (and the related
find_by_librenms_id code paths) currently treat a bare integer/string
librenms_id as valid for every server_key, which lets a legacy value on one
server shadow another and ignores string-backed IDs; change the logic so a bare
int or bare string is only accepted as a legacy fallback when server_key ==
"default" (or whichever canonical legacy key your plugin uses), otherwise return
None; also ensure the string branch normalises and saves only for the canonical
legacy key and mirror the same server-key-aware behaviour in find_by_librenms_id
to avoid cross-server shadowing and to surface string-backed IDs in multi-server
setups.
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 312-320: The current except block around ValidationError conflates
platform creation errors and device.full_clean() errors; separate the concerns
by splitting the try/except: wrap platform creation/assignment (the code that
uses platform_name to create or save a Platform) in its own try/except that
catches ValidationError and logs/messages the slug-collision message referencing
platform_name and pk, and wrap device.full_clean() (or any device validation
calls) in a separate try/except that logs/messages a device validation failure
(include pk and the ValidationError details). Update the logger.error and
messages.error calls accordingly so platform-specific errors only use the "slug
collision" message and device.full_clean() errors use a distinct message that
reports device validation problems.
- Around line 445-449: The code that normalizes legacy bare-integer custom field
values (the block starting with cf_value =
obj.custom_field_data.get("librenms_id") and the subsequent "Normalize legacy
bare-integer" logic) only handles int types; update it (and the equivalent
second occurrence handling later in the same module) to also detect numeric
strings (e.g., isinstance(cf_value, str) and cf_value.isdigit()) and normalize
them to the same dict form (e.g., {"default": int(cf_value)}) before the
server_key membership check so legacy IDs stored as "42" are treated the same as
42 and mapping cleanup works. Ensure you adjust both places where cf_value is
normalized (the first cf_value normalization and the similar block around the
later mapping removal logic) and keep server_key usage unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 39defe9f-0d6f-4a11-8e58-15580f18afa5
📒 Files selected for processing (7)
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.jsnetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: test-netbox (3.14)
🧰 Additional context used
📓 Path-based instructions (8)
netbox_librenms_plugin/{templates,static}/**/*.{html,js}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/{templates,static}/**/*.{html,js}: All HTMX requests andfetch()calls must include a CSRF token. The standard pattern isdocument.querySelector('[name=csrfmiddlewaretoken]').value(from a hidden form input). The import JS also usesgetCookie('csrftoken')as a fallback — prefer the hidden input approach for consistency.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers. Buttons target thehtmx-modal-contentelement and JavaScript inlibrenms_import.htmltoggles the wrapper. Do not reintroducedata-bs-toggleor duplicate modal IDs.
Files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
netbox_librenms_plugin/static/**/*.js
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/static/**/*.js: Always checkresponse.okbefore processing fetch responses to catch HTTP errors. In catch blocks, showerror.messagefor debugging rather than generic messages.
ThecreateCacheCountdown()function is a generic countdown timer for cache expiration display.
Files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
**/librenms_sync.js
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
**/librenms_sync.js: JavaScript inlibrenms_sync.jsmust not be wrapped in an IIFE and must use a master initializerinitializeScripts()that runs on bothDOMContentLoadedandhtmx:afterSwapevents.
JavaScript checkbox management must include functionsinitializeTableCheckboxes()andupdateBulkActionButton()to handle multi-table checkbox selection and bulk action button state.
JavaScript TomSelect dropdown initialization must use aTOMSELECT_INIT_DELAY_MS = 100constant and implement delayed initialization after HTMX swaps. Required initializer functions:initializeVCMemberSelect(),initializeVRFSelects(),initializeVlanGroupSelects(),initializeVlanSyncGroupSelects().
JavaScript verification functions must includehandleInterfaceChange(),handleCableChange(),handleVRFChange()that POST to single-item verify endpoints to validate resource changes.
JavaScript VLAN modal functions must implementopenVlanDetailModal(),verifyVlanInGroup(),verifyVlanSyncGroup()for per-interface VLAN detail editing.
JavaScript bulk operations must include functionsinitializeBulkEditApply()anddeleteSelectedInterfaces()to handle bulk edit and delete actions.
JavaScript table filtering must implementinitializeTableFilters()andfilterTable()functions for client-side row filtering.
JavaScript URL and tab state management must implementinitializeTabs(),getDeviceIdFromUrl(), andsetInterfaceNameFieldFromURL()to maintain browser state and URL synchronization.
JavaScript cache countdown functionality must implementinitializeCountdown()andinitializeCountdowns()functions to display and manage cache expiration timers.
JavaScript CSRF token must be extracted viadocument.querySelector('[name=csrfmiddlewaretoken]').valuefor all POST requests.
Files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_librenms_id.py
netbox_librenms_plugin/views/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/**/*.py: Views must follow a three-layer structure: Base views inviews/base/, Object sync views inviews/object_sync/, and Sync action views inviews/sync/with shared mixins fromviews/mixins.py
New views must extend the closest base class and compose mixins fromviews/mixins.py(LibreNMSPermissionMixin,NetBoxObjectPermissionMixin,LibreNMSAPIMixin,CacheMixin,VlanAssignmentMixin)
All views must inheritLibreNMSPermissionMixinfromviews/mixins.pywithpermission_required = PERM_VIEW_PLUGIN
Declarerequired_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request
Use_get_safe_redirect_url(request)to validate referrer URLs in permission checks to prevent open-redirect attacks
Files:
netbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.py
**/views/base/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
**/views/base/**/*.py: Base view classes (BaseLibreNMSSyncView,BaseInterfaceTableView,BaseCableTableView,BaseIPAddressTableView,BaseVLANTableView) must implement the data pipeline pattern: fetch data from LibreNMS API, cache results withCacheMixinkeys likelibrenms_{data_type}_{model_name}_{pk}, compare against NetBox objects, and render a django-tables2 table in a partial template.
Base table view classes must implement resource-specific comparison logic: interface matching by name, IP matching by address/mask, VLAN matching by VID+group, and cables by matching remote devices and checking cable status.
VlanAssignmentMixinmust resolve VLAN group scope in order: Rack → Location → Site → SiteGroup → Region → Global, and must provide auto-selection of the most-specific VLAN group and lookup map building for interface and VLAN sync.
Files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
netbox_librenms_plugin/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/sync/**/*.py: Sync POST handlers must callrequire_all_permissions()(not justrequire_write_permission()) and return early if it returns a response; userequire_all_permissions_json()for AJAX/JSON endpoints
Follow sync conventions defined in.github/instructions/sync.instructions.mdfor sync views, base views, tables, and sync JavaScript
Files:
netbox_librenms_plugin/views/sync/device_fields.py
**/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
Sync action views must follow the pattern: check permissions with
LibreNMSPermissionMixinandNetBoxObjectPermissionMixin, read selected items fromrequest.POST.getlist('select'), load cached data usingCacheMixin.get_cache_key(), apply changes insidetransaction.atomic(), and redirect to the sync tab with?tab=<resource>.
Files:
netbox_librenms_plugin/views/sync/device_fields.py
🧠 Learnings (46)
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript VLAN modal functions must implement `openVlanDetailModal()`, `verifyVlanInGroup()`, `verifyVlanSyncGroup()` for per-interface VLAN detail editing.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.jsnetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript verification functions must include `handleInterfaceChange()`, `handleCableChange()`, `handleVRFChange()` that POST to single-item verify endpoints to validate resource changes.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.jsnetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript bulk operations must include functions `initializeBulkEditApply()` and `deleteSelectedInterfaces()` to handle bulk edit and delete actions.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : The import page uses `ModalManager` class and `filterModalManager` instance—always use this reference in fetch callbacks, not undefined `modalInstance` variables.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript URL and tab state management must implement `initializeTabs()`, `getDeviceIdFromUrl()`, and `setInterfaceNameFieldFromURL()` to maintain browser state and URL synchronization.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.jsnetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript TomSelect dropdown initialization must use a `TOMSELECT_INIT_DELAY_MS = 100` constant and implement delayed initialization after HTMX swaps. Required initializer functions: `initializeVCMemberSelect()`, `initializeVRFSelects()`, `initializeVlanGroupSelects()`, `initializeVlanSyncGroupSelects()`.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,static}/**/*.{html,js} : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers. Buttons target the `htmx-modal-content` element and JavaScript in `librenms_import.html` toggles the wrapper. Do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*.html : HTMX fragments live in `templates/netbox_librenms_plugin/htmx/` and include: `device_import_row.html` (individual import row updates), `device_validation_details.html` (expandable validation details), `device_vc_details.html` (virtual chassis member details), `bulk_import_confirm.html` (import confirmation modal content). Keep server responses and HTMX targets in sync when modifying these fragments.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript checkbox management must include functions `initializeTableCheckboxes()` and `updateBulkActionButton()` to handle multi-table checkbox selection and bulk action button state.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript table filtering must implement `initializeTableFilters()` and `filterTable()` functions for client-side row filtering.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.js
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Follow sync conventions defined in `.github/instructions/sync.instructions.md` for sync views, base views, tables, and sync JavaScript
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_netbox_librenms_plugin.py : `test_netbox_librenms_plugin.py` is an empty placeholder — do not add tests there.
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module.
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Reuse fixtures from `tests/conftest.py` instead of creating ad-hoc mocks: Configuration (`mock_multi_server_config`, `mock_legacy_config`), API client (`mock_librenms_api`), NetBox objects (`mock_netbox_device`, `mock_netbox_vm`, `mock_netbox_site`, `mock_netbox_platform`, `mock_netbox_device_type`, `mock_netbox_device_role`, `mock_netbox_cluster`, `mock_netbox_rack`), HTTP responses (`mock_response_factory`, `mock_success_response`, `mock_device_response`, `mock_error_response`, `mock_auth_error_response`), and Import workflow (`sample_librenms_device`, `sample_librenms_device_minimal`, `sample_validation_state`, `sample_validation_state_vm`).
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/_*_sync{,_content}.html : Each sync resource has two templates following a naming convention: `_<resource>_sync.html` (the tab wrapper, loaded once when the tab is selected) and `_<resource>_sync_content.html` (the HTMX-swappable inner fragment, refreshed on data changes without a full page reload). Current resources: `_interface_sync`, `_cable_sync`, `_ipaddress_sync`, `_vlan_sync`. When adding a new sync resource, create both the wrapper and content templates following this pattern.
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`.
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances.
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions in `utils.py` (`find_matching_site`, `match_librenms_hardware_to_device_type`, `find_matching_platform`)
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Hook into NetBox Django 5 plugin APIs via `navigation.py`, `urls.py`, and `api/` modules under `netbox_librenms_plugin/`; respect NetBox plugin conventions
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_librenms_id.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/jobs.py : Follow background job conventions defined in `.github/instructions/background-jobs.instructions.md` for `jobs.py`, import views, and import utilities
Applied to files:
netbox_librenms_plugin/utils.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Views must follow a three-layer structure: Base views in `views/base/`, Object sync views in `views/object_sync/`, and Sync action views in `views/sync/` with shared mixins from `views/mixins.py`
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py : Use `LibreNMSAPIMixin` to provide lazy-loaded `LibreNMSAPI` instance via `self.librenms_api` property and `get_server_info()` method for template context.
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/views.py : API endpoints must use `LibreNMSPluginPermission` class in `api/views.py` where GET checks `view_librenmssettings` and other methods check `change_librenmssettings`
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,tables}/**/*.{html,py} : Templates live in `templates/netbox_librenms_plugin/`; reuse/includes under `inc/`. Sync pages extend `librenms_sync_base.html`. Tables emit HTMX-enabled columns and buttons (`tables/*.py`), so prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/sync/**/*.py : Sync action views must follow the pattern: check permissions with `LibreNMSPermissionMixin` and `NetBoxObjectPermissionMixin`, read selected items from `request.POST.getlist('select')`, load cached data using `CacheMixin.get_cache_key()`, apply changes inside `transaction.atomic()`, and redirect to the sync tab with `?tab=<resource>`.
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/settings.html : `settings.html` uses a split-form pattern: two separate Django forms (`ServerConfigForm` + `ImportSettingsForm`) sharing one page, differentiated by a hidden `form_type` field (`"server_config"` or `"import_settings"`). The test-connection button is an HTMX POST to `TestLibreNMSConnectionView`, returning an inline alert fragment.
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/models.py : Coordinate schema changes through Django migrations in `migrations/` directory; update `models.py`, admin, and Pydantic representations accordingly
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Sync POST handlers must call `require_all_permissions()` (not just `require_write_permission()`) and return early if it returns a response; use `require_all_permissions_json()` for AJAX/JSON endpoints
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `BulkImportDevicesView` (POST) must execute import: background mode enqueues `ImportDevicesJob`; sync mode calls `bulk_import_devices()` + `bulk_import_vms()` and returns OOB row swaps with `HX-Trigger: closeModal`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Declare `required_object_permissions` as a dict mapping HTTP methods to `[(action, Model)]` tuples for NetBox model operations; some views may set this dynamically per-request
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_permissions.py : For `NetBoxObjectPermissionMixin` tests, set `required_object_permissions` on the instance before calling `check_object_permissions()`.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`)
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/object_sync/**/*.py : Object sync view methods must create instances of concrete table views, copy the `request` object, and call `get_context_data()`. VMs must skip cables and VLANs by returning `None` from those `get_*_context()` methods.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Per-device dropdown update views (`DeviceRoleUpdateView`, `DeviceClusterUpdateView`, `DeviceRackUpdateView`) must apply selection to validation state and return re-rendered row via `render_device_row()`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `ImportDevicesJob` background job: imports devices and VMs. Must call `bulk_import_devices_shared()` for devices and `bulk_import_vms()` for VMs. `job.data` keys must include `imported_device_pks`, `imported_vm_pks`, `imported_libre_device_ids`, `imported_libre_vm_ids`, `server_key`, `total`, `success_count`, `failed_count`, `skipped_count`, `virtual_chassis_created`, `errors`, `completed`
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `validate_device_for_import(device, ...)` is the core validation function that produces validation state dict
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `process_device_filters(filters, ...)` must fetch and validate devices from LibreNMS, returning a list
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_devices_shared(devices, user, ...)` must be the shared implementation between sync and background import
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_vms(vm_imports, user, ...)` must implement VM import
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Test view decision logic by setting `view._filter_form_data = {...}` directly, not via HTTP requests.
Applied to files:
netbox_librenms_plugin/tests/test_librenms_id.py
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 446-450: The code only normalizes bare integer custom field values
(cf_value) into the per-server dict form but misses string-backed legacy IDs
(e.g. "42"); update the normalization logic that reads
obj.custom_field_data.get("librenms_id") so it also recognizes numeric strings:
if cf_value is a str and cf_value.isdigit(), convert it to an int (or directly
to the {"default": int(cf_value)} dict) before the existing dict-check and
server_key lookup. Apply the same change to the second occurrence handling the
same cf_value logic (the block around the other check referenced in the review)
so string numeric legacy IDs are upgraded consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: d765affa-63a1-496e-b03f-e501cc591cfc
📒 Files selected for processing (1)
netbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/views/sync/device_fields.py
netbox_librenms_plugin/views/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/**/*.py: Views must follow a three-layer structure: Base views inviews/base/, Object sync views inviews/object_sync/, and Sync action views inviews/sync/with shared mixins fromviews/mixins.py
New views must extend the closest base class and compose mixins fromviews/mixins.py(LibreNMSPermissionMixin,NetBoxObjectPermissionMixin,LibreNMSAPIMixin,CacheMixin,VlanAssignmentMixin)
All views must inheritLibreNMSPermissionMixinfromviews/mixins.pywithpermission_required = PERM_VIEW_PLUGIN
Declarerequired_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request
Use_get_safe_redirect_url(request)to validate referrer URLs in permission checks to prevent open-redirect attacks
Files:
netbox_librenms_plugin/views/sync/device_fields.py
netbox_librenms_plugin/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/sync/**/*.py: Sync POST handlers must callrequire_all_permissions()(not justrequire_write_permission()) and return early if it returns a response; userequire_all_permissions_json()for AJAX/JSON endpoints
Follow sync conventions defined in.github/instructions/sync.instructions.mdfor sync views, base views, tables, and sync JavaScript
Files:
netbox_librenms_plugin/views/sync/device_fields.py
**/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
Sync action views must follow the pattern: check permissions with
LibreNMSPermissionMixinandNetBoxObjectPermissionMixin, read selected items fromrequest.POST.getlist('select'), load cached data usingCacheMixin.get_cache_key(), apply changes insidetransaction.atomic(), and redirect to the sync tab with?tab=<resource>.
Files:
netbox_librenms_plugin/views/sync/device_fields.py
🧠 Learnings (22)
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/sync/**/*.py : Sync action views must follow the pattern: check permissions with `LibreNMSPermissionMixin` and `NetBoxObjectPermissionMixin`, read selected items from `request.POST.getlist('select')`, load cached data using `CacheMixin.get_cache_key()`, apply changes inside `transaction.atomic()`, and redirect to the sync tab with `?tab=<resource>`.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Follow sync conventions defined in `.github/instructions/sync.instructions.md` for sync views, base views, tables, and sync JavaScript
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Views must follow a three-layer structure: Base views in `views/base/`, Object sync views in `views/object_sync/`, and Sync action views in `views/sync/` with shared mixins from `views/mixins.py`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,tables}/**/*.{html,py} : Templates live in `templates/netbox_librenms_plugin/`; reuse/includes under `inc/`. Sync pages extend `librenms_sync_base.html`. Tables emit HTMX-enabled columns and buttons (`tables/*.py`), so prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/models.py : Coordinate schema changes through Django migrations in `migrations/` directory; update `models.py`, admin, and Pydantic representations accordingly
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Sync POST handlers must call `require_all_permissions()` (not just `require_write_permission()`) and return early if it returns a response; use `require_all_permissions_json()` for AJAX/JSON endpoints
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript VLAN modal functions must implement `openVlanDetailModal()`, `verifyVlanInGroup()`, `verifyVlanSyncGroup()` for per-interface VLAN detail editing.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions in `utils.py` (`find_matching_site`, `match_librenms_hardware_to_device_type`, `find_matching_platform`)
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript verification functions must include `handleInterfaceChange()`, `handleCableChange()`, `handleVRFChange()` that POST to single-item verify endpoints to validate resource changes.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `BulkImportDevicesView` (POST) must execute import: background mode enqueues `ImportDevicesJob`; sync mode calls `bulk_import_devices()` + `bulk_import_vms()` and returns OOB row swaps with `HX-Trigger: closeModal`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Declare `required_object_permissions` as a dict mapping HTTP methods to `[(action, Model)]` tuples for NetBox model operations; some views may set this dynamically per-request
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_permissions.py : For `NetBoxObjectPermissionMixin` tests, set `required_object_permissions` on the instance before calling `check_object_permissions()`.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/views.py : API endpoints must use `LibreNMSPluginPermission` class in `api/views.py` where GET checks `view_librenmssettings` and other methods check `change_librenmssettings`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`)
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/object_sync/**/*.py : Object sync view methods must create instances of concrete table views, copy the `request` object, and call `get_context_data()`. VMs must skip cables and VLANs by returning `None` from those `get_*_context()` methods.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Per-device dropdown update views (`DeviceRoleUpdateView`, `DeviceClusterUpdateView`, `DeviceRackUpdateView`) must apply selection to validation state and return re-rendered row via `render_device_row()`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
🔇 Additional comments (2)
netbox_librenms_plugin/views/sync/device_fields.py (2)
285-322: Good call wrapping platform creation and assignment in one transaction.This removes the partial-write case where a
Platformcould be created even if assigning it to the device failed, and the splitIntegrityError/ValidationErrorpaths are much clearer.
425-436: Per-request permission scoping is the right fix.Normalizing
object_typebeforerequire_all_permissions()keeps device-only and VM-only editors from needing unrelated change permissions.As per coding guidelines, "Declare
required_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request" and "Sync POST handlers must callrequire_all_permissions()(not justrequire_write_permission()) and return early if it returns a response."
4b012ef to
5050c88
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
netbox_librenms_plugin/views/object_sync/vms.py (1)
25-29:⚠️ Potential issue | 🟠 MajorCopy the request onto
VMInterfaceTableViewbefore usingself.librenms_api.
get_table()now depends onself.librenms_api.server_key, butVMLibreNMSSyncView.get_interface_context()still instantiatesVMInterfaceTableView()without assigningrequestlike the device path does. On multi-server sync pages the VM interface table can therefore resolve against the default server instead of the selected one.Based on learnings: Object sync view methods must create instances of concrete table views, copy the
requestobject, and callget_context_data().Also applies to: 48-54
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/object_sync/vms.py` around lines 25 - 29, The VM interface context code in VMLibreNMSSyncView's get_interface_context instantiates VMInterfaceTableView() without copying the incoming request, causing table.get_table()/self.librenms_api to use the default server; update get_interface_context (and the similar block at lines 48-54) to create a VMInterfaceTableView instance, set its request attribute to the passed-in request (e.g., table_view.request = request) before calling get_context_data(request, obj, interface_name_field), so the table view resolves self.librenms_api.server_key from the selected server.netbox_librenms_plugin/views/object_sync/devices.py (1)
119-129:⚠️ Potential issue | 🟠 MajorUse the same VC sync device for VLAN override cache keys.
SaveVlanGroupOverridesViewstill keys TTL and override state off the posteddevice, whileSingleInterfaceVerifyViewnormalizes VC members to the primary/sync device before reading the ports cache. On a VC member page the TTL lookup can miss and return “No cached port data” even though the interface table is already loaded. Resolve the same sync device here before callingget_cache_key()andget_vlan_overrides_key().Based on learnings: Use
get_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations.Also applies to: 343-362
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/object_sync/devices.py` around lines 119 - 129, The VC member device must be normalized to the sync/primary device before building cache keys: in SaveVlanGroupOverridesView replace the current primary_device selection with a call to get_librenms_sync_device(selected_device) (or if you start from a port use get_virtual_chassis_member(port) first to map port→member then pass that member to get_librenms_sync_device), then use that resolved sync device when calling get_cache_key(...) and get_vlan_overrides_key(...); apply the same change for the similar block around lines 343-362 so both TTL and override state use the normalized VC sync device.
♻️ Duplicate comments (3)
netbox_librenms_plugin/forms.py (1)
584-586:⚠️ Potential issue | 🟡 MinorAvoid making warm cache reads depend on
LibreNMSAPI()construction.This still instantiates
LibreNMSAPI()beforecache.get(), so a missing or invalid selected-server config prevents serving cached location choices at all. Resolve the server key cheaply first, then build the client only after a cache miss.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/forms.py` around lines 584 - 586, Resolve the server key without instantiating LibreNMSAPI first: compute the server key cheaply (e.g., read the selected-server config or call the helper that returns server_key) and build the cache_key and call cache.get(cache_key); only if cache.get returns a miss should you instantiate LibreNMSAPI() and proceed. Update the block using the symbols LibreNMSAPI, cache_key, cache.get, and cached_choices so the API client is created after a cache miss.netbox_librenms_plugin/views/imports/actions.py (1)
1121-1131:⚠️ Potential issue | 🔴 CriticalFinish migrating the rest of the conflict-resolution flow to server-aware ID helpers.
migrate_librenms_idis server-aware, butlink,update,update_serial, and the LibreNMS ID collision lookup above still treatlibrenms_idas a scalar. On a non-default server that can overwrite an existing JSON mapping and miss conflicts against dict-backed records, so linking from a second LibreNMS server can corrupt the first server's association. Route those paths through the same server-aware helpers as well.Based on learnings: Use
LibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/imports/actions.py` around lines 1121 - 1131, The conflict-resolution flow still treats librenms_id as a scalar in the "link", "update", "update_serial" branches and in the collision lookup; change those code paths to use the server-aware helpers (e.g. call self.librenms_api.get_librenms_id(existing_device) instead of reading the librenms_id custom field directly and use set_librenms_device_id / set_librenms_vm_id when writing) so IDs become per-server dicts like migrate_librenms_id does; ensure you use LibreNMSAPI.get_librenms_id for lookups and the set_librenms_* helpers for writes and keep the _save_device(existing_device) call after modifications.netbox_librenms_plugin/tests/test_import_utils.py (1)
1345-1356:⚠️ Potential issue | 🟠 MajorScope
_setup_librenms_id_match()to the Q-based librenms-id lookup.The blanket
filter.return_value.first.return_value = existing_devicemakes everyDevice.objects.filter(...).first()look like a librenms-id hit, so these tests still pass if the implementation falls through to hostname or serial matching instead offind_by_librenms_id().Proposed fix
def _setup_librenms_id_match(self, existing_device, as_vm=False): - """Configure mocks so that a device is found by librenms_id. - - Uses return_value instead of side_effect because find_by_librenms_id() - calls filter() with Q objects (positional args), not keyword args. - """ - if as_vm: - self.mock_vm.objects.filter.return_value.first.return_value = existing_device - self.mock_device.objects.filter.return_value.first.return_value = None - else: - self.mock_vm.objects.filter.return_value.first.return_value = None - self.mock_device.objects.filter.return_value.first.return_value = existing_device + """Configure mocks so only the librenms_id lookup returns a match.""" + def device_filter(*args, **kwargs): + result = MagicMock() + result.first.return_value = existing_device if args else None + return result + + if as_vm: + self.mock_vm.objects.filter.side_effect = device_filter + self.mock_device.objects.filter.return_value.first.return_value = None + else: + self.mock_vm.objects.filter.return_value.first.return_value = None + self.mock_device.objects.filter.side_effect = device_filter🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_import_utils.py` around lines 1345 - 1356, The helper _setup_librenms_id_match is too broad: instead of making every Device/VM filter().first() return the existing_device, change self.mock_device.objects.filter and self.mock_vm.objects.filter to use a side_effect that inspects the positional Q args and only returns a mock whose first.return_value is existing_device when the Q contains the librenms_id lookup; otherwise return a mock whose first.return_value is None. Update the side_effect logic used in _setup_librenms_id_match so tests only get a match when the filter() call is the Q-based librenms_id lookup (affecting the mock objects referenced in _setup_librenms_id_match).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/tables/device_status.py`:
- Around line 491-496: The button markup drops the aria-label when btn_label is
set, causing screen-readers to announce the static label text instead of the
action; change the aria_attr logic so an aria-label with the action text
(btn_title) is always rendered (use btn_title as the accessible name) and keep
including aria_attr in the button string assembly where buttons.append(...) is
built (references: btn_title, aria_attr, btn_label, buttons, btn_class).
In `@netbox_librenms_plugin/tests/test_librenms_id.py`:
- Around line 122-131: The test should assert the exact filter predicate passed
to mock_model.objects.filter so the non-default path only queries
custom_field_data__librenms_id__production; after calling
find_by_librenms_id(mock_model, 999, "production"), inspect
mock_model.objects.filter.call_args (or call_args_list) and assert the Q/filter
arg equals (or contains) the predicate targeting
custom_field_data__librenms_id__production=999 and does not include a bare
librenms_id or librenms_id__exact predicate; update the test to fetch the filter
call (e.g., call_args[0][0] or keyword args) and assert it matches
Q(custom_field_data__librenms_id__production=999).
In `@netbox_librenms_plugin/tests/test_sync_devices.py`:
- Around line 196-210: The test
TestUpdateDeviceSerialViewWiring.test_has_all_required_mixins currently only
asserts LibreNMSAPIMixin; update it to also assert that LibreNMSPermissionMixin
and NetBoxObjectPermissionMixin are in UpdateDeviceSerialView.__mro__ (import
these mixins from netbox_librenms_plugin.views.mixins alongside
LibreNMSAPIMixin) so the test matches the three-mixin inheritance pattern used
by UpdateDeviceNameView.
In `@netbox_librenms_plugin/tests/test_sync_interfaces.py`:
- Around line 134-153: The test test_sets_librenms_id_when_port_id_present
currently sets iface.cf = {"librenms_id": {"default": 1}}, so it still exercises
the old mapping-path; change the setup to start with an empty custom-field
mapping (e.g., iface.cf = {}) so the test truly verifies first-time writes call
set_librenms_device_id via view.update_interface_attributes, keeping the rest of
the test (mac_addresses, librenms_data, patches, and
mock_set.assert_called_once_with(...)) unchanged.
In `@netbox_librenms_plugin/utils.py`:
- Around line 560-582: find_by_librenms_id currently only checks for legacy
bare-integer IDs (custom_field_data__librenms_id=librenms_id) and therefore
misses legacy string-stored IDs (e.g., "42"); update find_by_librenms_id to also
OR a query that matches the string form of the ID when server_key == "default"
(e.g., custom_field_data__librenms_id=str(librenms_id)) so pre-normalized string
entries inserted via the NetBox UI are found; reference the existing functions
find_by_librenms_id, set_librenms_device_id, and get_librenms_device_id to
ensure behavior is consistent with the string normalization already handled in
set_librenms_device_id/get_librenms_device_id.
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 149-158: The code assumes plugins_cfg.get("servers", {}) returns a
mapping but may be None or non-mapping; normalize it before use by checking the
value from plugins_cfg.get("servers") and replacing it with an empty dict if
it's falsy or not an instance of dict so subsequent access to
servers_config.get(...) and the legacy fallback check (sk == "default" and not
servers_config) won't raise; update the initialization of servers_config in
librenms_sync_view.py (the plugins_cfg/servers_config block) to coerce
non-mapping values to {} (mirror the defensive behavior of get_server_info()).
---
Outside diff comments:
In `@netbox_librenms_plugin/views/object_sync/devices.py`:
- Around line 119-129: The VC member device must be normalized to the
sync/primary device before building cache keys: in SaveVlanGroupOverridesView
replace the current primary_device selection with a call to
get_librenms_sync_device(selected_device) (or if you start from a port use
get_virtual_chassis_member(port) first to map port→member then pass that member
to get_librenms_sync_device), then use that resolved sync device when calling
get_cache_key(...) and get_vlan_overrides_key(...); apply the same change for
the similar block around lines 343-362 so both TTL and override state use the
normalized VC sync device.
In `@netbox_librenms_plugin/views/object_sync/vms.py`:
- Around line 25-29: The VM interface context code in VMLibreNMSSyncView's
get_interface_context instantiates VMInterfaceTableView() without copying the
incoming request, causing table.get_table()/self.librenms_api to use the default
server; update get_interface_context (and the similar block at lines 48-54) to
create a VMInterfaceTableView instance, set its request attribute to the
passed-in request (e.g., table_view.request = request) before calling
get_context_data(request, obj, interface_name_field), so the table view resolves
self.librenms_api.server_key from the selected server.
---
Duplicate comments:
In `@netbox_librenms_plugin/forms.py`:
- Around line 584-586: Resolve the server key without instantiating LibreNMSAPI
first: compute the server key cheaply (e.g., read the selected-server config or
call the helper that returns server_key) and build the cache_key and call
cache.get(cache_key); only if cache.get returns a miss should you instantiate
LibreNMSAPI() and proceed. Update the block using the symbols LibreNMSAPI,
cache_key, cache.get, and cached_choices so the API client is created after a
cache miss.
In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 1345-1356: The helper _setup_librenms_id_match is too broad:
instead of making every Device/VM filter().first() return the existing_device,
change self.mock_device.objects.filter and self.mock_vm.objects.filter to use a
side_effect that inspects the positional Q args and only returns a mock whose
first.return_value is existing_device when the Q contains the librenms_id
lookup; otherwise return a mock whose first.return_value is None. Update the
side_effect logic used in _setup_librenms_id_match so tests only get a match
when the filter() call is the Q-based librenms_id lookup (affecting the mock
objects referenced in _setup_librenms_id_match).
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1121-1131: The conflict-resolution flow still treats librenms_id
as a scalar in the "link", "update", "update_serial" branches and in the
collision lookup; change those code paths to use the server-aware helpers (e.g.
call self.librenms_api.get_librenms_id(existing_device) instead of reading the
librenms_id custom field directly and use set_librenms_device_id /
set_librenms_vm_id when writing) so IDs become per-server dicts like
migrate_librenms_id does; ensure you use LibreNMSAPI.get_librenms_id for lookups
and the set_librenms_* helpers for writes and keep the
_save_device(existing_device) call after modifications.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0948a0f9-1e17-43f8-9f86-80d612600698
📒 Files selected for processing (29)
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/_interface_sync_content.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/conftest.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/base/interfaces_view.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/mixins.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/sync/interfaces.py
134f8d7 to
cda90aa
Compare
f260db7 to
b271013
Compare
151ddc0 to
325953b
Compare
…ate, wiring tests CreateAndAssignPlatformView: - Add transaction.atomic() wrapping platform creation + device assignment - Call platform.full_clean() before platform.save() to surface ValidationError - Fetch device with select_for_update() inside transaction (TOCTOU guard) - Add logger.exception() for both ValidationError and IntegrityError - Better error messages (include actual error, not hard-coded 'slug collision') New independent test files (multi-server tests excluded): - test_view_wiring.py: MRO/mixin wiring, CacheMixin, permissions, template syntax - test_reviewer_fixes.py: vc_member_name_pattern, XSS escape, GenerateVcMemberName, lazy validation api passthrough, CreateAndAssignPlatformView full_clean - test_mixins.py: LibreNMSAPIMixin lazy init, get_server_info, CacheMixin key format - test_sync_devices.py: AddDeviceToLibreNMSView, UpdateDeviceLocation, field view wiring
… sync delegation list.py: Check validated-cache metadata (which includes use_sysname and strip_domain in its key) instead of raw device cache when deciding whether to short-circuit the background job. A naming preference change now correctly shows the cache as cold and routes through the background path instead of doing synchronous re-validation. librenms_sync_view.py: Always delegate VC device resolution to get_librenms_sync_device() instead of first checking get_librenms_device_id(). In a partially migrated VC, a member with an explicit per-server dict is preferred over one with a legacy bare-int — the previous guard could pick the wrong member. Includes 5 new tests covering both fixes.
The cherry-picked VC sync delegation tests passed server_key= to get_librenms_sync_device(), which doesn't exist on this pre-id branch. Replaced with tests that exercise the actual priority order on this branch: member with librenms_id > master with IP > any member with IP.
…gement
Replace bare-integer librenms_id custom field with a per-server JSON dict
format (e.g., {"primary": 42, "secondary": 99}) to support multi-server
LibreNMS deployments.
Key changes:
- Add get/set/find/migrate helpers for server-scoped librenms_id in utils.py
- Server-aware cache keys across all sync views (interfaces, cables, VLANs, IPs)
- Legacy bare-int IDs act as universal fallback for any server_key
- Convert ID badge on sync page for migrating legacy IDs with serial verification
- RemoveServerMappingView and ConvertLegacyLibreNMSIdView with collision checks
- Bool rejection guards on all librenms_id helpers (bool is subclass of int)
- CSRF token and response.ok guard in cable verify JS/HTML
- Normalize server_key to "default" early in verify/VLAN-override views
- DoesNotExist guard on select_for_update in CreateAndAssignPlatformView
- Comprehensive tests: test_librenms_id, test_mixins, test_sync_devices,
test_sync_interfaces, test_sync_view_mismatch, test_permissions,
test_view_wiring smoke tests for new views
…_key in sync redirects SingleCableVerifyView.post() now strips derived fields from cached link data and re-enriches remote side from current NetBox state, preventing DoesNotExist when remote devices/interfaces are deleted after caching. Sync redirect URLs for interfaces, cables, IP addresses, and VLANs now preserve the server_key query parameter so users return to the correct multi-server tab.
Add regression tests for stale-field stripping and XSS escaping in SingleCableVerifyView.post().
…t check, htmx URLs - Add bool guard to set_librenms_device_id() input parameter - Add bool guards to legacy-migration detection in device_operations.py - Use find_by_librenms_id() for conflict checks in device_fields.py and actions.py (catches legacy bare-int owners, removes hand-rolled Q) - Tighten legacy-ID detection in librenms_sync_view.py to require digit-only strings (not arbitrary non-numeric strings) - Fix test_view_wiring.py to check SyncInterfacesView directly - Add server_key to htmx_url for interfaces, cables, modules, IP tables - Add server_key hidden input to cable/VLAN/IP sync form templates - Read server_key from POST in cable/VLAN/IP sync views for correct cache lookups and redirects in multi-server flows
…p dedup - Remove private _get_cache_key() from SingleIPAddressVerifyView; use CacheMixin.get_cache_key() with server_key from POST body so cache reads match the key format written by _prepare_context(). - Add server_key to handleVRFChange JS fetch body (matching other verify endpoints). - Add server_key hidden input to cable verify "Sync Cable" form so sync/cables.py reads the correct cache namespace. - Move django.setup() to class-scoped fixture in test_view_wiring.py. - Add test_ip_verify.py with 6 regression tests for cache key format and server_key propagation.
…cy display
- Reject boolean inputs in _librenms_id_q to prevent false matches on
ID 1/0.
- Add response.ok check to handleVRFChange JS fetch (consistency with
other endpoints).
- Unify view.request binding in test_import_utils.py so permission
checks and business logic use the same request object.
- Support string-digit legacy IDs in migrate_legacy_librenms_id
(e.g. "42" → {"server": 42}).
- Fix btn_title/visual mismatch in device_status.py when
name_sync and migration are both true.
- Guard get_librenms_device_id dict branch to only return int values
(reject floats, lists, dicts).
- Clamp cache_ttl > 0 in all 5 sync views to prevent bogus countdown
from negative TTLs.
- Make _build_sync_info VM-safe: use getattr for serial/device_type.
- Legacy display_name fallback in _build_id_server_info for
single-server installs.
- Remove reflected XSS in device_fields.py error responses.
…ng, cable verify server_key, re-check locked row preconditions
…locked-row recheck, aria-labels, tests Code fixes: - cables.py: escape member.name in render_device_selection (XSS) - librenms_sync.js: add server_key to handleCableChange, guard radio lookup, close VLAN modal only on success - utils.py: boolean guard in find_by_librenms_id, auto_save=False skips mutation - device_fields.py: bool/strict-digit in _normalize_librenms_mapping, recheck locked row preconditions, getattr for VM serial - ip_addresses_view.py: auto_save=False in prefetch, id on template hidden input - librenms_sync_view.py: validate did in server mapping loop - virtual_chassis.py: validate _load_vc_member_name_pattern return - vm_operations.py: reject boolean device_id - device_status.py: aria-labels on icon-only buttons - device_validation_details.html: use resolved_name Tests: - test_librenms_id.py: auto_save mutation tests, find_by_librenms_id bool guard - test_vm_operations.py: boolean device_id rejection - test_reviewer_fixes.py: _load_vc_member_name_pattern, _normalize_librenms_mapping, _build_all_server_mappings did validation, render_device_selection XSS
vm_operations.py was missing server_key parameter, transaction.atomic(), and set_librenms_device_id() — using the old custom_field_data pattern that doesn't support multi-server. bulk_import.py was missing server_key in validate_device_for_import calls and using raw server_key parameter instead of api.server_key in import_single_device.
- Use _resolve_naming_preferences() in bulk import sync_options instead of inline string equality checks (wrong key and wrong truthy value) - Add custom_field_data__librenms_id__default key assertion to test_default_server_key_is_default
…alidation
- Record correct naming source ("device-{id}") when both sysName and
hostname are empty
- Reject unsupported VM actions with explicit 400 instead of misleading 404
- Skip invalid entries (bool, None, non-numeric) in per-server ID mappings
- Guard against bool device_id before int coercion in conflict handler
55d9d70 to
50fd794
Compare
… sync delegation list.py: Check validated-cache metadata (which includes use_sysname and strip_domain in its key) instead of raw device cache when deciding whether to short-circuit the background job. A naming preference change now correctly shows the cache as cold and routes through the background path instead of doing synchronous re-validation. librenms_sync_view.py: Always delegate VC device resolution to get_librenms_sync_device() instead of first checking get_librenms_device_id(). In a partially migrated VC, a member with an explicit per-server dict is preferred over one with a legacy bare-int — the previous guard could pick the wrong member. Includes 5 new tests covering both fixes.
50fd794 to
2e52600
Compare
- #12: Pass manufacturer as explicit parameter to _build_row/_build_table_rows; remove self._device_manufacturer instance attribute (hidden temporal coupling) - #13: Eliminate second resolve_module_type loop in _build_table_rows; track installable flag inline during first pass via sub_row.get('module_type_id') - #14: Pre-compute ignore_cache dict once in _build_context; pass to _find_transparent_indices and _collect_top_items instead of double evaluation - #15: Fix convert_speed_to_kbps return type annotation to -> int | None - #16: Extract _apply_rules() inner helper in apply_normalization_rules to eliminate duplicated regex loop between manufacturer/non-manufacturer branches - #17/#18: Extract BaseSNMPForm with 6 shared fields; rename AddToLIbreSNMPV1V2 -> AddToLibreSNMPV1V2 and AddToLIbreSNMPV3 -> AddToLibreSNMPV3 (typo fix); add backwards-compatible aliases for existing imports - #19: Rename related_name='librenms_mappings' -> 'librenms_device_type_mappings' on DeviceTypeMapping.netbox_device_type and 'librenms_module_type_mappings' on ModuleTypeMapping.netbox_module_type to avoid ambiguity; migration 0011 - #20: Add filterset_class to all six API ViewSets (InterfaceTypeMapping, DeviceTypeMapping, ModuleTypeMapping, ModuleBayMapping, NormalizationRule, InventoryIgnoreRule)
- XSS (#18): escape() user-controlled error strings in bulk confirm HTML response - XSS (#19,#20): escape() object_type param in device_fields.py HTTP responses - Stack trace (#8): replace str(exc) with generic message in bulk import HTMX error - Stack trace (#9): replace str(exc) with generic message in interfaces transaction error - Stack trace (#13): replace catch-all str(e) with generic message in ip_addresses_view - JS XSS (#4,#5): fix innerHTML spinner — store/restore full innerHTML, use DOM for label to avoid reinterpreting textContent as HTML - Workflow permissions (#1-#3): add permissions: contents: read to all three workflows; publish-pypi job-level permissions also gains contents: read alongside id-token: write - Devcontainer logging (#17): stop printing raw codespace name / CSRF / allowed-host values to stdout in devcontainer config - URL redirect (#6,#7): add comment — _get_safe_redirect_url already validates via url_has_allowed_host_and_scheme; CodeQL false positive - Lint: fix E741 ambiguous variable name in e2e test
* feat(inventory): modules/inventory sync tab with mapping rules
Add inventory/modules sync functionality:
- Six mapping model types: DeviceTypeMapping, ModuleTypeMapping,
ModuleBayMapping, NormalizationRule, InventoryIgnoreRule, PlatformMapping
- Migration 0010 creating all mapping tables
- Modules sync tab on Device/VM detail pages with ENTITY-MIB inventory data
- Install, replace, and move module actions
- Mapping CRUD views with YAML bulk export for all mapping models
- LibreNMS API: get_device_transceivers() for transceiver data
- Platform matching: PlatformMapping lookup before name-exact match
- contrib/ YAML files with example mappings and rules
- Comprehensive test coverage (test_sync_modules, test_modules_view,
test_module_replace, test_platform_mapping, test_tables_modules)
* fix tests: update db-fallback tests to match new RQ-only cancellation behavior
_is_job_cancelled now returns False on RQ/Redis unavailability instead
of falling back to DB status. Update 5 tests that were asserting the
old DB-fallback behavior:
- test_db_fallback_logs_via_module_logger_when_job_logger_none →
test_rq_unavailable_does_not_cancel_import: RQ unavailable means
processing continues, not cancelled
- test_job_db_fallback_stopped/errored_before_validation_loop →
test_job_cancelled_before_validation_loop_returns_empty /
test_rq_unavailable_job_not_cancelled_in_preloop: patch
_is_job_cancelled directly to test early-exit behavior
- test_job_validation_loop_db_fallback_stop →
test_job_cancelled_in_validation_loop_returns_empty: use
_is_job_cancelled side_effect to simulate mid-loop cancellation
- test_job_rq_check_exception_uses_db_status_and_exits →
test_rq_fetch_exception_does_not_cancel_process_filters: assert
result has 1 device (not []) when RQ unavailable
* fix: updated tests
* Apply CR findings: code quality, test, docs, and template fixes
- Remove dead _resolve_naming_preferences from actions.py (never called)
- Unify vc_detection_enabled parsing in BulkImportConfirmView (POST+GET)
- Add ambiguity detection to get_module_types_indexed second loop
- Fix get_validated_device_cache_key doctest (wrong e3b0 hash)
- Add status=='ok' envelope check before transceivers shape validation
- Strip netbox_bay_name in ModuleBayMapping.clean()
- Use server_info.server_key in _module_sync.html refresh form
- Guard module_mismatch_modal Update Serial form on serial_conflict/installed
- Remove duplicate NoSuchJobError import in test_coverage_api2.py
- Fix _is_job_cancelled side_effect count for in-loop cancellation test
- Precompute sibling_counts in _build_table_rows to eliminate N+1 queries
- Accept sibling_counts in has_nested_name_conflict (DB fallback preserved)
- Update test_has_installable_children fake_build_row signature
- Coerce entPhysicalParentRelPos to int in siblings sort (prevent TypeError)
- Move get_queue inside try block in api/views.py sync_job_status
- Replace MD5 with SHA256 for VC domain fingerprint (FIPS compliance)
- Fix test_role_is_read_from_validation to test validation dict path
- Add test_module_replace.py to docs/development/testing.md
- Add platform_mappings.yaml row to contrib/README.md
- Fix QSFP28-DD-2X100G-LR4 typo in module_type_mappings.yaml
- Clarify librenms_id auto-create in docs/usage_tips/custom_field.md
* Apply second batch of CR findings on pr/inventory-core
- Unify librenms_id auto-create version reference to 0.4.3 in docs
- Move _LIBRENMS_JOB_NAMES to module-level constant in api/views.py
- Add status=='ok' envelope check in port handler in librenms_api.py
- Split mapping ambiguity tracking in get_module_types_indexed (separate
mapping_seen/mapping_ambiguous so explicit mappings always win over base
ModuleType ambiguity)
- Handle match_type=='ambiguous' in find_matching_platform caller with
dedicated warning message about conflicting platform mappings
- Move vc_requested parse before validate_device_for_import in
BulkImportConfirmView and pass include_vc_detection=vc_requested
- Guard Replace Module form with {% if installed_module %} in
module_mismatch_modal.html to prevent empty module_id submission
- Add mock_db_job.status/completed assertions to api2 test
- Extend cancellation test side_effects to 4 entries to target in-loop
check (lines 507, 534, 566, 574) in both RQ and _is_job_cancelled tests
- Assert success list in test_no_warning_when_cluster_found
* Apply CR findings batch 3: test hardening, PlatformMapping lazy import, module bay normalization, BulkExportYAMLView permissions
- test_coverage_sync_views2: patch get_object_or_404 to isolate from ORM
- test_coverage_device_fields: assert save/full_clean not called on no-match
- test_librenms_id: use exact tuple set comparison for Q branch children
- test_init: assert DB alias used in custom field creation
- utils.py: move PlatformMapping import to function scope (lazy); update all test patches to models.PlatformMapping; remove create=True
- test_platform_mapping: patch require_object_permissions instead of require_write_permission
- test_sync_modules: mock apply_normalization_rules in _match_module_bay tests
- views/base/modules_view.py: apply NormalizationRule(scope=module_bay) to candidate names in _match_module_bay
- views/mapping_views.py: BulkExportYAMLView uses NetBoxObjectPermissionMixin with view permission
- views/object_sync/devices.py: precompute has_write_permission once in get_table()
* cr: batch 4 — prefetch_related, deterministic export, test improvements
- utils.py: add prefetch_related('interfacetemplates') to ModuleType query
and prefetch_related('netbox_module_type__interfacetemplates') to
ModuleTypeMapping query in get_module_types_indexed() to avoid N+1
queries in has_nested_name_conflict()
- views/mapping_views.py: add .order_by('pk') to BulkExportYAMLView filter
for deterministic YAML export; add select_related() to all 4 export
subclass querysets (DeviceTypeMapping, ModuleTypeMapping,
NormalizationRule, PlatformMapping)
- tests/test_utils.py: assert exact Platform.objects.get kwargs in 2 platform
tests; fix vc.master = None -> vc.master = master to exercise designated-
master-without-IP branch
- tests/test_platform_mapping.py: update mock chain for .order_by(); complete
test_returns_yaml_content_type with actual view call and content-type assertion
- tests/test_sync_modules.py: fix mock chain for .prefetch_related() in
TestGetModuleTypesIndexed
* cr: batch 5 — normalization scoping bug, test fixes
- utils.py: fix apply_normalization_rules() else-branch to filter
manufacturer__isnull=True so callers without manufacturer context never
have vendor-specific rules applied to their values
- tests/test_sync_modules.py: add test_mapping_overrides_ambiguous_base_key
to lock down the separate-ambiguous-sets behaviour in
get_module_types_indexed(); fix test_regex_mapping_with_backreference to
use 'Optics 0/0/0/5' (with space) so the assertion cannot pass via
exact-name fallback — only the regex expansion path can produce a match
- tests/test_platform_mapping.py: remove dangling assertions accidentally
left inside test_returns_200_with_empty_selection (PlatformMapping import
and existence check now live in test_all_mapping_bulk_export_yaml_views_exist)
* cr: batch 6 — normalization rule caching, test correctness
- utils.py: add preload_normalization_rules() helper that preloads
NormalizationRule rows for a (scope, manufacturer) combination into a
dict keyed by (scope, manufacturer_pk_or_None); update
apply_normalization_rules() to accept preloaded_rules kwarg and use
preloaded lists when provided (skipping DB queries); update
resolve_module_type() to accept norm_rules kwarg and thread it through
to apply_normalization_rules — eliminates N+1 DB queries in
_match_module_bay and _build_row loops
- views/base/modules_view.py: call preload_normalization_rules() in
_build_context for both 'module_bay' and 'module_type' scopes; pass
preloaded rules via self._norm_rules_bay/_norm_rules_type to
_match_module_bay and _build_row respectively
- tests/test_platform_mapping.py: add test_multiple_platform_mappings_returns_ambiguous
asserting PlatformMapping.MultipleObjectsReturned yields
match_type='ambiguous' and that Platform.objects.get is never called
- tests/test_sync_modules.py: fix test_mapping_overrides_ambiguous_base_key
to use distinct mapping key 'SFP-1G-LX-EXPLICIT' so 'SFP-1G-LX' is
absent and only the explicit key is present; fix
test_uninstalled_bay_is_skipped to add grandparent bay with installed
module (pk=99) and assert walk continues past empty bay to return 99;
fix test_class_scoped_mapping_preferred to pass [m_generic, m_class] so
priority logic must actively prefer class-scoped mapping; patch
preload_normalization_rules in tests that call _build_context directly;
update apply_normalization_rules lambda patches to accept **kw
* fix: FPC slot int/str comparison, stale docstring, test patch targets
- _fpc_slot_matches: convert match.group(1) and parent_bay.position to int
before comparing (Python 3: '1' == 1 is False); handle ValueError with
safe fallbacks
- apply_normalization_rules docstring: clarify that manufacturer=None applies
only unscoped (manufacturer__isnull=True) rules, not all scope rules
- test_modules_view._run_build_context: patch load_bay_mappings and
get_enabled_ignore_rules at utils level instead of patching model classes
that _build_context never references directly; remove now-unused
mock_ignore_qs variable
* fix: surface ambiguous platform, preloaded-rules fallback, serial mismatch, transceiver ignore
- actions.py sync_platform: add explicit elif for match_type='ambiguous' so
users get a clear conflict message instead of generic 'not found' error when
multiple PlatformMapping rows match the same OS string (works towards #51)
- utils.py apply_normalization_rules: when preloaded_rules is provided, check
key presence before using the dict; fall back to DB query when (scope,
mfg_pk) or (scope, None) is absent, preventing silent omission of vendor
rules for manufacturers not included in the preloaded dict
- utils.py match_librenms_hardware_to_device_type: update Returns docstring to
dict | None and document the MultipleObjectsReturned → None case
- modules_view.py _apply_installed_status: drop nb_serial from the guard so a
module with a LibreNMS serial is flagged as Serial Mismatch even when NetBox
has no serial recorded (lnms_serial and lnms_serial != nb_serial)
- modules_view.py _collect_top_items: apply ignore-rule check to transceiver-
synthesised items before appending, so InventoryIgnoreRules can suppress
optics from get_device_transceivers()
- test_modules_view.py: rename test that expected the old nb_serial-required
behavior; update assertions to Serial Mismatch + can_update_serial + can_replace
- test_modules_view.py: wrap two early-return _detect_serial_conflicts tests in
patch(dcim.models.Module) and assert filter was never called
* fix: canonical librenms_id lookup, None guard, transceiver transparent, vc flag case
- utils.py find_by_librenms_id: strip whitespace and canonicalize leading-zero
string IDs before building Q filters; '042' and '42 ' now resolve to
int_value=42 / canonical_str='42' and both forms are added to the query so
they match records stored as numeric 42 or string '42'
- modules_view.py _find_parent_container_name: use (... or '') pattern to guard
against entPhysicalName being explicitly None in the ENTITY-MIB payload
- modules_view.py _match_module_bay: same None guard for entPhysicalName,
entPhysicalDescr, entPhysicalClass on the item dict
- modules_view.py _collect_top_items: treat 'transparent' the same as 'skip'
for transceiver-synthesised rows so transparent synthetic items are not
added to top_items
- actions.py BulkImportDevicesView: normalise vc_detection_enabled flag with
.lower() before membership test so 'ON', 'True', 'TRUE' all parse correctly,
consistent with BulkImportConfirmView
* Apply CR batch 10 fixes
- bulk_import: check cancellation every iteration (not every 5th)
- models: always call full_clean() on save, remove update_fields guard
- tables/modules: add has_write_permission param to LibreNMSModuleTable,
gate selection column and render_actions on it
- modules_view: normalize placeholder model/serial strings in transceiver
merge; filter placeholder serials from inv_serials set
- api/views: skip DB status update when job is already in a terminal state
- utils: add 'ambiguous' case to find_matching_platform docstring
- utils: memoize DB fallback into preloaded_rules in apply_normalization_rules
- utils: use try/except int() instead of isdigit() for +42 style IDs
- devices: pass has_write_permission to LibreNMSModuleTable constructor
- test_modules_view: use SimpleNamespace instead of MagicMock in
_determine_status tests for unambiguous truthiness checks
- test_sync_modules: assert checkbox HTML in selection cell; update
test_install_module_view_not_in_base to assert public import path;
add order-independent check for class-scoped mapping preference
- test_tables_modules: set has_write_permission=True in _make_table helper;
add no-write-permission test case
- test_utils: assert exact kwargs on DeviceTypeMapping.objects.get and
PlatformMapping.objects.get calls
- test_vm_operations: patch _is_job_cancelled directly instead of mutating
job.job.status via refresh_from_db side_effect
- test_coverage_base_views2: rename test to reflect actual behavior
(cache entry present but lacks port_id)
- test_coverage_devices: update constructor assertion to include
has_write_permission kwarg
* Apply CR batch 11 fixes
- models: add FullCleanOnSaveMixin + clean() to InterfaceTypeMapping to
enforce uniqueness for NULL-speed rows (SQL UNIQUE skips NULL=NULL)
- utils: expand match_librenms_hardware_to_device_type docstring to
document all three fail-closed None cases (mapping, part_number, model
MultipleObjectsReturned), not just the mapping-table one
- test_vm_operations: remove stale mock_job.job.status='running' from
_run_bulk_with_mappings helper; patch _is_job_cancelled=False instead
* fix: normalize librenms_hardware/os to lowercase, fix convert_speed_to_kbps docstring
- DeviceTypeMapping.clean() and PlatformMapping.clean() now lowercase
the stored value after stripping, preventing case-variant duplicates
(e.g. 'IOS' and 'ios') that would cause MultipleObjectsReturned on
__iexact lookups. Closes #51.
- Fix convert_speed_to_kbps docstring: Returns section now reads
'int | None' to match the signature and implementation.
- Add TestDeviceTypeMappingModel tests for DeviceTypeMapping.clean()
(strip, lowercase, blank validation).
- Add test_clean_normalizes_to_lowercase and test_clean_strips_and_lowercases
to TestPlatformMappingModel.
* fix(js): address PR #258 review findings
- hideModal: remove all backdrops via querySelectorAll+forEach
- htmx:afterSettle: derive label from aria-labelledby, fallback to id
- initializeVlanModalSave: truncate/extract error body before display
* fix: handle unsaved manufacturer in normalization, fix JS modal/fetch issues
- utils.py: guard unsaved manufacturer (pk=None) in preload_normalization_rules
and apply_normalization_rules to prevent ValueError on DB query
- librenms_sync.js: add id="htmx-modal-label" to module mismatch modal header
so aria-labelledby target is preserved after innerHTML replacement
- librenms_sync.js: check response.ok before response.json() in deleteUrl fetch
so HTTP errors surface their status instead of a parse error
* fix: type annotation, non-positive librenms_id guard, htmx label listener
- utils.py: fix convert_speed_to_kbps parameter annotation to int|None
- utils.py: guard non-positive numeric librenms_id (<=0) same as None;
skip Q canonicalization for string '0'/'-1' after int parse
- librenms_sync.js: extract updateHtmxModalLabel(), listen at document
level for htmx:afterSettle, call from module-replace fetch completion
* fix: use _PLACEHOLDER_VALUES for serial normalization, fix docstring
- modules_view.py: _apply_installed_status and _detect_serial_conflicts
now use _PLACEHOLDER_VALUES set instead of only guarding against "-"
so serials like 'unknown', 'n/a', 'na' are treated as absent
- utils.py: update convert_speed_to_kbps Args docstring to int|None
* Fix CR batch: device_type ambiguity, platform MOR, cache invalidation, ChainMap bay lookup
- device_fields.py: distinguish None (ambiguous) from failed match result;
surface a specific error for duplicate DeviceType mappings
- utils.py: find_matching_platform returns {found:False, match_type='ambiguous'}
on Platform.MultipleObjectsReturned instead of silently taking .first()
- modules_view.py: invalidate stale inventory cache before early-return renders
when librenms_id is falsy or inventory fetch fails
- modules_view.py: _lookup_regex_bay_mapping iterates all ChainMap scopes
so same-named bays in different scopes are all checked
- tests: update TestFindMatchingPlatformMultipleReturned to expect ambiguous
* Promote SKIP_TYPES and _NON_HARDWARE_CLASSES to module-level constants
Move inline set literals SKIP_TYPES and _NON_HARDWARE_CLASSES out of their
method bodies and into module scope, consistent with _PLACEHOLDER_VALUES and
other module-level constants. Rename SKIP_TYPES to _SKIP_TRANSCEIVER_TYPES
to clarify its domain.
* Fix find_matching_platform docstring and non-positive string librenms_id guard
- utils.py: broaden find_matching_platform docstring to state 'ambiguous'
applies both to multiple PlatformMapping entries and to duplicate
exact-name Platform rows (Platform.MultipleObjectsReturned)
- utils.py: add early-return guard in find_by_librenms_id for string
librenms_id values that parse to <= 0 (e.g. '0', '-1', '000'), preventing
them from reaching the Q clauses and matching stale/corrupted records
* fix: normalize placeholder serials and txr_type in modules_view
_check_ignore_rules: normalize item_serial, device_serial, and
ancestor_serial against _PLACEHOLDER_VALUES so sentinels like
'unknown'/'n/a' are treated as absent and do not trigger or
short-circuit serial_matches_device rules or require_serial_match_parent
ancestor walks.
_merge_transceiver_data: normalize txr_type against _PLACEHOLDER_VALUES
(same as model/serial) so placeholder types like 'unknown' do not bypass
the _SKIP_TRANSCEIVER_TYPES guard and produce synthetic rows with
display_model set to a placeholder string.
* Fix whitespace-only librenms_id, BUILTIN model placeholder, FPC-scope exact-bay fallback, has_write_permission in HTMX render
- utils.py: treat whitespace-only librenms_id strings as absent (return None
after strip() when cleaned == "") so they don't reach the Q-object builder
- modules_view.py: extend transceiver backfill to also replace 'BUILTIN' model
and serial values, not just those already in _PLACEHOLDER_VALUES
- modules_view.py: exact-name bay fallback now iterates ChainMap scopes and
calls _fpc_slot_matches() to avoid returning the wrong-scope bay when
duplicate bay names exist across scopes (mirrors the regex path behaviour)
- modules_view.py: add has_write_permission to all render() calls in post()
so the Install Selected button is visible in HTMX-refreshed content
* Fix non-integer librenms_id passthrough, stale cache on missing ID, exact-mapping ChainMap scope
- utils.py: non-integer strings (e.g. 'abc') now return None in
find_by_librenms_id() instead of falling through to build Q objects;
changed 'except ValueError: pass' to 'except ValueError: return None'
- modules_view.py: get_context_data() re-validates the device's current
LibreNMS ID before serving cached inventory; clears cache and returns
empty context when the mapping has been removed since the cache was written
- modules_view.py: _lookup_exact_bay_mapping() now iterates ChainMap scopes
and calls _fpc_slot_matches() before returning, matching the existing
behaviour of _lookup_regex_bay_mapping() and the name-fallback path
* fix: normalize _GENERIC_CONTAINER_MODELS to lowercase; embed librenms_id in inventory cache
- modules_view: lowercase _GENERIC_CONTAINER_MODELS set and apply .lower() at
all 5 comparison sites so values like 'builtin', 'default', 'n/a' received
from LibreNMS in any case are treated as generic containers
- modules_view: store {'inventory': data, 'librenms_id': id} in the inventory
cache instead of the raw list; get_context_data validates the embedded
librenms_id against the current mapping so remapped devices never serve stale
inventory (non-dict/legacy entries are treated as cache misses)
* fix: treat duplicate-serial module conflicts as ambiguous instead of silently overwriting
When multiple Module objects share the same serial, the old loop would
overwrite row["serial_conflict_module"] nondeterministically. Now we
group conflicts by serial and only set the move target when exactly one
candidate exists; multiple candidates set serial_conflict_ambiguous.
* fix: clear stale cache on legacy payload; ungate generic-model filter from phys class
- modules_view: get_context_data now calls cache.delete(cache_key) before
returning when the cached payload is not the new dict format so pre-upgrade
list-form entries are evicted and the next request regenerates fresh data
- modules_view: collapse the two-check current-item filter into a single
'model in _GENERIC_CONTAINER_MODELS' test (removes the phys_class=='container'
gate) so empty-model non-container items are also treated as generic
- modules_view: remove the anc_class=='container' guard from the ancestor walk
so any inventory-class ancestor with a generic model (e.g. a 'module' row
with model='builtin') is treated as transparent instead of blocking its
subtree
* Remove dead vc_requested assignment left by rebase
The rebase onto pr/code-quality-fixes dropped the consumer of vc_requested
in favor of the hoisted vc_detection_enabled variable, leaving the
assignment itself as an orphan that ruff F841 flags.
* fix: VC zero-based detection all-zeros guard; align VC-perm tests with fail-closed behavior
- virtual_chassis.py: only treat stack as 0-based when positions span 0 AND a
positive value. When every entPhysicalParentRelPos is 0 the data is invalid
and the shift produced colliding positions (all members → slot 1); fall
through to the per-member idx+1 fallback instead.
- test_coverage_bulk_import.py: PR #257 fails stack imports fast when the
user lacks dcim.add_virtualchassis. Update both VC-permission tests to
assert failure + error logging instead of silent success.
* fix: CR batch 12 — serial normalization, conflict disambiguation, modal guard, test fixes
- sync/modules.py: replace all 'serial == "-"' guards with _PLACEHOLDER_VALUES
normalization (import from modules_view) for consistency across InstallModuleView,
InstallBranchView, PreviewModuleReplaceView, ReplaceModuleView
- sync/modules.py: PreviewModuleReplaceView — use count() instead of .first() to
detect ambiguous serial conflicts; pass serial_conflict_ambiguous=True to template
- sync/modules.py: ReplaceModuleView — use count() to detect ambiguous conflicts;
return error redirect when count > 1 instead of silently picking one
- module_mismatch_modal.html: add serial_conflict_ambiguous branch (warning alert);
gate 'Update Serial Only' and 'Replace Module' buttons when conflict is ambiguous
- tables/mappings.py: render em-dash for serial_matches_device rules in
render_require_serial_match_parent (flag has no effect for that match type)
- test_coverage_bulk_import.py: remove xfail marker — TTL refresh is now implemented
- test_coverage_utils.py: assert PlatformMapping.objects.get called with librenms_os__iexact
- test_modules_view.py: assert row.get('serial_conflict_module') is None (not 'not in row')
- test_module_replace.py: add count.return_value to mock chain for new count() calls
- tests/e2e/test_module_install.py: extend os.environ instead of replacing in subprocess
* fix: address deferred CR findings from issues #53-#56
where can_move_from and serial_conflict_module are set. Button posts to
move_module view with conflict_module_id and target_bay_id.
valid dict from fetch_device_with_cache mock instead of None, so the
test exercises the full sync code path including cache dict population.
- save.assert_called_once() → assert_called_once_with(update_fields=
['status', 'completed']) in test_does_not_overwrite_completed_when_not_in_rq
- Use distinct server_key 'prod-server' in DeviceModuleTableView tests
instead of 'default' to catch accidental default fallback
- Use permission-specific has_perm side_effect instead of return_value=True
per name (dict[str, list]) instead of keeping only the first. Resolve
mapping-based lookups by checking which bay has an installed_module when
duplicates exist — return only if unambiguous (exactly one occupied bay).
Closes #53
Closes #54
Closes #55
Closes #56
* fix: address 3 remaining CR findings from PR #50
test_coverage_actions.py: Fix false-positive test — wrong POST key
'vc_detection_enabled' replaced with the actual key 'enable_vc_detection'
that _resolve_vc_detection_enabled() reads. Previously the mock returned
None for every key and the assertion passed via the default=False fallback.
views/sync/modules.py: Unwrap cached inventory dict payload before use.
The inventory cache stores {"inventory": [...], "librenms_id": ...} but all
4 sync action views (InstallBranchView, InstallSelectedView,
ModuleMismatchPreviewView, ReplaceModuleView) iterated the raw payload as
a list, causing item.get("entPhysicalIndex") to iterate dict keys instead
of inventory rows. Added _extract_inventory_list() helper that unwraps the
dict payload and tolerates legacy list payloads.
views/sync/modules.py: Apply ignore rules to root item in _collect_branch().
Previously only children were filtered by ignore rules; the root/parent
item was always enqueued. Now _collect_branch() checks the root item:
'skip' → return []; 'transparent' → collect children only, not root.
Also updated InstallSelectedView to filter both 'skip' and 'transparent'
(was only 'skip') to match table view parity.
* refactor: drop _extract_inventory_list legacy-list fallback
The "legacy" list payload referenced in e96b3f3 never existed in any
released or upstream code — the bare-list writer was replaced inside
this same branch (7dfd436) before any consumer shipped, so no
deployed cache can hold one. The fallback also contradicted
BaseModuleTableView.get_context_data, which evicts non-dict payloads
as cache misses.
Drop the `or []` non-dict branch so the helper matches the canonical
reader, and update the inventory-cache stubs in test_module_replace.py
and test_sync_modules.py to use the dict format produced by the writer.
* fix: race conditions, class-aware mappings, stale snapshot in module sync
_install_single: Lock target bay with select_for_update() before checking
occupancy. Previously two concurrent requests could both observe the bay
as empty, causing an IntegrityError that rolled back the entire batch.
Now consistent with InstallModuleView's locking pattern.
_find_parent_module_id: Make ancestor mapping resolution class-aware.
Exact mappings are now indexed by (librenms_name, librenms_class) with
class-specific preference and class-empty fallback. Regex mappings also
prefer class-matching rules before falling back to class-empty rules,
consistent with _lookup_regex_bay_mapping() in the base view.
ReplaceModuleView: Move target_bay/old_type_name/old_bay_name reads
after select_for_update() so they reflect the locked row's current state.
Previously these were captured from the pre-lock snapshot, which could
be stale if another request moved or replaced the module first.
MoveModuleView: Lock target bay with select_for_update() inside the
atomic block instead of using the pre-lock get_object_or_404 snapshot.
Tests updated for select_for_update chains and librenms_class on mock
mapping helpers.
* fix: normalize placeholder serials in UpdateModuleSerialView, fix regex fallback in _find_parent_module_id
UpdateModuleSerialView: add placeholder normalization consistent with all
other serial-handling paths (InstallModuleView, _install_single,
ReplaceModuleView, ModuleMismatchPreviewView).
_find_parent_module_id: replace 'class_matches or fallback_matches' with
'class_matches + fallback_matches' so empty-class regex rules are still
tried when class-specific rules exist but none match the name — matching
the base view's _lookup_regex_bay_mapping pattern.
* fix: lock module row before updating serial in UpdateModuleSerialView
Move Module fetch inside transaction.atomic() with select_for_update()
to prevent stale-instance writes when a concurrent replace/move changes
the module between the read and the save. Consistent with locking pattern
used by InstallModuleView, ReplaceModuleView, and MoveModuleView.
Update test to mock Module.objects.select_for_update() chain instead of
get_object_or_404 for the module.
* fix: use fullmatch+expand in _find_parent_module_id regex matching
Mirror base view's _lookup_regex_bay_mapping behavior:
- Use rm._compiled_pattern.fullmatch(name) instead of re.search()
- Use match.expand(rm.netbox_bay_name) for capture group substitution
- Prevents false-positive partial matches and enables regex backrefs
Update test helpers to set _compiled_pattern on mock regex mappings.
* fix: address CR review batch - regex, tests, docs
- Tighten Nokia regex: \w → [0-9] for part number digits, [A-Z0-9]
for transceiver cleanup pattern
- Use real dict for request.GET mock in test_background_jobs.py
- Make devcontainer discovery deterministic: env var override,
error on multiple matches
- Fix brittle '156 objects' banner filter in e2e tests
- Fix typos in virtual_chassis.md (NEtbox → NetBox, dispalys)
- Make README install snippet idempotent with grep guard
* revert: restore original docs/usage_tips/virtual_chassis.md
Revert grammar/typo edits to virtual_chassis.md — these changes
do not belong in this PR. Any docs updates should go through the
upstream repository directly.
* fix: revert bulk_import cancellation, VC comment, VM docstring, sync template improvements
- Restore periodic job cancellation checks (every 5th iteration)
- Improve VC zero-based position detection comment clarity
- Simplify VM create docstring, reorder server_key parameter
- Use resolved_name instead of sysName in sync template
- Move VC serial count badge to button label
- Add name check tooltip
* fix: restore naming resolution and VC logic lost during rebase
Rebase regression stripped resolve_naming_preferences(),
_determine_device_name(), and _generate_vc_member_name() from both
UpdateDeviceNameView and the sync base view's get_librenms_device_info().
Restored:
- resolved_name computation using user naming preferences (sysName vs
hostname, strip domain) in librenms_sync_view.py
- VC member name generation for virtual chassis devices
- VC sync device lookup for members without their own librenms_id
- resolved_name template context variable (used by sync_base template)
- Proper early bailout when LibreNMS has no usable name
Updated test mocks to set virtual_chassis=None and patch the restored
naming functions.
* revert: restore original README.md and virtual_chassis.md
These files should not be modified in this PR — docs and README
changes belong in upstream repository directly.
* fix: improve ambiguity test assertion and error message wording
- Rename test to test_match_none_returns_ambiguous_error and assert
'Ambiguous' appears in the error message to detect regressions
- Broaden error message to cover both DeviceTypeMapping and DeviceType
ambiguity (match_librenms_hardware_to_device_type returns None for
either case)
* fix: CR review - modal close, test line refs, generic e2e
- librenms_sync.js: replace closeBtn.click() with hideModal() in VLAN
modal save handler (aligns with upstream fix 7bf533f)
- test_coverage_bulk_import/devices/list: strip hardcoded line-number
references from docstrings to avoid drift
- tests/e2e/test_module_install: make device-agnostic — auto-detect
device, discover modules from table, generic assertions
* fix: three bugs in module inventory sync reported in PR #261 review
- select_for_update(of=("self",)) avoids FOR UPDATE on the nullable side
of the installed_module outer join, fixing the Install Selected crash
- add has_write_permission to full-page context so Install Selected form
renders on page load, not only after an htmx Refresh Modules swap
- guard htmx.process() with typeof check to prevent 'htmx is not defined'
error when the Replace modal fetches its preview fragment
* fix: improve module table readability in dark mode
Remove table-success row highlight from installed modules — the Installed
badge is sufficient to identify state, and the green row background makes
linked text unreadable in dark theme.
Add text-white/text-dark contrast classes to all badge colors so they
remain legible in both light and dark themes.
* feat: split Mappings into its own navigation group
Move all mapping items (Interface, Device Type, Module Type, Module Bay,
Normalization Rules, Inventory Ignore Rules, Platform) out of the
Settings group into a dedicated Mappings group for clearer navigation.
* refactor: reorder navigation groups to Import, Status Check, Mappings, Settings
* fix: remove table-light from mismatch modal thead for dark mode
table-light forces a white background regardless of theme. Removing it
lets Bootstrap 5.3 theme-aware styling apply to the header row.
* fix: replace table-danger/warning cell backgrounds with text colors in mismatch modal
table-danger/table-warning produce a pinkish/yellow background that
clashes with Bootstrap's link color in dark mode. Switch to text-danger
and text-warning with fw-semibold — pure text color works on any
background and reads correctly in both light and dark themes.
* fix: remove all row background highlighting from module sync table
table-warning and table-danger row classes cause unreadable contrast in
dark mode. The status badge is sufficient to identify each row state.
Remove the row_class field entirely along with the dead row_attrs entry.
* refactor: reorder Mappings group — all mappings first, then Ignore Rules, Normalization Rules
* fix: update tests to reflect row_class removal
Replace assertions on table-success/danger/warning row_class values
with assert "row_class" not in row now that row highlighting is gone.
* fix: auto-generate slug when creating platform from sync page (#279)
Platform() without a slug fails full_clean() in NetBox 4.x. Derive the
slug from the platform name via slugify so the modal submit succeeds.
* Revert "fix: auto-generate slug when creating platform from sync page (#279)"
This reverts commit 3029cd3c2fc7ef054104e721a663ccde2109bdbb.
* fix: generate slug when creating Platform via CreateAndAssignPlatformView
Platform requires a slug field; omitting it caused full_clean() to raise
a ValidationError before the platform could be saved. Fixes #279.
* test: assert Platform constructor receives slug in CreateAndAssignPlatformView
Regression test for #279 — verifies slugify(name) is passed to the
Platform constructor so the missing-slug error cannot recur undetected.
* fix: wrap OOB row in table to survive HTMX HTML parsing
When AddDeviceTypeMappingView returns a combined response with both
the modal OOB (a <div>) and the row OOB (a <tr>), HTMX wraps the
response in a <template> for parsing. A <tr> following a <div> in
that context is invalid HTML and gets silently dropped by the browser
parser, so the row never updates.
Wrap row_html in <table><tbody>...</tbody></table> before appending
to ensure the <tr> element survives parsing. The <div id='django-messages'>
inside is foster-parented outside the table by the parser, so both
OOBs are found and applied correctly.
* fix(imports): add HTTP status codes, remove redundant full_clean, fix JS Content-Type case-sensitivity
- AddDeviceTypeMappingView: return 400 for missing/invalid device_type_id,
404 for DeviceType.DoesNotExist, 500 for unexpected exceptions
- Remove redundant mapping.full_clean() before save() — DeviceTypeMapping
inherits FullCleanOnSaveMixin which calls full_clean() inside save()
- librenms_sync.js: lowercase Content-Type header before .includes() check
to handle case variants (e.g. 'Application/JSON')
* fix(js): extract JSON error message in delete-interfaces handler; guard updateHtmxModalLabel self-assignment
- DeleteNetBoxInterfacesView fetch: parse JSON error body (error/message/detail)
instead of throwing raw JSON string, consistent with other fetch handlers
- updateHtmxModalLabel: skip textContent assignment when header === label to
prevent stripping icon markup from the modal title element
* refactor(js): extract fetchErrorMessage() helper to unify fetch error handling
inline text/JSON handling. Helper extracts error/message/detail from
JSON responses, falls back to raw text, and truncates at 300 chars.
Eliminates drift between handlers and simplifies future additions.
* feat: exact-name-first platform lookup; optional mapping on platform create
- find_matching_platform(): try exact case-insensitive name match first,
fall back to PlatformMapping only when no direct name match exists;
update docstring accordingly
- _get_platform_info(): delegate to find_matching_platform() instead of
inline Platform.objects.get(); use 'or "-"' for null LibreNMS fields
- CreateAndAssignPlatformView: read 'librenms_os' + 'create_mapping' POST
params; create PlatformMapping(librenms_os, platform) when checkbox
checked and no existing mapping for that OS
- librenms_sync_base.html: add librenms_os hidden input, 'Add platform
mapping' checkbox, and JS warning when platform name differs from OS
- device_validation_details.html: inline device-type search form when
no matching device type
- urls.py + views/__init__.py: register and export AddDeviceTypeMappingView
- tests: align mocks and assertions to new lookup order
* fix(pr#50): address CR feedback on AddDeviceTypeMapping form and platform-mapping creation
- device_validation_details.html: add visually-hidden <label> for the
device-type search input (HTMLHint a11y)
- device_validation_details.html: replace hardcoded "/api/dcim/device-types/"
with {% url 'dcim-api:devicetype-list' %} so NetBox BASE_PATH deployments
work correctly
- CreateAndAssignPlatformView: surface PlatformMapping creation failures
to the user via messages.warning, and inform when an existing mapping
was found (no longer fails silently)
* fix(pr#50): isolate PlatformMapping save; ignore stale autocomplete results
- CreateAndAssignPlatformView: wrap PlatformMapping save in nested
transaction.atomic() so an IntegrityError on the unique librenms_os
constraint only rolls back the mapping savepoint, not the outer
device-platform-assignment transaction (which was previously left in
needs_rollback state, so messages.success would fire after a discarded
device save)
- device_validation_details.html: add monotonically increasing requestSeq
to the device-type autocomplete so out-of-order fetch responses can no
longer overwrite the dropdown with results for an older query
* fix(pr#50): check add_platformmapping permission when create_mapping is requested
CreateAndAssignPlatformView now reads 'create_mapping' from POST before the
permission check and dynamically adds ('add', PlatformMapping) to
required_object_permissions so require_all_permissions() enforces it.
Without this, a user with only add_platform + change_device could silently
create PlatformMapping objects through the checkbox.
* fix: address CodeQL security scan findings
- XSS (#18): escape() user-controlled error strings in bulk confirm HTML response
- XSS (#19,#20): escape() object_type param in device_fields.py HTTP responses
- Stack trace (#8): replace str(exc) with generic message in bulk import HTMX error
- Stack trace (#9): replace str(exc) with generic message in interfaces transaction error
- Stack trace (#13): replace catch-all str(e) with generic message in ip_addresses_view
- JS XSS (#4,#5): fix innerHTML spinner — store/restore full innerHTML, use DOM
for label to avoid reinterpreting textContent as HTML
- Workflow permissions (#1-#3): add permissions: contents: read to all three workflows;
publish-pypi job-level permissions also gains contents: read alongside id-token: write
- Devcontainer logging (#17): stop printing raw codespace name / CSRF / allowed-host
values to stdout in devcontainer config
- URL redirect (#6,#7): add comment — _get_safe_redirect_url already validates via
url_has_allowed_host_and_scheme; CodeQL false positive
- Lint: fix E741 ambiguous variable name in e2e test
* revert: remove workflow permissions additions (tracked separately)
* Fix PR review findings: available_roles, CSRF, test quality
- bulk_import.py: preserve available_roles in elif-not-actual_is_vm branch
(matches pattern of other clearing paths at lines 363, 384)
- librenms_sync.js: remove getCookie('csrftoken') fallback from two fetch
call sites; align with hidden-input-only CSRF convention used elsewhere
- test_vm_operations.py: strengthen log assertion to assert_any_call with
expected checkpoint messages (VM 1 and VM 5 of 10)
- test_vm_operations.py: delete duplicate test_job_cancellation_with_errored_status
(identical logic already covered by test_job_cancellation_breaks_loop)
- Skip mock-target change: apply_cluster/role_to_validation are lazy (in-function)
imports, so patching import_validation_helpers.X is correct — patching
vm_operations.X would fail as those names don't exist at module level
* Fix PR #58 review findings (batch 2)
- librenms_sync_view.py: Fix inverted comment on find_matching_platform order
- actions.py: Replace str(exc) with generic message in non-HTMX bulk import error handler
- actions.py: AddDeviceTypeMappingView — honour server_key, add NetBoxObjectPermissionMixin
with DeviceTypeMapping add/change permissions
- device_validation_details.html: Add hidden server_key input to dt-mapping form
- interfaces.py: Add logger.exception in DeleteNetBoxInterfacesView catch-all
- test_sync_devices.py: Strengthen VM type assertion to assert_called_once_with
including VirtualMachine class and pk kwarg
* Fix PR #58 review findings (batch 3)
- device_validation_details.html: Disable 'Add Mapping' button until device type
is selected from dropdown; enable on selection, re-disable on input clear
- devices.py: Use copy.copy(request) for child table view instances, consistent
with vms.py pattern
- test_coverage_devices.py: Update request assertions to use == (copy equality)
instead of 'is' identity to match new copy.copy behaviour
* Fix #59–#62: YAML anchor, DynamicModelChoiceField, ToggleColumn, write perms
Closes #59: Anchor Juniper MX regex pattern (^...$) so generic transceiver
pattern sorts first and MX-specific fallback works correctly.
Closes #60: Replace plain ModelChoiceField with DynamicModelChoiceField in
DeviceTypeMappingForm and ModuleTypeMappingForm for API-backed typeahead.
Closes #61: Add 'pk' to fields/default_columns in all 7 mapping tables so
NetBoxTable's built-in ToggleColumn renders the bulk-select checkbox.
Closes #62: Add LibreNMSWritePermissionMixin (permission_required =
PERM_CHANGE_PLUGIN) to mixins.py and apply it to all 35 mutation views
(Create, Edit, Delete, BulkImport, BulkDelete) in mapping_views.py.
* Fix PR #63 review findings: ordering, error handling, modal title, click listener cleanup
- utils.py: add .order_by('pk') to get_enabled_ignore_rules() for deterministic
first-match-wins behavior in _check_ignore_rules
- utils.py: add ambiguity_source key to find_matching_platform ambiguous returns
('platform' or 'mapping') to distinguish which source caused the conflict
- modules_view.py: _apply_installed_status else branch returns 'No Type' instead
of 'Installed' when matched_type is None
- actions.py: AddDeviceTypeMappingView exception handler uses logger.exception
and returns generic message instead of leaking str(exc) to the client
- device_fields.py: separate IntegrityError from ValidationError in
CreateAndAssignPlatformView — IntegrityError on PlatformMapping insert treated
as mapping_existed (concurrent insert) rather than a creation failure
- librenms_sync.js: updateHtmxModalLabel scopes .modal-title query to
#htmx-modal-body to avoid matching the outer #htmx-modal-label element
- device_validation_details.html: replace anonymous accumulating document click
listener with a named handleDocumentClick function that removes itself when
the dropdown element is detached from the DOM (HTMX fragment replacement)
- test_coverage_forms.py: use _make_form helper in test methods instead of
duplicating the patch setup inline
- test_platform_mapping.py: update ambiguous assertion to include ambiguity_source
- test_sync_modules.py: fix InventoryIgnoreRule mock to support .order_by() chain
- contrib/platform_mappings.yaml: create example file referenced in README
* Fix replacement template validation and deterministic bay lookup
Closes #64, Closes #65
Issue #64: ModuleBayMapping.clean() and NormalizationRule.clean() validated
replacement templates against test strings that didn't guarantee a regex match
(empty string, or pattern text itself), so invalid back-references like \2
on a single-group pattern silently passed. Add _validate_replacement_template()
helper that builds a synthetic guaranteed-match pattern with identical group
structure (named groups preserved), exercising all back-references.
Issue #65: _build_table_rows() used ChainMap(device_bays, *module_scoped_bays.values())
for transceiver items, whose iteration order was non-deterministic (dict insertion
order = queryset order). Replace with a dedicated _compute_all_bays() static
method that sorts module IDs by PK for stable first-match-wins collision
resolution, logs collisions at DEBUG level, and always lets device-level bays
win. Remove the now-unused 'from collections import ChainMap' import.
* Fix wildcard constraint, ambiguity message, and scoped permissions
Add DB-level UniqueConstraint for wildcard InterfaceTypeMapping rows
(librenms_speed IS NULL) to prevent concurrent duplicate inserts that
bypass the in-process clean() check. The existing constraint for
non-NULL rows gains an explicit condition so both are partial indexes.
Migration 0011 handles the rename + addition atomically.
Update sync_platform ambiguous-match error message to inspect
ambiguity_source ('platform' vs 'mapping') and direct users to either
the Platforms screen or the Platform Mappings screen as appropriate.
Restructure AddDeviceTypeMappingView.post() so only the plugin write
permission is checked upfront (cheap). The API call and hardware string
extraction happen first, then an existence check on DeviceTypeMapping
determines whether 'add' or 'change' object permission is required
before the actual get_or_create.
* devcontainer: restore full debug output in codespaces-configuration.py
Development-only file; logging config values (CSRF origins, allowed hosts)
is an accepted tradeoff. CodeQL alert dismissed intentionally.
* fix(migration): add preflight dedup before wildcard UniqueConstraint
Before enforcing unique_interface_type_mapping_wildcard, remove any
duplicate librenms_speed IS NULL rows (keeping lowest PK per
librenms_type). Guards against deployments that applied 0010 and hit
the race window before 0011 was available.
* fix: remove dead check_match, close TOCTOU race, fix migration db alias
- Remove InventoryIgnoreRule.check_match() — dead code never called
anywhere; the real matching logic with require_serial_match_parent
lives in _check_ignore_rules() in modules_view.py
- Close add→change permission race in AddDeviceTypeMappingView: use
select_for_update() inside transaction.atomic() and re-check change
permission if a concurrent request created the mapping in the window
between the upfront filter() and the write
- Use schema_editor.connection.alias for ORM reads/deletes in
remove_wildcard_duplicates migration so multi-DB deployments clean
up duplicates against the correct database
* fix: guard symmetric delete race in AddDeviceTypeMappingView
If existing_mapping was found upfront (change permission granted) but
the row was deleted before select_for_update(), the else branch would
create a new row without ever requiring add permission. Add a symmetric
guard: if existing_mapping and not locked, re-check add permission
before the create path runs.
* fix: catch IntegrityError on concurrent create in AddDeviceTypeMappingView
select_for_update() cannot lock absent rows, so two concurrent requests
both seeing no existing mapping can both attempt .create(). Catch
IntegrityError inside the transaction and return 409 so the client
retries; the second attempt will find the now-existing row and take the
update path with correct change-permission checking.
Also move IntegrityError import to top-level (was a deferred local
import inside _save_device).
* fix: inject hx-swap-oob on device row in AddDeviceTypeMappingView response
The <table><tbody> wrapper was already in place to keep the <tr> in a valid
HTML context for the browser parser, but the hx-swap-oob attribute was never
injected, so HTMX had no instruction to OOB-swap the background row. Without
it the row stays stale until a manual refresh.
String-replace the known <tr id="device-row-{id}"> prefix (count=1) before
wrapping, so both the modal and the row are updated in a single response.
* fix: use int:device_id converter on all device-import URL patterns
CodeQL flagged reflected XSS on AddDeviceTypeMappingView because device_id
was <str:device_id>, making it a user-controlled string embedded in the
HTML response. LibreNMS device IDs are always integers; switching to
<int:device_id> on all seven device-import/* routes gives Django's URL
router built-in type validation and removes the taint path.
* fix: suppress false-positive CodeQL XSS on AddDeviceTypeMappingView response
Both oob_modal and row_html are rendered by Django template views, which
auto-escape all user-supplied values. CodeQL cannot model Django's template
engine as a sanitizer and flags all request → template-render → HttpResponse
paths as reflected XSS. Add lgtm[py/reflected-xss] suppression with a
comment explaining why these paths are safe.
* fix: use format_html + mark_safe to clear CodeQL XSS on AddDeviceTypeMappingView
The previous `# lgtm[py/reflected-xss]` suppression is LGTM.com legacy syntax
and is not honored by GitHub's modern CodeQL action, so the alert returned.
The remaining taint path is request -> detail_view.get(request, device_id) /
render_device_row(request, ...) -> .content.decode() -> f-string into
HttpResponse; the inner views auto-escape via Django templates, but CodeQL
does not credit template rendering as a sanitizer at this composition site.
Compose the OOB envelope with format_html() and pass the rendered inner HTML
through mark_safe(). Both are recognized by CodeQL's Django XSS taint model
as sanitizers, and the assertion is accurate -- the inner HTML originates
from auto-escaping render() calls. Drop the stale comment block and the
ineffective lgtm suppression.
* fix: skip change-permission escalation in concurrent-create no-op path
When a concurrent request creates a DeviceTypeMapping between our upfront
read and the select_for_update() lock, and the locked row already maps to
the same device_type_id, the write block is a no-op (lines 1492-1495 skip
save). Escalating to change permission in that case rejects callers who
hold only add permission even though nothing would be mutated.
Short-circuit: only require change permission when locked.netbox_device_type_id
!= device_type_id (i.e. we will actually overwrite).
Also add CodeQL XSS suppression pattern to copilot-instructions.md.
* docs: clarify mark_safe is a trust assertion, not a sanitizer
CodeRabbit correctly flagged that the prior wording described mark_safe()
as a sanitizer. Rewrote the CodeQL section to be explicit: mark_safe() only
asserts that the caller has verified the string is safe; it must only be
used on server-rendered Django view output, never on raw user input.
---------
Co-authored-by: Andy Norwood <2754635+bonzo81@users.noreply.github.com>
* feat(inventory): modules/inventory sync tab with mapping rules
Add inventory/modules sync functionality:
- Six mapping model types: DeviceTypeMapping, ModuleTypeMapping,
ModuleBayMapping, NormalizationRule, InventoryIgnoreRule, PlatformMapping
- Migration 0010 creating all mapping tables
- Modules sync tab on Device/VM detail pages with ENTITY-MIB inventory data
- Install, replace, and move module actions
- Mapping CRUD views with YAML bulk export for all mapping models
- LibreNMS API: get_device_transceivers() for transceiver data
- Platform matching: PlatformMapping lookup before name-exact match
- contrib/ YAML files with example mappings and rules
- Comprehensive test coverage (test_sync_modules, test_modules_view,
test_module_replace, test_platform_mapping, test_tables_modules)
* fix tests: update db-fallback tests to match new RQ-only cancellation behavior
_is_job_cancelled now returns False on RQ/Redis unavailability instead
of falling back to DB status. Update 5 tests that were asserting the
old DB-fallback behavior:
- test_db_fallback_logs_via_module_logger_when_job_logger_none →
test_rq_unavailable_does_not_cancel_import: RQ unavailable means
processing continues, not cancelled
- test_job_db_fallback_stopped/errored_before_validation_loop →
test_job_cancelled_before_validation_loop_returns_empty /
test_rq_unavailable_job_not_cancelled_in_preloop: patch
_is_job_cancelled directly to test early-exit behavior
- test_job_validation_loop_db_fallback_stop →
test_job_cancelled_in_validation_loop_returns_empty: use
_is_job_cancelled side_effect to simulate mid-loop cancellation
- test_job_rq_check_exception_uses_db_status_and_exits →
test_rq_fetch_exception_does_not_cancel_process_filters: assert
result has 1 device (not []) when RQ unavailable
* fix: updated tests
* Apply CR findings: code quality, test, docs, and template fixes
- Remove dead _resolve_naming_preferences from actions.py (never called)
- Unify vc_detection_enabled parsing in BulkImportConfirmView (POST+GET)
- Add ambiguity detection to get_module_types_indexed second loop
- Fix get_validated_device_cache_key doctest (wrong e3b0 hash)
- Add status=='ok' envelope check before transceivers shape validation
- Strip netbox_bay_name in ModuleBayMapping.clean()
- Use server_info.server_key in _module_sync.html refresh form
- Guard module_mismatch_modal Update Serial form on serial_conflict/installed
- Remove duplicate NoSuchJobError import in test_coverage_api2.py
- Fix _is_job_cancelled side_effect count for in-loop cancellation test
- Precompute sibling_counts in _build_table_rows to eliminate N+1 queries
- Accept sibling_counts in has_nested_name_conflict (DB fallback preserved)
- Update test_has_installable_children fake_build_row signature
- Coerce entPhysicalParentRelPos to int in siblings sort (prevent TypeError)
- Move get_queue inside try block in api/views.py sync_job_status
- Replace MD5 with SHA256 for VC domain fingerprint (FIPS compliance)
- Fix test_role_is_read_from_validation to test validation dict path
- Add test_module_replace.py to docs/development/testing.md
- Add platform_mappings.yaml row to contrib/README.md
- Fix QSFP28-DD-2X100G-LR4 typo in module_type_mappings.yaml
- Clarify librenms_id auto-create in docs/usage_tips/custom_field.md
* Apply second batch of CR findings on pr/inventory-core
- Unify librenms_id auto-create version reference to 0.4.3 in docs
- Move _LIBRENMS_JOB_NAMES to module-level constant in api/views.py
- Add status=='ok' envelope check in port handler in librenms_api.py
- Split mapping ambiguity tracking in get_module_types_indexed (separate
mapping_seen/mapping_ambiguous so explicit mappings always win over base
ModuleType ambiguity)
- Handle match_type=='ambiguous' in find_matching_platform caller with
dedicated warning message about conflicting platform mappings
- Move vc_requested parse before validate_device_for_import in
BulkImportConfirmView and pass include_vc_detection=vc_requested
- Guard Replace Module form with {% if installed_module %} in
module_mismatch_modal.html to prevent empty module_id submission
- Add mock_db_job.status/completed assertions to api2 test
- Extend cancellation test side_effects to 4 entries to target in-loop
check (lines 507, 534, 566, 574) in both RQ and _is_job_cancelled tests
- Assert success list in test_no_warning_when_cluster_found
* Apply CR findings batch 3: test hardening, PlatformMapping lazy import, module bay normalization, BulkExportYAMLView permissions
- test_coverage_sync_views2: patch get_object_or_404 to isolate from ORM
- test_coverage_device_fields: assert save/full_clean not called on no-match
- test_librenms_id: use exact tuple set comparison for Q branch children
- test_init: assert DB alias used in custom field creation
- utils.py: move PlatformMapping import to function scope (lazy); update all test patches to models.PlatformMapping; remove create=True
- test_platform_mapping: patch require_object_permissions instead of require_write_permission
- test_sync_modules: mock apply_normalization_rules in _match_module_bay tests
- views/base/modules_view.py: apply NormalizationRule(scope=module_bay) to candidate names in _match_module_bay
- views/mapping_views.py: BulkExportYAMLView uses NetBoxObjectPermissionMixin with view permission
- views/object_sync/devices.py: precompute has_write_permission once in get_table()
* cr: batch 4 — prefetch_related, deterministic export, test improvements
- utils.py: add prefetch_related('interfacetemplates') to ModuleType query
and prefetch_related('netbox_module_type__interfacetemplates') to
ModuleTypeMapping query in get_module_types_indexed() to avoid N+1
queries in has_nested_name_conflict()
- views/mapping_views.py: add .order_by('pk') to BulkExportYAMLView filter
for deterministic YAML export; add select_related() to all 4 export
subclass querysets (DeviceTypeMapping, ModuleTypeMapping,
NormalizationRule, PlatformMapping)
- tests/test_utils.py: assert exact Platform.objects.get kwargs in 2 platform
tests; fix vc.master = None -> vc.master = master to exercise designated-
master-without-IP branch
- tests/test_platform_mapping.py: update mock chain for .order_by(); complete
test_returns_yaml_content_type with actual view call and content-type assertion
- tests/test_sync_modules.py: fix mock chain for .prefetch_related() in
TestGetModuleTypesIndexed
* cr: batch 5 — normalization scoping bug, test fixes
- utils.py: fix apply_normalization_rules() else-branch to filter
manufacturer__isnull=True so callers without manufacturer context never
have vendor-specific rules applied to their values
- tests/test_sync_modules.py: add test_mapping_overrides_ambiguous_base_key
to lock down the separate-ambiguous-sets behaviour in
get_module_types_indexed(); fix test_regex_mapping_with_backreference to
use 'Optics 0/0/0/5' (with space) so the assertion cannot pass via
exact-name fallback — only the regex expansion path can produce a match
- tests/test_platform_mapping.py: remove dangling assertions accidentally
left inside test_returns_200_with_empty_selection (PlatformMapping import
and existence check now live in test_all_mapping_bulk_export_yaml_views_exist)
* cr: batch 6 — normalization rule caching, test correctness
- utils.py: add preload_normalization_rules() helper that preloads
NormalizationRule rows for a (scope, manufacturer) combination into a
dict keyed by (scope, manufacturer_pk_or_None); update
apply_normalization_rules() to accept preloaded_rules kwarg and use
preloaded lists when provided (skipping DB queries); update
resolve_module_type() to accept norm_rules kwarg and thread it through
to apply_normalization_rules — eliminates N+1 DB queries in
_match_module_bay and _build_row loops
- views/base/modules_view.py: call preload_normalization_rules() in
_build_context for both 'module_bay' and 'module_type' scopes; pass
preloaded rules via self._norm_rules_bay/_norm_rules_type to
_match_module_bay and _build_row respectively
- tests/test_platform_mapping.py: add test_multiple_platform_mappings_returns_ambiguous
asserting PlatformMapping.MultipleObjectsReturned yields
match_type='ambiguous' and that Platform.objects.get is never called
- tests/test_sync_modules.py: fix test_mapping_overrides_ambiguous_base_key
to use distinct mapping key 'SFP-1G-LX-EXPLICIT' so 'SFP-1G-LX' is
absent and only the explicit key is present; fix
test_uninstalled_bay_is_skipped to add grandparent bay with installed
module (pk=99) and assert walk continues past empty bay to return 99;
fix test_class_scoped_mapping_preferred to pass [m_generic, m_class] so
priority logic must actively prefer class-scoped mapping; patch
preload_normalization_rules in tests that call _build_context directly;
update apply_normalization_rules lambda patches to accept **kw
* fix: FPC slot int/str comparison, stale docstring, test patch targets
- _fpc_slot_matches: convert match.group(1) and parent_bay.position to int
before comparing (Python 3: '1' == 1 is False); handle ValueError with
safe fallbacks
- apply_normalization_rules docstring: clarify that manufacturer=None applies
only unscoped (manufacturer__isnull=True) rules, not all scope rules
- test_modules_view._run_build_context: patch load_bay_mappings and
get_enabled_ignore_rules at utils level instead of patching model classes
that _build_context never references directly; remove now-unused
mock_ignore_qs variable
* fix: surface ambiguous platform, preloaded-rules fallback, serial mismatch, transceiver ignore
- actions.py sync_platform: add explicit elif for match_type='ambiguous' so
users get a clear conflict message instead of generic 'not found' error when
multiple PlatformMapping rows match the same OS string (works towards #51)
- utils.py apply_normalization_rules: when preloaded_rules is provided, check
key presence before using the dict; fall back to DB query when (scope,
mfg_pk) or (scope, None) is absent, preventing silent omission of vendor
rules for manufacturers not included in the preloaded dict
- utils.py match_librenms_hardware_to_device_type: update Returns docstring to
dict | None and document the MultipleObjectsReturned → None case
- modules_view.py _apply_installed_status: drop nb_serial from the guard so a
module with a LibreNMS serial is flagged as Serial Mismatch even when NetBox
has no serial recorded (lnms_serial and lnms_serial != nb_serial)
- modules_view.py _collect_top_items: apply ignore-rule check to transceiver-
synthesised items before appending, so InventoryIgnoreRules can suppress
optics from get_device_transceivers()
- test_modules_view.py: rename test that expected the old nb_serial-required
behavior; update assertions to Serial Mismatch + can_update_serial + can_replace
- test_modules_view.py: wrap two early-return _detect_serial_conflicts tests in
patch(dcim.models.Module) and assert filter was never called
* fix: canonical librenms_id lookup, None guard, transceiver transparent, vc flag case
- utils.py find_by_librenms_id: strip whitespace and canonicalize leading-zero
string IDs before building Q filters; '042' and '42 ' now resolve to
int_value=42 / canonical_str='42' and both forms are added to the query so
they match records stored as numeric 42 or string '42'
- modules_view.py _find_parent_container_name: use (... or '') pattern to guard
against entPhysicalName being explicitly None in the ENTITY-MIB payload
- modules_view.py _match_module_bay: same None guard for entPhysicalName,
entPhysicalDescr, entPhysicalClass on the item dict
- modules_view.py _collect_top_items: treat 'transparent' the same as 'skip'
for transceiver-synthesised rows so transparent synthetic items are not
added to top_items
- actions.py BulkImportDevicesView: normalise vc_detection_enabled flag with
.lower() before membership test so 'ON', 'True', 'TRUE' all parse correctly,
consistent with BulkImportConfirmView
* Apply CR batch 10 fixes
- bulk_import: check cancellation every iteration (not every 5th)
- models: always call full_clean() on save, remove update_fields guard
- tables/modules: add has_write_permission param to LibreNMSModuleTable,
gate selection column and render_actions on it
- modules_view: normalize placeholder model/serial strings in transceiver
merge; filter placeholder serials from inv_serials set
- api/views: skip DB status update when job is already in a terminal state
- utils: add 'ambiguous' case to find_matching_platform docstring
- utils: memoize DB fallback into preloaded_rules in apply_normalization_rules
- utils: use try/except int() instead of isdigit() for +42 style IDs
- devices: pass has_write_permission to LibreNMSModuleTable constructor
- test_modules_view: use SimpleNamespace instead of MagicMock in
_determine_status tests for unambiguous truthiness checks
- test_sync_modules: assert checkbox HTML in selection cell; update
test_install_module_view_not_in_base to assert public import path;
add order-independent check for class-scoped mapping preference
- test_tables_modules: set has_write_permission=True in _make_table helper;
add no-write-permission test case
- test_utils: assert exact kwargs on DeviceTypeMapping.objects.get and
PlatformMapping.objects.get calls
- test_vm_operations: patch _is_job_cancelled directly instead of mutating
job.job.status via refresh_from_db side_effect
- test_coverage_base_views2: rename test to reflect actual behavior
(cache entry present but lacks port_id)
- test_coverage_devices: update constructor assertion to include
has_write_permission kwarg
* Apply CR batch 11 fixes
- models: add FullCleanOnSaveMixin + clean() to InterfaceTypeMapping to
enforce uniqueness for NULL-speed rows (SQL UNIQUE skips NULL=NULL)
- utils: expand match_librenms_hardware_to_device_type docstring to
document all three fail-closed None cases (mapping, part_number, model
MultipleObjectsReturned), not just the mapping-table one
- test_vm_operations: remove stale mock_job.job.status='running' from
_run_bulk_with_mappings helper; patch _is_job_cancelled=False instead
* fix: normalize librenms_hardware/os to lowercase, fix convert_speed_to_kbps docstring
- DeviceTypeMapping.clean() and PlatformMapping.clean() now lowercase
the stored value after stripping, preventing case-variant duplicates
(e.g. 'IOS' and 'ios') that would cause MultipleObjectsReturned on
__iexact lookups. Closes #51.
- Fix convert_speed_to_kbps docstring: Returns section now reads
'int | None' to match the signature and implementation.
- Add TestDeviceTypeMappingModel tests for DeviceTypeMapping.clean()
(strip, lowercase, blank validation).
- Add test_clean_normalizes_to_lowercase and test_clean_strips_and_lowercases
to TestPlatformMappingModel.
* fix(js): address PR #258 review findings
- hideModal: remove all backdrops via querySelectorAll+forEach
- htmx:afterSettle: derive label from aria-labelledby, fallback to id
- initializeVlanModalSave: truncate/extract error body before display
* fix: handle unsaved manufacturer in normalization, fix JS modal/fetch issues
- utils.py: guard unsaved manufacturer (pk=None) in preload_normalization_rules
and apply_normalization_rules to prevent ValueError on DB query
- librenms_sync.js: add id="htmx-modal-label" to module mismatch modal header
so aria-labelledby target is preserved after innerHTML replacement
- librenms_sync.js: check response.ok before response.json() in deleteUrl fetch
so HTTP errors surface their status instead of a parse error
* fix: type annotation, non-positive librenms_id guard, htmx label listener
- utils.py: fix convert_speed_to_kbps parameter annotation to int|None
- utils.py: guard non-positive numeric librenms_id (<=0) same as None;
skip Q canonicalization for string '0'/'-1' after int parse
- librenms_sync.js: extract updateHtmxModalLabel(), listen at document
level for htmx:afterSettle, call from module-replace fetch completion
* fix: use _PLACEHOLDER_VALUES for serial normalization, fix docstring
- modules_view.py: _apply_installed_status and _detect_serial_conflicts
now use _PLACEHOLDER_VALUES set instead of only guarding against "-"
so serials like 'unknown', 'n/a', 'na' are treated as absent
- utils.py: update convert_speed_to_kbps Args docstring to int|None
* Fix CR batch: device_type ambiguity, platform MOR, cache invalidation, ChainMap bay lookup
- device_fields.py: distinguish None (ambiguous) from failed match result;
surface a specific error for duplicate DeviceType mappings
- utils.py: find_matching_platform returns {found:False, match_type='ambiguous'}
on Platform.MultipleObjectsReturned instead of silently taking .first()
- modules_view.py: invalidate stale inventory cache before early-return renders
when librenms_id is falsy or inventory fetch fails
- modules_view.py: _lookup_regex_bay_mapping iterates all ChainMap scopes
so same-named bays in different scopes are all checked
- tests: update TestFindMatchingPlatformMultipleReturned to expect ambiguous
* Promote SKIP_TYPES and _NON_HARDWARE_CLASSES to module-level constants
Move inline set literals SKIP_TYPES and _NON_HARDWARE_CLASSES out of their
method bodies and into module scope, consistent with _PLACEHOLDER_VALUES and
other module-level constants. Rename SKIP_TYPES to _SKIP_TRANSCEIVER_TYPES
to clarify its domain.
* Fix find_matching_platform docstring and non-positive string librenms_id guard
- utils.py: broaden find_matching_platform docstring to state 'ambiguous'
applies both to multiple PlatformMapping entries and to duplicate
exact-name Platform rows (Platform.MultipleObjectsReturned)
- utils.py: add early-return guard in find_by_librenms_id for string
librenms_id values that parse to <= 0 (e.g. '0', '-1', '000'), preventing
them from reaching the Q clauses and matching stale/corrupted records
* fix: normalize placeholder serials and txr_type in modules_view
_check_ignore_rules: normalize item_serial, device_serial, and
ancestor_serial against _PLACEHOLDER_VALUES so sentinels like
'unknown'/'n/a' are treated as absent and do not trigger or
short-circuit serial_matches_device rules or require_serial_match_parent
ancestor walks.
_merge_transceiver_data: normalize txr_type against _PLACEHOLDER_VALUES
(same as model/serial) so placeholder types like 'unknown' do not bypass
the _SKIP_TRANSCEIVER_TYPES guard and produce synthetic rows with
display_model set to a placeholder string.
* Fix whitespace-only librenms_id, BUILTIN model placeholder, FPC-scope exact-bay fallback, has_write_permission in HTMX render
- utils.py: treat whitespace-only librenms_id strings as absent (return None
after strip() when cleaned == "") so they don't reach the Q-object builder
- modules_view.py: extend transceiver backfill to also replace 'BUILTIN' model
and serial values, not just those already in _PLACEHOLDER_VALUES
- modules_view.py: exact-name bay fallback now iterates ChainMap scopes and
calls _fpc_slot_matches() to avoid returning the wrong-scope bay when
duplicate bay names exist across scopes (mirrors the regex path behaviour)
- modules_view.py: add has_write_permission to all render() calls in post()
so the Install Selected button is visible in HTMX-refreshed content
* Fix non-integer librenms_id passthrough, stale cache on missing ID, exact-mapping ChainMap scope
- utils.py: non-integer strings (e.g. 'abc') now return None in
find_by_librenms_id() instead of falling through to build Q objects;
changed 'except ValueError: pass' to 'except ValueError: return None'
- modules_view.py: get_context_data() re-validates the device's current
LibreNMS ID before serving cached inventory; clears cache and returns
empty context when the mapping has been removed since the cache was written
- modules_view.py: _lookup_exact_bay_mapping() now iterates ChainMap scopes
and calls _fpc_slot_matches() before returning, matching the existing
behaviour of _lookup_regex_bay_mapping() and the name-fallback path
* fix: normalize _GENERIC_CONTAINER_MODELS to lowercase; embed librenms_id in inventory cache
- modules_view: lowercase _GENERIC_CONTAINER_MODELS set and apply .lower() at
all 5 comparison sites so values like 'builtin', 'default', 'n/a' received
from LibreNMS in any case are treated as generic containers
- modules_view: store {'inventory': data, 'librenms_id': id} in the inventory
cache instead of the raw list; get_context_data validates the embedded
librenms_id against the current mapping so remapped devices never serve stale
inventory (non-dict/legacy entries are treated as cache misses)
* fix: treat duplicate-serial module conflicts as ambiguous instead of silently overwriting
When multiple Module objects share the same serial, the old loop would
overwrite row["serial_conflict_module"] nondeterministically. Now we
group conflicts by serial and only set the move target when exactly one
candidate exists; multiple candidates set serial_conflict_ambiguous.
* fix: clear stale cache on legacy payload; ungate generic-model filter from phys class
- modules_view: get_context_data now calls cache.delete(cache_key) before
returning when the cached payload is not the new dict format so pre-upgrade
list-form entries are evicted and the next request regenerates fresh data
- modules_view: collapse the two-check current-item filter into a single
'model in _GENERIC_CONTAINER_MODELS' test (removes the phys_class=='container'
gate) so empty-model non-container items are also treated as generic
- modules_view: remove the anc_class=='container' guard from the ancestor walk
so any inventory-class ancestor with a generic model (e.g. a 'module' row
with model='builtin') is treated as transparent instead of blocking its
subtree
* Remove dead vc_requested assignment left by rebase
The rebase onto pr/code-quality-fixes dropped the consumer of vc_requested
in favor of the hoisted vc_detection_enabled variable, leaving the
assignment itself as an orphan that ruff F841 flags.
* fix: VC zero-based detection all-zeros guard; align VC-perm tests with fail-closed behavior
- virtual_chassis.py: only treat stack as 0-based when positions span 0 AND a
positive value. When every entPhysicalParentRelPos is 0 the data is invalid
and the shift produced colliding positions (all members → slot 1); fall
through to the per-member idx+1 fallback instead.
- test_coverage_bulk_import.py: PR #257 fails stack imports fast when the
user lacks dcim.add_virtualchassis. Update both VC-permission tests to
assert failure + error logging instead of silent success.
* fix: CR batch 12 — serial normalization, conflict disambiguation, modal guard, test fixes
- sync/modules.py: replace all 'serial == "-"' guards with _PLACEHOLDER_VALUES
normalization (import from modules_view) for consistency across InstallModuleView,
InstallBranchView, PreviewModuleReplaceView, ReplaceModuleView
- sync/modules.py: PreviewModuleReplaceView — use count() instead of .first() to
detect ambiguous serial conflicts; pass serial_conflict_ambiguous=True to template
- sync/modules.py: ReplaceModuleView — use count() to detect ambiguous conflicts;
return error redirect when count > 1 instead of silently picking one
- module_mismatch_modal.html: add serial_conflict_ambiguous branch (warning alert);
gate 'Update Serial Only' and 'Replace Module' buttons when conflict is ambiguous
- tables/mappings.py: render em-dash for serial_matches_device rules in
render_require_serial_match_parent (flag has no effect for that match type)
- test_coverage_bulk_import.py: remove xfail marker — TTL refresh is now implemented
- test_coverage_utils.py: assert PlatformMapping.objects.get called with librenms_os__iexact
- test_modules_view.py: assert row.get('serial_conflict_module') is None (not 'not in row')
- test_module_replace.py: add count.return_value to mock chain for new count() calls
- tests/e2e/test_module_install.py: extend os.environ instead of replacing in subprocess
* fix: address deferred CR findings from issues #53-#56
where can_move_from and serial_conflict_module are set. Button posts to
move_module view with conflict_module_id and target_bay_id.
valid dict from fetch_device_with_cache mock instead of None, so the
test exercises the full sync code path including cache dict population.
- save.assert_called_once() → assert_called_once_with(update_fields=
['status', 'completed']) in test_does_not_overwrite_completed_when_not_in_rq
- Use distinct server_key 'prod-server' in DeviceModuleTableView tests
instead of 'default' to catch accidental default fallback
- Use permission-specific has_perm side_effect instead of return_value=True
per name (dict[str, list]) instead of keeping only the first. Resolve
mapping-based lookups by checking which bay has an installed_module when
duplicates exist — return only if unambiguous (exactly one occupied bay).
Closes #53
Closes #54
Closes #55
Closes #56
* fix: address 3 remaining CR findings from PR #50
test_coverage_actions.py: Fix false-positive test — wrong POST key
'vc_detection_enabled' replaced with the actual key 'enable_vc_detection'
that _resolve_vc_detection_enabled() reads. Previously the mock returned
None for every key and the assertion passed via the default=False fallback.
views/sync/modules.py: Unwrap cached inventory dict payload before use.
The inventory cache stores {"inventory": [...], "librenms_id": ...} but all
4 sync action views (InstallBranchView, InstallSelectedView,
ModuleMismatchPreviewView, ReplaceModuleView) iterated the raw payload as
a list, causing item.get("entPhysicalIndex") to iterate dict keys instead
of inventory rows. Added _extract_inventory_list() helper that unwraps the
dict payload and tolerates legacy list payloads.
views/sync/modules.py: Apply ignore rules to root item in _collect_branch().
Previously only children were filtered by ignore rules; the root/parent
item was always enqueued. Now _collect_branch() checks the root item:
'skip' → return []; 'transparent' → collect children only, not root.
Also updated InstallSelectedView to filter both 'skip' and 'transparent'
(was only 'skip') to match table view parity.
* refactor: drop _extract_inventory_list legacy-list fallback
The "legacy" list payload referenced in e96b3f3 never existed in any
released or upstream code — the bare-list writer was replaced inside
this same branch (7dfd436) before any consumer shipped, so no
deployed cache can hold one. The fallback also contradicted
BaseModuleTableView.get_context_data, which evicts non-dict payloads
as cache misses.
Drop the `or []` non-dict branch so the helper matches the canonical
reader, and update the inventory-cache stubs in test_module_replace.py
and test_sync_modules.py to use the dict format produced by the writer.
* fix: race conditions, class-aware mappings, stale snapshot in module sync
_install_single: Lock target bay with select_for_update() before checking
occupancy. Previously two concurrent requests could both observe the bay
as empty, causing an IntegrityError that rolled back the entire batch.
Now consistent with InstallModuleView's locking pattern.
_find_parent_module_id: Make ancestor mapping resolution class-aware.
Exact mappings are now indexed by (librenms_name, librenms_class) with
class-specific preference and class-empty fallback. Regex mappings also
prefer class-matching rules before falling back to class-empty rules,
consistent with _lookup_regex_bay_mapping() in the base view.
ReplaceModuleView: Move target_bay/old_type_name/old_bay_name reads
after select_for_update() so they reflect the locked row's current state.
Previously these were captured from the pre-lock snapshot, which could
be stale if another request moved or replaced the module first.
MoveModuleView: Lock target bay with select_for_update() inside the
atomic block instead of using the pre-lock get_object_or_404 snapshot.
Tests updated for select_for_update chains and librenms_class on mock
mapping helpers.
* fix: normalize placeholder serials in UpdateModuleSerialView, fix regex fallback in _find_parent_module_id
UpdateModuleSerialView: add placeholder normalization consistent with all
other serial-handling paths (InstallModuleView, _install_single,
ReplaceModuleView, ModuleMismatchPreviewView).
_find_parent_module_id: replace 'class_matches or fallback_matches' with
'class_matches + fallback_matches' so empty-class regex rules are still
tried when class-specific rules exist but none match the name — matching
the base view's _lookup_regex_bay_mapping pattern.
* fix: lock module row before updating serial in UpdateModuleSerialView
Move Module fetch inside transaction.atomic() with select_for_update()
to prevent stale-instance writes when a concurrent replace/move changes
the module between the read and the save. Consistent with locking pattern
used by InstallModuleView, ReplaceModuleView, and MoveModuleView.
Update test to mock Module.objects.select_for_update() chain instead of
get_object_or_404 for the module.
* fix: use fullmatch+expand in _find_parent_module_id regex matching
Mirror base view's _lookup_regex_bay_mapping behavior:
- Use rm._compiled_pattern.fullmatch(name) instead of re.search()
- Use match.expand(rm.netbox_bay_name) for capture group substitution
- Prevents false-positive partial matches and enables regex backrefs
Update test helpers to set _compiled_pattern on mock regex mappings.
* fix: address CR review batch - regex, tests, docs
- Tighten Nokia regex: \w → [0-9] for part number digits, [A-Z0-9]
for transceiver cleanup pattern
- Use real dict for request.GET mock in test_background_jobs.py
- Make devcontainer discovery deterministic: env var override,
error on multiple matches
- Fix brittle '156 objects' banner filter in e2e tests
- Fix typos in virtual_chassis.md (NEtbox → NetBox, dispalys)
- Make README install snippet idempotent with grep guard
* revert: restore original docs/usage_tips/virtual_chassis.md
Revert grammar/typo edits to virtual_chassis.md — these changes
do not belong in this PR. Any docs updates should go through the
upstream repository directly.
* fix: revert bulk_import cancellation, VC comment, VM docstring, sync template improvements
- Restore periodic job cancellation checks (every 5th iteration)
- Improve VC zero-based position detection comment clarity
- Simplify VM create docstring, reorder server_key parameter
- Use resolved_name instead of sysName in sync template
- Move VC serial count badge to button label
- Add name check tooltip
* fix: restore naming resolution and VC logic lost during rebase
Rebase regression stripped resolve_naming_preferences(),
_determine_device_name(), and _generate_vc_member_name() from both
UpdateDeviceNameView and the sync base view's get_librenms_device_info().
Restored:
- resolved_name computation using user naming preferences (sysName vs
hostname, strip domain) in librenms_sync_view.py
- VC member name generation for virtual chassis devices
- VC sync device lookup for members without their own librenms_id
- resolved_name template context variable (used by sync_base template)
- Proper early bailout when LibreNMS has no usable name
Updated test mocks to set virtual_chassis=None and patch the restored
naming functions.
* revert: restore original README.md and virtual_chassis.md
These files should not be modified in this PR — docs and README
changes belong in upstream repository directly.
* fix: improve ambiguity test assertion and error message wording
- Rename test to test_match_none_returns_ambiguous_error and assert
'Ambiguous' appears in the error message to detect regressions
- Broaden error message to cover both DeviceTypeMapping and DeviceType
ambiguity (match_librenms_hardware_to_device_type returns None for
either case)
* fix: CR review - modal close, test line refs, generic e2e
- librenms_sync.js: replace closeBtn.click() with hideModal() in VLAN
modal save handler (aligns with upstream fix 7bf533f)
- test_coverage_bulk_import/devices/list: strip hardcoded line-number
references from docstrings to avoid drift
- tests/e2e/test_module_install: make device-agnostic — auto-detect
device, discover modules from table, generic assertions
* docs: update instruction files for accuracy after v0.4.4-v0.4.6 changes
* fix: three bugs in module inventory sync reported in PR #261 review
- select_for_update(of=("self",)) avoids FOR UPDATE on the nullable side
of the installed_module outer join, fixing the Install Selected crash
- add has_write_permission to full-page context so Install Selected form
renders on page load, not only after an htmx Refresh Modules swap
- guard htmx.process() with typeof check to prevent 'htmx is not defined'
error when the Replace modal fetches its preview fragment
* fix: improve module table readability in dark mode
Remove table-success row highlight from installed modules — the Installed
badge is sufficient to identify state, and the green row background makes
linked text unreadable in dark theme.
Add text-white/text-dark contrast classes to all badge colors so they
remain legible in both light and dark themes.
* feat: split Mappings into its own navigation group
Move all mapping items (Interface, Device Type, Module Type, Module Bay,
Normalization Rules, Inventory Ignore Rules, Platform) out of the
Settings group into a dedicated Mappings group for clearer navigation.
* refactor: reorder navigation groups to Import, Status Check, Mappings, Settings
* fix: remove table-light from mismatch modal thead for dark mode
table-light forces a white background regardless of theme. Removing it
lets Bootstrap 5.3 theme-aware styling apply to the header row.
* fix: replace table-danger/warning cell backgrounds with text colors in mismatch modal
table-danger/table-warning produce a pinkish/yellow background that
clashes with Bootstrap's link color in dark mode. Switch to text-danger
and text-warning with fw-semibold — pure text color works on any
background and reads correctly in both light and dark themes.
* fix: remove all row background highlighting from module sync table
table-warning and table-danger row classes cause unreadable contrast in
dark mode. The status badge is sufficient to identify each row state.
Remove the row_class field entirely along with the dead row_attrs entry.
* refactor: reorder Mappings group — all mappings first, then Ignore Rules, Normalization Rules
* fix: update tests to reflect row_class removal
Replace assertions on table-success/danger/warning row_class values
with assert "row_class" not in row now that row highlighting is gone.
* fix: auto-generate slug when creating platform from sync page (#279)
Platform() without a slug fails full_clean() in NetBox 4.x. Derive the
slug from the platform name via slugify so the modal submit succeeds.
* Revert "fix: auto-generate slug when creating platform from sync page (#279)"
This reverts commit 3029cd3c2fc7ef054104e721a663ccde2109bdbb.
* fix: generate slug when creating Platform via CreateAndAssignPlatformView
Platform requires a slug field; omitting it caused full_clean() to raise
a ValidationError before the platform could be saved. Fixes #279.
* test: assert Platform constructor receives slug in CreateAndAssignPlatformView
Regression test for #279 — verifies slugify(name) is passed to the
Platform constructor so the missing-slug error cannot recur undetected.
* fix: generate slug when creating Platform via CreateAndAssignPlatformView
Platform requires a slug field; omitting it caused full_clean() to raise
a ValidationError before the platform could be saved. Fixes #279.
* test: assert Platform constructor receives slug in CreateAndAssignPlatformView
Regression test for #279 — verifies slugify(name) is passed to the
Platform constructor so the missing-slug error cannot recur undetected.
* fix: wrap OOB row in table to survive HTMX HTML parsing
When AddDeviceTypeMappingView returns a combined response with both
the modal OOB (a <div>) and the row OOB (a <tr>), HTMX wraps the
response in a <template> for parsing. A <tr> following a <div> in
that context is invalid HTML and gets silently dropped by the browser
parser, so the row never updates.
Wrap row_html in <table><tbody>...</tbody></table> before appending
to ensure the <tr> element survives parsing. The <div id='django-messages'>
inside is foster-parented outside the table by the parser, so both
OOBs are found and applied correctly.
* fix(imports): add HTTP status codes, remove redundant full_clean, fix JS Content-Type case-sensitivity
- AddDeviceTypeMappingView: return 400 for missing/invalid device_type_id,
404 for DeviceType.DoesNotExist, 500 for unexpected exceptions
- Remove redundant mapping.full_clean() before save() — DeviceTypeMapping
inherits FullCleanOnSaveMixin which calls full_clean() inside save()
- librenms_sync.js: lowercase Content-Type header before .includes() check
to handle case variants (e.g. 'Application/JSON')
* fix(js): extract JSON error message in delete-interfaces handler; guard updateHtmxModalLabel self-assignment
- DeleteNetBoxInterfacesView fetch: parse JSON error body (error/message/detail)
instead of throwing raw JSON string, consistent with other fetch handlers
- updateHtmxModalLabel: skip textContent assignment when header === label to
prevent stripping icon markup from the modal title element
* refactor(js): extract fetchErrorMessage() helper to unify fetch error handling
inline text/JSON handling. Helper extracts error/message/detail from
JSON responses, falls back to raw text, and truncates at 300 chars.
Eliminates drift between handlers and simplifies future additions.
* feat: exact-name-first platform lookup; optional mapping on platform create
- find_matching_platform(): try exact case-insensitive name match first,
fall back to PlatformMapping only when no direct name match exists;
update docstring accordingly
- _get_platform_info(): delegate to find_matching_platform() instead of
inline Platform.objects.get(); use 'or "-"' for null LibreNMS fields
- CreateAndAssignPlatformView: read 'librenms_os' + 'create_mapping' POST
params; create PlatformMapping(librenms_os, platform) when checkbox
checked and no existing mapping for that OS
- librenms_sync_base.html: add librenms_os hidden input, 'Add platform
mapping' checkbox, and JS warning when platform name differs from OS
- device_validation_details.html: inline device-type search form when
no matching device type
- urls.py + views/__init__.py: register and export AddDeviceTypeMappingView
- tests: align mocks and assertions to new lookup order
* fix(pr#50): address CR feedback on AddDeviceTypeMapping form and platform-mapping creation
- device_validation_details.html: add visually-hidden <label> for the
device-type search input (HTMLHint a11y)
- device_validation_details.html: replace hardcoded "/api/dcim/device-types/"
with {% url 'dcim-api:devicetype-list' %} so NetBox BASE_PATH deployments
work correctly
- CreateAndAssignPlatformView: surface PlatformMapping creation failures
to the user via messages.warning, and inform when an existing mapping
was found (no longer fails silently)
* fix(pr#50): isolate PlatformMapping save; ignore stale autocomplete results
- CreateAndAssignPlatformView: wrap PlatformMapping save in nested
transaction.atomic() so an IntegrityError on the unique librenms_os
constraint only rolls back the mapping savepoint, not the outer
device-platform-assignment transaction (which was previously left in
needs_rollback state, so messages.success would fire after a discarded
device save)
- device_validation_details.html: add monotonically increasing requestSeq
to the device-type autocomplete so out-of-order fetch responses can no
longer overwrite the dropdown with results for an older query
* fix(pr#50): check add_platformmapping permission when create_mapping is requested
CreateAndAssignPlatformView now reads 'create_mapping' from POST before the
permission check and dynamically adds ('add', PlatformMapping) to
required_object_permissions so require_all_permissions() enforces it.
Without this, a user with only add_platform + change_device could silently
create PlatformMapping objects through the checkbox.
* fix: address CodeQL security scan findings
- XSS (#18): escape() user-controlled error strings in bulk confirm HTML response
- XSS (#19,#20): escape() object_type param in device_fields.py HTTP responses
- Stack trace (#8): replace str(exc) with generic message in bulk import HTMX error
- Stack trace (#9): replace str(exc) with generic message in interfaces transaction error
- Stack trace (#13): replace catch-all str(e) with generic message in ip_addresses_view
- JS XSS (#4,#5): fix innerHTML spinner — store/restore full innerHTML, use DOM
for label to avoid reinterpreting textContent as HTML
- Workflow permissions (#1-#3): add permissions: contents: read to all three workflows;
publish-pypi job-level permissions also gains contents: read alongside id-token: write
- Devcontainer logging (#17): stop printing raw codespace name / CSRF / allowed-host
values to stdout in devcontainer config
- URL redirect (#6,#7): add comment — _get_safe_redirect_url already validates via
url_has_allowed_host_and_scheme; CodeQL false positive
- Lint: fix E741 ambiguous variable name in e2e test
* revert: remove workflow permissions additions (tracked separately)
* Fix PR review findings: available_roles, CSRF, test quality
- bulk_import.py: preserve available_roles in elif-not-actual_is_vm branch
(matches pattern of other clearing paths at lines 363, 384)
- librenms_sync.js: remove getCookie('csrftoken') fallback from two fetch
call sites; align with hidden-input-only CSRF convention used elsewhere
- test_vm_operations.py: strengthen log assertion to assert_any_call with
expected checkpoint messages (VM 1 and VM 5 of 10)
- test_vm_operations.py: delete duplicate test_job_cancellation_with_errored_status
(identical logic already covered by test_job_cancellation_breaks_loop)
- Skip mock-target change: apply_cluster/role_to_validation are lazy (in-function)
imports, so patching import_validation_helpers.X is correct — patching
vm_operations.X would fail as those names don't exist at module level
* Fix PR #58 review findings (batch 2)
- librenms_sync_view.py: Fix inverted comment on find_matching_platform order
- actions.py: Replace str(exc) with generic message in non-HTMX bulk import error handler
- actions.py: AddDeviceTypeMappingView — honour server_key, add NetBoxObjectPermissionMixin
with DeviceTypeMapping add/change permissions
- device_validation_details.html: Add hidden server_key input to dt-mapping form
- interfaces.py: Add logger.exception in DeleteNetBoxInterfacesView catch-all
- test_sync_devices.py: Strengthen VM type assertion to assert_called_once_with
including VirtualMachine class and pk kwarg
* Fix PR #58 review findings (batch 3)
- device_validation_details.html: Disable 'Add Mapping' button until device type
is selected from dropdown; enable on selection, re-disable on input clear
- devices.py: Use copy.copy(request) for child table view instances, consistent
with vms.py pattern
- test_coverage_devices.py: Update request assertions to use == (copy equality)
instead of 'is' identity to match new copy.copy behaviour
* Fix #59–#62: YAML anchor, DynamicModelChoiceField, ToggleColumn, write perms
Closes #59: Anchor Juniper MX regex pattern (^...$) so generic transceiver
pattern sorts first and MX-specific fallback works correctly.
Closes #60: Replace plain ModelChoiceField with DynamicModelChoiceField in
DeviceTypeMappingForm and ModuleTypeMappingForm for API-backed typeahead.
Closes #61: Add 'pk' to fields/default_columns in all 7 mapping tables so
NetBoxTable's built-in ToggleColumn renders the bulk-select checkbox.
Closes #62: Add LibreNMSWritePermissionMixin (permission_required =
PERM_CHANGE_PLUGIN) to mixins.py and apply it to all 35 mutation views
(Create, Edit, Delete, BulkImport, BulkDelete) in mapping_views.py.
* Fix PR #63 review findings: ordering, error handling, modal title, click listener cleanup
- utils.py: add .order_by('pk') to get_enabled_ignore_rules() for deterministic
first-match-wins behavior in _check_ignore_rules
- utils.py: add ambiguity_source key to find_matching_platform ambiguous returns
('platform' or 'mapping') to distinguish which source caused the conflict
- modules_view.py: _apply_installed_status else branch returns 'No Type' instead
of 'Installed' when matched_type is None
- actions.py: AddDeviceTypeMappingView exception handler uses logger.exception
and returns generic message instead of leaking str(exc) to the client
- device_fields.py: separate IntegrityError from ValidationError in
CreateAndAssignPlatformView — IntegrityError on PlatformMapping insert treated
as mapping_existed (concurrent insert) rather than a creation failure
- librenms_sync.js: updateHtmxModalLabel scopes .modal-title query to
#htmx-modal-body to avoid matching the outer #htmx-modal-label element
- device_validation_details.html: replace anonymous accumulating document click
listener with a named handleDocumentClick function that removes itself when
the dropdown element is detached from the DOM (HTMX fragment replacement)
- test_coverage_forms.py: use _make_form helper in test methods instead of
duplicating the patch setup inline
- test_platform_mapping.py: update ambiguous assertion to include ambiguity_source
- test_sync_modules.py: fix InventoryIgnoreRule mock to support .order_by() chain
- contrib/platform_mappings.yaml: create example file referenced in README
* Fix replacement template validation and deterministic bay lookup
Closes #64, Closes #65
Issue #64: ModuleBayMapping.clean() and NormalizationRule.clean() validated
replacement templates against test strings that didn't guarantee a regex match
(empty string, or pattern text itself), so invalid back-references like \2
on a single-group pattern silently passed. Add _validate_replacement_template()
helper that builds a synthetic guaranteed-match pattern with identical group
structure (named groups preserved), exercising all back-references.
Issue #65: _build_table_rows() used ChainMap(device_bays, *module_scoped_bays.values())
for transceiver items, whose iteration order was non-deterministic (dict insertion
order = queryset order). Replace with a dedicated _compute_all_bays() static
method that sorts module IDs by PK for stable first-match-wins collision
resolution, logs collisions at DEBUG level, and always lets device-level bays
win. Remove the now-unused 'from collections import ChainMap' import.
* Fix wildcard constraint, ambiguity message, and scoped permissions
Add DB-level UniqueConstraint for wildcard InterfaceTypeMapping rows
(librenms_speed IS NULL) to prevent concurrent duplicate inserts that
bypass the in-process clean() check. The existing constraint for
non-NULL rows gains an explicit condition so both are partial indexes.
Migration 0011 handles the rename + addition atomically.
Update sync_platform ambiguous-match error message to inspect
ambiguity_source ('platform' vs 'mapping') and direct users to either
the Platforms screen or the Platform Mappings screen as appropriate.
Restructure AddDeviceTypeMappingView.post() so only the plugin write
permission is checked upfront (cheap). The API call and hardware string
extraction happen first, then an existence check on DeviceTypeMapping
determines whether 'add' or 'change' object permission is required
before the actual get_or_create.
* devcontainer: restore full debug output in codespaces-configuration.py
Development-only file; logging config values (CSRF origins, allowed hosts)
is an accepted tradeoff. CodeQL alert dismissed intentionally.
* fix(migration): add preflight dedup before wildcard UniqueConstraint
Before enforcing unique_interface_type_mapping_wildcard, remove any
duplicate librenms_speed IS NULL rows (keeping lowest PK per
librenms_type). Guards against deployments that applied 0010 and hit
the race window before 0011 was available.
* fix: remove dead check_match, close TOCTOU race, fix migration db alias
- Remove InventoryIgnoreRule.check_match() — dead code never called
anywhere; the real matching logic with require_serial_match_parent
lives in _check_ignore_rules() in modules_view.py
- Close add→change permission race in AddDeviceTypeMappingView: use
select_for_update() inside transaction.atomic() and re-check change
permission if a concurrent request created the mapping in the window
between the upfront filter() and the write
- Use schema_editor.connection.alias for ORM reads/deletes in
remove_wildcard_duplicates migration so multi-DB deployments clean
up duplicates against the correct database
* fix: guard symmetric delete race in AddDeviceTypeMappingView
If existing_mapping was found upfront (change permission granted) but
the row was deleted before select_for_update(), the else branch would
create a new row without ever requiring add permission. Add a symmetric
guard: if existing_mapping and not locked, re-check add permission
before the create path runs.
* fix: catch IntegrityError on concurrent create in AddDeviceTypeMappingView
select_for_update() cannot lock absent rows, so two concurrent requests
both seeing no existing mapping can both attempt .create(). Catch
IntegrityError inside the transaction and return 409 so the client
retries; the second attempt will find the now-existing row and take the
update path with correct change-permission checking.
Also move IntegrityError import to top-level (was a deferred local
import inside _save_device).
* fix: inject hx-swap-oob on device row in AddDeviceTypeMappingView response
The <table><tbody> wrapper was already in place to keep the <tr> in a valid
HTML context for the browser parser, but the hx-swap-oob attribute was never
injected, so HTMX had no instruction to OOB-swap the background row. Without
it the row stays stale until a manual refresh.
String-replace the known <tr id="device-row-{id}"> prefix (count=1) before
wrapping, so both the modal and the row are updated in a single response.
* fix: use int:device_id converter on all device-import URL patterns
CodeQL flagged reflected XSS on AddDeviceTypeMappingView because device_id
was <str:device_id>, making it a user-controlled string embedded in the
HTML response. LibreNMS device IDs are always integers; switching to
<int:device_id> on all seven device-import/* routes gives Django's URL
router built-in type validation and removes the taint path.
* fix: suppress false-positive CodeQL XSS on AddDeviceTypeMappingView response
Both oob_modal and row_html are rendered by Django template views, which
auto-escape all user-supplied values. CodeQL cannot model Django's template
engine as a sanitizer and flags all request → template-render → HttpResponse
paths as reflected XSS. Add lgtm[py/reflected-xss] suppression with a
comment explaining why these paths are safe.
* fix: use format_html + mark_safe to clear CodeQL XSS on AddDeviceTypeMappingView
The previous `# lgtm[py/reflected-xss]` suppression is LGTM.com legacy syntax
and is not honored by GitHub's modern CodeQL action, so the alert returned.
The remaining taint path is request -> detail_view.get(request, device_id) /
render_device_row(request, ...) -> .content.decode() -> f-string into
HttpResponse; the inner views auto-escape via Django templates, but CodeQL
does not credit template rendering as a sanitizer at this composition site.
Compose the OOB envelope with format_html() and pass the rendered inner HTML
through mark_safe(). Both are recognized by CodeQL's Django XSS taint model
as sanitizers, and the assertion is accurate -- the inner HTML originates
from auto-escaping render() calls. Drop the stale comment block and the
ineffective lgtm suppression.
* fix: skip change-permission escalation in concurrent-create no-op path
When a concurrent request creates a DeviceTypeMapping between our upfront
read and the select_for_update() lock, and the locked row already maps to
the same device_type_id, the write block is a no-op (lines 1492-1495 skip
save). Escalating to change permission in that case rejects callers who
hold only add permission even though nothing would be mutated.
Short-circuit: only require change permission when locked.netbox_device_type_id
!= device_type_id (i.e. we will actually overwrite).
Also add CodeQL XSS suppression pattern to copilot-instructions.md.
* docs: clarify mark_safe is a trust assertion, not a sanitizer
CodeRabbit correctly flagged that the prior wording described mark_safe()
as a sanitizer. Rewrote the CodeQL section to be explicit: mark_safe() only
asserts that the caller has verified the string is safe; it must only be
used on server-rendered Django view output, never on raw user input.
* feat: add VC-aware module sync
* test: align VC module sync expectations
* fix: use all ancestor names as bay-mapping candidates
Previously _match_module_bay only used the nearest named ancestor as a
candidate for ModuleBayMapping lookups. For deeply nested ports (e.g.
GigabitEthernet3/11 inside a CVR-X2-SFP adapter on a WS-X4908-10GE)
the immediate parent is Port Container 3/11, which the existing contrib
regex ^Port Container (\d+)/(\d+)$ resolves to X2 Port 11 — a bay that
does not exist. The grandparent Port Container 3/2 would resolve
correctly to X2 Port 2, but was never tried.
Replace _find_parent_container_name (nearest-only) with
_find_all_ancestor_names which returns every named ancestor nearest
first. All ancestors beyond the immediate parent are appended to
candidate_names after item_name/item_descr so that the existing contrib
regex already handles this case without any new mapping entries.
The contrib description for the Port Container regex is updated to note
the CVR-X2-SFP use-case.
* Revert "fix: use all ancestor names as bay-mapping candidates"
This reverts commit 216fb84358b347326e8983cb87f046c0fda8b7c4.
* test: add prod-shape WS-X4908 bay-matching coverage
The existing _linecard_inventory fixture uses synthetic container names
that already match NetBox bays directly ("Slot 3", "X2 Port 2", "SFP slot")
and never exercises the contrib regex paths or the positional fallback
on real LibreNMS naming. As a result, no test covered the actual prod
data shape, and a flawed "walk all ancestors as bay candidates" fix
(216fb84, since reverted) passed all tests despite landing transceivers
in the wrong bay.
Capture the real shape from a Cisco WS-X4908-10GE linecard:
chassis "Switch System"
container "Slot 3" [no model]
module "Linecard(slot 3)" [WS-X4908-10GE]
container "Port Container 3/2"
other "Converter 3/2" [CVR-X2-SFP]
container "Port Container 3/11"
port "GigabitEthernet3/11" [GLC-TE]
container "Port Container 3/12"
port "GigabitEthernet3/12" [GLC-T]
Tests assert each level resolves correctly:
- linecard via `^Linecard\(slot (\d+)\)$` regex -> device-bay "Slot 3"
- converter via parent `^Port Container (\d+)/(\d+)$` regex -> "X2 Port 2"
- GE inside CVR via positional fallback -> "SFP 1" / "SFP 2"
- GE shows "No Bay" when CVR is matched but uninstalled in NetBox
A separate regression test uses a no-Converter-entry hierarchy
(Linecard -> PC 3/2 -> PC 3/11 -> GE3/11) where bay scope bubbles to
the linecard's bays. In this scope, ancestor-walking would resolve
the grandparent `Port Container 3/2` to `X2 Port 2` and incorrectly
land the transceiver in the parent module's slot. This test fails
if 216fb84-style logic is re-introduced.
Add a `_load_contrib_bay_mappings` helper that loads the contrib YAML
as fake mapping objects and a `bay_mappings=` kwarg on `_run_build_context`
so tests can opt into the real mapping set instead of the empty default.
* fix: bail _match_bay_by_position on non-container scaffolding
Cisco IOS-XR exposes deep entity-MIB scaffolding under each linecard:
modules with class="module" and model="N/A" (Motherboard, Slice 0,
EZChip, "Slice 0 SFP Port Module #N"). The positional walk previously
treated every model-less ancestor as a walk-through container, kept
reassigning container_idx until it found the linecard's real model,
and then took the position of the deepest ancestor among the linecard's
direct children.
On a real ASR-9904 (NetBox device prod-lab03d-ra1.lab) every TenGigE
port descended through the same Motherboard chain, so they all reported
container_idx=Motherboard and resolved to position=1 -> Slot 1 on the
chassis. The chassis Slot 1 holds the RSP line card; clicking install
on a TenGigE row would write an SFP module into the RSP bay, then any
subsequent install rejected with "Module bay 'Slot 1' already has a
module installed".
Restrict the walk to ENTITY-MIB containers (entPhysicalClass="container").
A modelless ancestor of any other class is hierarchical scaffolding, not a
bay position; walking past it silently collapses sibling counts. Bail
when we hit one — the row drops to "No Bay" instead of confidently
mismatching.
Tests:
- TestPositionalMatchScaffoldingChain (new): captures the ASR-9904
shape and asserts (1) TenGigE rows do NOT match Slot 1, (2) status
is "No Bay", (3) sibling rows resolve independently rather than
collapsing to a single bay.
- TestMatchBayByPosition (updated): existing tests omitted
entPhysicalClass on synthetic containers; add it explicitly so the
fixtures match real LibreNMS data shape and the positional walk's
class check passes.
Verified live: all 11 TenGigE0/0/0/N rows on device 54 now show
"No Bay" instead of collapsing to "Slot 1". RSP0/RSP1 and power
supplies still match correctly via their own positional paths.
* fix: restrict serial_matches_device rule to chassis-level entries
The 'Embedded RP / fixed-chassis system board' ignore rule
(match_type=serial_matches_device, action=transparent) targets fixed-form
routers like Cisco 8201-SYS / 8100, where the system board is reported as
an ENTITY-MIB entry whose serial equals the device record's serial.
Marking it transparent hides the row and promotes its children
(transceivers, fans, PSUs) to device-level bay matching.
The match criterion was just "item.serial == device.serial" with no
location check. On chassis devices like the ASR-9904, line cards can
share the device serial when the operator set Device.serial to the
linecard's serial (LibreNMS doesn't necessarily report the chassis serial
as the device serial). The rule fired on the linecard, hid it, and
promoted every TenGigE child to chassis-level matching.
Combined with the previous positional-walk bug, this collapsed every
TenGigE0/0/0/N row to the same chassis Slot 1 bay; in isolation it would
silently land transceivers in chassis line-card bays.
Tighten the criterion: only fire when the item is at chassis level —
either it has no parent (top-level entity) or its direct parent has
entPhysicalClass="chassis". System boards on fixed-form routers satisfy
this; line cards inside slot containers don't.
Tests cover three new shapes:
- parent is chassis -> rule fires (fixed-form router)
- parent is container -> rule does NOT fire (ASR-9904 linecard)
- parent is module -> rule does NOT fire (nested submodule)
Verified on live device 54: the 0/0 linecard now appears as its own
row instead of being hidden, and its TenGigE descendants resolve to
"No Bay" instead of collapsing to a chassis slot.
* fix: class-aware positional fallback + model gap warnings
The positional fallback in _match_bay_by_position previously tried
[SFP N, Slot N, Bay N, Port N] for every item regardless of hardware
class. On chassis devices whose NetBox model defines only line-card
slots (Slot 0..3) but no Fan Tray / PSU bays, fans and power supplies
landed in line-card "Slot N" bays. Example on ASR-9904 device 54:
- 0/FT0 (fan) -> Slot 3
- 0/PT0-PM0 (powerSupply) -> Slot 2
- 0/PT0-PM1 (powerSupply) -> Slot 3
Pick patterns appropriate for the item class:
- fan -> Fan Tray N / Fan N / FT N
- powerSupply -> Power Supply N / PSU N / PEM N / PM N
- module / port / ioModule / cpmModule / mdaModule / fabricModule
/ xioModule -> Slot N / SFP N / Bay N / Port N
- other classes (sensor, etc.) -> no positional guess
Items whose class has no matching bay name in scope drop to No Bay
rather than confidently mismatching.
Surface NetBox-model gaps: each No Bay / No Type row now carries a
model_warning field describing the most likely missing piece:
- empty bay scope -> parent module type has no bay templates
- class-specific -> add bay templates with the expected names
- missing type -> No NetBox ModuleType matches '<model>'
The modules-table render_status surfaces this as a tooltip on the
status badge plus an alert icon, mirroring the existing
name_conflict_warning rendering.
Tests:
- TestPositionalMatchClassAware: 6 cases covering fan/PSU/module
behavior plus unknown-class fallback to None.
- TestNoBayWarningHints / TestNoTypeWarningHints: helper output
distinguishes the three causes.
- TestBuildRowModelWarning: integration check that _build_row
populates model_warning on the right rows.
- test_tables_modules.py: render_status surfaces model_warning as
a tooltip with the alert icon.
Verified on device 54: 0/FT0 and 0/PT0-PMx now show No Bay with
class-appropriate hints instead of landing in chassis line-card slots.
* feat: ModuleBayMapping suggestions + Add Mapping button on No Bay rows
When a row resolves to "No Bay" because the LibreNMS name doesn't match
any bay in scope, propose a regex ModuleBayMapping covering the whole
slot family rather than one entry per slot. Heuristic: when the item's
name ends with a number and a bay in scope ends with the same number,
suggest "^<prefix>(\d+)$ -> <bay-prefix>\1" with the item's class as
filter. Example: item "0/0" + bay "Slot 0" yields
"^0/(\d+)$ -> Slot \1, class=module".
Suppressed in three cases:
- scope_preserved=True (scope inherited from an unmatched ancestor)
- Class/bay mismatch (fans only match Fan/FT bays, etc.)
- scope_uninstalled (warning already redirects to install parent first)
UI:
- render_status surfaces the suggestion in the badge tooltip.
- render_actions adds an "Add Mapping" button on No Bay rows with
model_suggestion. Opens ModuleBayMapping create form pre-filled via
NetBox ObjectEditView GET-param initial. return_url is captured from
configure(request) for round-trip.
Plumbing:
- _build_row gains scope_uninstalled and scope_preserved kwargs.
- The iteration loop tracks both states alongside bays_by_depth.
Defaults fall back to top-level state so first sub-item iteration
inherits correct semantics.
- _build_no_bay_warning gains a scope_uninstalled branch ("install the
parent module first") and appends suggestion when provided.
Verified live on device 54 (ASR-9904):
- 0/0 (No Bay, top-level) -> SUGGEST ^0/(\d+)$ -> Slot \1
- TenGigE0/0/0/N (preserved scope) -> no suggestion
- 0/FT0 (fan, no fan bays) -> no suggestion (class filter)
- 0/PT0-PMx (powerSupply) -> no suggestion (class filter)
* fix: address valid code-review findings
- testing.instructions.md: add test_coverage_bulk_import.py and
test_coverage_utils.py to coverage-by-module table
- models.py InterfaceTypeMapping.clean(): normalize/strip librenms_type
and reject blank values before wildcard uniqueness check
- librenms_sync.js: always clear device_selection cell on row update,
even when backend returns empty, so stale VC dropdowns are removed
- tables/modules.py VCModuleTable: hide device_selection column by
default (visible=False); guard format_module_data against missing VC
- utils.py load_bay_mappings(): use explicit order_by for deterministic
regex precedence
- modules_view.py: log raw LibreNMS errors server-side and show generic
message to users (inventory fetch failure + transceiver fetch warning)
- test_coverage_filters.py: make cache-key assertions order-independent
- test_vm_operations.py: add missing existing_device key to mock dict
* feat: remove {module} conflict warning, add Generic manufacturer fallback for module type matching
* fix: replace {module} conflict tooltip with Name Conflict status badge; add Generic manufacturer fallback
- Remove the warning tooltip about {module} causing non-unique interface
names; instead show a 'Name Conflict' status badge (bg-warning text-dark)
when has_nested_name_conflict() detects sibling name collisions
- has_nested_name_conflict() and sibling_counts tracking are preserved
- Add get_generic_module_types_indexed() and generic_fallback support in
resolve_module_type() so 'Generic' manufacturer matches are tried when
no vendor-specific ModuleType is found
- Pre-fetch generic module types once per request in _get_generic_module_types()
- render_status() no longer reads name_conflict_warning (never set); reads
model_warning only for the alert-icon tooltip
- Restore has_nest…
* feat(inventory): modules/inventory sync tab with mapping rules
Add inventory/modules sync functionality:
- Six mapping model types: DeviceTypeMapping, ModuleTypeMapping,
ModuleBayMapping, NormalizationRule, InventoryIgnoreRule, PlatformMapping
- Migration 0010 creating all mapping tables
- Modules sync tab on Device/VM detail pages with ENTITY-MIB inventory data
- Install, replace, and move module actions
- Mapping CRUD views with YAML bulk export for all mapping models
- LibreNMS API: get_device_transceivers() for transceiver data
- Platform matching: PlatformMapping lookup before name-exact match
- contrib/ YAML files with example mappings and rules
- Comprehensive test coverage (test_sync_modules, test_modules_view,
test_module_replace, test_platform_mapping, test_tables_modules)
* fix tests: update db-fallback tests to match new RQ-only cancellation behavior
_is_job_cancelled now returns False on RQ/Redis unavailability instead
of falling back to DB status. Update 5 tests that were asserting the
old DB-fallback behavior:
- test_db_fallback_logs_via_module_logger_when_job_logger_none →
test_rq_unavailable_does_not_cancel_import: RQ unavailable means
processing continues, not cancelled
- test_job_db_fallback_stopped/errored_before_validation_loop →
test_job_cancelled_before_validation_loop_returns_empty /
test_rq_unavailable_job_not_cancelled_in_preloop: patch
_is_job_cancelled directly to test early-exit behavior
- test_job_validation_loop_db_fallback_stop →
test_job_cancelled_in_validation_loop_returns_empty: use
_is_job_cancelled side_effect to simulate mid-loop cancellation
- test_job_rq_check_exception_uses_db_status_and_exits →
test_rq_fetch_exception_does_not_cancel_process_filters: assert
result has 1 device (not []) when RQ unavailable
* fix: updated tests
* Apply CR findings: code quality, test, docs, and template fixes
- Remove dead _resolve_naming_preferences from actions.py (never called)
- Unify vc_detection_enabled parsing in BulkImportConfirmView (POST+GET)
- Add ambiguity detection to get_module_types_indexed second loop
- Fix get_validated_device_cache_key doctest (wrong e3b0 hash)
- Add status=='ok' envelope check before transceivers shape validation
- Strip netbox_bay_name in ModuleBayMapping.clean()
- Use server_info.server_key in _module_sync.html refresh form
- Guard module_mismatch_modal Update Serial form on serial_conflict/installed
- Remove duplicate NoSuchJobError import in test_coverage_api2.py
- Fix _is_job_cancelled side_effect count for in-loop cancellation test
- Precompute sibling_counts in _build_table_rows to eliminate N+1 queries
- Accept sibling_counts in has_nested_name_conflict (DB fallback preserved)
- Update test_has_installable_children fake_build_row signature
- Coerce entPhysicalParentRelPos to int in siblings sort (prevent TypeError)
- Move get_queue inside try block in api/views.py sync_job_status
- Replace MD5 with SHA256 for VC domain fingerprint (FIPS compliance)
- Fix test_role_is_read_from_validation to test validation dict path
- Add test_module_replace.py to docs/development/testing.md
- Add platform_mappings.yaml row to contrib/README.md
- Fix QSFP28-DD-2X100G-LR4 typo in module_type_mappings.yaml
- Clarify librenms_id auto-create in docs/usage_tips/custom_field.md
* Apply second batch of CR findings on pr/inventory-core
- Unify librenms_id auto-create version reference to 0.4.3 in docs
- Move _LIBRENMS_JOB_NAMES to module-level constant in api/views.py
- Add status=='ok' envelope check in port handler in librenms_api.py
- Split mapping ambiguity tracking in get_module_types_indexed (separate
mapping_seen/mapping_ambiguous so explicit mappings always win over base
ModuleType ambiguity)
- Handle match_type=='ambiguous' in find_matching_platform caller with
dedicated warning message about conflicting platform mappings
- Move vc_requested parse before validate_device_for_import in
BulkImportConfirmView and pass include_vc_detection=vc_requested
- Guard Replace Module form with {% if installed_module %} in
module_mismatch_modal.html to prevent empty module_id submission
- Add mock_db_job.status/completed assertions to api2 test
- Extend cancellation test side_effects to 4 entries to target in-loop
check (lines 507, 534, 566, 574) in both RQ and _is_job_cancelled tests
- Assert success list in test_no_warning_when_cluster_found
* Apply CR findings batch 3: test hardening, PlatformMapping lazy import, module bay normalization, BulkExportYAMLView permissions
- test_coverage_sync_views2: patch get_object_or_404 to isolate from ORM
- test_coverage_device_fields: assert save/full_clean not called on no-match
- test_librenms_id: use exact tuple set comparison for Q branch children
- test_init: assert DB alias used in custom field creation
- utils.py: move PlatformMapping import to function scope (lazy); update all test patches to models.PlatformMapping; remove create=True
- test_platform_mapping: patch require_object_permissions instead of require_write_permission
- test_sync_modules: mock apply_normalization_rules in _match_module_bay tests
- views/base/modules_view.py: apply NormalizationRule(scope=module_bay) to candidate names in _match_module_bay
- views/mapping_views.py: BulkExportYAMLView uses NetBoxObjectPermissionMixin with view permission
- views/object_sync/devices.py: precompute has_write_permission once in get_table()
* cr: batch 4 — prefetch_related, deterministic export, test improvements
- utils.py: add prefetch_related('interfacetemplates') to ModuleType query
and prefetch_related('netbox_module_type__interfacetemplates') to
ModuleTypeMapping query in get_module_types_indexed() to avoid N+1
queries in has_nested_name_conflict()
- views/mapping_views.py: add .order_by('pk') to BulkExportYAMLView filter
for deterministic YAML export; add select_related() to all 4 export
subclass querysets (DeviceTypeMapping, ModuleTypeMapping,
NormalizationRule, PlatformMapping)
- tests/test_utils.py: assert exact Platform.objects.get kwargs in 2 platform
tests; fix vc.master = None -> vc.master = master to exercise designated-
master-without-IP branch
- tests/test_platform_mapping.py: update mock chain for .order_by(); complete
test_returns_yaml_content_type with actual view call and content-type assertion
- tests/test_sync_modules.py: fix mock chain for .prefetch_related() in
TestGetModuleTypesIndexed
* cr: batch 5 — normalization scoping bug, test fixes
- utils.py: fix apply_normalization_rules() else-branch to filter
manufacturer__isnull=True so callers without manufacturer context never
have vendor-specific rules applied to their values
- tests/test_sync_modules.py: add test_mapping_overrides_ambiguous_base_key
to lock down the separate-ambiguous-sets behaviour in
get_module_types_indexed(); fix test_regex_mapping_with_backreference to
use 'Optics 0/0/0/5' (with space) so the assertion cannot pass via
exact-name fallback — only the regex expansion path can produce a match
- tests/test_platform_mapping.py: remove dangling assertions accidentally
left inside test_returns_200_with_empty_selection (PlatformMapping import
and existence check now live in test_all_mapping_bulk_export_yaml_views_exist)
* cr: batch 6 — normalization rule caching, test correctness
- utils.py: add preload_normalization_rules() helper that preloads
NormalizationRule rows for a (scope, manufacturer) combination into a
dict keyed by (scope, manufacturer_pk_or_None); update
apply_normalization_rules() to accept preloaded_rules kwarg and use
preloaded lists when provided (skipping DB queries); update
resolve_module_type() to accept norm_rules kwarg and thread it through
to apply_normalization_rules — eliminates N+1 DB queries in
_match_module_bay and _build_row loops
- views/base/modules_view.py: call preload_normalization_rules() in
_build_context for both 'module_bay' and 'module_type' scopes; pass
preloaded rules via self._norm_rules_bay/_norm_rules_type to
_match_module_bay and _build_row respectively
- tests/test_platform_mapping.py: add test_multiple_platform_mappings_returns_ambiguous
asserting PlatformMapping.MultipleObjectsReturned yields
match_type='ambiguous' and that Platform.objects.get is never called
- tests/test_sync_modules.py: fix test_mapping_overrides_ambiguous_base_key
to use distinct mapping key 'SFP-1G-LX-EXPLICIT' so 'SFP-1G-LX' is
absent and only the explicit key is present; fix
test_uninstalled_bay_is_skipped to add grandparent bay with installed
module (pk=99) and assert walk continues past empty bay to return 99;
fix test_class_scoped_mapping_preferred to pass [m_generic, m_class] so
priority logic must actively prefer class-scoped mapping; patch
preload_normalization_rules in tests that call _build_context directly;
update apply_normalization_rules lambda patches to accept **kw
* fix: FPC slot int/str comparison, stale docstring, test patch targets
- _fpc_slot_matches: convert match.group(1) and parent_bay.position to int
before comparing (Python 3: '1' == 1 is False); handle ValueError with
safe fallbacks
- apply_normalization_rules docstring: clarify that manufacturer=None applies
only unscoped (manufacturer__isnull=True) rules, not all scope rules
- test_modules_view._run_build_context: patch load_bay_mappings and
get_enabled_ignore_rules at utils level instead of patching model classes
that _build_context never references directly; remove now-unused
mock_ignore_qs variable
* fix: surface ambiguous platform, preloaded-rules fallback, serial mismatch, transceiver ignore
- actions.py sync_platform: add explicit elif for match_type='ambiguous' so
users get a clear conflict message instead of generic 'not found' error when
multiple PlatformMapping rows match the same OS string (works towards #51)
- utils.py apply_normalization_rules: when preloaded_rules is provided, check
key presence before using the dict; fall back to DB query when (scope,
mfg_pk) or (scope, None) is absent, preventing silent omission of vendor
rules for manufacturers not included in the preloaded dict
- utils.py match_librenms_hardware_to_device_type: update Returns docstring to
dict | None and document the MultipleObjectsReturned → None case
- modules_view.py _apply_installed_status: drop nb_serial from the guard so a
module with a LibreNMS serial is flagged as Serial Mismatch even when NetBox
has no serial recorded (lnms_serial and lnms_serial != nb_serial)
- modules_view.py _collect_top_items: apply ignore-rule check to transceiver-
synthesised items before appending, so InventoryIgnoreRules can suppress
optics from get_device_transceivers()
- test_modules_view.py: rename test that expected the old nb_serial-required
behavior; update assertions to Serial Mismatch + can_update_serial + can_replace
- test_modules_view.py: wrap two early-return _detect_serial_conflicts tests in
patch(dcim.models.Module) and assert filter was never called
* fix: canonical librenms_id lookup, None guard, transceiver transparent, vc flag case
- utils.py find_by_librenms_id: strip whitespace and canonicalize leading-zero
string IDs before building Q filters; '042' and '42 ' now resolve to
int_value=42 / canonical_str='42' and both forms are added to the query so
they match records stored as numeric 42 or string '42'
- modules_view.py _find_parent_container_name: use (... or '') pattern to guard
against entPhysicalName being explicitly None in the ENTITY-MIB payload
- modules_view.py _match_module_bay: same None guard for entPhysicalName,
entPhysicalDescr, entPhysicalClass on the item dict
- modules_view.py _collect_top_items: treat 'transparent' the same as 'skip'
for transceiver-synthesised rows so transparent synthetic items are not
added to top_items
- actions.py BulkImportDevicesView: normalise vc_detection_enabled flag with
.lower() before membership test so 'ON', 'True', 'TRUE' all parse correctly,
consistent with BulkImportConfirmView
* Apply CR batch 10 fixes
- bulk_import: check cancellation every iteration (not every 5th)
- models: always call full_clean() on save, remove update_fields guard
- tables/modules: add has_write_permission param to LibreNMSModuleTable,
gate selection column and render_actions on it
- modules_view: normalize placeholder model/serial strings in transceiver
merge; filter placeholder serials from inv_serials set
- api/views: skip DB status update when job is already in a terminal state
- utils: add 'ambiguous' case to find_matching_platform docstring
- utils: memoize DB fallback into preloaded_rules in apply_normalization_rules
- utils: use try/except int() instead of isdigit() for +42 style IDs
- devices: pass has_write_permission to LibreNMSModuleTable constructor
- test_modules_view: use SimpleNamespace instead of MagicMock in
_determine_status tests for unambiguous truthiness checks
- test_sync_modules: assert checkbox HTML in selection cell; update
test_install_module_view_not_in_base to assert public import path;
add order-independent check for class-scoped mapping preference
- test_tables_modules: set has_write_permission=True in _make_table helper;
add no-write-permission test case
- test_utils: assert exact kwargs on DeviceTypeMapping.objects.get and
PlatformMapping.objects.get calls
- test_vm_operations: patch _is_job_cancelled directly instead of mutating
job.job.status via refresh_from_db side_effect
- test_coverage_base_views2: rename test to reflect actual behavior
(cache entry present but lacks port_id)
- test_coverage_devices: update constructor assertion to include
has_write_permission kwarg
* Apply CR batch 11 fixes
- models: add FullCleanOnSaveMixin + clean() to InterfaceTypeMapping to
enforce uniqueness for NULL-speed rows (SQL UNIQUE skips NULL=NULL)
- utils: expand match_librenms_hardware_to_device_type docstring to
document all three fail-closed None cases (mapping, part_number, model
MultipleObjectsReturned), not just the mapping-table one
- test_vm_operations: remove stale mock_job.job.status='running' from
_run_bulk_with_mappings helper; patch _is_job_cancelled=False instead
* fix: normalize librenms_hardware/os to lowercase, fix convert_speed_to_kbps docstring
- DeviceTypeMapping.clean() and PlatformMapping.clean() now lowercase
the stored value after stripping, preventing case-variant duplicates
(e.g. 'IOS' and 'ios') that would cause MultipleObjectsReturned on
__iexact lookups. Closes #51.
- Fix convert_speed_to_kbps docstring: Returns section now reads
'int | None' to match the signature and implementation.
- Add TestDeviceTypeMappingModel tests for DeviceTypeMapping.clean()
(strip, lowercase, blank validation).
- Add test_clean_normalizes_to_lowercase and test_clean_strips_and_lowercases
to TestPlatformMappingModel.
* fix(js): address PR #258 review findings
- hideModal: remove all backdrops via querySelectorAll+forEach
- htmx:afterSettle: derive label from aria-labelledby, fallback to id
- initializeVlanModalSave: truncate/extract error body before display
* fix: handle unsaved manufacturer in normalization, fix JS modal/fetch issues
- utils.py: guard unsaved manufacturer (pk=None) in preload_normalization_rules
and apply_normalization_rules to prevent ValueError on DB query
- librenms_sync.js: add id="htmx-modal-label" to module mismatch modal header
so aria-labelledby target is preserved after innerHTML replacement
- librenms_sync.js: check response.ok before response.json() in deleteUrl fetch
so HTTP errors surface their status instead of a parse error
* fix: type annotation, non-positive librenms_id guard, htmx label listener
- utils.py: fix convert_speed_to_kbps parameter annotation to int|None
- utils.py: guard non-positive numeric librenms_id (<=0) same as None;
skip Q canonicalization for string '0'/'-1' after int parse
- librenms_sync.js: extract updateHtmxModalLabel(), listen at document
level for htmx:afterSettle, call from module-replace fetch completion
* fix: use _PLACEHOLDER_VALUES for serial normalization, fix docstring
- modules_view.py: _apply_installed_status and _detect_serial_conflicts
now use _PLACEHOLDER_VALUES set instead of only guarding against "-"
so serials like 'unknown', 'n/a', 'na' are treated as absent
- utils.py: update convert_speed_to_kbps Args docstring to int|None
* Fix CR batch: device_type ambiguity, platform MOR, cache invalidation, ChainMap bay lookup
- device_fields.py: distinguish None (ambiguous) from failed match result;
surface a specific error for duplicate DeviceType mappings
- utils.py: find_matching_platform returns {found:False, match_type='ambiguous'}
on Platform.MultipleObjectsReturned instead of silently taking .first()
- modules_view.py: invalidate stale inventory cache before early-return renders
when librenms_id is falsy or inventory fetch fails
- modules_view.py: _lookup_regex_bay_mapping iterates all ChainMap scopes
so same-named bays in different scopes are all checked
- tests: update TestFindMatchingPlatformMultipleReturned to expect ambiguous
* Promote SKIP_TYPES and _NON_HARDWARE_CLASSES to module-level constants
Move inline set literals SKIP_TYPES and _NON_HARDWARE_CLASSES out of their
method bodies and into module scope, consistent with _PLACEHOLDER_VALUES and
other module-level constants. Rename SKIP_TYPES to _SKIP_TRANSCEIVER_TYPES
to clarify its domain.
* Fix find_matching_platform docstring and non-positive string librenms_id guard
- utils.py: broaden find_matching_platform docstring to state 'ambiguous'
applies both to multiple PlatformMapping entries and to duplicate
exact-name Platform rows (Platform.MultipleObjectsReturned)
- utils.py: add early-return guard in find_by_librenms_id for string
librenms_id values that parse to <= 0 (e.g. '0', '-1', '000'), preventing
them from reaching the Q clauses and matching stale/corrupted records
* fix: normalize placeholder serials and txr_type in modules_view
_check_ignore_rules: normalize item_serial, device_serial, and
ancestor_serial against _PLACEHOLDER_VALUES so sentinels like
'unknown'/'n/a' are treated as absent and do not trigger or
short-circuit serial_matches_device rules or require_serial_match_parent
ancestor walks.
_merge_transceiver_data: normalize txr_type against _PLACEHOLDER_VALUES
(same as model/serial) so placeholder types like 'unknown' do not bypass
the _SKIP_TRANSCEIVER_TYPES guard and produce synthetic rows with
display_model set to a placeholder string.
* Fix whitespace-only librenms_id, BUILTIN model placeholder, FPC-scope exact-bay fallback, has_write_permission in HTMX render
- utils.py: treat whitespace-only librenms_id strings as absent (return None
after strip() when cleaned == "") so they don't reach the Q-object builder
- modules_view.py: extend transceiver backfill to also replace 'BUILTIN' model
and serial values, not just those already in _PLACEHOLDER_VALUES
- modules_view.py: exact-name bay fallback now iterates ChainMap scopes and
calls _fpc_slot_matches() to avoid returning the wrong-scope bay when
duplicate bay names exist across scopes (mirrors the regex path behaviour)
- modules_view.py: add has_write_permission to all render() calls in post()
so the Install Selected button is visible in HTMX-refreshed content
* Fix non-integer librenms_id passthrough, stale cache on missing ID, exact-mapping ChainMap scope
- utils.py: non-integer strings (e.g. 'abc') now return None in
find_by_librenms_id() instead of falling through to build Q objects;
changed 'except ValueError: pass' to 'except ValueError: return None'
- modules_view.py: get_context_data() re-validates the device's current
LibreNMS ID before serving cached inventory; clears cache and returns
empty context when the mapping has been removed since the cache was written
- modules_view.py: _lookup_exact_bay_mapping() now iterates ChainMap scopes
and calls _fpc_slot_matches() before returning, matching the existing
behaviour of _lookup_regex_bay_mapping() and the name-fallback path
* fix: normalize _GENERIC_CONTAINER_MODELS to lowercase; embed librenms_id in inventory cache
- modules_view: lowercase _GENERIC_CONTAINER_MODELS set and apply .lower() at
all 5 comparison sites so values like 'builtin', 'default', 'n/a' received
from LibreNMS in any case are treated as generic containers
- modules_view: store {'inventory': data, 'librenms_id': id} in the inventory
cache instead of the raw list; get_context_data validates the embedded
librenms_id against the current mapping so remapped devices never serve stale
inventory (non-dict/legacy entries are treated as cache misses)
* fix: treat duplicate-serial module conflicts as ambiguous instead of silently overwriting
When multiple Module objects share the same serial, the old loop would
overwrite row["serial_conflict_module"] nondeterministically. Now we
group conflicts by serial and only set the move target when exactly one
candidate exists; multiple candidates set serial_conflict_ambiguous.
* fix: clear stale cache on legacy payload; ungate generic-model filter from phys class
- modules_view: get_context_data now calls cache.delete(cache_key) before
returning when the cached payload is not the new dict format so pre-upgrade
list-form entries are evicted and the next request regenerates fresh data
- modules_view: collapse the two-check current-item filter into a single
'model in _GENERIC_CONTAINER_MODELS' test (removes the phys_class=='container'
gate) so empty-model non-container items are also treated as generic
- modules_view: remove the anc_class=='container' guard from the ancestor walk
so any inventory-class ancestor with a generic model (e.g. a 'module' row
with model='builtin') is treated as transparent instead of blocking its
subtree
* Remove dead vc_requested assignment left by rebase
The rebase onto pr/code-quality-fixes dropped the consumer of vc_requested
in favor of the hoisted vc_detection_enabled variable, leaving the
assignment itself as an orphan that ruff F841 flags.
* fix: VC zero-based detection all-zeros guard; align VC-perm tests with fail-closed behavior
- virtual_chassis.py: only treat stack as 0-based when positions span 0 AND a
positive value. When every entPhysicalParentRelPos is 0 the data is invalid
and the shift produced colliding positions (all members → slot 1); fall
through to the per-member idx+1 fallback instead.
- test_coverage_bulk_import.py: PR #257 fails stack imports fast when the
user lacks dcim.add_virtualchassis. Update both VC-permission tests to
assert failure + error logging instead of silent success.
* fix: CR batch 12 — serial normalization, conflict disambiguation, modal guard, test fixes
- sync/modules.py: replace all 'serial == "-"' guards with _PLACEHOLDER_VALUES
normalization (import from modules_view) for consistency across InstallModuleView,
InstallBranchView, PreviewModuleReplaceView, ReplaceModuleView
- sync/modules.py: PreviewModuleReplaceView — use count() instead of .first() to
detect ambiguous serial conflicts; pass serial_conflict_ambiguous=True to template
- sync/modules.py: ReplaceModuleView — use count() to detect ambiguous conflicts;
return error redirect when count > 1 instead of silently picking one
- module_mismatch_modal.html: add serial_conflict_ambiguous branch (warning alert);
gate 'Update Serial Only' and 'Replace Module' buttons when conflict is ambiguous
- tables/mappings.py: render em-dash for serial_matches_device rules in
render_require_serial_match_parent (flag has no effect for that match type)
- test_coverage_bulk_import.py: remove xfail marker — TTL refresh is now implemented
- test_coverage_utils.py: assert PlatformMapping.objects.get called with librenms_os__iexact
- test_modules_view.py: assert row.get('serial_conflict_module') is None (not 'not in row')
- test_module_replace.py: add count.return_value to mock chain for new count() calls
- tests/e2e/test_module_install.py: extend os.environ instead of replacing in subprocess
* fix: address deferred CR findings from issues #53-#56
where can_move_from and serial_conflict_module are set. Button posts to
move_module view with conflict_module_id and target_bay_id.
valid dict from fetch_device_with_cache mock instead of None, so the
test exercises the full sync code path including cache dict population.
- save.assert_called_once() → assert_called_once_with(update_fields=
['status', 'completed']) in test_does_not_overwrite_completed_when_not_in_rq
- Use distinct server_key 'prod-server' in DeviceModuleTableView tests
instead of 'default' to catch accidental default fallback
- Use permission-specific has_perm side_effect instead of return_value=True
per name (dict[str, list]) instead of keeping only the first. Resolve
mapping-based lookups by checking which bay has an installed_module when
duplicates exist — return only if unambiguous (exactly one occupied bay).
Closes #53
Closes #54
Closes #55
Closes #56
* fix: address 3 remaining CR findings from PR #50
test_coverage_actions.py: Fix false-positive test — wrong POST key
'vc_detection_enabled' replaced with the actual key 'enable_vc_detection'
that _resolve_vc_detection_enabled() reads. Previously the mock returned
None for every key and the assertion passed via the default=False fallback.
views/sync/modules.py: Unwrap cached inventory dict payload before use.
The inventory cache stores {"inventory": [...], "librenms_id": ...} but all
4 sync action views (InstallBranchView, InstallSelectedView,
ModuleMismatchPreviewView, ReplaceModuleView) iterated the raw payload as
a list, causing item.get("entPhysicalIndex") to iterate dict keys instead
of inventory rows. Added _extract_inventory_list() helper that unwraps the
dict payload and tolerates legacy list payloads.
views/sync/modules.py: Apply ignore rules to root item in _collect_branch().
Previously only children were filtered by ignore rules; the root/parent
item was always enqueued. Now _collect_branch() checks the root item:
'skip' → return []; 'transparent' → collect children only, not root.
Also updated InstallSelectedView to filter both 'skip' and 'transparent'
(was only 'skip') to match table view parity.
* refactor: drop _extract_inventory_list legacy-list fallback
The "legacy" list payload referenced in e96b3f3 never existed in any
released or upstream code — the bare-list writer was replaced inside
this same branch (7dfd436) before any consumer shipped, so no
deployed cache can hold one. The fallback also contradicted
BaseModuleTableView.get_context_data, which evicts non-dict payloads
as cache misses.
Drop the `or []` non-dict branch so the helper matches the canonical
reader, and update the inventory-cache stubs in test_module_replace.py
and test_sync_modules.py to use the dict format produced by the writer.
* fix: race conditions, class-aware mappings, stale snapshot in module sync
_install_single: Lock target bay with select_for_update() before checking
occupancy. Previously two concurrent requests could both observe the bay
as empty, causing an IntegrityError that rolled back the entire batch.
Now consistent with InstallModuleView's locking pattern.
_find_parent_module_id: Make ancestor mapping resolution class-aware.
Exact mappings are now indexed by (librenms_name, librenms_class) with
class-specific preference and class-empty fallback. Regex mappings also
prefer class-matching rules before falling back to class-empty rules,
consistent with _lookup_regex_bay_mapping() in the base view.
ReplaceModuleView: Move target_bay/old_type_name/old_bay_name reads
after select_for_update() so they reflect the locked row's current state.
Previously these were captured from the pre-lock snapshot, which could
be stale if another request moved or replaced the module first.
MoveModuleView: Lock target bay with select_for_update() inside the
atomic block instead of using the pre-lock get_object_or_404 snapshot.
Tests updated for select_for_update chains and librenms_class on mock
mapping helpers.
* fix: normalize placeholder serials in UpdateModuleSerialView, fix regex fallback in _find_parent_module_id
UpdateModuleSerialView: add placeholder normalization consistent with all
other serial-handling paths (InstallModuleView, _install_single,
ReplaceModuleView, ModuleMismatchPreviewView).
_find_parent_module_id: replace 'class_matches or fallback_matches' with
'class_matches + fallback_matches' so empty-class regex rules are still
tried when class-specific rules exist but none match the name — matching
the base view's _lookup_regex_bay_mapping pattern.
* fix: lock module row before updating serial in UpdateModuleSerialView
Move Module fetch inside transaction.atomic() with select_for_update()
to prevent stale-instance writes when a concurrent replace/move changes
the module between the read and the save. Consistent with locking pattern
used by InstallModuleView, ReplaceModuleView, and MoveModuleView.
Update test to mock Module.objects.select_for_update() chain instead of
get_object_or_404 for the module.
* fix: use fullmatch+expand in _find_parent_module_id regex matching
Mirror base view's _lookup_regex_bay_mapping behavior:
- Use rm._compiled_pattern.fullmatch(name) instead of re.search()
- Use match.expand(rm.netbox_bay_name) for capture group substitution
- Prevents false-positive partial matches and enables regex backrefs
Update test helpers to set _compiled_pattern on mock regex mappings.
* fix: address CR review batch - regex, tests, docs
- Tighten Nokia regex: \w → [0-9] for part number digits, [A-Z0-9]
for transceiver cleanup pattern
- Use real dict for request.GET mock in test_background_jobs.py
- Make devcontainer discovery deterministic: env var override,
error on multiple matches
- Fix brittle '156 objects' banner filter in e2e tests
- Fix typos in virtual_chassis.md (NEtbox → NetBox, dispalys)
- Make README install snippet idempotent with grep guard
* revert: restore original docs/usage_tips/virtual_chassis.md
Revert grammar/typo edits to virtual_chassis.md — these changes
do not belong in this PR. Any docs updates should go through the
upstream repository directly.
* fix: revert bulk_import cancellation, VC comment, VM docstring, sync template improvements
- Restore periodic job cancellation checks (every 5th iteration)
- Improve VC zero-based position detection comment clarity
- Simplify VM create docstring, reorder server_key parameter
- Use resolved_name instead of sysName in sync template
- Move VC serial count badge to button label
- Add name check tooltip
* fix: restore naming resolution and VC logic lost during rebase
Rebase regression stripped resolve_naming_preferences(),
_determine_device_name(), and _generate_vc_member_name() from both
UpdateDeviceNameView and the sync base view's get_librenms_device_info().
Restored:
- resolved_name computation using user naming preferences (sysName vs
hostname, strip domain) in librenms_sync_view.py
- VC member name generation for virtual chassis devices
- VC sync device lookup for members without their own librenms_id
- resolved_name template context variable (used by sync_base template)
- Proper early bailout when LibreNMS has no usable name
Updated test mocks to set virtual_chassis=None and patch the restored
naming functions.
* revert: restore original README.md and virtual_chassis.md
These files should not be modified in this PR — docs and README
changes belong in upstream repository directly.
* fix: improve ambiguity test assertion and error message wording
- Rename test to test_match_none_returns_ambiguous_error and assert
'Ambiguous' appears in the error message to detect regressions
- Broaden error message to cover both DeviceTypeMapping and DeviceType
ambiguity (match_librenms_hardware_to_device_type returns None for
either case)
* fix: CR review - modal close, test line refs, generic e2e
- librenms_sync.js: replace closeBtn.click() with hideModal() in VLAN
modal save handler (aligns with upstream fix 7bf533f)
- test_coverage_bulk_import/devices/list: strip hardcoded line-number
references from docstrings to avoid drift
- tests/e2e/test_module_install: make device-agnostic — auto-detect
device, discover modules from table, generic assertions
* fix: three bugs in module inventory sync reported in PR #261 review
- select_for_update(of=("self",)) avoids FOR UPDATE on the nullable side
of the installed_module outer join, fixing the Install Selected crash
- add has_write_permission to full-page context so Install Selected form
renders on page load, not only after an htmx Refresh Modules swap
- guard htmx.process() with typeof check to prevent 'htmx is not defined'
error when the Replace modal fetches its preview fragment
* fix: improve module table readability in dark mode
Remove table-success row highlight from installed modules — the Installed
badge is sufficient to identify state, and the green row background makes
linked text unreadable in dark theme.
Add text-white/text-dark contrast classes to all badge colors so they
remain legible in both light and dark themes.
* feat: split Mappings into its own navigation group
Move all mapping items (Interface, Device Type, Module Type, Module Bay,
Normalization Rules, Inventory Ignore Rules, Platform) out of the
Settings group into a dedicated Mappings group for clearer navigation.
* refactor: reorder navigation groups to Import, Status Check, Mappings, Settings
* fix: remove table-light from mismatch modal thead for dark mode
table-light forces a white background regardless of theme. Removing it
lets Bootstrap 5.3 theme-aware styling apply to the header row.
* fix: replace table-danger/warning cell backgrounds with text colors in mismatch modal
table-danger/table-warning produce a pinkish/yellow background that
clashes with Bootstrap's link color in dark mode. Switch to text-danger
and text-warning with fw-semibold — pure text color works on any
background and reads correctly in both light and dark themes.
* fix: remove all row background highlighting from module sync table
table-warning and table-danger row classes cause unreadable contrast in
dark mode. The status badge is sufficient to identify each row state.
Remove the row_class field entirely along with the dead row_attrs entry.
* refactor: reorder Mappings group — all mappings first, then Ignore Rules, Normalization Rules
* fix: update tests to reflect row_class removal
Replace assertions on table-success/danger/warning row_class values
with assert "row_class" not in row now that row highlighting is gone.
* fix: auto-generate slug when creating platform from sync page (#279)
Platform() without a slug fails full_clean() in NetBox 4.x. Derive the
slug from the platform name via slugify so the modal submit succeeds.
* Revert "fix: auto-generate slug when creating platform from sync page (#279)"
This reverts commit 3029cd3c2fc7ef054104e721a663ccde2109bdbb.
* fix: generate slug when creating Platform via CreateAndAssignPlatformView
Platform requires a slug field; omitting it caused full_clean() to raise
a ValidationError before the platform could be saved. Fixes #279.
* test: assert Platform constructor receives slug in CreateAndAssignPlatformView
Regression test for #279 — verifies slugify(name) is passed to the
Platform constructor so the missing-slug error cannot recur undetected.
* fix: generate slug when creating Platform via CreateAndAssignPlatformView
Platform requires a slug field; omitting it caused full_clean() to raise
a ValidationError before the platform could be saved. Fixes #279.
* test: assert Platform constructor receives slug in CreateAndAssignPlatformView
Regression test for #279 — verifies slugify(name) is passed to the
Platform constructor so the missing-slug error cannot recur undetected.
* fix: wrap OOB row in table to survive HTMX HTML parsing
When AddDeviceTypeMappingView returns a combined response with both
the modal OOB (a <div>) and the row OOB (a <tr>), HTMX wraps the
response in a <template> for parsing. A <tr> following a <div> in
that context is invalid HTML and gets silently dropped by the browser
parser, so the row never updates.
Wrap row_html in <table><tbody>...</tbody></table> before appending
to ensure the <tr> element survives parsing. The <div id='django-messages'>
inside is foster-parented outside the table by the parser, so both
OOBs are found and applied correctly.
* fix(imports): add HTTP status codes, remove redundant full_clean, fix JS Content-Type case-sensitivity
- AddDeviceTypeMappingView: return 400 for missing/invalid device_type_id,
404 for DeviceType.DoesNotExist, 500 for unexpected exceptions
- Remove redundant mapping.full_clean() before save() — DeviceTypeMapping
inherits FullCleanOnSaveMixin which calls full_clean() inside save()
- librenms_sync.js: lowercase Content-Type header before .includes() check
to handle case variants (e.g. 'Application/JSON')
* fix(js): extract JSON error message in delete-interfaces handler; guard updateHtmxModalLabel self-assignment
- DeleteNetBoxInterfacesView fetch: parse JSON error body (error/message/detail)
instead of throwing raw JSON string, consistent with other fetch handlers
- updateHtmxModalLabel: skip textContent assignment when header === label to
prevent stripping icon markup from the modal title element
* refactor(js): extract fetchErrorMessage() helper to unify fetch error handling
inline text/JSON handling. Helper extracts error/message/detail from
JSON responses, falls back to raw text, and truncates at 300 chars.
Eliminates drift between handlers and simplifies future additions.
* feat: exact-name-first platform lookup; optional mapping on platform create
- find_matching_platform(): try exact case-insensitive name match first,
fall back to PlatformMapping only when no direct name match exists;
update docstring accordingly
- _get_platform_info(): delegate to find_matching_platform() instead of
inline Platform.objects.get(); use 'or "-"' for null LibreNMS fields
- CreateAndAssignPlatformView: read 'librenms_os' + 'create_mapping' POST
params; create PlatformMapping(librenms_os, platform) when checkbox
checked and no existing mapping for that OS
- librenms_sync_base.html: add librenms_os hidden input, 'Add platform
mapping' checkbox, and JS warning when platform name differs from OS
- device_validation_details.html: inline device-type search form when
no matching device type
- urls.py + views/__init__.py: register and export AddDeviceTypeMappingView
- tests: align mocks and assertions to new lookup order
* fix(pr#50): address CR feedback on AddDeviceTypeMapping form and platform-mapping creation
- device_validation_details.html: add visually-hidden <label> for the
device-type search input (HTMLHint a11y)
- device_validation_details.html: replace hardcoded "/api/dcim/device-types/"
with {% url 'dcim-api:devicetype-list' %} so NetBox BASE_PATH deployments
work correctly
- CreateAndAssignPlatformView: surface PlatformMapping creation failures
to the user via messages.warning, and inform when an existing mapping
was found (no longer fails silently)
* fix(pr#50): isolate PlatformMapping save; ignore stale autocomplete results
- CreateAndAssignPlatformView: wrap PlatformMapping save in nested
transaction.atomic() so an IntegrityError on the unique librenms_os
constraint only rolls back the mapping savepoint, not the outer
device-platform-assignment transaction (which was previously left in
needs_rollback state, so messages.success would fire after a discarded
device save)
- device_validation_details.html: add monotonically increasing requestSeq
to the device-type autocomplete so out-of-order fetch responses can no
longer overwrite the dropdown with results for an older query
* fix(pr#50): check add_platformmapping permission when create_mapping is requested
CreateAndAssignPlatformView now reads 'create_mapping' from POST before the
permission check and dynamically adds ('add', PlatformMapping) to
required_object_permissions so require_all_permissions() enforces it.
Without this, a user with only add_platform + change_device could silently
create PlatformMapping objects through the checkbox.
* fix: address CodeQL security scan findings
- XSS (#18): escape() user-controlled error strings in bulk confirm HTML response
- XSS (#19,#20): escape() object_type param in device_fields.py HTTP responses
- Stack trace (#8): replace str(exc) with generic message in bulk import HTMX error
- Stack trace (#9): replace str(exc) with generic message in interfaces transaction error
- Stack trace (#13): replace catch-all str(e) with generic message in ip_addresses_view
- JS XSS (#4,#5): fix innerHTML spinner — store/restore full innerHTML, use DOM
for label to avoid reinterpreting textContent as HTML
- Workflow permissions (#1-#3): add permissions: contents: read to all three workflows;
publish-pypi job-level permissions also gains contents: read alongside id-token: write
- Devcontainer logging (#17): stop printing raw codespace name / CSRF / allowed-host
values to stdout in devcontainer config
- URL redirect (#6,#7): add comment — _get_safe_redirect_url already validates via
url_has_allowed_host_and_scheme; CodeQL false positive
- Lint: fix E741 ambiguous variable name in e2e test
* revert: remove workflow permissions additions (tracked separately)
* Fix PR review findings: available_roles, CSRF, test quality
- bulk_import.py: preserve available_roles in elif-not-actual_is_vm branch
(matches pattern of other clearing paths at lines 363, 384)
- librenms_sync.js: remove getCookie('csrftoken') fallback from two fetch
call sites; align with hidden-input-only CSRF convention used elsewhere
- test_vm_operations.py: strengthen log assertion to assert_any_call with
expected checkpoint messages (VM 1 and VM 5 of 10)
- test_vm_operations.py: delete duplicate test_job_cancellation_with_errored_status
(identical logic already covered by test_job_cancellation_breaks_loop)
- Skip mock-target change: apply_cluster/role_to_validation are lazy (in-function)
imports, so patching import_validation_helpers.X is correct — patching
vm_operations.X would fail as those names don't exist at module level
* Fix PR #58 review findings (batch 2)
- librenms_sync_view.py: Fix inverted comment on find_matching_platform order
- actions.py: Replace str(exc) with generic message in non-HTMX bulk import error handler
- actions.py: AddDeviceTypeMappingView — honour server_key, add NetBoxObjectPermissionMixin
with DeviceTypeMapping add/change permissions
- device_validation_details.html: Add hidden server_key input to dt-mapping form
- interfaces.py: Add logger.exception in DeleteNetBoxInterfacesView catch-all
- test_sync_devices.py: Strengthen VM type assertion to assert_called_once_with
including VirtualMachine class and pk kwarg
* Fix PR #58 review findings (batch 3)
- device_validation_details.html: Disable 'Add Mapping' button until device type
is selected from dropdown; enable on selection, re-disable on input clear
- devices.py: Use copy.copy(request) for child table view instances, consistent
with vms.py pattern
- test_coverage_devices.py: Update request assertions to use == (copy equality)
instead of 'is' identity to match new copy.copy behaviour
* Fix #59–#62: YAML anchor, DynamicModelChoiceField, ToggleColumn, write perms
Closes #59: Anchor Juniper MX regex pattern (^...$) so generic transceiver
pattern sorts first and MX-specific fallback works correctly.
Closes #60: Replace plain ModelChoiceField with DynamicModelChoiceField in
DeviceTypeMappingForm and ModuleTypeMappingForm for API-backed typeahead.
Closes #61: Add 'pk' to fields/default_columns in all 7 mapping tables so
NetBoxTable's built-in ToggleColumn renders the bulk-select checkbox.
Closes #62: Add LibreNMSWritePermissionMixin (permission_required =
PERM_CHANGE_PLUGIN) to mixins.py and apply it to all 35 mutation views
(Create, Edit, Delete, BulkImport, BulkDelete) in mapping_views.py.
* Fix PR #63 review findings: ordering, error handling, modal title, click listener cleanup
- utils.py: add .order_by('pk') to get_enabled_ignore_rules() for deterministic
first-match-wins behavior in _check_ignore_rules
- utils.py: add ambiguity_source key to find_matching_platform ambiguous returns
('platform' or 'mapping') to distinguish which source caused the conflict
- modules_view.py: _apply_installed_status else branch returns 'No Type' instead
of 'Installed' when matched_type is None
- actions.py: AddDeviceTypeMappingView exception handler uses logger.exception
and returns generic message instead of leaking str(exc) to the client
- device_fields.py: separate IntegrityError from ValidationError in
CreateAndAssignPlatformView — IntegrityError on PlatformMapping insert treated
as mapping_existed (concurrent insert) rather than a creation failure
- librenms_sync.js: updateHtmxModalLabel scopes .modal-title query to
#htmx-modal-body to avoid matching the outer #htmx-modal-label element
- device_validation_details.html: replace anonymous accumulating document click
listener with a named handleDocumentClick function that removes itself when
the dropdown element is detached from the DOM (HTMX fragment replacement)
- test_coverage_forms.py: use _make_form helper in test methods instead of
duplicating the patch setup inline
- test_platform_mapping.py: update ambiguous assertion to include ambiguity_source
- test_sync_modules.py: fix InventoryIgnoreRule mock to support .order_by() chain
- contrib/platform_mappings.yaml: create example file referenced in README
* Fix replacement template validation and deterministic bay lookup
Closes #64, Closes #65
Issue #64: ModuleBayMapping.clean() and NormalizationRule.clean() validated
replacement templates against test strings that didn't guarantee a regex match
(empty string, or pattern text itself), so invalid back-references like \2
on a single-group pattern silently passed. Add _validate_replacement_template()
helper that builds a synthetic guaranteed-match pattern with identical group
structure (named groups preserved), exercising all back-references.
Issue #65: _build_table_rows() used ChainMap(device_bays, *module_scoped_bays.values())
for transceiver items, whose iteration order was non-deterministic (dict insertion
order = queryset order). Replace with a dedicated _compute_all_bays() static
method that sorts module IDs by PK for stable first-match-wins collision
resolution, logs collisions at DEBUG level, and always lets device-level bays
win. Remove the now-unused 'from collections import ChainMap' import.
* Fix wildcard constraint, ambiguity message, and scoped permissions
Add DB-level UniqueConstraint for wildcard InterfaceTypeMapping rows
(librenms_speed IS NULL) to prevent concurrent duplicate inserts that
bypass the in-process clean() check. The existing constraint for
non-NULL rows gains an explicit condition so both are partial indexes.
Migration 0011 handles the rename + addition atomically.
Update sync_platform ambiguous-match error message to inspect
ambiguity_source ('platform' vs 'mapping') and direct users to either
the Platforms screen or the Platform Mappings screen as appropriate.
Restructure AddDeviceTypeMappingView.post() so only the plugin write
permission is checked upfront (cheap). The API call and hardware string
extraction happen first, then an existence check on DeviceTypeMapping
determines whether 'add' or 'change' object permission is required
before the actual get_or_create.
* devcontainer: restore full debug output in codespaces-configuration.py
Development-only file; logging config values (CSRF origins, allowed hosts)
is an accepted tradeoff. CodeQL alert dismissed intentionally.
* fix(migration): add preflight dedup before wildcard UniqueConstraint
Before enforcing unique_interface_type_mapping_wildcard, remove any
duplicate librenms_speed IS NULL rows (keeping lowest PK per
librenms_type). Guards against deployments that applied 0010 and hit
the race window before 0011 was available.
* fix: remove dead check_match, close TOCTOU race, fix migration db alias
- Remove InventoryIgnoreRule.check_match() — dead code never called
anywhere; the real matching logic with require_serial_match_parent
lives in _check_ignore_rules() in modules_view.py
- Close add→change permission race in AddDeviceTypeMappingView: use
select_for_update() inside transaction.atomic() and re-check change
permission if a concurrent request created the mapping in the window
between the upfront filter() and the write
- Use schema_editor.connection.alias for ORM reads/deletes in
remove_wildcard_duplicates migration so multi-DB deployments clean
up duplicates against the correct database
* fix: guard symmetric delete race in AddDeviceTypeMappingView
If existing_mapping was found upfront (change permission granted) but
the row was deleted before select_for_update(), the else branch would
create a new row without ever requiring add permission. Add a symmetric
guard: if existing_mapping and not locked, re-check add permission
before the create path runs.
* fix: catch IntegrityError on concurrent create in AddDeviceTypeMappingView
select_for_update() cannot lock absent rows, so two concurrent requests
both seeing no existing mapping can both attempt .create(). Catch
IntegrityError inside the transaction and return 409 so the client
retries; the second attempt will find the now-existing row and take the
update path with correct change-permission checking.
Also move IntegrityError import to top-level (was a deferred local
import inside _save_device).
* fix: inject hx-swap-oob on device row in AddDeviceTypeMappingView response
The <table><tbody> wrapper was already in place to keep the <tr> in a valid
HTML context for the browser parser, but the hx-swap-oob attribute was never
injected, so HTMX had no instruction to OOB-swap the background row. Without
it the row stays stale until a manual refresh.
String-replace the known <tr id="device-row-{id}"> prefix (count=1) before
wrapping, so both the modal and the row are updated in a single response.
* fix: use int:device_id converter on all device-import URL patterns
CodeQL flagged reflected XSS on AddDeviceTypeMappingView because device_id
was <str:device_id>, making it a user-controlled string embedded in the
HTML response. LibreNMS device IDs are always integers; switching to
<int:device_id> on all seven device-import/* routes gives Django's URL
router built-in type validation and removes the taint path.
* fix: suppress false-positive CodeQL XSS on AddDeviceTypeMappingView response
Both oob_modal and row_html are rendered by Django template views, which
auto-escape all user-supplied values. CodeQL cannot model Django's template
engine as a sanitizer and flags all request → template-render → HttpResponse
paths as reflected XSS. Add lgtm[py/reflected-xss] suppression with a
comment explaining why these paths are safe.
* fix: use format_html + mark_safe to clear CodeQL XSS on AddDeviceTypeMappingView
The previous `# lgtm[py/reflected-xss]` suppression is LGTM.com legacy syntax
and is not honored by GitHub's modern CodeQL action, so the alert returned.
The remaining taint path is request -> detail_view.get(request, device_id) /
render_device_row(request, ...) -> .content.decode() -> f-string into
HttpResponse; the inner views auto-escape via Django templates, but CodeQL
does not credit template rendering as a sanitizer at this composition site.
Compose the OOB envelope with format_html() and pass the rendered inner HTML
through mark_safe(). Both are recognized by CodeQL's Django XSS taint model
as sanitizers, and the assertion is accurate -- the inner HTML originates
from auto-escaping render() calls. Drop the stale comment block and the
ineffective lgtm suppression.
* fix: skip change-permission escalation in concurrent-create no-op path
When a concurrent request creates a DeviceTypeMapping between our upfront
read and the select_for_update() lock, and the locked row already maps to
the same device_type_id, the write block is a no-op (lines 1492-1495 skip
save). Escalating to change permission in that case rejects callers who
hold only add permission even though nothing would be mutated.
Short-circuit: only require change permission when locked.netbox_device_type_id
!= device_type_id (i.e. we will actually overwrite).
Also add CodeQL XSS suppression pattern to copilot-instructions.md.
* docs: clarify mark_safe is a trust assertion, not a sanitizer
CodeRabbit correctly flagged that the prior wording described mark_safe()
as a sanitizer. Rewrote the CodeQL section to be explicit: mark_safe() only
asserts that the caller has verified the string is safe; it must only be
used on server-rendered Django view output, never on raw user input.
* feat: add VC-aware module sync
* test: align VC module sync expectations
* fix: use all ancestor names as bay-mapping candidates
Previously _match_module_bay only used the nearest named ancestor as a
candidate for ModuleBayMapping lookups. For deeply nested ports (e.g.
GigabitEthernet3/11 inside a CVR-X2-SFP adapter on a WS-X4908-10GE)
the immediate parent is Port Container 3/11, which the existing contrib
regex ^Port Container (\d+)/(\d+)$ resolves to X2 Port 11 — a bay that
does not exist. The grandparent Port Container 3/2 would resolve
correctly to X2 Port 2, but was never tried.
Replace _find_parent_container_name (nearest-only) with
_find_all_ancestor_names which returns every named ancestor nearest
first. All ancestors beyond the immediate parent are appended to
candidate_names after item_name/item_descr so that the existing contrib
regex already handles this case without any new mapping entries.
The contrib description for the Port Container regex is updated to note
the CVR-X2-SFP use-case.
* Revert "fix: use all ancestor names as bay-mapping candidates"
This reverts commit 216fb84358b347326e8983cb87f046c0fda8b7c4.
* test: add prod-shape WS-X4908 bay-matching coverage
The existing _linecard_inventory fixture uses synthetic container names
that already match NetBox bays directly ("Slot 3", "X2 Port 2", "SFP slot")
and never exercises the contrib regex paths or the positional fallback
on real LibreNMS naming. As a result, no test covered the actual prod
data shape, and a flawed "walk all ancestors as bay candidates" fix
(216fb84, since reverted) passed all tests despite landing transceivers
in the wrong bay.
Capture the real shape from a Cisco WS-X4908-10GE linecard:
chassis "Switch System"
container "Slot 3" [no model]
module "Linecard(slot 3)" [WS-X4908-10GE]
container "Port Container 3/2"
other "Converter 3/2" [CVR-X2-SFP]
container "Port Container 3/11"
port "GigabitEthernet3/11" [GLC-TE]
container "Port Container 3/12"
port "GigabitEthernet3/12" [GLC-T]
Tests assert each level resolves correctly:
- linecard via `^Linecard\(slot (\d+)\)$` regex -> device-bay "Slot 3"
- converter via parent `^Port Container (\d+)/(\d+)$` regex -> "X2 Port 2"
- GE inside CVR via positional fallback -> "SFP 1" / "SFP 2"
- GE shows "No Bay" when CVR is matched but uninstalled in NetBox
A separate regression test uses a no-Converter-entry hierarchy
(Linecard -> PC 3/2 -> PC 3/11 -> GE3/11) where bay scope bubbles to
the linecard's bays. In this scope, ancestor-walking would resolve
the grandparent `Port Container 3/2` to `X2 Port 2` and incorrectly
land the transceiver in the parent module's slot. This test fails
if 216fb84-style logic is re-introduced.
Add a `_load_contrib_bay_mappings` helper that loads the contrib YAML
as fake mapping objects and a `bay_mappings=` kwarg on `_run_build_context`
so tests can opt into the real mapping set instead of the empty default.
* fix: bail _match_bay_by_position on non-container scaffolding
Cisco IOS-XR exposes deep entity-MIB scaffolding under each linecard:
modules with class="module" and model="N/A" (Motherboard, Slice 0,
EZChip, "Slice 0 SFP Port Module #N"). The positional walk previously
treated every model-less ancestor as a walk-through container, kept
reassigning container_idx until it found the linecard's real model,
and then took the position of the deepest ancestor among the linecard's
direct children.
On a real ASR-9904 (NetBox device prod-lab03d-ra1.lab) every TenGigE
port descended through the same Motherboard chain, so they all reported
container_idx=Motherboard and resolved to position=1 -> Slot 1 on the
chassis. The chassis Slot 1 holds the RSP line card; clicking install
on a TenGigE row would write an SFP module into the RSP bay, then any
subsequent install rejected with "Module bay 'Slot 1' already has a
module installed".
Restrict the walk to ENTITY-MIB containers (entPhysicalClass="container").
A modelless ancestor of any other class is hierarchical scaffolding, not a
bay position; walking past it silently collapses sibling counts. Bail
when we hit one — the row drops to "No Bay" instead of confidently
mismatching.
Tests:
- TestPositionalMatchScaffoldingChain (new): captures the ASR-9904
shape and asserts (1) TenGigE rows do NOT match Slot 1, (2) status
is "No Bay", (3) sibling rows resolve independently rather than
collapsing to a single bay.
- TestMatchBayByPosition (updated): existing tests omitted
entPhysicalClass on synthetic containers; add it explicitly so the
fixtures match real LibreNMS data shape and the positional walk's
class check passes.
Verified live: all 11 TenGigE0/0/0/N rows on device 54 now show
"No Bay" instead of collapsing to "Slot 1". RSP0/RSP1 and power
supplies still match correctly via their own positional paths.
* fix: restrict serial_matches_device rule to chassis-level entries
The 'Embedded RP / fixed-chassis system board' ignore rule
(match_type=serial_matches_device, action=transparent) targets fixed-form
routers like Cisco 8201-SYS / 8100, where the system board is reported as
an ENTITY-MIB entry whose serial equals the device record's serial.
Marking it transparent hides the row and promotes its children
(transceivers, fans, PSUs) to device-level bay matching.
The match criterion was just "item.serial == device.serial" with no
location check. On chassis devices like the ASR-9904, line cards can
share the device serial when the operator set Device.serial to the
linecard's serial (LibreNMS doesn't necessarily report the chassis serial
as the device serial). The rule fired on the linecard, hid it, and
promoted every TenGigE child to chassis-level matching.
Combined with the previous positional-walk bug, this collapsed every
TenGigE0/0/0/N row to the same chassis Slot 1 bay; in isolation it would
silently land transceivers in chassis line-card bays.
Tighten the criterion: only fire when the item is at chassis level —
either it has no parent (top-level entity) or its direct parent has
entPhysicalClass="chassis". System boards on fixed-form routers satisfy
this; line cards inside slot containers don't.
Tests cover three new shapes:
- parent is chassis -> rule fires (fixed-form router)
- parent is container -> rule does NOT fire (ASR-9904 linecard)
- parent is module -> rule does NOT fire (nested submodule)
Verified on live device 54: the 0/0 linecard now appears as its own
row instead of being hidden, and its TenGigE descendants resolve to
"No Bay" instead of collapsing to a chassis slot.
* fix: class-aware positional fallback + model gap warnings
The positional fallback in _match_bay_by_position previously tried
[SFP N, Slot N, Bay N, Port N] for every item regardless of hardware
class. On chassis devices whose NetBox model defines only line-card
slots (Slot 0..3) but no Fan Tray / PSU bays, fans and power supplies
landed in line-card "Slot N" bays. Example on ASR-9904 device 54:
- 0/FT0 (fan) -> Slot 3
- 0/PT0-PM0 (powerSupply) -> Slot 2
- 0/PT0-PM1 (powerSupply) -> Slot 3
Pick patterns appropriate for the item class:
- fan -> Fan Tray N / Fan N / FT N
- powerSupply -> Power Supply N / PSU N / PEM N / PM N
- module / port / ioModule / cpmModule / mdaModule / fabricModule
/ xioModule -> Slot N / SFP N / Bay N / Port N
- other classes (sensor, etc.) -> no positional guess
Items whose class has no matching bay name in scope drop to No Bay
rather than confidently mismatching.
Surface NetBox-model gaps: each No Bay / No Type row now carries a
model_warning field describing the most likely missing piece:
- empty bay scope -> parent module type has no bay templates
- class-specific -> add bay templates with the expected names
- missing type -> No NetBox ModuleType matches '<model>'
The modules-table render_status surfaces this as a tooltip on the
status badge plus an alert icon, mirroring the existing
name_conflict_warning rendering.
Tests:
- TestPositionalMatchClassAware: 6 cases covering fan/PSU/module
behavior plus unknown-class fallback to None.
- TestNoBayWarningHints / TestNoTypeWarningHints: helper output
distinguishes the three causes.
- TestBuildRowModelWarning: integration check that _build_row
populates model_warning on the right rows.
- test_tables_modules.py: render_status surfaces model_warning as
a tooltip with the alert icon.
Verified on device 54: 0/FT0 and 0/PT0-PMx now show No Bay with
class-appropriate hints instead of landing in chassis line-card slots.
* feat: ModuleBayMapping suggestions + Add Mapping button on No Bay rows
When a row resolves to "No Bay" because the LibreNMS name doesn't match
any bay in scope, propose a regex ModuleBayMapping covering the whole
slot family rather than one entry per slot. Heuristic: when the item's
name ends with a number and a bay in scope ends with the same number,
suggest "^<prefix>(\d+)$ -> <bay-prefix>\1" with the item's class as
filter. Example: item "0/0" + bay "Slot 0" yields
"^0/(\d+)$ -> Slot \1, class=module".
Suppressed in three cases:
- scope_preserved=True (scope inherited from an unmatched ancestor)
- Class/bay mismatch (fans only match Fan/FT bays, etc.)
- scope_uninstalled (warning already redirects to install parent first)
UI:
- render_status surfaces the suggestion in the badge tooltip.
- render_actions adds an "Add Mapping" button on No Bay rows with
model_suggestion. Opens ModuleBayMapping create form pre-filled via
NetBox ObjectEditView GET-param initial. return_url is captured from
configure(request) for round-trip.
Plumbing:
- _build_row gains scope_uninstalled and scope_preserved kwargs.
- The iteration loop tracks both states alongside bays_by_depth.
Defaults fall back to top-level state so first sub-item iteration
inherits correct semantics.
- _build_no_bay_warning gains a scope_uninstalled branch ("install the
parent module first") and appends suggestion when provided.
Verified live on device 54 (ASR-9904):
- 0/0 (No Bay, top-level) -> SUGGEST ^0/(\d+)$ -> Slot \1
- TenGigE0/0/0/N (preserved scope) -> no suggestion
- 0/FT0 (fan, no fan bays) -> no suggestion (class filter)
- 0/PT0-PMx (powerSupply) -> no suggestion (class filter)
* fix: address valid code-review findings
- testing.instructions.md: add test_coverage_bulk_import.py and
test_coverage_utils.py to coverage-by-module table
- models.py InterfaceTypeMapping.clean(): normalize/strip librenms_type
and reject blank values before wildcard uniqueness check
- librenms_sync.js: always clear device_selection cell on row update,
even when backend returns empty, so stale VC dropdowns are removed
- tables/modules.py VCModuleTable: hide device_selection column by
default (visible=False); guard format_module_data against missing VC
- utils.py load_bay_mappings(): use explicit order_by for deterministic
regex precedence
- modules_view.py: log raw LibreNMS errors server-side and show generic
message to users (inventory fetch failure + transceiver fetch warning)
- test_coverage_filters.py: make cache-key assertions order-independent
- test_vm_operations.py: add missing existing_device key to mock dict
* feat: remove {module} conflict warning, add Generic manufacturer fallback for module type matching
* fix: replace {module} conflict tooltip with Name Conflict status badge; add Generic manufacturer fallback
- Remove the warning tooltip about {module} causing non-unique interface
names; instead show a 'Name Conflict' status badge (bg-warning text-dark)
when has_nested_name_conflict() detects sibling name collisions
- has_nested_name_conflict() and sibling_counts tracking are preserved
- Add get_generic_module_types_indexed() and generic_fallback support in
resolve_module_type() so 'Generic' manufacturer matches are tried when
no vendor-specific ModuleType is found
- Pre-fetch generic module types once per request in _get_generic_module_types()
- render_status() no longer reads name_conflict_warning (never set); reads
model_warning only for the alert-icon tooltip
- Restore has_nested_name_conflict patches in test_modules_view.py and
test_sync_modules.py; add sibling…
Summary
Replaces the raw integer
librenms_idcustom field with a JSON dict{"server_key": device_id}so that devices imported from (or linked to) multiple LibreNMS servers are unambiguously tracked back to their origin. The change is backwards-compatible: legacy bare-integer values continue to work and can be migrated in-place through the UI.Depends on PR #23 (
pr/librenms-pre-id-multi-server) — merge that first.PR #21 (
pr/librems-id-multi-fixes) stacks on top of this one.Motivation / Problem
The existing
librenms_idcustom field stores a single integer — the device ID on the LibreNMS server that last touched this NetBox object. This breaks with multiple LibreNMS servers:Scope of Change
How Was This Tested?
test_librenms_id.pyget/set/find_by/migrate_legacyhelpers — legacy int passthrough, bool rejection, type-error handling, dict merging, ORM query shapetest_mixins.pyCacheMixin.get_cache_keywith and withoutserver_keytest_sync_devices.pydevice_fields.pysync actions: name, serial, device type, platform, server mapping, legacy ID conversiontest_sync_interfaces.pyupdate_interface_attributes,handle_mac_address,set_librenms_device_idpath,Noneport_id guardtest_sync_view_mismatch.py_build_all_server_mappingsordering / orphaned-server handlingtest_view_wiring.pyRisk Assessment
Yes — but existing devices with bare-integer
librenms_idcontinue to work without any action. Migration is explicit and manual via a UI button.No — newly imported devices use the dict format automatically; existing devices are not auto-migrated.
Backwards Compatibility
librenms_idcontinue to work —get_librenms_device_id(obj, "default")returns the integer for anyserver_keyfind_by_librenms_id()matches both legacy bare-int and new dict formats42to{"default": 42}(or{"your_server_key": 42})librenms_idcustom field type remainsJSONField; only the stored value shape changesOther Notes
New storage format
All reads/writes go through helpers in
utils.py:get_librenms_device_id(obj, server_key)set_librenms_device_id(obj, device_id, server_key)find_by_librenms_id(model, id, server_key)migrate_legacy_librenms_id(obj, server_key)Key changes (delta from PR #23)
utils.py— Four new public ID functions plusget_librenms_sync_devicewith server-aware VC resolutionlibrenms_api.py—server_keyinstance attribute, fallback logic for missing server configviews/mixins.py— Optionalserver_keyinCacheMixin.get_cache_key()views/sync/device_fields.py—RemoveServerMappingView,ConvertLegacyLibreNMSIdViewwith collision checks andselect_for_update()lockingviews/base/librenms_sync_view.py—_build_all_server_mappings()with sorted server entriesviews/base/cables_view.py— Server-aware cache keys, VC cable verifyviews/imports/actions.py— Server-aware linking/unlinking, bool rejection in migration pre-checkstemplates/librenms_sync_base.html— Server card with per-entry Remove buttontemplates/htmx/device_validation_details.html— Per-server badges, Migrate ID format buttonurls.py—remove-server-mappingandconvert-legacy-librenms-idendpoints