Skip to content

feat: librenms_id JSON migration with multi-server support and PR rev… - #15

Closed
marcinpsk wants to merge 13 commits into
developfrom
refactor/librenms_id-rebased
Closed

feat: librenms_id JSON migration with multi-server support and PR rev…#15
marcinpsk wants to merge 13 commits into
developfrom
refactor/librenms_id-rebased

Conversation

@marcinpsk

@marcinpsk marcinpsk commented Mar 3, 2026

Copy link
Copy Markdown
Owner

…iew fixes

  • Add librenms_id JSON multi-server format (get_librenms_id/set_librenms_id helpers)
  • Add legacy integer migration detection and action (migrate_librenms_id)
  • Add server_key threading throughout import/sync pipeline
  • Add serial-number conflict resolution and serial sync actions
  • Fix show_disabled to use 'disabled' field (not 'status')
  • Fix progress log off-by-one (idx not idx+1)
  • Fix VC creation to use .get('device_id') instead of direct access
  • Remove manual rollback from transaction.atomic() except blocks
  • Add _is_truthy() helper for consistent truthy string parsing
  • Log platform exceptions server-side with generic user message
  • Fix test patch targets for cache and Cluster/Rack
  • Fix permission test assertions for custom_field_data
  • Rebase onto origin/develop (includes utils-package-split + serial PR#227)

Summary

Briefly describe what this PR does in plain English, and provide as much of the following information as possible.

Motivation / Problem

What issue does this solve?

  • Bug
  • Feature
  • Refactor
  • Maintenance / cleanup

Link any related issues if applicable.

Scope of Change

Delete items that don’t apply:

  • Sync/Import logic
  • NetBox models / ORM
  • LibreNMS API interaction
  • Config / settings
  • Web UI / templates
  • Database migrations
  • Tests
  • Docs only
  • Other:

How Was This Tested?

Delete items that don’t apply and describe briefly.

  • Unit tests: <yes/no + what>
  • Manual testing: <yes/no + what>
  • Not tested:

Manual Test Steps (if applicable)

Risk Assessment

  • Does this change affect existing users?
  • Could this cause unintended imports / updates?

Explain briefly.

Backwards Compatibility

  • No breaking changes
  • Breaking change (explain and document)

Other Notes

Anything the maintainer(s) should pay particular attention to?

Summary by CodeRabbit

  • New Features

    • Multi-server LibreNMS UI: per-connection mapping display, remove-mapping action/route, multi-ID display, and Legacy ID migration UI.
    • Naming options (use_sysname, strip_domain) exposed and persisted in import jobs and UI.
  • Bug Fixes

    • Per-server deterministic cache keys and improved job cancellation behavior.
    • Safer tooltip reinitialization, accessibility improvements, and rel attributes on external links.
  • Refactor

    • Server-scoped ID handling across import/sync/VC flows, improved VC member naming, and transactional safety for ID operations.
  • Tests

    • Extensive new unit and integration tests, including a local mock LibreNMS server.

@coderabbitai

coderabbitai Bot commented Mar 3, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds per-server scoping for LibreNMS IDs and cache keys; propagates server_key, use_sysname, and strip_domain through import/validation/cache/VC flows; introduces librenms_id utilities and migration, RemoveServerMapping endpoint/UI, VC naming refinements, deterministic cache keys, and related UI/accessibility updates. (50 words)

Changes

Cohort / File(s) Summary
Librenms ID utilities
netbox_librenms_plugin/utils.py
Add get_librenms_device_id, set_librenms_device_id, find_by_librenms_id, migrate_legacy_librenms_id, logger and server-scoped ID handling.
Import core & orchestration
netbox_librenms_plugin/import_utils/bulk_import.py, netbox_librenms_plugin/import_utils/device_operations.py, netbox_librenms_plugin/import_utils/vm_operations.py
Propagate server_key widely; add _safe_disabled, _empty_return; improved cancellation checks, VC dedupe and naming, per-device logging; store librenms_id per-server in custom fields.
Caching & filtering
netbox_librenms_plugin/import_utils/cache.py, netbox_librenms_plugin/import_utils/filters.py
Deterministic SHA‑256 keys; include use_sysname/strip_domain in metadata and keys; add get_import_search_cache_key; scope cache keys by server_key.
Virtual chassis handling
netbox_librenms_plugin/import_utils/virtual_chassis.py
Enforce 1‑based VC positions; add _load_vc_member_name_pattern and pattern-aware _generate_vc_member_name; reduce per-member DB hits and safer position selection.
Views: sync/imports, routing & actions
netbox_librenms_plugin/views/sync/device_fields.py, netbox_librenms_plugin/views/imports/actions.py, netbox_librenms_plugin/views/imports/list.py, netbox_librenms_plugin/urls.py, netbox_librenms_plugin/views/__init__.py
Add RemoveServerMappingView and export it; transactional removal and platform creation; propagate use_sysname/strip_domain to jobs/cache; add migration and server-scoped action flows.
Views: base and object sync
netbox_librenms_plugin/views/base/librenms_sync_view.py, .../cables_view.py, .../ip_addresses_view.py, .../object_sync/devices.py, .../object_sync/vms.py
Add _build_all_server_mappings; use server-scoped Q filters; pass server_key into table constructors and lookups.
Import UI & validation templates
netbox_librenms_plugin/templates/.../device_validation_details.html, netbox_librenms_plugin/views/imports/actions.py
Expose per-server mappings and legacy-ID migration UI with serial confirmation; accessibility and security attributes added; action wiring updated for server-scoped flows.
Tables & interface rendering
netbox_librenms_plugin/tables/interfaces.py, netbox_librenms_plugin/tables/device_status.py
Tables accept server_key; rendering uses get_librenms_device_id; add legacy-ID migration indicator/button styling.
Librenms API delegation
netbox_librenms_plugin/librenms_api.py
Delegate get/set librenms_id behavior to new utils instead of direct custom_field_data mutation.
Sync interfaces & port writes
netbox_librenms_plugin/views/sync/interfaces.py
Use set_librenms_device_id for port ID writes; guard writes when port_id is None; adjust VLAN/MAC handling and view MRO.
Forms & client JS
netbox_librenms_plugin/forms.py, netbox_librenms_plugin/static/.../librenms_import.js
Scope location cache keys by server_key and reuse API client; use bootstrap.Tooltip.getOrCreateInstance on HTMX swaps.
Jobs & tests
netbox_librenms_plugin/jobs.py, netbox_librenms_plugin/tests/*, tests/mock_librenms_server.py
Persist use_sysname and strip_domain in job data; expand tests for naming flags, job metadata, RemoveServerMapping and multi-server behaviors; add mock LibreNMS test server and integration tests.
Templates: sync base UI
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
Replace single-server display with all_server_mappings rendering and per-mapping actions; keep legacy fallback and add Modules tab when enabled.
Exports
netbox_librenms_plugin/import_utils/__init__.py, netbox_librenms_plugin/views/__init__.py
Re-export get_import_search_cache_key and RemoveServerMappingView.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Browser as "User / Browser"
  participant View as "Sync/Import View"
  participant Job as "Background Job (RQ)"
  participant API as "LibreNMS API Client"
  participant Cache as "Cache (Redis)"
  participant DB as "NetBox DB"

  rect rgba(200,200,255,0.5)
    Browser->>View: trigger import/list (server_key, use_sysname, strip_domain)
    View->>Job: enqueue job with metadata (server_key, use_sysname, strip_domain)
  end

  rect rgba(200,255,200,0.5)
    Job->>Cache: check validated_device cache key (includes server_key, use_sysname, strip_domain)
    alt cache miss
      Job->>API: fetch devices (api.server_key)
      API-->>Job: libre_devices
      Job->>DB: find_by_librenms_id / existing device lookups (server-scoped)
      DB-->>Job: existing device info
      Job->>Cache: write validated entries and metadata (server-scoped keys)
    else cache hit
      Cache-->>Job: return cached results
    end
    Job->>View: store job results / notify completion
    View->>Browser: render results (all_server_mappings, validation UI)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰

I hopped through mappings, old and new,
Scoped each ID to its own view,
Caches hashed and names aligned,
Legacy digits left behind,
Hooray — the rabbits fixed the queue!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description includes the summary section but lacks complete details in most template sections (Motivation/Problem, Scope of Change, How Was This Tested, Risk Assessment, Backwards Compatibility, and Other Notes are mostly empty). Complete the remaining sections of the PR description template, particularly Risk Assessment and Backwards Compatibility, to ensure reviewers have full context for merge decisions.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: introducing librenms_id JSON migration with multi-server support and addressing PR review fixes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/librenms_id-rebased

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

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/views/sync/interfaces.py (1)

21-22: ⚠️ Potential issue | 🔴 Critical

Add LibreNMSAPIMixin before dereferencing self.librenms_api.

update_interface_attributes() now reads self.librenms_api.server_key, but this view class does not declare LibreNMSAPIMixin in its MRO. That can raise AttributeError during sync.

🔧 Proposed fix
 from netbox_librenms_plugin.views.mixins import (
     CacheMixin,
+    LibreNMSAPIMixin,
     LibreNMSPermissionMixin,
     NetBoxObjectPermissionMixin,
     VlanAssignmentMixin,
 )
@@
-class SyncInterfacesView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, VlanAssignmentMixin, CacheMixin, View):
+class SyncInterfacesView(
+    LibreNMSPermissionMixin,
+    NetBoxObjectPermissionMixin,
+    LibreNMSAPIMixin,
+    VlanAssignmentMixin,
+    CacheMixin,
+    View,
+):
As per coding guidelines: New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`).

Also applies to: 240-240

🤖 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 21 - 22, The
SyncInterfacesView class lacks LibreNMSAPIMixin in its MRO but
update_interface_attributes() accesses self.librenms_api.server_key, so add
LibreNMSAPIMixin into the inheritance list for SyncInterfacesView (placing it
before any code that dereferences self.librenms_api) following the recommended
mixin order (LibreNMSPermissionMixin, NetBoxObjectPermissionMixin,
LibreNMSAPIMixin, CacheMixin, VlanAssignmentMixin) so self.librenms_api is
defined and update_interface_attributes() can safely use server_key.
🤖 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/__init__.py`:
- Line 5: The release line in netbox_librenms_plugin/__init__.py incorrectly
sets __version__ = "0.4.2", causing a version rollback; update the __version__
symbol to the correct, non-regressive release string (e.g., the intended release
>= the previously published 0.4.3 or the new bump such as "0.4.4") so package
resolution will upgrade cleanly.

In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 319-320: The current branch that runs when hasattr(refreshed,
"role") and refreshed.role replaces validation["device_role"] with a partial
dict and drops other keys like available_roles; instead, ensure you preserve the
existing device_role schema by making validation["device_role"] a dict if not
already (e.g., set to {} when missing or not a dict) and then update it with the
refreshed values (e.g., set "found": True and "role": refreshed.role via
dict.update) so available_roles and other keys remain intact; apply this change
in the block that currently assigns validation["device_role"] = {"found": True,
"role": refreshed.role}.
- Line 446: The list comprehension that filters disabled devices (libre_devices
= [d for d in libre_devices if int(d.get("disabled", 0)) != 1]) can raise on
non-numeric or None values; update the filtering to parse the "disabled" field
defensively in the bulk_import logic by reading d.get("disabled"), handling
None/empty/"-" etc., converting safely to an int inside a try/except (defaulting
to 0 on failure) or by normalizing string values before numeric conversion, and
then use that safe value to decide inclusion in libre_devices; locate the
comprehension that assigns to libre_devices and replace it with a tolerant check
that references the same "disabled" key.

In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 772-773: The code currently sets "custom_field_data":
{"librenms_id": {api.server_key: int(device_id)}} directly; replace this direct
assignment by using the centralized helper set_librenms_device_id to produce the
proper custom_field_data/formatting. Locate the create path that builds the
payload (the dict containing "custom_field_data") and remove the inline
librenms_id construction, calling set_librenms_device_id(payload_or_account_obj,
int(device_id)) (or whatever signature set_librenms_device_id expects) to
inject/return the normalized custom_field_data so formatting/normalization is
consistent across create/update flows.

In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 41-43: The list-comprehension that filters based on disabled uses
int(d.get("disabled", 0)) which can raise on None/""/unexpected strings; update
the filtering logic around the show_disabled check to safely coerce the disabled
value instead of calling int() directly: extract val = d.get("disabled", 0),
handle TypeError/ValueError or non-numeric strings by mapping common truthy
strings ("1","true","yes","on") to 1 and everything else to 0, then filter
devices by comparing the safe integer (e.g., disabled != 1); adjust the
comprehension that references devices so it uses this safe-parsed disabled
value.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 201-207: Ensure VC member positions are strictly 1-based by
validating parsed values: when you parse raw_position into position (the
variables raw_position and position in virtual_chassis.py), coerce to int and
then enforce position = max(1, parsed_value) (or fall back to idx+1 if
parsed_value < 1) instead of allowing 0; also apply the same validation in the
second parsing block around lines 309-319. This prevents
update_vc_member_suggested_names (referenced by name) from adding +1 to
already-correct values and avoids off-by-one suggested names.

In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py`:
- Around line 331-334: The test defines a local helper _make_obj that builds a
MagicMock with custom_field_data; remove this ad-hoc builder and use the shared
fixture provided in tests/conftest.py instead: stop creating _make_obj, accept
or call the appropriate fixture (e.g., the object/device factory defined in
conftest) in your test and set or parametrize its custom_field_data to
{"librenms_id": cf_librenms_id} as needed, or extend the conftest fixture to
accept an id argument so tests call the fixture with cf_librenms_id rather than
constructing a new MagicMock.

In `@netbox_librenms_plugin/urls.py`:
- Around line 225-229: The new route registers RemoveServerMappingView but the
view writes the wrong custom-field key due to a typo: it assigns to
custom_field_data["librenrenns_id"] (or "librenrenms_id") instead of the correct
"librenms_id", which leaves the real mapping intact and corrupts CF state;
update the handler inside RemoveServerMappingView to use the exact key
"librenms_id" when removing/clearing the mapping and ensure any save()/update
call persists that corrected key (also add a small unit/check to confirm the key
exists before and after mutation to prevent regressions).

In `@netbox_librenms_plugin/utils.py`:
- Around line 45-67: The function set_librenms_device_id currently stores
device_id without normalization which allows strings to be persisted and later
break integer-based lookups like find_by_librenms_id; coerce/validate device_id
to an int before storing (e.g., attempt int(device_id) and handle conversion
errors), ensure only an integer is written into
obj.custom_field_data["librenms_id"][server_key], and if conversion fails raise
or log a clear error so callers cannot accidentally persist non-integer IDs.

In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 86-89: Extract the duplicated Q(...) OR Q(...) predicate into a
single helper function (e.g. add a method on LibreNMSAPI like
get_librenms_id_predicate(server_key, remote_id) that returns a Django Q object)
and use it wherever Devices/VMs are being looked up (replace direct usages in
Device.objects.get and similar queries in cables_view.py — locations around the
current Device lookup and the other occurrences called out in the comment). Use
LibreNMSAPI.get_librenms_id() (or the new helper) instead of directly
referencing custom_field_data__librenms_id to build the predicate so all lookups
go through the centralized logic and then pass that Q into the existing
.get/.filter calls.

In `@netbox_librenms_plugin/views/base/ip_addresses_view.py`:
- Around line 108-112: The dict comprehension in interfaces_by_librenms_id calls
get_librenms_device_id(interface, server_key) twice per iteration; compute the
id once into a local variable (e.g., id = get_librenms_device_id(interface,
server_key)) inside the comprehension/loop and use that variable in both the key
and the conditional so each interface invokes get_librenms_device_id only once;
update the comprehension using the unique symbols interfaces_by_librenms_id,
all_interfaces and get_librenms_device_id to locate the change.

In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 146-155: _build_all_server_mappings currently only checks
servers_config and marks the legacy "default" mapping as unconfigured; change
the logic so that if srv_cfg is None and sk == "default" you also inspect
plugins_cfg.get("librenms_url") (and optional plugins_cfg.get("display_name"))
and treat that as an active configuration: set is_configured True, librenms_url
from plugins_cfg["librenms_url"], display_name from plugins_cfg["display_name"]
or sk, and build device_url from that librenms_url; update the branches that
compute srv_cfg, is_configured, librenms_url, display_name, and device_url
accordingly in _build_all_server_mappings (use the existing variables
servers_config, plugins_cfg, srv_cfg, librenms_url, display_name, device_url to
locate the change).

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1203-1227: Protect the read-check-write sequence by acquiring a DB
row lock on the device before inspecting or mutating custom_field_data: wrap the
logic in a transaction.atomic() block and re-query the device with
select_for_update() (use the Device PK from existing_device) to get a locked
instance, perform the isinstance/int checks against locked.custom_field_data,
verify IDs and serial_confirmed, call migrate_legacy_librenms_id on the locked
instance and then call _save_device(locked); this prevents concurrent updates
from being lost by ensuring the migration operates on a locked row.

In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 270-281: The user preference values returned by get_user_pref
(e.g., _use_sysname_pref and _strip_domain_pref) may be strings like "false"
which are truthy; coerce/normalize these persisted prefs to strict booleans
before using them in _use_sysname and _strip_domain (and the same block around
lines 445–467). Implement normalization logic (or a small helper) to treat
values like "false", "0", "no" (case-insensitive) as False and "true", "1",
"yes" as True, handle actual booleans and None unchanged, then use the
normalized boolean when applying the settings fallback via getattr(settings,
"..._default", ...).

In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 460-467: The except blocks in device_fields.py that catch
ValidationError and Exception currently include raw exception text in
messages.error (the handlers around messages.error(request, f"...{exc}")) —
change these to log the exception server-side (use logger.exception() or similar
referencing the same except blocks) and replace the user-facing messages.error
text with a generic, non-sensitive message (e.g., "Validation error removing
mapping." and "Error removing mapping for server.") while preserving
transaction.set_rollback(True) and the
redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk); keep
server_key in logs only, not in the flash message.

---

Outside diff comments:
In `@netbox_librenms_plugin/views/sync/interfaces.py`:
- Around line 21-22: The SyncInterfacesView class lacks LibreNMSAPIMixin in its
MRO but update_interface_attributes() accesses self.librenms_api.server_key, so
add LibreNMSAPIMixin into the inheritance list for SyncInterfacesView (placing
it before any code that dereferences self.librenms_api) following the
recommended mixin order (LibreNMSPermissionMixin, NetBoxObjectPermissionMixin,
LibreNMSAPIMixin, CacheMixin, VlanAssignmentMixin) so self.librenms_api is
defined and update_interface_attributes() can safely use server_key.

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45b6b7a and 7a25806.

📒 Files selected for processing (33)
  • docs/changelog.md
  • netbox_librenms_plugin/__init__.py
  • netbox_librenms_plugin/forms.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/jobs.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/test_background_jobs.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/tests/test_sync_view_mismatch.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/views/object_sync/devices.py
  • netbox_librenms_plugin/views/object_sync/vms.py
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • pyproject.toml
💤 Files with no reviewable changes (1)
  • docs/changelog.md

Comment thread netbox_librenms_plugin/__init__.py Outdated

__author__ = "Andy Norwood"
__version__ = "0.4.3"
__version__ = "0.4.2"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid version rollback for a feature-bearing release.

Line 5 downgrades to 0.4.2. If any deployment already has 0.4.3, this release will not upgrade cleanly via normal package resolution.

📌 Suggested fix
-__version__ = "0.4.2"
+__version__ = "0.4.4"
📝 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.

Suggested change
__version__ = "0.4.2"
__version__ = "0.4.4"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/__init__.py` at line 5, The release line in
netbox_librenms_plugin/__init__.py incorrectly sets __version__ = "0.4.2",
causing a version rollback; update the __version__ symbol to the correct,
non-regressive release string (e.g., the intended release >= the previously
published 0.4.3 or the new bump such as "0.4.4") so package resolution will
upgrade cleanly.

Comment thread netbox_librenms_plugin/import_utils/bulk_import.py Outdated
Comment thread netbox_librenms_plugin/import_utils/bulk_import.py Outdated
Comment thread netbox_librenms_plugin/import_utils/device_operations.py Outdated
Comment thread netbox_librenms_plugin/import_utils/filters.py
Comment thread netbox_librenms_plugin/views/base/ip_addresses_view.py Outdated
Comment thread netbox_librenms_plugin/views/base/librenms_sync_view.py Outdated
Comment thread netbox_librenms_plugin/views/imports/actions.py Outdated
Comment on lines +270 to +281
_use_sysname_pref = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname")
_strip_domain_pref = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain")
_use_sysname = (
_use_sysname_pref
if _use_sysname_pref is not None
else (getattr(settings, "use_sysname_default", True) if settings else True)
)
_strip_domain = (
_strip_domain_pref
if _strip_domain_pref is not None
else (getattr(settings, "strip_domain_default", False) if settings else False)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize persisted naming preferences to strict booleans before using them.

Line [270] and Line [445] consume get_user_pref(...) values directly. If a stored value is "false" (string), it is truthy and will be treated as enabled, causing wrong naming behavior and cache-key mismatches.

Proposed fix
+def _coerce_bool(value, default: bool) -> bool:
+    if value is None:
+        return default
+    if isinstance(value, bool):
+        return value
+    if isinstance(value, str):
+        return value.strip().lower() in {"1", "true", "on", "yes"}
+    return bool(value)
...
-            _use_sysname = (
-                _use_sysname_pref
-                if _use_sysname_pref is not None
-                else (getattr(settings, "use_sysname_default", True) if settings else True)
-            )
-            _strip_domain = (
-                _strip_domain_pref
-                if _strip_domain_pref is not None
-                else (getattr(settings, "strip_domain_default", False) if settings else False)
-            )
+            _use_sysname = _coerce_bool(
+                _use_sysname_pref,
+                getattr(settings, "use_sysname_default", True) if settings else True,
+            )
+            _strip_domain = _coerce_bool(
+                _strip_domain_pref,
+                getattr(settings, "strip_domain_default", False) if settings else False,
+            )
...
-        use_sysname = (
-            _use_sysname_toggle
-            if _use_sysname_toggle is not None
-            else use_sysname_pref
-            if use_sysname_pref is not None
-            else (getattr(_settings, "use_sysname_default", True) if _settings else True)
-        )
+        use_sysname = _coerce_bool(
+            _use_sysname_toggle if _use_sysname_toggle is not None else use_sysname_pref,
+            getattr(_settings, "use_sysname_default", True) if _settings else True,
+        )
...
-        strip_domain = (
-            _strip_domain_toggle
-            if _strip_domain_toggle is not None
-            else strip_domain_pref
-            if strip_domain_pref is not None
-            else (getattr(_settings, "strip_domain_default", False) if _settings else False)
-        )
+        strip_domain = _coerce_bool(
+            _strip_domain_toggle if _strip_domain_toggle is not None else strip_domain_pref,
+            getattr(_settings, "strip_domain_default", False) if _settings else False,
+        )

Also applies to: 445-467

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/list.py` around lines 270 - 281, The
user preference values returned by get_user_pref (e.g., _use_sysname_pref and
_strip_domain_pref) may be strings like "false" which are truthy;
coerce/normalize these persisted prefs to strict booleans before using them in
_use_sysname and _strip_domain (and the same block around lines 445–467).
Implement normalization logic (or a small helper) to treat values like "false",
"0", "no" (case-insensitive) as False and "true", "1", "yes" as True, handle
actual booleans and None unchanged, then use the normalized boolean when
applying the settings fallback via getattr(settings, "..._default", ...).

Comment thread netbox_librenms_plugin/views/sync/device_fields.py
@marcinpsk
marcinpsk force-pushed the refactor/librenms_id-rebased branch from 7a25806 to 27eac04 Compare March 4, 2026 11:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

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/views/imports/actions.py (1)

68-70: ⚠️ Potential issue | 🟠 Major

Persisted user preferences should be coerced to strict booleans here too.

If get_user_pref() returns "false"/"0" as strings, these branches treat them as truthy and can produce wrong hostname decisions during conflict actions.

💡 Proposed fix
     _TRUTHY = frozenset({"on", "true", "1"})
+    _FALSY = frozenset({"off", "false", "0", "no"})

     def _is_truthy(val):
-        return val.lower() in _TRUTHY if val is not None else False
+        if isinstance(val, bool):
+            return val
+        if val is None:
+            return False
+        return str(val).strip().lower() in _TRUTHY
+
+    def _coerce_bool(val, default: bool) -> bool:
+        if val is None:
+            return default
+        if isinstance(val, bool):
+            return val
+        lowered = str(val).strip().lower()
+        if lowered in _TRUTHY:
+            return True
+        if lowered in _FALSY:
+            return False
+        return default
...
         pref = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname")
-        if pref is not None:
-            use_sysname = pref
-        else:
-            settings = LibreNMSSettings.objects.first()
-            use_sysname = getattr(settings, "use_sysname_default", True) if settings else True
+        settings = LibreNMSSettings.objects.first()
+        use_sysname = _coerce_bool(
+            pref,
+            getattr(settings, "use_sysname_default", True) if settings else True,
+        )
...
         pref = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain")
-        if pref is not None:
-            strip_domain = pref
-        else:
-            if settings is None:
-                settings = LibreNMSSettings.objects.first()
-            strip_domain = getattr(settings, "strip_domain_default", False) if settings else False
+        if settings is None:
+            settings = LibreNMSSettings.objects.first()
+        strip_domain = _coerce_bool(
+            pref,
+            getattr(settings, "strip_domain_default", False) if settings else False,
+        )

Also applies to: 83-86

🤖 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 68 - 70, The
user preference retrieved via get_user_pref is being assigned directly to
use_sysname (and a similar variable later) which treats string values like
"false" or "0" as truthy; change the assignment to coerce pref to a strict
boolean (e.g., if pref is already a bool keep it, otherwise normalize with
str(pref).lower() in ("true","1") or similar) before setting use_sysname; update
both occurrences where get_user_pref is checked (the branches that set
use_sysname and the later identical block around lines 83-86) so the decision
logic uses the normalized boolean value.
♻️ Duplicate comments (8)
netbox_librenms_plugin/views/base/ip_addresses_view.py (1)

108-112: 🧹 Nitpick | 🔵 Trivial

Avoid calling get_librenms_device_id() twice per interface.

At Line 109 and Line 111, the same lookup runs twice for each item; compute once and reuse.

♻️ Proposed refactor
-        interfaces_by_librenms_id = {
-            get_librenms_device_id(interface, server_key): interface
-            for interface in all_interfaces
-            if get_librenms_device_id(interface, server_key)
-        }
+        interfaces_by_librenms_id = {}
+        for interface in all_interfaces:
+            mapped_id = get_librenms_device_id(interface, server_key)
+            if mapped_id:
+                interfaces_by_librenms_id[mapped_id] = interface
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/base/ip_addresses_view.py` around lines 108 -
112, The dict comprehension in interfaces_by_librenms_id repeatedly calls
get_librenms_device_id(interface, server_key) twice; compute the id once per
iteration by assigning it to a local variable (e.g., lib_id) inside the
comprehension or rewrite as a simple for-loop, then use that variable both as
the key and in the truthiness filter; update references to
get_librenms_device_id in the comprehension to use lib_id and keep the same
behavior with all_interfaces and server_key.
netbox_librenms_plugin/import_utils/filters.py (1)

41-43: ⚠️ Potential issue | 🟠 Major

Harden disabled filtering to avoid runtime crashes.

At Line 42, int(d.get("disabled", 0)) can raise for None, empty strings, or non-numeric flags and abort the count path.

🔧 Proposed fix
-    if not show_disabled:
-        devices = [d for d in devices if int(d.get("disabled", 0)) != 1]
+    if not show_disabled:
+        def _is_disabled(value) -> bool:
+            if isinstance(value, bool):
+                return value
+            if isinstance(value, (int, float)):
+                return int(value) == 1
+            return str(value).strip().lower() in {"1", "true", "yes", "on"}
+
+        devices = [d for d in devices if not _is_disabled(d.get("disabled", 0))]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/filters.py` around lines 41 - 43, The
current filter uses int(d.get("disabled", 0)) which can raise on None, empty or
non-numeric values; change the filtering to defensively parse the disabled flag
for each item in devices (when show_disabled is False) by reading
d.get("disabled", 0), converting to string, trimming and lowercasing, then
treating "1", "true", "yes" as disabled or otherwise fall back to 0 (or wrap
int(...) in try/except and treat parsing errors as not-disabled). Update the
list comprehension that builds devices (and/or add a small helper like
is_disabled(device)) so it won't raise on bad values from the "disabled" key.
netbox_librenms_plugin/tests/test_sync_view_mismatch.py (1)

331-334: ⚠️ Potential issue | 🟡 Minor

Use shared conftest.py fixtures instead of a local _make_obj helper.

Line 331 introduces the same ad-hoc builder pattern previously flagged; please switch this setup to the shared test fixtures to keep mocks consistent across the suite.

Based on learnings: "Reuse fixtures from tests/conftest.py instead of creating ad-hoc mocks..."

🤖 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 -
334, Replace the ad-hoc builder function _make_obj (which creates a MagicMock
with custom_field_data {"librenms_id": ...}) with the shared fixture provided in
tests/conftest.py; locate usages of _make_obj in test_sync_view_mismatch.py and
switch them to the appropriate conftest fixture (the one that supplies a mock
object with custom_field_data/librenms_id), removing the local _make_obj helper
and updating test parameters to accept the fixture instead.
netbox_librenms_plugin/views/base/cables_view.py (1)

86-89: 🛠️ Refactor suggestion | 🟠 Major

Centralize the server-scoped librenms_id predicate instead of repeating raw JSON lookups.

The same Q(custom_field_data__librenms_id__<server_key>) | Q(custom_field_data__librenms_id=...) logic is duplicated across multiple paths, which is prone to lookup drift.

As per coding guidelines: Use LibreNMSAPI.get_librenms_id() instead of directly accessing the librenms_id custom field when mapping Devices/VMs to LibreNMS.

Also applies to: 131-134, 142-145, 170-172, 182-184, 389-390

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/base/cables_view.py` around lines 86 - 89,
Replace the repeated raw JSON lookups for the server-scoped librenms_id with the
central helper: instead of building
Q(**{f"custom_field_data__librenms_id__{server_key}": ...}) |
Q(custom_field_data__librenms_id=...), call
LibreNMSAPI.get_librenms_id(remote_device_id, server_key) (or the equivalent
signature provided by LibreNMSAPI) to produce the predicate/value used when
querying Device/VM objects; update the Device.objects.get(...) and similar
lookups in functions around the occurrences shown (e.g., the Device query at
lines 86-89 and the other duplicated spots) to use the helper so the
server-scoped key logic is centralized and consistent.
netbox_librenms_plugin/views/imports/actions.py (1)

1203-1227: ⚠️ Potential issue | 🟠 Major

Protect legacy-ID migration with a row lock.

This path still does an unlocked read-check-write on custom_field_data; concurrent link/update actions can overwrite each other.

💡 Proposed fix
         elif action == "migrate_librenms_id":
             from netbox_librenms_plugin.utils import migrate_legacy_librenms_id
-
-            cf_value = existing_device.custom_field_data.get("librenms_id")
-            if not isinstance(cf_value, int):
-                return HttpResponse(
-                    "Device librenms_id is already in JSON format; no migration needed.",
-                    status=400,
-                )
-            if cf_value != librenms_id:
-                return HttpResponse(
-                    f"Legacy librenms_id ({cf_value}) does not match the active device ID "
-                    f"({librenms_id}); cannot migrate safely.",
-                    status=400,
-                )
-            if not validation.get("serial_confirmed") and not force:
-                return HttpResponse(
-                    "Serial number not confirmed. Check the force checkbox to migrate without serial verification.",
-                    status=400,
-                )
-            migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key)
-            if err := _save_device(existing_device):
-                return err
+            with transaction.atomic():
+                try:
+                    locked_device = Device.objects.select_for_update().get(pk=existing_device.pk)
+                except Device.DoesNotExist:
+                    return HttpResponse(
+                        "Device no longer exists; it may have been deleted concurrently.",
+                        status=409,
+                    )
+
+                cf_value = locked_device.custom_field_data.get("librenms_id")
+                if not isinstance(cf_value, int):
+                    return HttpResponse(
+                        "Device librenms_id is already in JSON format; no migration needed.",
+                        status=400,
+                    )
+                if cf_value != librenms_id:
+                    return HttpResponse(
+                        f"Legacy librenms_id ({cf_value}) does not match the active device ID "
+                        f"({librenms_id}); cannot migrate safely.",
+                        status=400,
+                    )
+                if not validation.get("serial_confirmed") and not force:
+                    return HttpResponse(
+                        "Serial number not confirmed. Check the force checkbox to migrate without serial verification.",
+                        status=400,
+                    )
+                migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key)
+                if err := _save_device(locked_device):
+                    return err
🤖 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 1203 - 1227,
The legacy-ID migration performs an unlocked read-check-write on
existing_device.custom_field_data; wrap the entire check-and-migrate sequence in
a DB transaction and acquire a row lock on the device before reading/updating
custom_field_data (use a select_for_update() fetch of the Device record for the
same PK inside a transaction.atomic block), then re-check the locked record's
custom_field_data, perform migrate_legacy_librenms_id(existing_device,
self.librenms_api.server_key) and call _save_device(existing_device) while still
holding the lock to prevent concurrent link/update races.
netbox_librenms_plugin/views/sync/device_fields.py (1)

460-467: ⚠️ Potential issue | 🟠 Major

Do not expose internal exception details in mapping-removal flash messages.

These handlers currently leak raw exception text to users; log server-side and return a generic failure message instead.

💡 Proposed fix
-                except ValidationError as exc:
-                    transaction.set_rollback(True)
-                    messages.error(request, f"Validation error removing mapping: {exc}")
+                except ValidationError:
+                    logger.exception(
+                        "Validation error removing LibreNMS mapping for server '%s' on device pk=%s",
+                        server_key,
+                        pk,
+                    )
+                    messages.error(request, "Failed to remove mapping. Please contact an administrator.")
                     return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
-                except Exception as exc:
-                    transaction.set_rollback(True)
-                    messages.error(request, f"Error removing mapping for server '{server_key}': {exc}")
+                except Exception:
+                    logger.exception(
+                        "Unexpected error removing LibreNMS mapping for server '%s' on device pk=%s",
+                        server_key,
+                        pk,
+                    )
+                    messages.error(request, "Failed to remove mapping. Please contact an administrator.")
                     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 460 - 467,
The except blocks catching ValidationError and the generic Exception should not
include raw exception text in the user-facing messages; instead, log the full
exception server-side (use logger.exception(...) or logger.error(...,
exc_info=True)) referencing the except handlers for ValidationError and the
generic Exception, then call transaction.set_rollback(True) and call
messages.error(request, "...generic failure message...") without including exc
or server_key details, and finally return the existing
redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk) as
before.
netbox_librenms_plugin/views/imports/list.py (1)

95-97: ⚠️ Potential issue | 🟠 Major

Persisted naming preferences still need strict boolean normalization.

String values (for example "false") can remain truthy and will produce wrong naming behavior and cache-key mismatches in both load and job-result paths.

💡 Proposed fix
+def _coerce_bool(value, default: bool) -> bool:
+    if value is None:
+        return default
+    if isinstance(value, bool):
+        return value
+    if isinstance(value, str):
+        return value.strip().lower() in {"1", "true", "on", "yes"}
+    return bool(value)
...
-        use_sysname = job_data.get("use_sysname", True)
-        strip_domain = job_data.get("strip_domain", False)
+        use_sysname = _coerce_bool(job_data.get("use_sysname"), True)
+        strip_domain = _coerce_bool(job_data.get("strip_domain"), False)
...
-        _use_sysname = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname")
-        _strip_domain = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain")
-        if _use_sysname is None:
-            _use_sysname = getattr(settings_obj, "use_sysname_default", True) if settings_obj else True
-        if _strip_domain is None:
-            _strip_domain = getattr(settings_obj, "strip_domain_default", False) if settings_obj else False
+        _use_sysname_pref = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname")
+        _strip_domain_pref = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain")
+        _use_sysname = _coerce_bool(
+            _use_sysname_pref,
+            getattr(settings_obj, "use_sysname_default", True) if settings_obj else True,
+        )
+        _strip_domain = _coerce_bool(
+            _strip_domain_pref,
+            getattr(settings_obj, "strip_domain_default", False) if settings_obj else False,
+        )

Also applies to: 153-159

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/list.py` around lines 95 - 97, job_data
values like use_sysname and strip_domain are read as-is and string values such
as "false" remain truthy; normalize them to strict booleans by parsing the
job_data entries (e.g., replace use_sysname = job_data.get("use_sysname", True)
and strip_domain = job_data.get("strip_domain", False) with explicit boolean
normalization such as checking type and comparing lowercased strings or using a
helper like parse_bool(value) so that "false"/"0"/"" become False and True-like
values become True); apply the same strict boolean normalization to the other
similar variables in the same file (the block around the other job_data.get
usages mentioned in the review) so cache keys and naming behavior are
consistent.
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)

146-155: ⚠️ Potential issue | 🟠 Major

Legacy default mappings are still misclassified as unconfigured.

_build_all_server_mappings() only checks servers config. In legacy config mode (librenms_url without servers.default), active default links are still marked orphaned.

💡 Proposed fix
-        plugins_cfg = django_settings.PLUGINS_CONFIG.get("netbox_librenms_plugin", {})
+        plugins_cfg = getattr(django_settings, "PLUGINS_CONFIG", {}).get("netbox_librenms_plugin", {})
         servers_config = plugins_cfg.get("servers", {})
+        legacy_url = plugins_cfg.get("librenms_url")
+        legacy_display_name = plugins_cfg.get("display_name")

         result = []
         for sk, did in cf_value.items():
             srv_cfg = servers_config.get(sk)
-            is_configured = srv_cfg is not None
-            librenms_url = srv_cfg.get("librenms_url") if srv_cfg else None
-            display_name = (srv_cfg.get("display_name") or sk) if srv_cfg else sk
+            is_legacy_default = sk == "default" and srv_cfg is None and bool(legacy_url)
+            is_configured = (srv_cfg is not None) or is_legacy_default
+            librenms_url = srv_cfg.get("librenms_url") if srv_cfg else (legacy_url if is_legacy_default else None)
+            display_name = (
+                (srv_cfg.get("display_name") or sk)
+                if srv_cfg
+                else ((legacy_display_name or sk) if is_legacy_default else sk)
+            )
             device_url = f"{librenms_url}/device/device={did}/" if librenms_url else None
🤖 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 146 -
155, The code in _build_all_server_mappings currently treats entries as
unconfigured if servers_config lacks a specific key, which misses legacy
configs; update the loop that builds srv_cfg by falling back to a legacy/default
mapping: if servers_config.get(sk) is None then try
servers_config.get("default"), and if that is also missing, try
plugins_cfg.get("librenms_url") as a legacy top-level default; use that fallback
to populate librenms_url, display_name and device_url so legacy/default mappings
are considered configured (adjust variables servers_config, plugins_cfg,
srv_cfg, librenms_url, display_name and device_url in _build_all_server_mappings
accordingly).
🤖 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/bulk_import.py`:
- Around line 375-378: The code sets match_type = "hostname" when a device is
matched via sys_name in the block that checks new_device and sys_name (where
new_device is assigned from
Model.objects.filter(name__iexact=sys_name).first()); change the assigned
match_type to a more accurate value such as "sysname" (or "name" if you prefer a
generic label) so the variable reflects the actual matching source.

In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 182-190: Replace the inline cache-key construction in
import_utils.filters (the local _hash helper and the cache_key assignment using
api.server_key, api_filters and client_filters) with a centralized helper: add a
get_import_search_cache_key(server_key, api_filters, client_filters) function in
the cache helpers module that encapsulates the hashing logic (the
sha256/json.dumps logic currently in _hash) and then call that helper from
filters to produce cache_key; ensure you stop hardcoding the
"librenms_devices_import_" format in filters and instead pass api.server_key,
api_filters and client_filters to the new get_import_search_cache_key function.

In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js`:
- Around line 1103-1108: The ensureModalVisible() routine currently initializes
tooltips twice on the same modalContent (first at lines ~1096–1100 and again at
~1104–1108), causing duplicate bootstrap.Tooltip instances; update the tooltip
initialization to a single, idempotent loop over
modalContent.querySelectorAll('[data-bs-toggle="tooltip"]') and call
bootstrap.Tooltip.getOrCreateInstance(el) for each element (refer to
modalContent and ensureModalVisible) so repeated HTMX swaps do not create
multiple tooltip instances or duplicate event listeners.

In `@netbox_librenms_plugin/tests/test_permissions.py`:
- Around line 996-1001: The tests patch `django.conf.settings` which can miss
the `settings` object imported inside the view module; update the patches to
target the view module's settings (patch
"netbox_librenms_plugin.views.sync.device_fields.settings") so the code under
test sees the mocked config. In the same with-blocks that already patch
`netbox_librenms_plugin.views.sync.device_fields.get_object_or_404`, `Device`
(mock_Device_cls), `messages`, and `redirect`, replace
patch("django.conf.settings") with
patch("netbox_librenms_plugin.views.sync.device_fields.settings") for each
occurrence to make the tests deterministic. Ensure all three occurrences in this
test are updated.

In `@netbox_librenms_plugin/views/object_sync/vms.py`:
- Around line 48-50: The returned table view needs the current request bound
before using server-scoped behavior (self.librenms_api.server_key): in
get_interface_context(), instantiate the concrete view (LibreNMSVMInterfaceTable
or VMInterfaceTableView instance), assign its request attribute (e.g.,
interface_sync_view.request = request), then set any required properties
(server_key from self.librenms_api) and call
interface_sync_view.get_context_data() (or the view's equivalent) to produce the
context to return; update the return path to return that context rather than
directly constructing the table without binding request.

---

Outside diff comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 68-70: The user preference retrieved via get_user_pref is being
assigned directly to use_sysname (and a similar variable later) which treats
string values like "false" or "0" as truthy; change the assignment to coerce
pref to a strict boolean (e.g., if pref is already a bool keep it, otherwise
normalize with str(pref).lower() in ("true","1") or similar) before setting
use_sysname; update both occurrences where get_user_pref is checked (the
branches that set use_sysname and the later identical block around lines 83-86)
so the decision logic uses the normalized boolean value.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 41-43: The current filter uses int(d.get("disabled", 0)) which can
raise on None, empty or non-numeric values; change the filtering to defensively
parse the disabled flag for each item in devices (when show_disabled is False)
by reading d.get("disabled", 0), converting to string, trimming and lowercasing,
then treating "1", "true", "yes" as disabled or otherwise fall back to 0 (or
wrap int(...) in try/except and treat parsing errors as not-disabled). Update
the list comprehension that builds devices (and/or add a small helper like
is_disabled(device)) so it won't raise on bad values from the "disabled" key.

In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py`:
- Around line 331-334: Replace the ad-hoc builder function _make_obj (which
creates a MagicMock with custom_field_data {"librenms_id": ...}) with the shared
fixture provided in tests/conftest.py; locate usages of _make_obj in
test_sync_view_mismatch.py and switch them to the appropriate conftest fixture
(the one that supplies a mock object with custom_field_data/librenms_id),
removing the local _make_obj helper and updating test parameters to accept the
fixture instead.

In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 86-89: Replace the repeated raw JSON lookups for the server-scoped
librenms_id with the central helper: instead of building
Q(**{f"custom_field_data__librenms_id__{server_key}": ...}) |
Q(custom_field_data__librenms_id=...), call
LibreNMSAPI.get_librenms_id(remote_device_id, server_key) (or the equivalent
signature provided by LibreNMSAPI) to produce the predicate/value used when
querying Device/VM objects; update the Device.objects.get(...) and similar
lookups in functions around the occurrences shown (e.g., the Device query at
lines 86-89 and the other duplicated spots) to use the helper so the
server-scoped key logic is centralized and consistent.

In `@netbox_librenms_plugin/views/base/ip_addresses_view.py`:
- Around line 108-112: The dict comprehension in interfaces_by_librenms_id
repeatedly calls get_librenms_device_id(interface, server_key) twice; compute
the id once per iteration by assigning it to a local variable (e.g., lib_id)
inside the comprehension or rewrite as a simple for-loop, then use that variable
both as the key and in the truthiness filter; update references to
get_librenms_device_id in the comprehension to use lib_id and keep the same
behavior with all_interfaces and server_key.

In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 146-155: The code in _build_all_server_mappings currently treats
entries as unconfigured if servers_config lacks a specific key, which misses
legacy configs; update the loop that builds srv_cfg by falling back to a
legacy/default mapping: if servers_config.get(sk) is None then try
servers_config.get("default"), and if that is also missing, try
plugins_cfg.get("librenms_url") as a legacy top-level default; use that fallback
to populate librenms_url, display_name and device_url so legacy/default mappings
are considered configured (adjust variables servers_config, plugins_cfg,
srv_cfg, librenms_url, display_name and device_url in _build_all_server_mappings
accordingly).

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1203-1227: The legacy-ID migration performs an unlocked
read-check-write on existing_device.custom_field_data; wrap the entire
check-and-migrate sequence in a DB transaction and acquire a row lock on the
device before reading/updating custom_field_data (use a select_for_update()
fetch of the Device record for the same PK inside a transaction.atomic block),
then re-check the locked record's custom_field_data, perform
migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key) and
call _save_device(existing_device) while still holding the lock to prevent
concurrent link/update races.

In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 95-97: job_data values like use_sysname and strip_domain are read
as-is and string values such as "false" remain truthy; normalize them to strict
booleans by parsing the job_data entries (e.g., replace use_sysname =
job_data.get("use_sysname", True) and strip_domain =
job_data.get("strip_domain", False) with explicit boolean normalization such as
checking type and comparing lowercased strings or using a helper like
parse_bool(value) so that "false"/"0"/"" become False and True-like values
become True); apply the same strict boolean normalization to the other similar
variables in the same file (the block around the other job_data.get usages
mentioned in the review) so cache keys and naming behavior are consistent.

In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 460-467: The except blocks catching ValidationError and the
generic Exception should not include raw exception text in the user-facing
messages; instead, log the full exception server-side (use logger.exception(...)
or logger.error(..., exc_info=True)) referencing the except handlers for
ValidationError and the generic Exception, then call
transaction.set_rollback(True) and call messages.error(request, "...generic
failure message...") without including exc or server_key details, and finally
return the existing
redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk) as
before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5d6a8db4-820e-4e31-8f12-51e5facdba5e

📥 Commits

Reviewing files that changed from the base of the PR and between 7a25806 and 27eac04.

📒 Files selected for processing (30)
  • netbox_librenms_plugin/forms.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/jobs.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/test_background_jobs.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/tests/test_sync_view_mismatch.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/views/object_sync/devices.py
  • netbox_librenms_plugin/views/object_sync/vms.py
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/sync/interfaces.py

Comment thread netbox_librenms_plugin/import_utils/bulk_import.py Outdated
Comment thread netbox_librenms_plugin/import_utils/filters.py
Comment thread netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js Outdated
Comment on lines +996 to +1001
with (
patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device),
patch("netbox_librenms_plugin.views.sync.device_fields.Device") as mock_Device_cls,
patch("django.conf.settings") as mock_settings,
patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_messages,
patch("netbox_librenms_plugin.views.sync.device_fields.redirect"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Patch settings at the view module source to make these tests deterministic.

At Line 999, Line 1030, and Line 1059, patching django.conf.settings can miss the object actually referenced inside netbox_librenms_plugin.views.sync.device_fields, causing config-dependent flakiness.

🛠️ Suggested fix
-            patch("django.conf.settings") as mock_settings,
+            patch("netbox_librenms_plugin.views.sync.device_fields.settings") as mock_settings,
...
-            patch("django.conf.settings") as mock_settings,
+            patch("netbox_librenms_plugin.views.sync.device_fields.settings") as mock_settings,
...
-            patch("django.conf.settings") as mock_settings,
+            patch("netbox_librenms_plugin.views.sync.device_fields.settings") as mock_settings,

Based on learnings: "Patch deferred/inline imports at their source module (e.g., netbox_librenms_plugin.import_utils.process_device_filters), not the consuming module."

Also applies to: 1028-1033, 1056-1061

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/tests/test_permissions.py` around lines 996 - 1001,
The tests patch `django.conf.settings` which can miss the `settings` object
imported inside the view module; update the patches to target the view module's
settings (patch "netbox_librenms_plugin.views.sync.device_fields.settings") so
the code under test sees the mocked config. In the same with-blocks that already
patch `netbox_librenms_plugin.views.sync.device_fields.get_object_or_404`,
`Device` (mock_Device_cls), `messages`, and `redirect`, replace
patch("django.conf.settings") with
patch("netbox_librenms_plugin.views.sync.device_fields.settings") for each
occurrence to make the tests deterministic. Ensure all three occurrences in this
test are updated.

Comment on lines +48 to +50
return LibreNMSVMInterfaceTable(
data, device=obj, vlan_groups=vlan_groups, server_key=self.librenms_api.server_key
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bind request to VMInterfaceTableView before using self.librenms_api.server_key.

Line 49 now depends on self.librenms_api; ensure get_interface_context() sets interface_sync_view.request = request so server-scoped behavior remains correct.

🛠️ Suggested fix
 def get_interface_context(self, request, obj):
     """Return interface sync context for the virtual machine."""
     interface_name_field = get_interface_name_field(request)
     interface_sync_view = VMInterfaceTableView()
+    interface_sync_view.request = request
     return interface_sync_view.get_context_data(request, obj, interface_name_field)

As per coding guidelines **/views/object_sync/**/*.py: Object sync view methods must create instances of concrete table views, copy the request object, and call get_context_data().

🤖 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 48 - 50, The
returned table view needs the current request bound before using server-scoped
behavior (self.librenms_api.server_key): in get_interface_context(), instantiate
the concrete view (LibreNMSVMInterfaceTable or VMInterfaceTableView instance),
assign its request attribute (e.g., interface_sync_view.request = request), then
set any required properties (server_key from self.librenms_api) and call
interface_sync_view.get_context_data() (or the view's equivalent) to produce the
context to return; update the return path to return that context rather than
directly constructing the table without binding request.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
netbox_librenms_plugin/import_utils/bulk_import.py (2)

312-316: ⚠️ Potential issue | 🟡 Minor

Preserve the device_role schema when refreshing cached validation.

Line [315] replaces validation["device_role"] with a partial object, which can drop keys expected downstream (for example, available_roles).

Proposed fix
             if refreshed:
                 validation["existing_device"] = refreshed
-                if hasattr(refreshed, "role") and refreshed.role:
-                    validation["device_role"] = {"found": True, "role": refreshed.role}
+                if hasattr(refreshed, "role"):
+                    role_state = validation.get("device_role")
+                    if not isinstance(role_state, dict):
+                        role_state = {}
+                        validation["device_role"] = role_state
+                    role_state["found"] = bool(refreshed.role)
+                    role_state["role"] = refreshed.role
+                    role_state.setdefault("available_roles", [])

Based on learnings: Centralize validation state mutation during import using import_validation_helpers.py for role/cluster/rack assignment, issue removal, and status recalculation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 312 - 316,
When refreshing cached validation in bulk_import.py, don't overwrite the entire
validation["device_role"] dict (which can remove keys like "available_roles");
instead ensure validation.setdefault("device_role", {}) exists and only update
the specific fields (e.g., set "found" and "role" from refreshed) rather than
replacing the object. Locate the block that checks refreshed (the refreshed
variable and validation dict) and change it to preserve existing keys, or
better, delegate role/cluster/rack assignment and validation mutations to the
centralized helpers in import_validation_helpers.py so all mutations remain
consistent.

442-447: ⚠️ Potential issue | 🟠 Major

Make disabled filtering tolerant to non-numeric API values.

Line [446] can raise TypeError/ValueError when disabled is missing or non-numeric (None, "", "-"), which aborts filter processing.

Proposed fix
-    if not show_disabled:
-        libre_devices = [d for d in libre_devices if int(d.get("disabled", 0)) != 1]
+    def _is_disabled(value) -> bool:
+        if isinstance(value, bool):
+            return value
+        if isinstance(value, str):
+            v = value.strip().lower()
+            if v in {"1", "true", "yes", "on"}:
+                return True
+            if v in {"0", "false", "no", "off", "", "-"}:
+                return False
+        try:
+            return int(value) == 1
+        except (TypeError, ValueError):
+            return False
+
+    if not show_disabled:
+        libre_devices = [d for d in libre_devices if not _is_disabled(d.get("disabled", 0))]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 442 - 447,
The filter for disabled devices in bulk_import.py can raise TypeError/ValueError
when a device's "disabled" value is None or non-numeric; update the list
comprehension that builds libre_devices (the expression using
int(d.get("disabled", 0)) != 1) to perform a safe numeric conversion: retrieve
d.get("disabled"), attempt to coerce to int in a small try/except (or treat
non-digit strings as 0) and then compare to 1, so non-numeric/missing values are
treated as not-disabled instead of raising an exception.
netbox_librenms_plugin/views/imports/actions.py (1)

1197-1227: ⚠️ Potential issue | 🟠 Major

Protect legacy-ID migration with row locking.

Lines [1206]-[1227] still run a read-check-write sequence on custom_field_data without select_for_update(). Concurrent requests can race and overwrite migration state.

💡 Proposed fix
         elif action == "migrate_librenms_id":
@@
-            cf_value = existing_device.custom_field_data.get("librenms_id")
-            if not isinstance(cf_value, int):
-                return HttpResponse(
-                    "Device librenms_id is already in JSON format; no migration needed.",
-                    status=400,
-                )
-            # Verify the stored legacy ID matches the active LibreNMS device_id so we don't
-            # migrate a stale/incorrect association to the wrong server mapping.
-            if cf_value != librenms_id:
-                return HttpResponse(
-                    f"Legacy librenms_id ({cf_value}) does not match the active device ID "
-                    f"({librenms_id}); cannot migrate safely.",
-                    status=400,
-                )
-            if not validation.get("serial_confirmed") and not force:
-                return HttpResponse(
-                    "Serial number not confirmed. Check the force checkbox to migrate without serial verification.",
-                    status=400,
-                )
-            migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key)
-            if err := _save_device(existing_device):
-                return err
+            with transaction.atomic():
+                try:
+                    locked_device = Device.objects.select_for_update().get(pk=existing_device.pk)
+                except Device.DoesNotExist:
+                    return HttpResponse(
+                        "Device no longer exists; it may have been deleted concurrently.",
+                        status=409,
+                    )
+
+                cf_value = locked_device.custom_field_data.get("librenms_id")
+                if not isinstance(cf_value, int):
+                    return HttpResponse(
+                        "Device librenms_id is already in JSON format; no migration needed.",
+                        status=400,
+                    )
+                if cf_value != librenms_id:
+                    return HttpResponse(
+                        f"Legacy librenms_id ({cf_value}) does not match the active device ID "
+                        f"({librenms_id}); cannot migrate safely.",
+                        status=400,
+                    )
+                if not validation.get("serial_confirmed") and not force:
+                    return HttpResponse(
+                        "Serial number not confirmed. Check the force checkbox to migrate without serial verification.",
+                        status=400,
+                    )
+
+                migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key)
+                if err := _save_device(locked_device):
+                    return err
             logger.info(
-                f"Migrated legacy librenms_id on '{existing_device.name}' "
+                f"Migrated legacy librenms_id on '{locked_device.name}' "
                 f"to {{{self.librenms_api.server_key!r}: {cf_value}}}"
             )
🤖 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 1197 - 1227,
The migration path for action "migrate_librenms_id" performs a read-check-write
on existing_device.custom_field_data without a DB row lock; wrap the logic that
reads cf_value, validates cf_value==librenms_id, checks serial_confirmed/force,
calls migrate_legacy_librenms_id(existing_device, ...) and
_save_device(existing_device) inside a transaction.atomic() block and acquire a
select_for_update() lock on the device row (e.g., re-query the Device with
.select_for_update() into existing_device_locked) before reading or mutating
custom_field_data to prevent concurrent races and ensure the migration is
applied atomically.
netbox_librenms_plugin/utils.py (1)

482-505: ⚠️ Potential issue | 🟠 Major

Enforce integer normalization in LibreNMS ID write/read helpers.

Line [503] still persists device_id as-is, and Lines [524]-[525] query using unnormalized input. Mixed str/int values will miss matches and can produce duplicate associations.

💡 Proposed fix
 def set_librenms_device_id(obj, device_id, server_key: str = "default"):
@@
-    cf_value[server_key] = device_id
+    try:
+        normalized_device_id = int(device_id)
+    except (TypeError, ValueError):
+        raise ValueError(f"Invalid LibreNMS device_id: {device_id!r}") from None
+
+    cf_value[server_key] = normalized_device_id
     obj.custom_field_data["librenms_id"] = 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)
+    try:
+        normalized_id = int(librenms_id)
+    except (TypeError, ValueError):
+        return None
+
+    return model.objects.filter(
+        Q(**{f"custom_field_data__librenms_id__{server_key}": normalized_id})
+        | Q(custom_field_data__librenms_id=normalized_id)
     ).first()

Also applies to: 507-526

🤖 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 482 - 505,
set_librenms_device_id and the corresponding read helpers are storing and
comparing device IDs without normalizing types, causing mismatches between
str/int values; cast/normalize device_id to an int on write (in
set_librenms_device_id) before placing it into
obj.custom_field_data["librenms_id"] (handle/ log ValueError if conversion
fails) and update the read helper(s) (the functions that look up
cf_value[server_key] around the 507-526 region, e.g., get_librenms_device_id or
similar) to also normalize retrieved values to int (or safely compare by
converting both sides to int) so stored and queried IDs always use the same
integer type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 312-316: When refreshing cached validation in bulk_import.py,
don't overwrite the entire validation["device_role"] dict (which can remove keys
like "available_roles"); instead ensure validation.setdefault("device_role", {})
exists and only update the specific fields (e.g., set "found" and "role" from
refreshed) rather than replacing the object. Locate the block that checks
refreshed (the refreshed variable and validation dict) and change it to preserve
existing keys, or better, delegate role/cluster/rack assignment and validation
mutations to the centralized helpers in import_validation_helpers.py so all
mutations remain consistent.
- Around line 442-447: The filter for disabled devices in bulk_import.py can
raise TypeError/ValueError when a device's "disabled" value is None or
non-numeric; update the list comprehension that builds libre_devices (the
expression using int(d.get("disabled", 0)) != 1) to perform a safe numeric
conversion: retrieve d.get("disabled"), attempt to coerce to int in a small
try/except (or treat non-digit strings as 0) and then compare to 1, so
non-numeric/missing values are treated as not-disabled instead of raising an
exception.

In `@netbox_librenms_plugin/utils.py`:
- Around line 482-505: set_librenms_device_id and the corresponding read helpers
are storing and comparing device IDs without normalizing types, causing
mismatches between str/int values; cast/normalize device_id to an int on write
(in set_librenms_device_id) before placing it into
obj.custom_field_data["librenms_id"] (handle/ log ValueError if conversion
fails) and update the read helper(s) (the functions that look up
cf_value[server_key] around the 507-526 region, e.g., get_librenms_device_id or
similar) to also normalize retrieved values to int (or safely compare by
converting both sides to int) so stored and queried IDs always use the same
integer type.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1197-1227: The migration path for action "migrate_librenms_id"
performs a read-check-write on existing_device.custom_field_data without a DB
row lock; wrap the logic that reads cf_value, validates cf_value==librenms_id,
checks serial_confirmed/force, calls migrate_legacy_librenms_id(existing_device,
...) and _save_device(existing_device) inside a transaction.atomic() block and
acquire a select_for_update() lock on the device row (e.g., re-query the Device
with .select_for_update() into existing_device_locked) before reading or
mutating custom_field_data to prevent concurrent races and ensure the migration
is applied atomically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: cf5dc356-cf1a-47b9-908d-97498b0779d9

📥 Commits

Reviewing files that changed from the base of the PR and between 27eac04 and 1656b73.

📒 Files selected for processing (3)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
netbox_librenms_plugin/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

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
Use LibreNMSAPI.get_librenms_id() instead of directly accessing the librenms_id custom field when mapping Devices/VMs to LibreNMS
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
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)
Centralize validation state mutation during import using import_validation_helpers.py for role/cluster/rack assignment, issue removal, and status recalculation
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

Files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.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 in views/base/, Object sync views in views/object_sync/, and Sync action views in views/sync/ with shared mixins from views/mixins.py
New views must extend the closest base class and compose mixins from views/mixins.py (LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, CacheMixin, VlanAssignmentMixin)
All views must inherit LibreNMSPermissionMixin from views/mixins.py with permission_required = PERM_VIEW_PLUGIN
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
Use _get_safe_redirect_url(request) to validate referrer URLs in permission checks to prevent open-redirect attacks

Files:

  • netbox_librenms_plugin/views/imports/actions.py
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Job cancellation flow: (1) Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job, (2) Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database, (3) Frontend polling detects status changes and redirects appropriately
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status, update modal messages based on status transitions (queued, started, finished, stopped, failed), handle all RQ status values explicitly to avoid infinite polling, and use cancelInProgress flag to prevent polling interference during cancellation
NetBox's /api/core/background-tasks/ endpoint requires superuser (IsSuperuser in BaseRQViewSet); non-superuser users cannot poll job status and get 403 Forbidden. The plugin must automatically fall back to synchronous mode for non-superusers via should_use_background_job() in list.py and actions.py
Custom sync endpoint api/views.py::sync_job_status() must sync database Job status with RQ job status, needed because NetBox worker doesn't always update DB when jobs stop before processing starts
Import page supports synchronous mode (calls process_device_filters() directly, renders results inline) and background mode (enqueues FilterDevicesJob, returns JsonResponse with job_id/job_pk/poll_url, frontend polls and redirects on completion)
Result loading via _load_job_results(job_id) must read job.data['device_ids'] and reconstruct devices from per-device cache using get_validated_device_cache_key()
Import filter fields must include: librenms_location, librenms_type, librenms_os, librenms_hostname, librenms_sysname, librenms_hardware, enable_vc_detection, show_disabled, exclude_existing
DeviceImportHelperMixin must provide get_validated_device_with_selections() and render_device_row() for HTMX row rendering, shared by update views
BulkImportConfirmView (POST) must render confirmation modal with selected device...

Files:

  • netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (38)
📓 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 : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
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
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`).
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/**/*.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/import_utils/bulk_import.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.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/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.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/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.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/** : Job cancellation flow: (1) Call `/api/core/background-tasks/{uuid}/stop/` to stop RQ job, (2) Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database, (3) Frontend polling detects status changes and redirects appropriately

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.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,**/import_utils.py : Both synchronous and background modes must use `get_validated_device_cache_key()` from `import_utils.py` to generate cache keys, ensuring `_load_job_results()` in the list view can retrieve devices regardless of which mode produced them. Never hardcode cache key formats; always use the helper functions

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/** : Result loading via `_load_job_results(job_id)` must read `job.data['device_ids']` and reconstruct devices from per-device cache using `get_validated_device_cache_key()`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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 : Background jobs must use standalone helpers from `import_utils.py` (`check_user_permissions`, `require_permissions`) instead of view mixins; non-superusers fall back to synchronous mode

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.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/bulk_import.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 `pollJobStatus()` function polls `/api/core/background-tasks/{jobId}/` every 2s, updates progress messages, handles cancel button, and redirects on completion.

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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_validation_helpers.py : Validation state mutation helpers in `import_validation_helpers.py` must include: `apply_role_to_validation()`, `apply_cluster_to_validation()`, `apply_rack_to_validation()` to update validation state when user selects role/cluster/rack

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/import_utils/bulk_import.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/import_utils/bulk_import.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.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/bulk_import.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 : `fetch_device_with_cache(device_id, ...)` must retrieve and cache individual device data

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/import_utils/bulk_import.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.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/import_utils/bulk_import.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.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/bulk_import.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/bulk_import.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 : `FilterDevicesJob` background job: filters devices with VC detection. `job.data` keys must include `device_ids`, `total_processed`, `filters`, `server_key`, `vc_detection_enabled`, `cache_timeout`, `cached_at`, `completed`. Devices are cached individually via shared cache keys from `get_validated_device_cache_key()`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/** : Import page supports synchronous mode (calls `process_device_filters()` directly, renders results inline) and background mode (enqueues `FilterDevicesJob`, returns `JsonResponse` with `job_id`/`job_pk`/`poll_url`, frontend polls and redirects on completion)

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.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.py
  • netbox_librenms_plugin/views/imports/actions.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/utils.py
  • netbox_librenms_plugin/views/imports/actions.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/utils.py
  • netbox_librenms_plugin/views/imports/actions.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/views/imports/actions.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/*.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/views/imports/actions.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/imports/actions.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/imports/actions.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/imports/actions.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/imports/actions.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/imports/actions.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/imports/actions.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/imports/actions.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/** : `DeviceImportHelperMixin` must provide `get_validated_device_with_selections()` and `render_device_row()` for HTMX row rendering, shared by update views

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.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_validation_helpers.py : Validation helpers must include: `fetch_model_by_id()`, `extract_device_selections()` for reading form data

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.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/imports/actions.py
🔇 Additional comments (3)
netbox_librenms_plugin/import_utils/bulk_import.py (1)

532-539: Good cache-key scoping for naming preferences.

Including use_sysname and strip_domain in both validated-device keys and metadata keys correctly prevents cache collisions across different naming modes.

Also applies to: 609-615

netbox_librenms_plugin/utils.py (1)

529-559: Good separation of migration vs persistence responsibility.

migrate_legacy_librenms_id() mutates in-memory state and leaves persistence to callers; that keeps transaction boundaries explicit.

netbox_librenms_plugin/views/imports/actions.py (1)

204-212: Good fix: server_key is now threaded through validation paths.

This closes cross-server ambiguity during import validation and post-import row revalidation.

Also applies to: 352-359, 721-729

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)

146-155: ⚠️ Potential issue | 🟠 Major

Legacy default mapping is misclassified as unconfigured.

When using the legacy single-server configuration (where librenms_url is set at the plugin config root level rather than under servers), a default key in the librenms_id custom field will have srv_cfg = None and thus is_configured = False. This is incorrect because the legacy config is still an active, valid configuration.

Per LibreNMSAPI.__init__, the legacy fallback reads librenms_url directly from plugins_cfg, not from servers_config. The method should check for this case when sk == "default" and srv_cfg is None.

,

🔧 Proposed fix
         plugins_cfg = django_settings.PLUGINS_CONFIG.get("netbox_librenms_plugin", {})
         servers_config = plugins_cfg.get("servers", {})
+        legacy_url = plugins_cfg.get("librenms_url")

         result = []
         for sk, did in cf_value.items():
             srv_cfg = servers_config.get(sk)
-            is_configured = srv_cfg is not None
-            librenms_url = srv_cfg.get("librenms_url") if srv_cfg else None
-            display_name = (srv_cfg.get("display_name") or sk) if srv_cfg else sk
+            is_legacy_default = sk == "default" and not srv_cfg and bool(legacy_url)
+            is_configured = (srv_cfg is not None) or is_legacy_default
+            librenms_url = srv_cfg.get("librenms_url") if srv_cfg else (legacy_url if is_legacy_default else None)
+            display_name = (srv_cfg.get("display_name") or sk) if srv_cfg else sk
             device_url = f"{librenms_url}/device/device={did}/" if librenms_url else None
🤖 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 146 -
155, The loop that builds per-server data treats a legacy "default" cf_value key
as unconfigured because srv_cfg is None; update the loop in the block using
plugins_cfg, servers_config and cf_value so that when sk == "default" and
srv_cfg is None you read librenms_url and display_name from plugins_cfg (the
legacy root-level keys), set is_configured = True, and compute
device_url/display_name from those legacy values (mirroring LibreNMSAPI.__init__
fallback behavior) so the legacy single-server config is handled correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 146-155: The loop that builds per-server data treats a legacy
"default" cf_value key as unconfigured because srv_cfg is None; update the loop
in the block using plugins_cfg, servers_config and cf_value so that when sk ==
"default" and srv_cfg is None you read librenms_url and display_name from
plugins_cfg (the legacy root-level keys), set is_configured = True, and compute
device_url/display_name from those legacy values (mirroring LibreNMSAPI.__init__
fallback behavior) so the legacy single-server config is handled correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9145d377-7abf-43ae-a785-3438b2216dff

📥 Commits

Reviewing files that changed from the base of the PR and between 1656b73 and a312799.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.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 (3)
netbox_librenms_plugin/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

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
Use LibreNMSAPI.get_librenms_id() instead of directly accessing the librenms_id custom field when mapping Devices/VMs to LibreNMS
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
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)
Centralize validation state mutation during import using import_validation_helpers.py for role/cluster/rack assignment, issue removal, and status recalculation
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

Files:

  • netbox_librenms_plugin/views/base/librenms_sync_view.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 in views/base/, Object sync views in views/object_sync/, and Sync action views in views/sync/ with shared mixins from views/mixins.py
New views must extend the closest base class and compose mixins from views/mixins.py (LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, CacheMixin, VlanAssignmentMixin)
All views must inherit LibreNMSPermissionMixin from views/mixins.py with permission_required = PERM_VIEW_PLUGIN
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
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.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 with CacheMixin keys like librenms_{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.
VlanAssignmentMixin must 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 (10)
📚 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/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/**/*.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.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.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.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/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/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/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/**/*.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/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 : 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/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: 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/views/base/librenms_sync_view.py
🔇 Additional comments (3)
netbox_librenms_plugin/views/base/librenms_sync_view.py (3)

3-3: LGTM!

Clean import with aliasing to avoid naming conflicts with local settings objects.


118-118: LGTM!

Correctly passes the NetBox object and active server key to the new method. Per LibreNMSAPI.__init__, server_key is guaranteed to be non-empty.


156-170: LGTM!

The mapping structure, sorting logic, and return value handling are all correct. The sort order (active → configured → orphaned) provides intuitive UI display. Direct access to custom_field_data is appropriate here since this method needs to enumerate all server mappings, not just the active one.

- Store librenms_id as {server_key: device_id} dict instead of bare int
- Add get_librenms_device_id/set_librenms_device_id/find_by_librenms_id/
  migrate_legacy_librenms_id helpers in utils.py (at end of file)
- Thread server_key through import pipeline (filters, cache, bulk_import,
  device_operations, vm_operations, virtual_chassis)
- Deterministic SHA256 cache keys, None-safe filter inclusion
- Fix disabled field filter (use 'disabled' flag, not 'status')
- Truthy string parsing for use_sysname/strip_domain ('on'/'true'/'1')
- migrate_librenms_id action in DeviceConflictActionView
- RemoveServerMappingView for per-server librenms_id removal
- New tests: test_permissions.py, test_sync_view_mismatch.py
- RQ-based job cancellation check in bulk import

Reduce cosmetic diff vs develop:
- _save_device restored to before _resolve_naming_preferences in actions.py
- _empty_return moved to just before process_device_filters in bulk_import.py
- New utils.py helpers at end of file with imports merged into main block
@marcinpsk
marcinpsk force-pushed the refactor/librenms_id-rebased branch from a312799 to 62f838c Compare March 4, 2026 13:49
- librenms_sync_view: fall back to legacy root-level librenms_url when
  server_key=="default" and no matching servers config entry exists,
  so single-server legacy configs are shown as configured
- bulk_import: preserve existing device_role dict keys on refresh using
  .setdefault().update() instead of full replacement
- bulk_import/filters: replace int(d.get("disabled",0)) with _safe_disabled()
  helper to tolerate None/non-numeric values without raising TypeError
- bulk_import: fix match_type for sysname-based re-check (was "hostname",
  now "sysname") to reflect the actual matching source
- utils: cast device_id to int in set_librenms_device_id so stored values
  always use integer type, preventing str/int comparison mismatches
- actions: wrap migrate_librenms_id in transaction.atomic() +
  select_for_update() to prevent concurrent race on the same device
- librenms_import.js: deduplicate tooltip initialization in ensureModalVisible
  using bootstrap.Tooltip.getOrCreateInstance() instead of new Tooltip()
- ip_addresses_view: compute get_librenms_device_id() once per iteration
  instead of calling it twice in the dict comprehension
- device_fields: log exception server-side and show a generic error message
  instead of exposing exc details and server_key to the user
- cache/filters: extract _hash + cache_key construction into
  get_import_search_cache_key() in cache.py; filters.py now imports and
  uses the helper, removing duplicate hashing logic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (1)
netbox_librenms_plugin/views/imports/actions.py (1)

83-85: ⚠️ Potential issue | 🟠 Major

Normalize persisted preference fallbacks in _resolve_naming_preferences().

POST/GET values are normalized, but stored prefs are still used raw. If a stored value is "false"/"0", it is truthy and can corrupt naming resolution and cache behavior.

Proposed fix
-    def _is_truthy(val):
-        return val.lower() in _TRUTHY if val is not None else False
+    def _is_truthy(val):
+        if val is None:
+            return False
+        if isinstance(val, bool):
+            return val
+        return str(val).strip().lower() in _TRUTHY
+
+    def _coerce_bool(val, default: bool) -> bool:
+        if val is None:
+            return default
+        return _is_truthy(val)
...
         pref = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname")
         if pref is not None:
-            use_sysname = pref
+            use_sysname = _coerce_bool(pref, True)
         else:
             settings = LibreNMSSettings.objects.first()
             use_sysname = getattr(settings, "use_sysname_default", True) if settings else True
...
         pref = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain")
         if pref is not None:
-            strip_domain = pref
+            strip_domain = _coerce_bool(pref, False)
         else:
             if settings is None:
                 settings = LibreNMSSettings.objects.first()

Also applies to: 98-100

🤖 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 83 - 85, The
stored user prefs retrieved via get_user_pref in _resolve_naming_preferences
(e.g., when assigning use_sysname and prefer_name) are used raw and can be
truthy strings like "false" or "0"; normalize those persisted values the same
way POST/GET inputs are normalized (convert string booleans/"0"/"1" to actual
booleans or apply the same normalization helper used elsewhere) before assigning
to use_sysname and prefer_name so cached naming resolution behaves correctly;
update the assignments around get_user_pref(...) to pass the retrieved value
through the existing normalization routine (or implement a small normalize_pref
utility) rather than assigning raw pref.
♻️ Duplicate comments (11)
netbox_librenms_plugin/import_utils/filters.py (1)

15-21: ⚠️ Potential issue | 🟡 Minor

_safe_disabled() still misclassifies truthy string values.

Line [20]-Line [21] maps non-numeric values to 0, so "true", "yes", or "on" are treated as enabled and can bypass show_disabled=False.

🔧 Proposed fix
 def _safe_disabled(device: dict) -> int:
     """Return 1 if the device is disabled, 0 otherwise. Tolerates None/non-numeric values."""
     val = device.get("disabled", 0)
-    try:
-        return int(val)
-    except (TypeError, ValueError):
-        return 0
+    if isinstance(val, bool):
+        return 1 if val else 0
+    if isinstance(val, (int, float)):
+        return 1 if int(val) == 1 else 0
+    text = str(val).strip().lower()
+    return 1 if text in {"1", "true", "yes", "on"} else 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/filters.py` around lines 15 - 21,
_safe_disabled currently treats non-numeric truthy strings as 0, causing values
like "true"/"yes"/"on" to be misclassified as enabled; update _safe_disabled to
normalize string inputs from device.get("disabled", 0) (case-insensitive) and
return 1 for common truthy tokens ("true","yes","on","1") and 0 for falsy tokens
("false","no","off","0"), while still attempting int conversion and catching
TypeError/ValueError to default to 0 — keep the function name _safe_disabled and
its contract (returns int 0/1).
netbox_librenms_plugin/import_utils/bulk_import.py (1)

24-30: ⚠️ Potential issue | 🟡 Minor

Use the same tolerant disabled parsing here as well.

This helper has the same gap as the filter module: non-numeric truthy values (e.g., "true", "yes") become 0, which can incorrectly include disabled devices.

🔧 Proposed fix
 def _safe_disabled(device: dict) -> int:
     """Return 1 if the device is disabled, 0 otherwise. Tolerates None/non-numeric values."""
     val = device.get("disabled", 0)
-    try:
-        return int(val)
-    except (TypeError, ValueError):
-        return 0
+    if isinstance(val, bool):
+        return 1 if val else 0
+    if isinstance(val, (int, float)):
+        return 1 if int(val) == 1 else 0
+    text = str(val).strip().lower()
+    return 1 if text in {"1", "true", "yes", "on"} else 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 24 - 30, The
_safe_disabled helper currently converts non-numeric truthy strings to 0; update
_safe_disabled(device: dict) to be tolerant of boolean-like values by checking
the retrieved val = device.get("disabled", 0) for booleans and common truthy
strings before attempting int(): if val is a bool return int(val), if it's a str
normalize (.strip().lower()) and return 1 for values like "true","yes","on" (and
treat "1") and 0 for the rest, then fall back to the existing int() try/except
to handle numeric strings and other types; touch the _safe_disabled function
only.
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)

35-39: ⚠️ Potential issue | 🟠 Major

Keep VC member positions strictly 1-based end-to-end.

Line [315] still adds +1 to positions that are already 1-based, so suggested names can shift (e.g., position 1 renders as member 2). Also, Line [37] and Line [205] allow parsed 0/negative positions through unchanged.

🔧 Proposed fix
@@
-        raw_position = member_copy.get("position", idx + 1)
-        try:
-            member_copy["position"] = int(raw_position)
-        except (TypeError, ValueError):
-            member_copy["position"] = idx + 1  # 1-based fallback; position 0 is invalid
+        raw_position = member_copy.get("position", idx + 1)
+        try:
+            parsed_position = int(raw_position)
+        except (TypeError, ValueError):
+            parsed_position = idx + 1
+        member_copy["position"] = parsed_position if parsed_position >= 1 else idx + 1
@@
-            raw_position = chassis.get("entPhysicalParentRelPos", idx + 1)
-            try:
-                position = int(raw_position)
-            except (TypeError, ValueError):
-                position = idx + 1
+            raw_position = chassis.get("entPhysicalParentRelPos", idx + 1)
+            try:
+                parsed_position = int(raw_position)
+            except (TypeError, ValueError):
+                parsed_position = idx + 1
+            position = parsed_position if parsed_position >= 1 else idx + 1
@@
-    for idx, member in enumerate(vc_data.get("members", [])):
-        raw_position = member.get("position", idx)
-        try:
-            base_position = int(raw_position)
-        except (TypeError, ValueError):
-            base_position = idx
-        position = base_position + 1  # Convert to 1-based position
-        member["position"] = base_position
+    for idx, member in enumerate(vc_data.get("members", []), start=1):
+        raw_position = member.get("position", idx)
+        try:
+            parsed_position = int(raw_position)
+        except (TypeError, ValueError):
+            parsed_position = idx
+        position = parsed_position if parsed_position >= 1 else idx
+        member["position"] = position
         member["suggested_name"] = _generate_vc_member_name(
             master_name, position, serial=member.get("serial"), pattern=vc_pattern
         )

Also applies to: 203-207, 309-319

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 35 - 39,
The VC member position handling currently allows 0/negative positions and
sometimes adds an extra +1, shifting already 1-based positions; update the
parsing logic (where raw_position, member_copy and idx are used) to coerce
int(raw_position) and then if the result is <= 0 replace it with idx + 1
(1-based fallback), assign that value to member_copy["position"], and remove any
later code that adds an extra +1 to positions when building suggested names
(inspect the other occurrences around the blocks referenced at lines ~203-207,
~309-319 and the suggestion code near ~315) so positions remain strictly 1-based
end-to-end.
netbox_librenms_plugin/views/object_sync/vms.py (1)

25-29: ⚠️ Potential issue | 🟠 Major

Bind request onto VM table view instances before get_context_data().

Line 49 now depends on self.librenms_api.server_key; without binding request on the instantiated table views, server-scoped context can drift and this breaks the object-sync view contract.

Suggested fix
 def get_interface_context(self, request, obj):
     """Return interface sync context for the virtual machine."""
     interface_name_field = get_interface_name_field(request)
     interface_sync_view = VMInterfaceTableView()
+    interface_sync_view.request = request
     return interface_sync_view.get_context_data(request, obj, interface_name_field)
@@
 def get_ip_context(self, request, obj):
     """Return IP address sync context for the virtual machine."""
     ipaddress_sync_view = VMIPAddressTableView()
+    ipaddress_sync_view.request = request
     return ipaddress_sync_view.get_context_data(request, obj)

As per coding guidelines **/views/object_sync/**/*.py: Object sync view methods must create instances of concrete table views, copy the request object, and call get_context_data().

Also applies to: 35-38, 48-50

🤖 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
object-sync method get_interface_context creates a VMInterfaceTableView and
calls get_context_data without binding the request to the view instance, which
allows server-scoped state (self.librenms_api.server_key) to drift; instantiate
the concrete table view (VMInterfaceTableView), set its request attribute to a
shallow copy of the incoming request (e.g., copy.copy(request)) before calling
get_context_data, and then call get_context_data(request, obj,
interface_name_field); apply the same pattern to the other object-sync methods
in this file (the other table-view instances referenced around lines 35-38 and
48-50) so every table view instance has request bound/copied prior to
get_context_data.
netbox_librenms_plugin/tests/test_permissions.py (1)

999-1000: ⚠️ Potential issue | 🟠 Major

Patch settings at the view module source, not django.conf.settings.

These tests should patch netbox_librenms_plugin.views.sync.device_fields.settings so the code under test sees the mocked config deterministically.

🛠️ Suggested fix
- patch("django.conf.settings") as mock_settings,
+ patch("netbox_librenms_plugin.views.sync.device_fields.settings") as mock_settings,

Based on learnings: "Applies to tests/**/*.py : Patch deferred/inline imports at their source module ... not the consuming module."

Also applies to: 1030-1031, 1059-1060

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/tests/test_permissions.py` around lines 999 - 1000,
Replace patches that mock Django settings at the global import
("django.conf.settings") with patches that target the module where settings is
actually imported by the code under test: use
"netbox_librenms_plugin.views.sync.device_fields.settings" instead of
"django.conf.settings" (and keep the existing message patch target
"netbox_librenms_plugin.views.sync.device_fields.messages"); update the other
similar occurrences referenced (the patches around the blocks at the later
occurrences noted) so all tests patch settings at the source module the view
uses.
netbox_librenms_plugin/tests/test_sync_view_mismatch.py (1)

331-334: 🧹 Nitpick | 🔵 Trivial

Use shared fixture(s) instead of a local ad-hoc _make_obj builder.

This helper reintroduces custom mock shape drift; please switch to the existing tests/conftest.py fixture path for NetBox objects and override only the needed custom_field_data.

Based on learnings: "Applies to tests/**/*.py : Reuse fixtures from tests/conftest.py instead of creating ad-hoc mocks..."

🤖 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 -
334, Replace the local helper _make_obj in tests/test_sync_view_mismatch.py with
the shared NetBox object fixture defined in tests/conftest.py: stop creating an
ad-hoc MagicMock, instead grab the conftest fixture for the NetBox object (the
fixture name used in conftest.py) and only override its custom_field_data to
{"librenms_id": cf_librenms_id} in the test that needs it; remove the _make_obj
function and update tests to use the fixture instance (or a shallow copy of it)
with the modified custom_field_data to avoid custom mock shape drift.
netbox_librenms_plugin/utils.py (1)

503-512: ⚠️ Potential issue | 🟠 Major

Do not persist non-integer librenms_id values.

Current fallback stores invalid IDs as-is, which can silently break server-scoped lookups and matching logic later.

🛠️ Suggested fix
-    try:
-        cf_value[server_key] = int(device_id)
-    except (TypeError, ValueError):
-        logger.warning(
-            "librenms_id device_id %r is not a valid integer on %r; storing as-is.",
-            device_id,
-            obj,
-        )
-        cf_value[server_key] = device_id
+    try:
+        cf_value[server_key] = int(device_id)
+    except (TypeError, ValueError):
+        raise ValueError(f"Invalid LibreNMS device_id: {device_id!r}") from None
🤖 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 503 - 512, The code currently
writes non-integer device_id into cf_value[server_key] and persists it via
obj.custom_field_data["librenms_id"]; instead, only assign an int to
cf_value[server_key] (convert and set when int(device_id) succeeds) and on
TypeError/ValueError do not store the original device_id — just log the warning
and leave cf_value unmodified (or remove the server_key entry if it already
exists), then only write obj.custom_field_data["librenms_id"] when cf_value
contains valid integer entries; refer to cf_value, server_key, device_id and
obj.custom_field_data["librenms_id"] when making the change.
netbox_librenms_plugin/views/base/cables_view.py (1)

86-89: 🛠️ Refactor suggestion | 🟠 Major

Centralize the server-scoped librenms_id lookup predicate.

The same JSON/legacy Q(...) | Q(...) filter is repeated across several paths; extract one helper (e.g., build_librenms_id_q(server_key, value)) and reuse it everywhere.

As per coding guidelines: "Use LibreNMSAPI.get_librenms_id() instead of directly accessing the librenms_id custom field when mapping Devices/VMs to LibreNMS."

Also applies to: 131-134, 142-145, 170-172, 182-184, 389-390

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/base/cables_view.py` around lines 86 - 89,
Extract the repeated JSON/legacy lookup Q(...) | Q(...) into a single helper
function (e.g., build_librenms_id_q(server_key, value)) and replace all
instances where Device/VM lookups use the duplicated predicate (for example the
Device.objects.get(...) call that builds
Q(**{f"custom_field_data__librenms_id__{server_key}": remote_device_id}) |
Q(custom_field_data__librenms_id=remote_device_id)) to call this helper instead;
implement the helper to return the combined Q object and reuse it at the other
mentioned locations (the other similar Device/VM queries). Additionally, stop
accessing the custom field directly and use LibreNMSAPI.get_librenms_id()
wherever the code maps Devices/VMs to LibreNMS IDs so mapping logic calls
LibreNMSAPI.get_librenms_id() rather than reading custom_field_data__librenms_id
directly. Ensure all replacements keep the same semantics and update
imports/usages accordingly.
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)

146-147: ⚠️ Potential issue | 🟡 Minor

Guard PLUGINS_CONFIG access to avoid hard failures in minimal settings contexts.

Use a safe fallback (getattr) before .get(...) so this helper doesn’t raise AttributeError when PLUGINS_CONFIG is absent.

🛠️ Suggested fix
- plugins_cfg = django_settings.PLUGINS_CONFIG.get("netbox_librenms_plugin", {})
+ plugins_cfg = getattr(django_settings, "PLUGINS_CONFIG", {}).get("netbox_librenms_plugin", {})
🤖 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 146 -
147, The current lookup assumes django_settings.PLUGINS_CONFIG exists and will
raise AttributeError in minimal settings; update the access to guard against a
missing attribute by using getattr(django_settings, "PLUGINS_CONFIG", {}) before
calling .get(...), e.g., compute plugins_cfg from getattr(django_settings,
"PLUGINS_CONFIG", {}) and then derive servers_config =
plugins_cfg.get("servers", {}); update the code paths in librenms_sync_view.py
that reference plugins_cfg and servers_config (variables named plugins_cfg,
servers_config and the django_settings reference) so they use this safe
fallback.
netbox_librenms_plugin/import_utils/device_operations.py (1)

772-773: 🛠️ Refactor suggestion | 🟠 Major

Use set_librenms_device_id() instead of inline custom_field_data assignment.

Inline mapping construction duplicates ID-format logic and can drift from the central normalization path.

Proposed refactor
 from ..utils import (
     find_matching_platform,
     find_matching_site,
     match_librenms_hardware_to_device_type,
+    set_librenms_device_id,
 )
...
             device_data = {
                 "name": device_name,
                 "site": site,
                 "device_type": device_type,
                 "role": device_role,
                 "status": "active" if libre_device.get("status") == 1 else "offline",
                 "comments": f"Imported from LibreNMS by netbox-librenms-plugin on {import_time}",
-                "custom_field_data": {"librenms_id": {api.server_key: int(device_id)}},
             }
...
             device = Device(**device_data)
+            set_librenms_device_id(device, int(device_id), api.server_key)
             device.full_clean()
             device.save()
netbox_librenms_plugin/views/imports/list.py (1)

95-96: ⚠️ Potential issue | 🟠 Major

Normalize naming flags to strict booleans before deriving cache keys.

use_sysname/strip_domain can arrive as persisted strings (for example "false"), and passing them through raw makes them truthy in Python. That can skew naming mode and produce cache key mismatches between producer/consumer paths.

Proposed fix
+def _coerce_bool(value, default: bool) -> bool:
+    if value is None:
+        return default
+    if isinstance(value, bool):
+        return value
+    if isinstance(value, str):
+        return value.strip().lower() in {"1", "true", "on", "yes"}
+    return bool(value)
...
-        use_sysname = job_data.get("use_sysname", True)
-        strip_domain = job_data.get("strip_domain", False)
+        use_sysname = _coerce_bool(job_data.get("use_sysname"), True)
+        strip_domain = _coerce_bool(job_data.get("strip_domain"), False)
...
-        _use_sysname = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname")
-        _strip_domain = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain")
-        if _use_sysname is None:
-            _use_sysname = getattr(settings_obj, "use_sysname_default", True) if settings_obj else True
-        if _strip_domain is None:
-            _strip_domain = getattr(settings_obj, "strip_domain_default", False) if settings_obj else False
+        _use_sysname = _coerce_bool(
+            get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname"),
+            getattr(settings_obj, "use_sysname_default", True) if settings_obj else True,
+        )
+        _strip_domain = _coerce_bool(
+            get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain"),
+            getattr(settings_obj, "strip_domain_default", False) if settings_obj else False,
+        )

Also applies to: 114-115, 447-448

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/list.py` around lines 95 - 96, Normalize
use_sysname and strip_domain to strict booleans before using them for
naming/caching: read the raw values from job_data (the variables use_sysname and
strip_domain in netbox_librenms_plugin/views/imports/list.py and the same
occurrences around the other noted spots) and coerce string values like
"false"/"true"/"0"/"1"/"yes"/"no" to proper booleans (e.g. lowercasing and
comparing to accepted true tokens), otherwise fall back to bool() for non-string
types, then use those normalized booleans when deriving cache keys and
determining naming mode.
🤖 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/device_operations.py`:
- Line 224: The logic that sets naming_criteria["source"] can be wrong when
use_sysname is false but hostname is empty: in _determine_device_name() ensure
the source reflects the actual field used — if hostname is falsy and you fall
back to libre_device.get("sysName"), set naming_criteria["source"] to "sysname"
(or "sysName" consistent with your naming) instead of leaving it as "hostname";
update the ternary/if that assigns naming_criteria["source"] to check the actual
chosen name (hostname present ? "hostname" : libre_device.get("sysName") ?
"sysname" : whatever other fallback) so the recorded source matches the value
returned.

In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js`:
- Around line 1096-1102: The forEach callback currently uses a concise arrow
expression that implicitly returns the Tooltip instance (unused); update the
tooltip initialization in the modal swap code (the block that queries tooltipEls
and calls bootstrap.Tooltip.getOrCreateInstance) to use an explicit block-bodied
arrow or a for...of loop so the callback does not return a value (e.g., change
tooltipEls.forEach(el => bootstrap.Tooltip.getOrCreateInstance(el)) to a
block-bodied form or iterate with for (const el of tooltipEls) {
bootstrap.Tooltip.getOrCreateInstance(el); }) to make the intent clear and avoid
implicit returns.

In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 193-196: The span that shows the matched device type uses
Bootstrap's data-bs-toggle attribute (see the template block referencing
validation.device_type.match_type, validation.device_type.chassis_model, and
validation.device_type.device_type); remove the data-bs-toggle="tooltip"
entirely from that span so the fragment does not reintroduce Bootstrap toggle
helpers into the modal HTMX flow, and keep the title attribute (or replace with
a non-Bootstrap tooltip initialization if needed) so the explanatory text
remains available without using bootstrap.Modal helpers.

In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`:
- Around line 78-80: The inline onsubmit handler injects mapping.server_key
directly into a JS string which allows quote-breaking input to execute script;
update the template to JS-escape mapping.server_key before insertion (use
Django's escapejs filter on mapping.server_key inside the onsubmit attribute),
e.g. replace occurrences of {{ mapping.server_key }} in the onsubmit with {{
mapping.server_key|escapejs }} so the value is safely quoted in the confirm()
call and cannot break out into executable code.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1233-1241: Inside the lock, you must re-check that the current
legacy ID matches the expected librenms_id before migrating: after reading
cf_locked = locked_device.custom_field_data.get("librenms_id") ensure cf_locked
is an int and equals the librenms_id you intended to migrate (the same value
checked pre-lock, e.g. cf_value or librenms_id) and only then call
migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key) and
_save_device(locked_device); if cf_locked is not an int keep the existing
JSON-format response, and if cf_locked is an int but does not equal the expected
librenms_id return/skip migration to avoid migrating the wrong value.
- Around line 1139-1149: The serial conflict check in the sync flow (inside the
transaction block where you lock the target Device row via select_for_update and
use variables like locked_device and conflict_device) still has a race because
other transactions can concurrently insert/update different Device rows with the
same serial; add a database-level uniqueness constraint or unique index on the
Device.serial column (handling NULLs as needed) to enforce global uniqueness,
create the corresponding migration for the Device model, and update the
import/update code to catch and handle IntegrityError on save (or UPSERT) so the
UI returns a 409/meaningful message when the DB rejects a duplicate serial
instead of relying solely on the in-transaction conflict check.

---

Outside diff comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 83-85: The stored user prefs retrieved via get_user_pref in
_resolve_naming_preferences (e.g., when assigning use_sysname and prefer_name)
are used raw and can be truthy strings like "false" or "0"; normalize those
persisted values the same way POST/GET inputs are normalized (convert string
booleans/"0"/"1" to actual booleans or apply the same normalization helper used
elsewhere) before assigning to use_sysname and prefer_name so cached naming
resolution behaves correctly; update the assignments around get_user_pref(...)
to pass the retrieved value through the existing normalization routine (or
implement a small normalize_pref utility) rather than assigning raw pref.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 24-30: The _safe_disabled helper currently converts non-numeric
truthy strings to 0; update _safe_disabled(device: dict) to be tolerant of
boolean-like values by checking the retrieved val = device.get("disabled", 0)
for booleans and common truthy strings before attempting int(): if val is a bool
return int(val), if it's a str normalize (.strip().lower()) and return 1 for
values like "true","yes","on" (and treat "1") and 0 for the rest, then fall back
to the existing int() try/except to handle numeric strings and other types;
touch the _safe_disabled function only.

In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 15-21: _safe_disabled currently treats non-numeric truthy strings
as 0, causing values like "true"/"yes"/"on" to be misclassified as enabled;
update _safe_disabled to normalize string inputs from device.get("disabled", 0)
(case-insensitive) and return 1 for common truthy tokens ("true","yes","on","1")
and 0 for falsy tokens ("false","no","off","0"), while still attempting int
conversion and catching TypeError/ValueError to default to 0 — keep the function
name _safe_disabled and its contract (returns int 0/1).

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 35-39: The VC member position handling currently allows 0/negative
positions and sometimes adds an extra +1, shifting already 1-based positions;
update the parsing logic (where raw_position, member_copy and idx are used) to
coerce int(raw_position) and then if the result is <= 0 replace it with idx + 1
(1-based fallback), assign that value to member_copy["position"], and remove any
later code that adds an extra +1 to positions when building suggested names
(inspect the other occurrences around the blocks referenced at lines ~203-207,
~309-319 and the suggestion code near ~315) so positions remain strictly 1-based
end-to-end.

In `@netbox_librenms_plugin/tests/test_permissions.py`:
- Around line 999-1000: Replace patches that mock Django settings at the global
import ("django.conf.settings") with patches that target the module where
settings is actually imported by the code under test: use
"netbox_librenms_plugin.views.sync.device_fields.settings" instead of
"django.conf.settings" (and keep the existing message patch target
"netbox_librenms_plugin.views.sync.device_fields.messages"); update the other
similar occurrences referenced (the patches around the blocks at the later
occurrences noted) so all tests patch settings at the source module the view
uses.

In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py`:
- Around line 331-334: Replace the local helper _make_obj in
tests/test_sync_view_mismatch.py with the shared NetBox object fixture defined
in tests/conftest.py: stop creating an ad-hoc MagicMock, instead grab the
conftest fixture for the NetBox object (the fixture name used in conftest.py)
and only override its custom_field_data to {"librenms_id": cf_librenms_id} in
the test that needs it; remove the _make_obj function and update tests to use
the fixture instance (or a shallow copy of it) with the modified
custom_field_data to avoid custom mock shape drift.

In `@netbox_librenms_plugin/utils.py`:
- Around line 503-512: The code currently writes non-integer device_id into
cf_value[server_key] and persists it via obj.custom_field_data["librenms_id"];
instead, only assign an int to cf_value[server_key] (convert and set when
int(device_id) succeeds) and on TypeError/ValueError do not store the original
device_id — just log the warning and leave cf_value unmodified (or remove the
server_key entry if it already exists), then only write
obj.custom_field_data["librenms_id"] when cf_value contains valid integer
entries; refer to cf_value, server_key, device_id and
obj.custom_field_data["librenms_id"] when making the change.

In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 86-89: Extract the repeated JSON/legacy lookup Q(...) | Q(...)
into a single helper function (e.g., build_librenms_id_q(server_key, value)) and
replace all instances where Device/VM lookups use the duplicated predicate (for
example the Device.objects.get(...) call that builds
Q(**{f"custom_field_data__librenms_id__{server_key}": remote_device_id}) |
Q(custom_field_data__librenms_id=remote_device_id)) to call this helper instead;
implement the helper to return the combined Q object and reuse it at the other
mentioned locations (the other similar Device/VM queries). Additionally, stop
accessing the custom field directly and use LibreNMSAPI.get_librenms_id()
wherever the code maps Devices/VMs to LibreNMS IDs so mapping logic calls
LibreNMSAPI.get_librenms_id() rather than reading custom_field_data__librenms_id
directly. Ensure all replacements keep the same semantics and update
imports/usages accordingly.

In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 146-147: The current lookup assumes django_settings.PLUGINS_CONFIG
exists and will raise AttributeError in minimal settings; update the access to
guard against a missing attribute by using getattr(django_settings,
"PLUGINS_CONFIG", {}) before calling .get(...), e.g., compute plugins_cfg from
getattr(django_settings, "PLUGINS_CONFIG", {}) and then derive servers_config =
plugins_cfg.get("servers", {}); update the code paths in librenms_sync_view.py
that reference plugins_cfg and servers_config (variables named plugins_cfg,
servers_config and the django_settings reference) so they use this safe
fallback.

In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 95-96: Normalize use_sysname and strip_domain to strict booleans
before using them for naming/caching: read the raw values from job_data (the
variables use_sysname and strip_domain in
netbox_librenms_plugin/views/imports/list.py and the same occurrences around the
other noted spots) and coerce string values like
"false"/"true"/"0"/"1"/"yes"/"no" to proper booleans (e.g. lowercasing and
comparing to accepted true tokens), otherwise fall back to bool() for non-string
types, then use those normalized booleans when deriving cache keys and
determining naming mode.

In `@netbox_librenms_plugin/views/object_sync/vms.py`:
- Around line 25-29: The VM object-sync method get_interface_context creates a
VMInterfaceTableView and calls get_context_data without binding the request to
the view instance, which allows server-scoped state
(self.librenms_api.server_key) to drift; instantiate the concrete table view
(VMInterfaceTableView), set its request attribute to a shallow copy of the
incoming request (e.g., copy.copy(request)) before calling get_context_data, and
then call get_context_data(request, obj, interface_name_field); apply the same
pattern to the other object-sync methods in this file (the other table-view
instances referenced around lines 35-38 and 48-50) so every table view instance
has request bound/copied prior to get_context_data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 82c218dc-44b4-4356-946a-b3dea6584e8e

📥 Commits

Reviewing files that changed from the base of the PR and between a312799 and af62597.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (31)
  • netbox_librenms_plugin/forms.py
  • netbox_librenms_plugin/import_utils/__init__.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/jobs.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/test_background_jobs.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/tests/test_sync_view_mismatch.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/views/object_sync/devices.py
  • netbox_librenms_plugin/views/object_sync/vms.py
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/sync/interfaces.py

Comment thread netbox_librenms_plugin/import_utils/device_operations.py Outdated
Comment thread netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html Outdated
Comment thread netbox_librenms_plugin/views/imports/actions.py
Comment on lines +1233 to +1241
# Re-check under lock — another request may have already migrated it
cf_locked = locked_device.custom_field_data.get("librenms_id")
if not isinstance(cf_locked, int):
return HttpResponse(
"Device librenms_id is already in JSON format; no migration needed.",
status=400,
)
migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key)
if err := _save_device(locked_device):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Re-check legacy ID equality under lock before migrating.

The pre-lock cf_value == librenms_id check can become stale. Inside the lock you only verify type, so a changed integer can be migrated incorrectly.

Proposed fix
                 cf_locked = locked_device.custom_field_data.get("librenms_id")
                 if not isinstance(cf_locked, int):
                     return HttpResponse(
                         "Device librenms_id is already in JSON format; no migration needed.",
                         status=400,
                     )
+                if cf_locked != librenms_id:
+                    return HttpResponse(
+                        f"Legacy librenms_id ({cf_locked}) does not match the active device ID "
+                        f"({librenms_id}); cannot migrate safely.",
+                        status=409,
+                    )
                 migrate_legacy_librenms_id(locked_device, 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 1233 - 1241,
Inside the lock, you must re-check that the current legacy ID matches the
expected librenms_id before migrating: after reading cf_locked =
locked_device.custom_field_data.get("librenms_id") ensure cf_locked is an int
and equals the librenms_id you intended to migrate (the same value checked
pre-lock, e.g. cf_value or librenms_id) and only then call
migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key) and
_save_device(locked_device); if cf_locked is not an int keep the existing
JSON-format response, and if cf_locked is an int but does not equal the expected
librenms_id return/skip migration to avoid migrating the wrong value.

- device_operations: fix naming_criteria["source"] to reflect actual field
  used — when use_sysname=False and hostname is empty, fallback to sysName
  was not reflected; source now mirrors _determine_device_name logic
- librenms_sync_base.html: escapejs on mapping.server_key in confirm()
  to prevent quote injection/XSS from untrusted server key values
- actions.py: inside migrate_librenms_id lock verify cf_locked == librenms_id
  before migrating, guarding against value drift under concurrent lock
- actions.py: add comment explaining why DB-level unique constraint on serial
  is intentionally omitted (device moves allow temporary duplicates)
- _safe_disabled: handle bool values and common truthy/falsy string tokens
  (true/yes/on/false/no/off) in both bulk_import.py and filters.py
- utils.py: on TypeError/ValueError in set_librenms_device_id return early
  without persisting the invalid entry instead of storing as-is
- cables_view.py: extract _librenms_id_q(server_key, value) helper; replace
  all five duplicated Q(**{...}) | Q(...) patterns with calls to it
- librenms_sync_view.py: use getattr(django_settings, "PLUGINS_CONFIG", {})
  to safely handle environments where PLUGINS_CONFIG is absent
- virtual_chassis.py: enforce pos > 0 in _clone_virtual_chassis_data; in
  update_vc_member_suggested_names remove spurious +1 that was
  over-incrementing already-1-based entPhysicalParentRelPos positions
- librenms_import.js: use for..of instead of forEach to avoid implicit
  return from getOrCreateInstance in ensureModalVisible tooltip init
- device_validation_details.html: remove data-bs-toggle from device type
  span to avoid duplicate Bootstrap tooltip instances on HTMX modal swaps
@marcinpsk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 4, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)

205-208: ⚠️ Potential issue | 🟡 Minor

Enforce 1-based VC positions before building member data.

Line 206 can still preserve 0/negative values from entPhysicalParentRelPos, which breaks the 1-based invariant and can produce invalid output like Member-0 on Line 223.

🔧 Proposed fix
             try:
                 position = int(raw_position)
+                if position < 1:
+                    position = idx + 1
             except (TypeError, ValueError):
                 position = idx + 1

Also applies to: 221-223

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 205 -
208, The parsed VC position (raw_position / entPhysicalParentRelPos) can be zero
or negative; after converting to int in the block around variable position,
enforce a 1-based positive value by replacing any position < 1 with a sane
default (e.g., idx + 1 or 1) before it is used to construct member data (e.g.,
Member-{position}); apply the same check to the later parsing branch that builds
members (the block that produces Member-{position} around lines referenced) so
no Member-0 or negative positions are emitted.
netbox_librenms_plugin/import_utils/device_operations.py (1)

771-779: 🛠️ Refactor suggestion | 🟠 Major

Use the centralized librenms-id setter instead of inline custom_field_data construction.

This path still writes the librenms_id payload directly. That bypasses shared normalization behavior and can drift from the rest of the code.

♻️ Proposed refactor
             device_data = {
                 "name": device_name,
                 "site": site,
                 "device_type": device_type,
                 "role": device_role,
                 "status": "active" if libre_device.get("status") == 1 else "offline",
                 "comments": f"Imported from LibreNMS by netbox-librenms-plugin on {import_time}",
-                "custom_field_data": {"librenms_id": {api.server_key: int(device_id)}},
             }
@@
             # Create the device
             device = Device(**device_data)
+            from ..utils import set_librenms_device_id
+            set_librenms_device_id(device, int(device_id), api.server_key)
             device.full_clean()
             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/device_operations.py` around lines 771 -
779, The device_data construction is directly populating custom_field_data with
{"librenms_id": {api.server_key: int(device_id)}} which bypasses the centralized
librenms-id normalization; replace the inline construction in device_data with
the shared helper (e.g., call the existing set_librenms_id or equivalent helper)
to populate device_data['custom_field_data'] (pass api.server_key and device_id
to the helper or build the custom fields via the helper and assign the result to
device_data['custom_field_data']), ensuring you remove the direct use of
api.server_key/int conversion here and rely on the centralized setter for
consistent normalization.
🤖 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/bulk_import.py`:
- Around line 213-216: The code builds vc_domain by joining member_serials but
member_serial values may not be strings, causing
f"...,{','.join(member_serials)}" to throw; update the creation of
member_serials (used with vc_data, member_serials, and vc_domain) to normalize
each serial to a string (e.g., map str or use a comprehension converting
m.get("serial") to str for non-None values) before sorting and joining so join()
always receives strings and the success/failure accounting remains correct.
- Around line 24-42: The function _safe_disabled currently may return integers
other than 0/1 (e.g., "2" → 2); change the final conversion to clamp to strict
binary output by parsing int(val) into a local (e.g., int_val = int(val)) and
returning 1 only when int_val == 1, otherwise return 0, leaving the existing
boolean and string branches intact so the function always yields exactly 0 or 1.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Line 221: The member name generation currently calls
_generate_vc_member_name(master_name, position) per member which recomputes the
naming pattern each time; instead, resolve/load the VC naming pattern once at
the start of the surrounding loop (e.g., into a local variable named pattern)
and pass that pattern into _generate_vc_member_name (e.g.,
_generate_vc_member_name(master_name, position, pattern)) when assigning
member_data["suggested_name"], so the pattern is reused across members rather
than recalculated repeatedly.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 771-779: The device_data construction is directly populating
custom_field_data with {"librenms_id": {api.server_key: int(device_id)}} which
bypasses the centralized librenms-id normalization; replace the inline
construction in device_data with the shared helper (e.g., call the existing
set_librenms_id or equivalent helper) to populate
device_data['custom_field_data'] (pass api.server_key and device_id to the
helper or build the custom fields via the helper and assign the result to
device_data['custom_field_data']), ensuring you remove the direct use of
api.server_key/int conversion here and rely on the centralized setter for
consistent normalization.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 205-208: The parsed VC position (raw_position /
entPhysicalParentRelPos) can be zero or negative; after converting to int in the
block around variable position, enforce a 1-based positive value by replacing
any position < 1 with a sane default (e.g., idx + 1 or 1) before it is used to
construct member data (e.g., Member-{position}); apply the same check to the
later parsing branch that builds members (the block that produces
Member-{position} around lines referenced) so no Member-0 or negative positions
are emitted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: a5fab7df-8bb7-44b7-bc29-54cb7398cf4b

📥 Commits

Reviewing files that changed from the base of the PR and between af62597 and 1aaa7f5.

📒 Files selected for processing (11)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (10)
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">. Avoid outerHTML swaps; use OOB (Out-of-Band) or targeted innerHTML swaps to keep table layout intact.
Styling assumes Tabler defaults. Removing table-responsive wrappers 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/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 and fetch() calls must include a CSRF token. The standard pattern is document.querySelector('[name=csrfmiddlewaretoken]').value (from a hidden form input). The import JS also uses getCookie('csrftoken') as a fallback — prefer the hidden input approach for consistency.
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.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.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 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.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/static/**/*import*.js

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

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.
The import filter form uses fetch with Accept: application/json, text/html—JSON for background jobs, HTML for synchronous mode.
Import page JavaScript (librenms_import.js) is wrapped in an IIFE with window.LibreNMSImportInitialized guard to prevent re-initialization during HTMX swaps.
The ModalManager class wraps Bootstrap 5 modal show/hide with fallback.
The pollJobStatus() function polls /api/core/background-tasks/{jobId}/ every 2s, updates progress messages, handles cancel button, and redirects on completion.
The captureSelectionState() and restoreSelectionState() functions preserve checkbox state across HTMX content swaps.
The initializeFilterForm() function intercepts form submit, detects JSON response (background job), and starts polling.
CSRF token extraction should use getCookie('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 check response.ok before processing fetch responses to catch HTTP errors. In catch blocks, show error.message for debugging rather than generic messages.
The createCacheCountdown() function is a generic countdown timer for cache expiration display.

Files:

  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
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
netbox_librenms_plugin/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

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
Use LibreNMSAPI.get_librenms_id() instead of directly accessing the librenms_id custom field when mapping Devices/VMs to LibreNMS
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
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)
Centralize validation state mutation during import using import_validation_helpers.py for role/cluster/rack assignment, issue removal, and status recalculation
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

Files:

  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/import_utils/filters.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 in views/base/, Object sync views in views/object_sync/, and Sync action views in views/sync/ with shared mixins from views/mixins.py
New views must extend the closest base class and compose mixins from views/mixins.py (LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, CacheMixin, VlanAssignmentMixin)
All views must inherit LibreNMSPermissionMixin from views/mixins.py with permission_required = PERM_VIEW_PLUGIN
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
Use _get_safe_redirect_url(request) to validate referrer URLs in permission checks to prevent open-redirect attacks

Files:

  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/base/cables_view.py
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Job cancellation flow: (1) Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job, (2) Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database, (3) Frontend polling detects status changes and redirects appropriately
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status, update modal messages based on status transitions (queued, started, finished, stopped, failed), handle all RQ status values explicitly to avoid infinite polling, and use cancelInProgress flag to prevent polling interference during cancellation
NetBox's /api/core/background-tasks/ endpoint requires superuser (IsSuperuser in BaseRQViewSet); non-superuser users cannot poll job status and get 403 Forbidden. The plugin must automatically fall back to synchronous mode for non-superusers via should_use_background_job() in list.py and actions.py
Custom sync endpoint api/views.py::sync_job_status() must sync database Job status with RQ job status, needed because NetBox worker doesn't always update DB when jobs stop before processing starts
Import page supports synchronous mode (calls process_device_filters() directly, renders results inline) and background mode (enqueues FilterDevicesJob, returns JsonResponse with job_id/job_pk/poll_url, frontend polls and redirects on completion)
Result loading via _load_job_results(job_id) must read job.data['device_ids'] and reconstruct devices from per-device cache using get_validated_device_cache_key()
Import filter fields must include: librenms_location, librenms_type, librenms_os, librenms_hostname, librenms_sysname, librenms_hardware, enable_vc_detection, show_disabled, exclude_existing
DeviceImportHelperMixin must provide get_validated_device_with_selections() and render_device_row() for HTMX row rendering, shared by update views
BulkImportConfirmView (POST) must render confirmation modal with selected device...

Files:

  • netbox_librenms_plugin/views/imports/actions.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 with CacheMixin keys like librenms_{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.
VlanAssignmentMixin must 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/base/cables_view.py
🧠 Learnings (62)
📓 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 : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
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/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Import filter fields must include: `librenms_location`, `librenms_type`, `librenms_os`, `librenms_hostname`, `librenms_sysname`, `librenms_hardware`, `enable_vc_detection`, `show_disabled`, `exclude_existing`
📚 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.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_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/**/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
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/views/imports/actions.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.html
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.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/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/base/cables_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/**/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
  • 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/**/_*_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
  • netbox_librenms_plugin/views/imports/actions.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/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/base/cables_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 : 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.html
  • netbox_librenms_plugin/views/imports/actions.py
  • 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/**/*.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/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/import_utils/filters.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/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_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 CSRF token must be extracted via `document.querySelector('[name=csrfmiddlewaretoken]').value` for all POST requests.

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} : All HTMX requests and `fetch()` calls must include a CSRF token. The standard pattern is `document.querySelector('[name=csrfmiddlewaretoken]').value` (from a hidden form input). The import JS also uses `getCookie('csrftoken')` as a fallback — prefer the hidden input approach for consistency.

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/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 `captureSelectionState()` and `restoreSelectionState()` functions preserve checkbox state across HTMX content swaps.

Applied to files:

  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • 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 : 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/static/netbox_librenms_plugin/js/librenms_import.js
  • 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/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/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/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/import_utils/bulk_import.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 `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: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 in `librenms_sync.js` must not be wrapped in an IIFE and must use a master initializer `initializeScripts()` that runs on both `DOMContentLoaded` and `htmx:afterSwap` events.

Applied to files:

  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 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: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/htmx/device_validation_details.html
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/import_utils/filters.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 : 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.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 **/tables/**/*.py : Table classes in `tables/` must use `ToggleColumn(attrs={'input': {'name': 'select'}})` for selection, accept contextual parameters in constructors (e.g., `device`, `interface_name_field`, `vlan_groups`), set `self.tab` and `self.prefix` for multi-table pagination, include `data-*` attributes in row attrs, and VLAN columns must use `render_vlans()` with hidden inputs and JSON data.

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 VLAN modal functions must implement `openVlanDetailModal()`, `verifyVlanInGroup()`, `verifyVlanSyncGroup()` for per-interface VLAN detail editing.

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/** : `DeviceImportHelperMixin` must provide `get_validated_device_with_selections()` and `render_device_row()` for HTMX row rendering, shared by update views

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.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/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.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: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
  • netbox_librenms_plugin/import_utils/bulk_import.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
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/import_utils/device_operations.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/base/cables_view.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
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.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_validation_helpers.py : Validation helpers must include: `fetch_model_by_id()`, `extract_device_selections()` for reading form data

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/views/imports/actions.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 : `fetch_device_with_cache(device_id, ...)` must retrieve and cache individual device data

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/** : Result loading via `_load_job_results(job_id)` must read `job.data['device_ids']` and reconstruct devices from per-device cache using `get_validated_device_cache_key()`

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.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: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.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_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/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/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/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
  • netbox_librenms_plugin/views/imports/actions.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/imports/actions.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/base/cables_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/views/imports/actions.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/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.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/imports/actions.py
  • 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/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/imports/actions.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 : Background jobs must use standalone helpers from `import_utils.py` (`check_user_permissions`, `require_permissions`) instead of view mixins; non-superusers fall back to synchronous mode

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.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/imports/actions.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/imports/actions.py
  • netbox_librenms_plugin/views/base/cables_view.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/bulk_import.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_validation_helpers.py : Validation state mutation helpers in `import_validation_helpers.py` must include: `apply_role_to_validation()`, `apply_cluster_to_validation()`, `apply_rack_to_validation()` to update validation state when user selects role/cluster/rack

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.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/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.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,**/import_utils.py : Both synchronous and background modes must use `get_validated_device_cache_key()` from `import_utils.py` to generate cache keys, ensuring `_load_job_results()` in the list view can retrieve devices regardless of which mode produced them. Never hardcode cache key formats; always use the helper functions

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.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 : `FilterDevicesJob` background job: filters devices with VC detection. `job.data` keys must include `device_ids`, `total_processed`, `filters`, `server_key`, `vc_detection_enabled`, `cache_timeout`, `cached_at`, `completed`. Devices are cached individually via shared cache keys from `get_validated_device_cache_key()`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/** : Import page supports synchronous mode (calls `process_device_filters()` directly, renders results inline) and background mode (enqueues `FilterDevicesJob`, returns `JsonResponse` with `job_id`/`job_pk`/`poll_url`, frontend polls and redirects on completion)

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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.py
  • netbox_librenms_plugin/views/base/cables_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/**/*.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.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/views/base/cables_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|**/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/base/cables_view.py
  • netbox_librenms_plugin/import_utils/filters.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/import_utils/filters.py
🔇 Additional comments (2)
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js (1)

1098-1099: Good idempotent tooltip initialization.

Using bootstrap.Tooltip.getOrCreateInstance() here prevents duplicate tooltip instances during repeated HTMX swaps.

netbox_librenms_plugin/views/imports/actions.py (1)

1015-1040: Transactional lock + conflict re-check is a solid hardening improvement.

Good change: the check-and-write path is now atomic and guarded by select_for_update(), which materially reduces same-row race conditions during conflict actions.

Comment thread netbox_librenms_plugin/import_utils/bulk_import.py
Comment thread netbox_librenms_plugin/import_utils/bulk_import.py Outdated
Comment thread netbox_librenms_plugin/import_utils/virtual_chassis.py Outdated
…y-rebased

Code fixes (matching inventory-rebased):
- bulk_import.py: clamp _safe_disabled to strict 0/1 (int_val guard)
- bulk_import.py: normalize member_serials to str before join
- virtual_chassis.py: guard position <= 0 in detect_virtual_chassis_from_inventory
- virtual_chassis.py: load vc_name_pattern once before member loop
- device_operations.py: use set_librenms_device_id helper instead of inline CF construction

Tests ported from inventory-rebased:
- test_vm_operations.py: 21 tests, 100% coverage for vm_operations.py
- test_import_utils.py: port extra TestVCPositionHandling tests
- test_import_utils.py: add 11 new VC test classes covering detect, create, edge cases

Coverage: virtual_chassis.py 100%, vm_operations.py 100%
512 tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/virtual_chassis.py (1)

287-292: ⚠️ Potential issue | 🟠 Major

Handle malformed VC naming patterns without aborting creation/detection flows.

Line 288 can raise ValueError/IndexError (not only KeyError) for invalid format strings (e.g., unmatched braces or positional placeholders). Right now those escape and can fail the whole VC operation.

🔧 Proposed fix
-    except KeyError as e:
+    except (KeyError, ValueError, IndexError) as e:
         logger.error(f"Invalid placeholder in VC naming pattern '{pattern}': {e}. Using default.")
         return f"{master_name}-M{position}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 287 -
292, The try/except around pattern.format(**format_vars) only catches KeyError
but malformed format strings can raise ValueError or IndexError and currently
bubble up; update the exception handling in the VC naming routine to catch
KeyError, ValueError and IndexError (e.g., in the block using variables pattern,
formatted_suffix, master_name, format_vars, position) and on any of those
exceptions log the error and return the fallback f"{master_name}-M{position}" so
creation/detection continues.
♻️ Duplicate comments (1)
netbox_librenms_plugin/import_utils/bulk_import.py (1)

214-221: ⚠️ Potential issue | 🟠 Major

Exclude placeholder serials when deriving VC dedup domain.

"-" values are currently treated as real member serials, which can collapse unrelated stacks into the same vc_domain and skip VC creation for later devices.

Proposed fix
-                    member_serials = sorted(
-                        str(m.get("serial"))
-                        for m in vc_data.get("members", [])
-                        if m.get("serial") is not None and m.get("serial") != ""
-                    )
+                    member_serials = sorted(
+                        serial
+                        for m in vc_data.get("members", [])
+                        for serial in [str(m.get("serial")).strip()]
+                        if serial not in ("", "-")
+                    )
                     vc_domain = (
                         f"librenms-stack-{','.join(member_serials)}" if member_serials else f"librenms-{device_id}"
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 214 - 221,
The VC dedup domain currently includes placeholder serials like "-" because
member_serials is built from vc_data.get("members") without excluding "-"
values; update the filtering logic used to build member_serials (in the same
block that references member_serials, vc_data, and device_id) to also exclude
falsy/placeholder serials such as "-" (e.g., skip if m.get("serial") is None,
empty, or equals "-"/other known placeholders), and ensure you trim/normalize
serial strings before joining so vc_domain is only derived from valid serials.
🤖 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/virtual_chassis.py`:
- Around line 414-431: Discovered SNMP positions (discovered_pos) can collide if
multiple members report the same value, causing repeated chosen_pos and VC
position uniqueness failures; to fix, track used positions (e.g., a set
used_positions) while iterating members and when computing chosen_pos for
_generate_vc_member_name, if discovered_pos is not None but already in
used_positions, treat it as unavailable and pick the next free sequential
position (bumping position until it is not in used_positions), then add the
assigned chosen_pos to used_positions and update position = max(position,
chosen_pos + 1) so future assignments skip already-used slots; apply the same
logic where discovered_pos is None (consume next free slot) to ensure no
duplicates.

In `@netbox_librenms_plugin/tests/test_vm_operations.py`:
- Around line 23-33: The test repeatedly creates local mocks (mock_cluster,
mock_platform, validation, mock_vm) inline in test_vm_operations.py; replace
these ad-hoc setups with the shared pytest fixtures defined in tests/conftest.py
(e.g., use the existing cluster, platform, validation, and vm fixtures) and
update the tests that reference mock_cluster, mock_platform, validation, and
mock_vm to accept and use those fixture names instead, keeping only
behavior-specific overrides inside the test bodies; apply the same change
pattern for the other occurrences mentioned (lines ~180-195, ~310-324,
~475-486).

---

Outside diff comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 287-292: The try/except around pattern.format(**format_vars) only
catches KeyError but malformed format strings can raise ValueError or IndexError
and currently bubble up; update the exception handling in the VC naming routine
to catch KeyError, ValueError and IndexError (e.g., in the block using variables
pattern, formatted_suffix, master_name, format_vars, position) and on any of
those exceptions log the error and return the fallback
f"{master_name}-M{position}" so creation/detection continues.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 214-221: The VC dedup domain currently includes placeholder
serials like "-" because member_serials is built from vc_data.get("members")
without excluding "-" values; update the filtering logic used to build
member_serials (in the same block that references member_serials, vc_data, and
device_id) to also exclude falsy/placeholder serials such as "-" (e.g., skip if
m.get("serial") is None, empty, or equals "-"/other known placeholders), and
ensure you trim/normalize serial strings before joining so vc_domain is only
derived from valid serials.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2486ec65-51f6-47dc-bb91-e4581a4a381d

📥 Commits

Reviewing files that changed from the base of the PR and between 1aaa7f5 and a4fae7b.

📒 Files selected for processing (5)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_vm_operations.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). (3)
  • GitHub Check: test-netbox (3.12)
  • GitHub Check: test-netbox (3.13)
  • GitHub Check: test-netbox (3.14)
🧰 Additional context used
📓 Path-based instructions (1)
netbox_librenms_plugin/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

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
Use LibreNMSAPI.get_librenms_id() instead of directly accessing the librenms_id custom field when mapping Devices/VMs to LibreNMS
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
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)
Centralize validation state mutation during import using import_validation_helpers.py for role/cluster/rack assignment, issue removal, and status recalculation
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

Files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/import_utils/device_operations.py
🧠 Learnings (27)
📚 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/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • 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/bulk_import.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/bulk_import.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • 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 **/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/import_utils/bulk_import.py
  • netbox_librenms_plugin/tests/test_vm_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/bulk_import.py
  • netbox_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/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • 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_validation_helpers.py : Validation state mutation helpers in `import_validation_helpers.py` must include: `apply_role_to_validation()`, `apply_cluster_to_validation()`, `apply_rack_to_validation()` to update validation state when user selects role/cluster/rack

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.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/import_utils/bulk_import.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/import_utils/bulk_import.py
  • 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 : 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/import_utils/bulk_import.py
  • netbox_librenms_plugin/tests/test_vm_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 : `fetch_device_with_cache(device_id, ...)` must retrieve and cache individual device data

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_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 `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/import_utils/bulk_import.py
  • netbox_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 : 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/bulk_import.py
  • 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 : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`.

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/bulk_import.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,**/import_utils.py : Both synchronous and background modes must use `get_validated_device_cache_key()` from `import_utils.py` to generate cache keys, ensuring `_load_job_results()` in the list view can retrieve devices regardless of which mode produced them. Never hardcode cache key formats; always use the helper functions

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/tests/test_vm_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 **/jobs.py : `FilterDevicesJob` background job: filters devices with VC detection. `job.data` keys must include `device_ids`, `total_processed`, `filters`, `server_key`, `vc_detection_enabled`, `cache_timeout`, `cached_at`, `completed`. Devices are cached individually via shared cache keys from `get_validated_device_cache_key()`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/** : Result loading via `_load_job_results(job_id)` must read `job.data['device_ids']` and reconstruct devices from per-device cache using `get_validated_device_cache_key()`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • 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 **/views/imports/** : Import page supports synchronous mode (calls `process_device_filters()` directly, renders results inline) and background mode (enqueues `FilterDevicesJob`, returns `JsonResponse` with `job_id`/`job_pk`/`poll_url`, frontend polls and redirects on completion)

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/bulk_import.py
  • 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/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_vm_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_vm_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/tests/test_vm_operations.py
  • 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 : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances.

Applied to files:

  • netbox_librenms_plugin/tests/test_vm_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_vm_operations.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/*.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/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_validation_helpers.py : Validation helpers must include: `fetch_model_by_id()`, `extract_device_selections()` for reading form data

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
🔇 Additional comments (5)
netbox_librenms_plugin/import_utils/device_operations.py (1)

781-783: Good centralization of librenms_id write formatting.

Using set_librenms_device_id() here is the right direction for consistent multi-server custom-field structure across import flows.

netbox_librenms_plugin/import_utils/bulk_import.py (1)

24-43: Disabled-flag normalization is much safer now.

This parser handles mixed LibreNMS payload types defensively and keeps filtering logic stable.

netbox_librenms_plugin/import_utils/virtual_chassis.py (3)

35-40: Good 1-based VC position normalization across clone/detect/update paths.

Line 35, Line 206, and Line 317 now consistently enforce valid position >= 1 with sane fallbacks, which closes prior off-by-one/zero-position edge cases.

Also applies to: 204-212, 315-324


200-201: Pattern preloading is a solid performance improvement.

Loading the VC naming pattern once per loop and passing it into _generate_vc_member_name() avoids repeated settings lookups in hot paths.

Also applies to: 225-225, 312-313, 325-327, 394-395, 431-431


472-477: Atomic rollback handling is correctly simplified.

Relying on transaction.atomic() rollback and removing manual restoration avoids broken-transaction side effects.

Comment thread netbox_librenms_plugin/import_utils/virtual_chassis.py
Comment on lines +23 to +33
mock_cluster = MagicMock()
mock_platform = MagicMock()
validation = {
"can_import": True,
"cluster": {"cluster": mock_cluster},
"platform": {"platform": mock_platform},
}
mock_vm = MagicMock()
mock_vm.name = "vm01-computed"
mock_vm.pk = 10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Consolidate repeated mock setup into shared pytest fixtures.

This file repeatedly recreates API/user/device/validation mocks inline, which increases maintenance cost and test drift risk. Please switch these repeated setups to existing fixtures from tests/conftest.py and keep tests focused on behavior deltas only.

Based on learnings: "Reuse fixtures from tests/conftest.py instead of creating ad-hoc mocks."

Also applies to: 180-195, 310-324, 475-486

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/tests/test_vm_operations.py` around lines 23 - 33, The
test repeatedly creates local mocks (mock_cluster, mock_platform, validation,
mock_vm) inline in test_vm_operations.py; replace these ad-hoc setups with the
shared pytest fixtures defined in tests/conftest.py (e.g., use the existing
cluster, platform, validation, and vm fixtures) and update the tests that
reference mock_cluster, mock_platform, validation, and mock_vm to accept and use
those fixture names instead, keeping only behavior-specific overrides inside the
test bodies; apply the same change pattern for the other occurrences mentioned
(lines ~180-195, ~310-324, ~475-486).

- SyncInterfacesView: add LibreNMSAPIMixin to MRO (fixes AttributeError on librenms_api.server_key)
- virtual_chassis: prevent VC position collision when multiple members report the
  same SNMP position (track used_positions set); extend _generate_vc_member_name
  exception handling to also catch ValueError and IndexError
- librenms_sync_base.html: guard remove_server_mapping form with device-only check
  to prevent wrong URL resolution on VM sync pages
- ip_addresses_view: normalize interfaces_by_librenms_id keys to str to
  prevent type mismatch on port_id lookups
- vm_operations: add server_key to create_vm_from_librenms docstring

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

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/virtual_chassis.py (1)

365-397: 🧹 Nitpick | 🔵 Trivial

Load naming pattern once for both master and members in this transaction.

Master rename (Line 365) and member creation currently load pattern in two separate calls. Preloading once ensures a consistent snapshot and saves one DB read.

♻️ Proposed refactor
         with transaction.atomic():
+            vc_pattern = _load_vc_member_name_pattern()
             # Rename master device to include position 1 pattern
-            master_device_new_name = _generate_vc_member_name(original_master_name, 1, serial=master_device.serial)
+            master_device_new_name = _generate_vc_member_name(
+                original_master_name, 1, serial=master_device.serial, pattern=vc_pattern
+            )
@@
-            # Load naming pattern once to avoid a DB query per member
-            vc_pattern = _load_vc_member_name_pattern()
+            # Reuse the same naming pattern snapshot for member creation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 365 -
397, Preload the VC member name pattern once and reuse it for both the master
rename and member creation to avoid duplicate DB reads and ensure consistency:
call _load_vc_member_name_pattern() before invoking _generate_vc_member_name for
the master (used when computing master_device_new_name) and pass/reuse that same
vc_pattern when creating subsequent members (the loop that creates members and
uses _generate_vc_member_name), leaving VirtualChassis creation, master_device
assignment, and member position logic unchanged.
🤖 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/virtual_chassis.py`:
- Around line 243-253: The _load_vc_member_name_pattern function currently calls
LibreNMSSettings.objects.first() (nondeterministic) — change its signature to
accept a server_key (or server identifier), look up the LibreNMSSettings via the
server-aware accessor (resolve settings.selected_server or the same
server-scoped lookup pattern used by
get_librenms_device_id/find_by_librenms_id), and return
settings.vc_member_name_pattern for that server or the default "-M{position}" if
missing; update all callers to pass the appropriate server_key so VC pattern
resolution is deterministic in multi-server mode.

In `@netbox_librenms_plugin/import_utils/vm_operations.py`:
- Line 64: Replace the inline construction of custom_field_data["librenms_id"]
in vm_operations (the line building custom_field_data={"librenms_id":
{server_key: int(libre_device["device_id"])}}) with the centralized setter: call
set_librenms_device_id(...) on the NetBox object (or use the helper that returns
the properly formed payload) so the legacy-value migration and guardrails in
set_librenms_device_id() are applied; locate where the NetBox VM/device is
created or updated in this function and invoke set_librenms_device_id with the
correct server_key and device_id before persisting instead of manually
assembling the JSON.

In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`:
- Around line 36-40: The template currently checks
librenms_server_info.is_legacy inside the all_server_mappings branch which can
error or produce implicit behavior when librenms_server_info is missing; update
the conditional to explicitly guard existence and intent, e.g. make the anchor
conditional require both all_server_mappings and librenms_server_info to be
truthy before checking is_legacy (use a combined guard like "all_server_mappings
and librenms_server_info and not librenms_server_info.is_legacy"), or
alternatively base visibility solely on all_server_mappings by moving the
is_legacy check to a separate, well-guarded block; target the condition around
the anchor that references librenms_server_info/is_legacy to add the explicit
existence check.

---

Outside diff comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 365-397: Preload the VC member name pattern once and reuse it for
both the master rename and member creation to avoid duplicate DB reads and
ensure consistency: call _load_vc_member_name_pattern() before invoking
_generate_vc_member_name for the master (used when computing
master_device_new_name) and pass/reuse that same vc_pattern when creating
subsequent members (the loop that creates members and uses
_generate_vc_member_name), leaving VirtualChassis creation, master_device
assignment, and member position logic unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce2bc77c-040c-48f9-a2a8-3d8c5719920c

📥 Commits

Reviewing files that changed from the base of the PR and between fe96846 and 23c833e.

📒 Files selected for processing (5)
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/sync/interfaces.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.14)
  • GitHub Check: test-netbox (3.12)
🧰 Additional context used
📓 Path-based instructions (8)
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">. Avoid outerHTML swaps; use OOB (Out-of-Band) or targeted innerHTML swaps to keep table layout intact.
Styling assumes Tabler defaults. Removing table-responsive wrappers 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 and fetch() calls must include a CSRF token. The standard pattern is document.querySelector('[name=csrfmiddlewaretoken]').value (from a hidden form input). The import JS also uses getCookie('csrftoken') as a fallback — prefer the hidden input approach for consistency.
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.

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

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

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
Use LibreNMSAPI.get_librenms_id() instead of directly accessing the librenms_id custom field when mapping Devices/VMs to LibreNMS
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
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)
Centralize validation state mutation during import using import_validation_helpers.py for role/cluster/rack assignment, issue removal, and status recalculation
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

Files:

  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/vm_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 in views/base/, Object sync views in views/object_sync/, and Sync action views in views/sync/ with shared mixins from views/mixins.py
New views must extend the closest base class and compose mixins from views/mixins.py (LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, CacheMixin, VlanAssignmentMixin)
All views must inherit LibreNMSPermissionMixin from views/mixins.py with permission_required = PERM_VIEW_PLUGIN
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
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.py
  • netbox_librenms_plugin/views/base/ip_addresses_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 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
Follow sync conventions defined in .github/instructions/sync.instructions.md for sync views, base views, tables, and sync JavaScript

Files:

  • netbox_librenms_plugin/views/sync/interfaces.py
**/views/sync/**/*.py

📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)

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

Files:

  • netbox_librenms_plugin/views/sync/interfaces.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 with CacheMixin keys like librenms_{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.
VlanAssignmentMixin must 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/ip_addresses_view.py
🧠 Learnings (29)
📓 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/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
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 : 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
📚 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.html
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_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/**/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: 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/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/import_utils/vm_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/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.html
  • netbox_librenms_plugin/views/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/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/vm_operations.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.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: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.html
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/vm_operations.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: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/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/views/base/ip_addresses_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 CSRF token must be extracted via `document.querySelector('[name=csrfmiddlewaretoken]').value` for all POST requests.

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} : All HTMX requests and `fetch()` calls must include a CSRF token. The standard pattern is `document.querySelector('[name=csrfmiddlewaretoken]').value` (from a hidden form input). The import JS also uses `getCookie('csrftoken')` as a fallback — prefer the hidden input approach for consistency.

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/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_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 : 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/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_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/views/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/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/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_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/**/*.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/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/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.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/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_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/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`

Applied to files:

  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_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/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.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/views/base/ip_addresses_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/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/views/base/ip_addresses_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 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/views/base/ip_addresses_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/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/views/base/ip_addresses_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/views/base/ip_addresses_view.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/vm_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 **/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/vm_operations.py
🔇 Additional comments (15)
netbox_librenms_plugin/views/sync/interfaces.py (3)

12-24: Good fix: mixin wiring now supports server-aware API access.

Adding LibreNMSAPIMixin to SyncInterfacesView and imports is the right change for consistent self.librenms_api usage in this sync action.


240-243: Good change: librenms_id is now written through the multi-server helper.

Using set_librenms_device_id() with self.librenms_api.server_key (and guarding on port_id is not None) keeps the custom-field format consistent and avoids legacy direct writes.


253-255: Good guard: MAC sync is correctly limited to device interfaces.

Restricting MAC handling to Interface objects avoids invalid relation handling for VM interfaces.

netbox_librenms_plugin/views/base/ip_addresses_view.py (4)

14-14: LGTM!

The import of get_librenms_device_id aligns with the PR's centralized librenms_id utilities approach and supports server-scoped interface lookups.


107-112: LGTM!

The refactored loop computes get_librenms_device_id once per interface (addressing the previous review feedback) and correctly threads server_key for multi-server scoping. Stringifying the ID ensures type-consistent dictionary keys for downstream lookups.


195-196: LGTM!

The str(port_id) conversion ensures type-consistent dictionary lookups, matching the stringified keys built in _prefetch_netbox_data.


211-212: LGTM!

Consistent use of str(port_id) for dictionary lookups, matching the pattern in _enrich_existing_ip and the stringified keys from _prefetch_netbox_data.

netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (2)

45-71: Per-mapping status/link rendering looks solid.

The configured/unconfigured/active states and conditional device_url fallback are clean and resilient.


77-87: Good hardening on stale-mapping removal flow.

Line 80 correctly escapes server_key in JS confirm, and the POST form includes CSRF with the expected server_key payload.

As per coding guidelines, “All HTMX requests and fetch() calls must include a CSRF token”; this form follows the same CSRF protection pattern for POST actions.

netbox_librenms_plugin/import_utils/vm_operations.py (2)

1-1: server_key signature and docs update look correct

This update cleanly exposes multi-server context at VM creation time and keeps backward compatibility via the "default" fallback.

Also applies to: 16-18, 27-27


168-169: server_key propagation through import flow is consistent

Passing api.server_key into both validate_device_for_import() and create_vm_from_librenms() correctly preserves per-server scoping end-to-end.

Also applies to: 213-215

netbox_librenms_plugin/import_utils/virtual_chassis.py (4)

35-40: 1-based cache position normalization looks correct.

This defensively prevents invalid cached position=0 values from re-entering downstream VC logic.


179-227: Parent detection and member position handling are now robust.

Using parent_index is None and enforcing position >= 1 removes previous false-negatives/off-by-one behavior in VC detection and suggested-name generation.


415-455: vc_position collision handling is solid now.

Normalizing discovered positions, tracking used_positions, and persisting chosen_pos closes duplicate-position edge cases during member creation.


462-464: Expected-member warning and rollback behavior are correctly simplified.

The filtered expected-member count is clearer, and relying on transaction.atomic() rollback avoids broken-transaction side effects from manual restoration.

Also applies to: 479-483

Comment on lines +243 to +253
def _load_vc_member_name_pattern() -> str:
"""Load the VC member name pattern from settings, with fallback to default."""
from ..models import LibreNMSSettings

try:
settings = LibreNMSSettings.objects.first()
return settings.vc_member_name_pattern if settings else "-M{position}"
except Exception as e:
logger.warning(f"Could not load VC member name pattern from settings: {e}. Using default.")
return "-M{position}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect LibreNMSSettings model definition =="
fd "models.py" netbox_librenms_plugin --exec cat -n {} \; | head -200

echo
echo "== Search for LibreNMSSettings class and related constraints =="
rg -n "class LibreNMSSettings|vc_member_name_pattern|server_key|UniqueConstraint|Meta:" netbox_librenms_plugin --max-count 50

Repository: marcinpsk/netbox-librenms-plugin

Length of output: 49190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check _load_vc_member_name_pattern context and usage =="
rg -n -B5 -A10 "_load_vc_member_name_pattern" netbox_librenms_plugin

echo
echo "== Check vc_member_name_pattern field definition =="
rg -n "vc_member_name_pattern" netbox_librenms_plugin -B2 -A2

echo
echo "== Check for server-related fields in models =="
rg -n "server|Server" netbox_librenms_plugin/models.py -B1 -A1 | head -80

Repository: marcinpsk/netbox-librenms-plugin

Length of output: 50388


Pass server context to _load_vc_member_name_pattern() to ensure deterministic resolution in multi-server mode.

LibreNMSSettings.objects.first() at line 248 is nondeterministic. The model is not constrained to a singleton, and in multi-server configurations, retrieving the pattern without scoping to the active server risks applying the wrong naming convention. Update the function to accept server_key and resolve the pattern from the active LibreNMSSettings.selected_server context, or use a server-aware settings accessor pattern consistent with other server-keyed lookups in the codebase (e.g., get_librenms_device_id, find_by_librenms_id).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 243 -
253, The _load_vc_member_name_pattern function currently calls
LibreNMSSettings.objects.first() (nondeterministic) — change its signature to
accept a server_key (or server identifier), look up the LibreNMSSettings via the
server-aware accessor (resolve settings.selected_server or the same
server-scoped lookup pattern used by
get_librenms_device_id/find_by_librenms_id), and return
settings.vc_member_name_pattern for that server or the default "-M{position}" if
missing; update all callers to pass the appropriate server_key so VC pattern
resolution is deterministic in multi-server mode.

Comment thread netbox_librenms_plugin/import_utils/vm_operations.py Outdated
Comment thread netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html Outdated
… guard, IP id check, migrate conflict, MAC sync

- virtual_chassis.py: fix 'librenms-None' domain, preload vc_pattern before
  master rename, add order_by('pk') to _load_vc_member_name_pattern
- vm_operations.py: use set_librenms_device_id() instead of inline CF dict
- librenms_sync_base.html: guard librenms_server_info existence before .is_legacy
- ip_addresses_view.py: use 'if lib_id is not None:' instead of 'if lib_id:'
- actions.py: add conflict check for duplicate librenms_id in migrate_librenms_id
- interfaces.py: remove is_device_interface guard from MAC handling,
  add hasattr check so VMInterface is handled safely
- tests: update mocks for order_by().first() chain; update VM creation test
- Check validation resolved_name before raw hostname/sysname so
  newly imported devices are detected even when naming options
  (use_sysname/strip_domain) changed the stored name
- test_view_wiring: smoke tests for mixin/MRO wiring on all sync views
- test_librenms_id: get_librenms_device_id, find_by_librenms_id, migrate_legacy, roundtrip
- test_mixins: LibreNMSAPIMixin lazy init + get_server_info, CacheMixin key generation
- test_sync_interfaces: update_interface_attributes (all branches), handle_mac_address
- test_sync_devices: AddDeviceToLibreNMSView, UpdateDeviceLocationView, field view wiring
- mock_librenms_server: reusable HTTP mock for integration tests
- test_integration_sync: end-to-end API call tests via mock HTTP server

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (1)
netbox_librenms_plugin/import_utils/vm_operations.py (1)

58-69: ⚠️ Potential issue | 🟠 Major

Prevent partially-created VMs when device_id is invalid.

Line 58 writes the VM before Line 68 does int(libre_device["device_id"]). If conversion/key access fails, the function errors after persistence, leaving a created VM while upstream reports failure.

🛠️ Proposed fix
 def create_vm_from_librenms(
     libre_device: dict, validation: dict, use_sysname: bool = True, role=None, server_key: str = "default"
 ):
@@
-    # Create the VM with librenms_id custom field
+    try:
+        parsed_device_id = int(libre_device["device_id"])
+    except (KeyError, TypeError, ValueError) as exc:
+        raise ValueError("Invalid or missing LibreNMS device_id for VM import") from exc
+
+    # Create the VM first, then attach validated librenms_id mapping
     vm = VirtualMachine.objects.create(
         name=vm_name,
         cluster=cluster,
         role=role,  # Optional VM role
         platform=platform,
         comments=f"Imported from LibreNMS by netbox-librenms-plugin on {import_time}",
     )
@@
-    set_librenms_device_id(vm, int(libre_device["device_id"]), server_key)
+    set_librenms_device_id(vm, parsed_device_id, server_key=server_key)
     vm.save()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/vm_operations.py` around lines 58 - 69,
The VM is created before validating/converting libre_device["device_id"], which
can raise and leave a persisted VM; change the flow in vm_operations.py so you
validate and convert libre_device["device_id"] (and verify the key exists)
before calling VirtualMachine.objects.create or, alternatively, wrap the
creation and set_librenms_device_id(vm, int(...), server_key) call in a database
transaction (e.g., Django transaction.atomic) so failures roll back;
specifically, perform the int(libre_device["device_id"]) conversion and any key
existence checks prior to invoking VirtualMachine.objects.create and only call
set_librenms_device_id and vm.save after successful conversion, or wrap create +
set_librenms_device_id in an atomic block to ensure no partial persistence.
♻️ Duplicate comments (1)
netbox_librenms_plugin/tests/test_vm_operations.py (1)

23-33: 🧹 Nitpick | 🔵 Trivial

Consolidate repeated inline mock setup into shared fixtures.

This file still repeats local mock construction patterns across many tests, which increases maintenance overhead and drift risk.

Based on learnings: "Reuse fixtures from tests/conftest.py instead of creating ad-hoc mocks."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/tests/test_vm_operations.py` around lines 23 - 33,
Replace the repeated ad-hoc mocks in test_vm_operations.py (mock_cluster,
mock_platform, validation dict, mock_vm with name/pk) by reusing shared fixtures
defined in tests/conftest.py: create or use fixtures like vm_mock,
platform_mock, cluster_mock and validation_payload (or similar names) and update
tests to accept those fixtures instead of constructing MagicMock instances
inline; ensure the fixture returns the same structure currently expected
(validation containing "can_import", "cluster": {"cluster": cluster_mock},
"platform": {"platform": platform_mock}) and that vm_mock has the name and pk
attributes so existing assertions in the tests continue to work.
🤖 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/bulk_import.py`:
- Around line 214-221: The vc_domain generation uses member_serials built from
str(m.get("serial")) and currently allows placeholder tokens like "-" or
whitespace to slip through; update the member_serials generator in
bulk_import.py (the code that builds member_serials and vc_domain) to cast,
strip, and filter out empty or placeholder values (e.g., serial =
str(m.get("serial") or "").strip(); exclude if not serial or serial == "-")
before sorting/joining so vc_domain only includes real serials and dedup keys
are correct.

In `@netbox_librenms_plugin/tests/mock_librenms_server.py`:
- Around line 70-72: The stop method currently only calls
self._server.shutdown() which doesn't fully release the HTTP server socket or
wait for the background thread; update stop (the method on the mock server) to
call self._server.shutdown(), then self._server.server_close() to close the
listening socket, and finally join the worker thread (e.g.,
self._thread.join(timeout=...) or appropriate thread attribute) so the server
thread is cleaned up before teardown.

In `@netbox_librenms_plugin/tests/test_librenms_id.py`:
- Around line 71-83: Update the test_queries_server_key_and_legacy_integer test
to assert that mock_model.objects.filter was called with a query that includes
both the JSON server-key lookup and the legacy integer fallback: call
find_by_librenms_id(mock_model, 42, "default") and then inspect
mock_model.objects.filter.call_args (or use assert_called_once_with) to verify
the filter argument contains a Q or kwargs expressing custom_fields__server-key
== "42" (string) and librenms_id == 42; reference the find_by_librenms_id
function name and mock_model.objects.filter when locating where to change the
assertion.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1252-1257: The conflict check currently only queries the
JSON-scoped key using server_key and misses legacy integer-stored IDs; update
the check around server_key and conflict so it also looks for legacy integer
collisions (e.g. include an OR condition checking the legacy field name
"custom_field_data__librenms_id" against the integer form of cf_locked when
cf_locked is numeric). Ensure you still exclude locked_device.pk and use
.exists() as before; convert cf_locked to int safely (or guard non-numeric
values) and combine the two filters (JSON-scoped key and legacy integer field)
using a Q() OR so duplicate ownership is detected in both storage formats.

---

Outside diff comments:
In `@netbox_librenms_plugin/import_utils/vm_operations.py`:
- Around line 58-69: The VM is created before validating/converting
libre_device["device_id"], which can raise and leave a persisted VM; change the
flow in vm_operations.py so you validate and convert libre_device["device_id"]
(and verify the key exists) before calling VirtualMachine.objects.create or,
alternatively, wrap the creation and set_librenms_device_id(vm, int(...),
server_key) call in a database transaction (e.g., Django transaction.atomic) so
failures roll back; specifically, perform the int(libre_device["device_id"])
conversion and any key existence checks prior to invoking
VirtualMachine.objects.create and only call set_librenms_device_id and vm.save
after successful conversion, or wrap create + set_librenms_device_id in an
atomic block to ensure no partial persistence.

---

Duplicate comments:
In `@netbox_librenms_plugin/tests/test_vm_operations.py`:
- Around line 23-33: Replace the repeated ad-hoc mocks in test_vm_operations.py
(mock_cluster, mock_platform, validation dict, mock_vm with name/pk) by reusing
shared fixtures defined in tests/conftest.py: create or use fixtures like
vm_mock, platform_mock, cluster_mock and validation_payload (or similar names)
and update tests to accept those fixtures instead of constructing MagicMock
instances inline; ensure the fixture returns the same structure currently
expected (validation containing "can_import", "cluster": {"cluster":
cluster_mock}, "platform": {"platform": platform_mock}) and that vm_mock has the
name and pk attributes so existing assertions in the tests continue to work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: be88727e-1cc5-47e5-99cf-4c9f051b9d60

📥 Commits

Reviewing files that changed from the base of the PR and between 23c833e and 0673e6b.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • .github/dependabot.yml
  • .pre-commit-config.yaml
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/mock_librenms_server.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/tests/test_librenms_id.py
  • netbox_librenms_plugin/tests/test_mixins.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/tests/test_sync_interfaces.py
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • pyproject.toml
📜 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.13)
  • GitHub Check: test-netbox (3.14)
🧰 Additional context used
📓 Path-based instructions (9)
netbox_librenms_plugin/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

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
Use LibreNMSAPI.get_librenms_id() instead of directly accessing the librenms_id custom field when mapping Devices/VMs to LibreNMS
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
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)
Centralize validation state mutation during import using import_validation_helpers.py for role/cluster/rack assignment, issue removal, and status recalculation
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

Files:

  • netbox_librenms_plugin/tests/test_mixins.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/tests/mock_librenms_server.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/tests/test_librenms_id.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/tests/test_sync_interfaces.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 in views/base/, Object sync views in views/object_sync/, and Sync action views in views/sync/ with shared mixins from views/mixins.py
New views must extend the closest base class and compose mixins from views/mixins.py (LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, CacheMixin, VlanAssignmentMixin)
All views must inherit LibreNMSPermissionMixin from views/mixins.py with permission_required = PERM_VIEW_PLUGIN
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
Use _get_safe_redirect_url(request) to validate referrer URLs in permission checks to prevent open-redirect attacks

Files:

  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Job cancellation flow: (1) Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job, (2) Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database, (3) Frontend polling detects status changes and redirects appropriately
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status, update modal messages based on status transitions (queued, started, finished, stopped, failed), handle all RQ status values explicitly to avoid infinite polling, and use cancelInProgress flag to prevent polling interference during cancellation
NetBox's /api/core/background-tasks/ endpoint requires superuser (IsSuperuser in BaseRQViewSet); non-superuser users cannot poll job status and get 403 Forbidden. The plugin must automatically fall back to synchronous mode for non-superusers via should_use_background_job() in list.py and actions.py
Custom sync endpoint api/views.py::sync_job_status() must sync database Job status with RQ job status, needed because NetBox worker doesn't always update DB when jobs stop before processing starts
Import page supports synchronous mode (calls process_device_filters() directly, renders results inline) and background mode (enqueues FilterDevicesJob, returns JsonResponse with job_id/job_pk/poll_url, frontend polls and redirects on completion)
Result loading via _load_job_results(job_id) must read job.data['device_ids'] and reconstruct devices from per-device cache using get_validated_device_cache_key()
Import filter fields must include: librenms_location, librenms_type, librenms_os, librenms_hostname, librenms_sysname, librenms_hardware, enable_vc_detection, show_disabled, exclude_existing
DeviceImportHelperMixin must provide get_validated_device_with_selections() and render_device_row() for HTMX row rendering, shared by update views
BulkImportConfirmView (POST) must render confirmation modal with selected device...

Files:

  • netbox_librenms_plugin/views/imports/actions.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">. Avoid outerHTML swaps; use OOB (Out-of-Band) or targeted innerHTML swaps to keep table layout intact.
Styling assumes Tabler defaults. Removing table-responsive wrappers 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 and fetch() calls must include a CSRF token. The standard pattern is document.querySelector('[name=csrfmiddlewaretoken]').value (from a hidden form input). The import JS also uses getCookie('csrftoken') as a fallback — prefer the hidden input approach for consistency.
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.

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

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/views/sync/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

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
Follow sync conventions defined in .github/instructions/sync.instructions.md for sync views, base views, tables, and sync JavaScript

Files:

  • netbox_librenms_plugin/views/sync/interfaces.py
**/views/sync/**/*.py

📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)

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

Files:

  • netbox_librenms_plugin/views/sync/interfaces.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 with CacheMixin keys like librenms_{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.
VlanAssignmentMixin must 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/ip_addresses_view.py
🧠 Learnings (62)
📚 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/tests/test_mixins.py
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/views/base/ip_addresses_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/tests/test_mixins.py
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/tests/test_mixins.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_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/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_mixins.py
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/tests/mock_librenms_server.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/tests/test_librenms_id.py
  • netbox_librenms_plugin/tests/test_sync_interfaces.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_mixins.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/tests/mock_librenms_server.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/tests/test_librenms_id.py
  • 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/views/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`

Applied to files:

  • netbox_librenms_plugin/tests/test_mixins.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_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/tests/test_mixins.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/tests/mock_librenms_server.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/tests/test_mixins.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/tests/test_sync_interfaces.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_mixins.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/tests/test_librenms_id.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • 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/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/tests/test_mixins.py
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/views/base/ip_addresses_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|**/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/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 : 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/imports/actions.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/*.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/views/imports/actions.py
  • 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/views/imports/actions.py
  • 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 **/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/imports/actions.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/views/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/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/imports/actions.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/tests/test_sync_interfaces.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/imports/actions.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/imports/actions.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/tests/test_librenms_id.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/imports/actions.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/base/ip_addresses_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/views/imports/actions.py
  • netbox_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: 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/imports/actions.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/views/imports/actions.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/views/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 : 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/views/imports/actions.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/views/base/ip_addresses_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/imports/actions.py
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/views/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/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/views/imports/actions.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 : Background jobs must use standalone helpers from `import_utils.py` (`check_user_permissions`, `require_permissions`) instead of view mixins; non-superusers fall back to synchronous mode

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.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/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/** : `DeviceImportHelperMixin` must provide `get_validated_device_with_selections()` and `render_device_row()` for HTMX row rendering, shared by update views

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.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_validation_helpers.py : Validation helpers must include: `fetch_model_by_id()`, `extract_device_selections()` for reading form data

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.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/imports/actions.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/imports/actions.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/import_utils/bulk_import.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.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: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 CSRF token must be extracted via `document.querySelector('[name=csrfmiddlewaretoken]').value` for all POST requests.

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} : All HTMX requests and `fetch()` calls must include a CSRF token. The standard pattern is `document.querySelector('[name=csrfmiddlewaretoken]').value` (from a hidden form input). The import JS also uses `getCookie('csrftoken')` as a fallback — prefer the hidden input approach for consistency.

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/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.html
  • netbox_librenms_plugin/tests/test_view_wiring.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/views/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 **/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/**/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: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/tests/test_view_wiring.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: All base view classes must follow the three-layer architecture: Base views define abstract data pipelines via `get_*_context()` methods, object sync views wire base views to NetBox models using `register_model_view()`, and sync action views handle POST requests with permissions checks and transactional updates.

Applied to files:

  • netbox_librenms_plugin/tests/test_view_wiring.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_view_wiring.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/tests/test_sync_devices.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/vm_operations.py
  • netbox_librenms_plugin/tests/test_vm_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 **/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/vm_operations.py
  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/import_utils/bulk_import.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_vm_operations.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/tests/mock_librenms_server.py
  • netbox_librenms_plugin/tests/test_integration_sync.py
  • netbox_librenms_plugin/tests/test_librenms_id.py
  • netbox_librenms_plugin/tests/test_sync_interfaces.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_vm_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 : Never use `RequestFactory`—mock request objects directly or test method logic in isolation.

Applied to files:

  • netbox_librenms_plugin/tests/test_vm_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 **/views/imports/** : Result loading via `_load_job_results(job_id)` must read `job.data['device_ids']` and reconstruct devices from per-device cache using `get_validated_device_cache_key()`

Applied to files:

  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/import_utils/bulk_import.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,**/import_utils.py : Both synchronous and background modes must use `get_validated_device_cache_key()` from `import_utils.py` to generate cache keys, ensuring `_load_job_results()` in the list view can retrieve devices regardless of which mode produced them. Never hardcode cache key formats; always use the helper functions

Applied to files:

  • netbox_librenms_plugin/tests/test_vm_operations.py
  • netbox_librenms_plugin/import_utils/bulk_import.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/**,**/import_utils.py : Recognize database Job status values as `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)

Applied to files:

  • netbox_librenms_plugin/tests/test_vm_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 **/jobs.py,**/views/imports/** : Check `rq_job.is_stopped` or `rq_job.is_failed` flags in Redis for cancellation detection, not database status

Applied to files:

  • netbox_librenms_plugin/tests/test_vm_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/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.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/views/base/ip_addresses_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/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/views/base/ip_addresses_view.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/bulk_import.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/bulk_import.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_validation_helpers.py : Validation state mutation helpers in `import_validation_helpers.py` must include: `apply_role_to_validation()`, `apply_cluster_to_validation()`, `apply_rack_to_validation()` to update validation state when user selects role/cluster/rack

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/import_utils/bulk_import.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 : `fetch_device_with_cache(device_id, ...)` must retrieve and cache individual device data

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/bulk_import.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 : `FilterDevicesJob` background job: filters devices with VC detection. `job.data` keys must include `device_ids`, `total_processed`, `filters`, `server_key`, `vc_detection_enabled`, `cache_timeout`, `cached_at`, `completed`. Devices are cached individually via shared cache keys from `get_validated_device_cache_key()`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/** : Import page supports synchronous mode (calls `process_device_filters()` directly, renders results inline) and background mode (enqueues `FilterDevicesJob`, returns `JsonResponse` with `job_id`/`job_pk`/`poll_url`, frontend polls and redirects on completion)

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.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/tests/test_sync_interfaces.py

Comment on lines +214 to +221
member_serials = sorted(
str(m.get("serial"))
for m in vc_data.get("members", [])
if m.get("serial") is not None and m.get("serial") != ""
)
vc_domain = (
f"librenms-stack-{','.join(member_serials)}" if member_serials else f"librenms-{device_id}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exclude placeholder/empty serial tokens from VC dedup keys.

Line 214 currently includes placeholder-like values (e.g., "-" or whitespace variants after casting). That can make unrelated stacks share the same vc_domain, causing later VCs to be skipped incorrectly.

🛠️ Proposed fix
-                    member_serials = sorted(
-                        str(m.get("serial"))
-                        for m in vc_data.get("members", [])
-                        if m.get("serial") is not None and m.get("serial") != ""
-                    )
+                    member_serials = sorted(
+                        s
+                        for m in vc_data.get("members", [])
+                        for s in [str(m.get("serial")).strip()]
+                        if s not in {"", "-"}
+                    )
                     vc_domain = (
                         f"librenms-stack-{','.join(member_serials)}" if member_serials else f"librenms-{device_id}"
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 214 - 221,
The vc_domain generation uses member_serials built from str(m.get("serial")) and
currently allows placeholder tokens like "-" or whitespace to slip through;
update the member_serials generator in bulk_import.py (the code that builds
member_serials and vc_domain) to cast, strip, and filter out empty or
placeholder values (e.g., serial = str(m.get("serial") or "").strip(); exclude
if not serial or serial == "-") before sorting/joining so vc_domain only
includes real serials and dedup keys are correct.

Comment on lines +70 to +72
def stop(self):
self._server.shutdown()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Close the HTTP server socket and join the worker thread on teardown.

shutdown() alone does not fully release resources. This can leave background threads/sockets around and create intermittent test instability.

🛠️ Proposed fix
     def stop(self):
-        self._server.shutdown()
+        self._server.shutdown()
+        self._server.server_close()
+        if self._thread.is_alive():
+            self._thread.join(timeout=1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/tests/mock_librenms_server.py` around lines 70 - 72,
The stop method currently only calls self._server.shutdown() which doesn't fully
release the HTTP server socket or wait for the background thread; update stop
(the method on the mock server) to call self._server.shutdown(), then
self._server.server_close() to close the listening socket, and finally join the
worker thread (e.g., self._thread.join(timeout=...) or appropriate thread
attribute) so the server thread is cleaned up before teardown.

Comment on lines +71 to +83
def test_queries_server_key_and_legacy_integer(self):
from unittest.mock import MagicMock
from netbox_librenms_plugin.utils import find_by_librenms_id

mock_model = MagicMock()
mock_qs = MagicMock()
mock_model.objects.filter.return_value = mock_qs
mock_qs.first.return_value = None

find_by_librenms_id(mock_model, 42, "default")

mock_model.objects.filter.assert_called_once()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Strengthen this assertion to match the test’s intent.

Right now this only proves filter() was called, not that it includes both server-key JSON lookup and legacy integer fallback.

🔍 Proposed test hardening
     find_by_librenms_id(mock_model, 42, "default")

-    mock_model.objects.filter.assert_called_once()
+    mock_model.objects.filter.assert_called_once()
+    args, kwargs = mock_model.objects.filter.call_args
+    assert not kwargs
+    # Verify query shape includes both server-key JSON lookup and legacy integer fallback
+    query_repr = str(args[0])
+    assert "custom_field_data__librenms_id__default" in query_repr
+    assert "custom_field_data__librenms_id" in query_repr
🤖 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 71 - 83,
Update the test_queries_server_key_and_legacy_integer test to assert that
mock_model.objects.filter was called with a query that includes both the JSON
server-key lookup and the legacy integer fallback: call
find_by_librenms_id(mock_model, 42, "default") and then inspect
mock_model.objects.filter.call_args (or use assert_called_once_with) to verify
the filter argument contains a Q or kwargs expressing custom_fields__server-key
== "42" (string) and librenms_id == 42; reference the find_by_librenms_id
function name and mock_model.objects.filter when locating where to change the
assertion.

Comment on lines +1252 to +1257
server_key = self.librenms_api.server_key
conflict = (
Device.objects.filter(**{f"custom_field_data__librenms_id__{server_key}": cf_locked})
.exclude(pk=locked_device.pk)
.exists()
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Legacy-ID migration conflict check misses legacy integer collisions.

Line 1252 only checks JSON-scoped mappings. If another device still stores the same ID in legacy integer format, this migration can still create duplicate ownership.

✅ Proposed fix
-                conflict = (
-                    Device.objects.filter(**{f"custom_field_data__librenms_id__{server_key}": cf_locked})
-                    .exclude(pk=locked_device.pk)
-                    .exists()
-                )
+                from django.db.models import Q
+
+                conflict = (
+                    Device.objects.filter(
+                        Q(**{f"custom_field_data__librenms_id__{server_key}": cf_locked})
+                        | Q(custom_field_data__librenms_id=cf_locked)  # legacy bare-int records
+                    )
+                    .exclude(pk=locked_device.pk)
+                    .exists()
+                )
🤖 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 1252 - 1257,
The conflict check currently only queries the JSON-scoped key using server_key
and misses legacy integer-stored IDs; update the check around server_key and
conflict so it also looks for legacy integer collisions (e.g. include an OR
condition checking the legacy field name "custom_field_data__librenms_id"
against the integer form of cf_locked when cf_locked is numeric). Ensure you
still exclude locked_device.pk and use .exists() as before; convert cf_locked to
int safely (or guard non-numeric values) and combine the two filters
(JSON-scoped key and legacy integer field) using a Q() OR so duplicate ownership
is detected in both storage formats.

@marcinpsk marcinpsk closed this Mar 6, 2026
marcinpsk added a commit that referenced this pull request Mar 28, 2026
- #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)
marcinpsk added a commit that referenced this pull request Jun 24, 2026
…resolve perf

Five findings from the second /code-review sweep:

- _interface_sync_content.html: gate the migrated "Move" action on
  has_write_permission. A read-only user otherwise got a live HTMX POST button
  that can only fail at the permission gate; now show muted "read-only" text.
- librenms_sync_view._build_all_server_mappings: coerce the host id (and the OOB
  fallback id) via coerce_librenms_id, so an entry whose "id" is a corrupt
  non-None value (0/blank) but whose "oob.id" is valid still surfaces as an
  OOB-only mapping instead of being dropped by the `<= 0` guard (the user could
  not otherwise see/remove it).
- find_by_librenms_id: fast-path a single combined query for the common 0/1-match
  case (runs per-port during sync); fall through to the separate host/OOB
  predicates only when >=2 rows match, to classify and fail closed on the precise
  ambiguity. Behaviour unchanged.
- interfaces_view / cables_view / modules_view: reuse the sync device the request
  handler already resolved (passed through get_context_data / get_links_data /
  _build_context) instead of re-resolving it a second time per refresh.
- modules_view.get_context_data: rebind to the active server from the request
  query so the GET cache read + OOB fingerprint key on the same server post()
  cached under (a non-default-server tab otherwise cache-missed → empty table).

Red->green tests for the move-button gate, OOB-mapping visibility, the single-
query fast-path, and the GET rebind. (#15 is a transparent device-reuse passthrough
covered by the existing context tests + the full suite.)

Claude-Session: https://claude.ai/code/session_01RKRVyWizgrukFSHmTF168J
marcinpsk added a commit that referenced this pull request Jul 3, 2026
…resolve perf

Five findings from the second /code-review sweep:

- _interface_sync_content.html: gate the migrated "Move" action on
  has_write_permission. A read-only user otherwise got a live HTMX POST button
  that can only fail at the permission gate; now show muted "read-only" text.
- librenms_sync_view._build_all_server_mappings: coerce the host id (and the OOB
  fallback id) via coerce_librenms_id, so an entry whose "id" is a corrupt
  non-None value (0/blank) but whose "oob.id" is valid still surfaces as an
  OOB-only mapping instead of being dropped by the `<= 0` guard (the user could
  not otherwise see/remove it).
- find_by_librenms_id: fast-path a single combined query for the common 0/1-match
  case (runs per-port during sync); fall through to the separate host/OOB
  predicates only when >=2 rows match, to classify and fail closed on the precise
  ambiguity. Behaviour unchanged.
- interfaces_view / cables_view / modules_view: reuse the sync device the request
  handler already resolved (passed through get_context_data / get_links_data /
  _build_context) instead of re-resolving it a second time per refresh.
- modules_view.get_context_data: rebind to the active server from the request
  query so the GET cache read + OOB fingerprint key on the same server post()
  cached under (a non-default-server tab otherwise cache-missed → empty table).

Red->green tests for the move-button gate, OOB-mapping visibility, the single-
query fast-path, and the GET rebind. (#15 is a transparent device-reuse passthrough
covered by the existing context tests + the full suite.)
marcinpsk added a commit that referenced this pull request Jul 16, 2026
…resolve perf

Five findings from the second /code-review sweep:

- _interface_sync_content.html: gate the migrated "Move" action on
  has_write_permission. A read-only user otherwise got a live HTMX POST button
  that can only fail at the permission gate; now show muted "read-only" text.
- librenms_sync_view._build_all_server_mappings: coerce the host id (and the OOB
  fallback id) via coerce_librenms_id, so an entry whose "id" is a corrupt
  non-None value (0/blank) but whose "oob.id" is valid still surfaces as an
  OOB-only mapping instead of being dropped by the `<= 0` guard (the user could
  not otherwise see/remove it).
- find_by_librenms_id: fast-path a single combined query for the common 0/1-match
  case (runs per-port during sync); fall through to the separate host/OOB
  predicates only when >=2 rows match, to classify and fail closed on the precise
  ambiguity. Behaviour unchanged.
- interfaces_view / cables_view / modules_view: reuse the sync device the request
  handler already resolved (passed through get_context_data / get_links_data /
  _build_context) instead of re-resolving it a second time per refresh.
- modules_view.get_context_data: rebind to the active server from the request
  query so the GET cache read + OOB fingerprint key on the same server post()
  cached under (a non-default-server tab otherwise cache-missed → empty table).

Red->green tests for the move-button gate, OOB-mapping visibility, the single-
query fast-path, and the GET rebind. (#15 is a transparent device-reuse passthrough
covered by the existing context tests + the full suite.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant