Refactor/librenms - #12
Conversation
Break the monolithic import_utils.py into focused modules: - permissions.py: user permission checks - cache.py: cache key generation and management - filters.py: device filtering and retrieval from LibreNMS - virtual_chassis.py: VC detection, creation, member management - device_operations.py: device validation, import, and fetch - vm_operations.py: VM creation and bulk import - bulk_import.py: bulk device import orchestration and filter processing The __init__.py re-exports all public names, so existing callers (views, jobs, tests) continue working without import changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After splitting import_utils.py into a package, mock.patch decorators must target the actual submodule where each name is looked up, not the package __init__.py. Update all patch paths in test_import_utils.py and test_permissions.py to point to the correct submodules.
Keep Cluster as a top-level import so mock.patch targets can resolve it. Remove the redundant inline import inside validate_device_for_import.
Now that Cluster is a module-level import in device_operations, the test only needs to mock device_operations.Cluster — the extra patch on virtualization.models.Cluster was leftover from the inline-import era and caused the mock to target the wrong object.
bonzo81#227) - Add use_sysname/strip_domain params to validate_device_for_import - Store resolved name in validation result as resolved_name - Update import_single_device, bulk_import_devices_shared, bulk_import_vms to extract and pass naming prefs from sync_options - Add _resolve_naming_preferences() helper in actions.py with fallback chain: POST data -> user pref -> LibreNMSSettings -> plugin default - Update DeviceImportHelperMixin and BulkImportConfirmView to pass prefs - Replace _determine_device_name() in BulkImportConfirmView with resolved_name - Add TestDeviceNamingPreferences (5 tests) to test_import_utils.py
📝 WalkthroughWalkthroughThe monolithic import utilities were split into a package of focused submodules (cache, filters, device_operations, bulk_import, vm_operations, virtual_chassis, permissions) with a re-exporting init.py; librenms_id handling became server-scoped with migration utilities; views, tables, JS, templates, jobs, and tests updated to propagate server_key and naming preferences and to add conflict/name-update flows. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant View as BulkImportConfirmView
participant Filter as process_device_filters
participant Cache as DjangoCache
participant API as LibreNMSAPI
participant Validator as validate_device_for_import
participant NetBox as NetBoxDB
participant Job
User->>View: POST bulk import request
View->>Filter: process_device_filters(api, filters, vc_detection, job, use_sysname, strip_domain)
Filter->>Cache: lookup cache metadata
alt cached
Cache-->>Filter: cached device list
else not cached
Filter->>API: list_devices(api_filters)
API-->>Filter: devices
Filter->>Cache: store devices & metadata
end
Filter-->>View: validated_devices
loop per device
View->>API: fetch_device_with_cache(device_id)
API-->>View: libre_device
View->>Validator: validate_device_for_import(libre_device, use_sysname, strip_domain)
Validator->>NetBox: query by librenms_id/hostname/serial/IP
NetBox-->>Validator: existing matches (or none)
Validator-->>View: validation result
alt importable
View->>View: import_single_device(...)
View->>NetBox: create/update Device/VM
View-->>Job: update progress
else skip/failure
View-->>Job: log failure/skip
end
end
View-->>User: import summary (success/failed/skipped)
sequenceDiagram
participant Browser
participant View as DeviceConflictActionView
participant Validator as validate_device_for_import
participant Utils as utils.set_librenms_device_id
participant Cache as DjangoCache
participant NetBox as NetBoxDB
Browser->>View: POST conflict action (link/sync/update...)
View->>Validator: validate_device_for_import(libre_device)
Validator->>NetBox: lookup existing by librenms_id/hostname/serial
NetBox-->>Validator: validation result
alt link action
View->>Utils: set_librenms_device_id(device, librenms_id, server_key)
Utils-->>NetBox: persist custom_field_data
else sync/update action
View->>NetBox: update fields (name/serial/type/platform)
end
View->>Cache: invalidate import cache for device
View->>Validator: re-validate device
Validator-->>View: updated validation
View-->>Browser: return updated row HTML (HTMX)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 18
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)
501-507: 🧹 Nitpick | 🔵 TrivialDeduplicate the repeated cache build loop.
The same
parsed_ids -> fetch_device_with_cache()loop is duplicated for background and synchronous paths. Extract once and reuse.Also applies to: 563-569
🤖 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 501 - 507, Duplicate loops building libre_devices_cache by iterating parsed_ids and calling fetch_device_with_cache should be consolidated: extract the cache-building logic into a single helper (e.g., build_libre_devices_cache(parsed_ids, librenms_api)) or a small private method used by both the background and synchronous branches, so both branches reuse the same libre_devices_cache population instead of repeating the for-device_id -> fetch_device_with_cache(...) loop; ensure the helper returns the libre_devices_cache dict and replace the duplicated loops at the locations around the existing libre_devices_cache usage (including the other occurrence around lines 563-569) with calls to that helper.
🤖 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 380-383: process_device_filters currently returns [] on several
early-exit exception/interrupt paths which breaks callers that expect the
(devices, from_cache) tuple when return_cache_status=True; update every
early-return branch within process_device_filters (the except/if request blocks
that log client disconnects and cancellations) to return an appropriate
two-tuple: an empty devices list and the from_cache boolean (e.g., ([], False)
or ([], from_cache) depending on context) so callers always receive (devices,
from_cache); ensure the change covers all similar branches referencing request
and logger inside process_device_filters.
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 65-66: The cache key "librenms_locations_choices" is global and
can mix labels across LibreNMS servers; update the key construction where
location_cache_key is defined/used (the variable named location_cache_key and
the cache.get/cache.set calls that use it, e.g., the cached_locations lookup) to
include the server-specific identifier (server_key) so the key becomes unique
per server (for example by interpolating or concatenating server_key into the
key string) and ensure all reads/writes use the new server-scoped key.
- Around line 134-136: The current cache key uses Python's built-in hash() which
is process-randomized; change the computed filter_hash to a deterministic
SHA-256 of the filters by serializing filters (or filters.items()) with
canonical JSON (json.dumps(..., sort_keys=True, separators=(',',':'))), then
compute hashlib.sha256(serialized.encode()).hexdigest() (optionally truncating
for length) and use that value in the return string (the same
"validated_device_{server_key}_{filter_hash}_{device_id}_{vc_part}"
construction); update any references to filter_hash in this function so all
workers generate identical keys.
- Around line 74-78: The metadata parsing around cached_at should be made
robust: wrap the datetime.fromisoformat/age calculation (the cached_at,
cache_timeout, now, age_seconds, remaining_seconds logic) in a try/except that
catches TypeError and ValueError, logs or skips the malformed entry, and
continues the loop; additionally, normalize naive datetimes by checking if the
parsed cached_at is timezone-naive and, if so, make it timezone-aware (e.g.,
attach timezone.utc) before subtracting from now to avoid TypeError from
mismatched tzinfo. Ensure the code in the loop that computes age_seconds and
remaining_seconds for a given metadata entry gracefully skips any bad or missing
cached_at instead of propagating an exception.
In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 174-175: The cache key construction uses the raw server_key and
Python's builtin hash(), which is unstable and can collide; replace that with a
deterministic server identifier and a stable digest of the filters: derive a
stable_server_key = server_key if server_key is not None else getattr(api,
"server_key", getattr(api, "base_url", None)) and compute api_digest and
client_digest using hashlib.sha256(json.dumps(api_filters,
sort_keys=True).encode()).hexdigest() (and same for client_filters), then build
cache_key =
f"librenms_devices_import_{stable_server_key}_{api_digest}_{client_digest}";
update references to cache_key and keep from_cache logic unchanged.
In `@netbox_librenms_plugin/import_utils/permissions.py`:
- Around line 3-7: Remove the unused module-level logger by deleting the "import
logging" and the "logger = logging.getLogger(__name__)" line so only the
required import (django.core.exceptions.PermissionDenied) remains; this
eliminates the unused symbol "logger" from the module (check at top-level where
"logger" is defined).
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 257-263: The code currently calls LibreNMSSettings.objects.first()
inside a hot path to compute pattern on every member-name generation; instead
resolve and cache the pattern once and reuse it. Move the settings lookup and
try/except out of the per-member code (e.g., perform it at module load or once
when starting VC processing), store the result in a module-level constant like
VC_MEMBER_NAME_PATTERN (falling back to "-M{position}" on error and logging via
logger), and update the member-name generation function to use that cached
VC_MEMBER_NAME_PATTERN rather than calling LibreNMSSettings.objects.first() each
time.
- Around line 178-179: Change the falsy check on parent_index to an explicit
None check so numeric zero isn't treated as missing: replace the condition "if
not parent_index:" with "if parent_index is None:" in the function/method where
parent_index is evaluated (look for the parent_index variable usage in
virtual_chassis-related functions in virtual_chassis.py) to ensure 0 is handled
as a valid identifier.
In `@netbox_librenms_plugin/tables/device_status.py`:
- Around line 483-497: The icon-only button lacks an accessible name; in the
buttons.append construction (the block building the <button> using btn_class,
btn_icon, btn_label, btn_title, details_url and device_id) add an aria-label
attribute when btn_label is empty — e.g., if btn_label == "" set
aria-label="{btn_title}" on the button element (but do not duplicate when
btn_label is present) so the icon-only control has an explicit accessible name.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 100-101: Update all anchor tags that open links in a new tab to
include rel="noopener noreferrer": find the <a> elements using target="_blank"
in this template (e.g., the anchor rendering href="{{ existing_device_url }}"
with text "{{ validation.existing_device.name }}") and add rel="noopener
noreferrer"; repeat the same change for the other target="_blank" anchors noted
in the comment (the anchors around the other URL/context occurrences mentioned)
so every new-tab link uses rel="noopener noreferrer".
In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 2052-2060: The test patches dcim.models.Platform but
DeviceConflictActionView.post() resolves platforms through
netbox_librenms_plugin.utils.find_matching_platform for the "sync_platform"
action, so update the test to patch the actual resolution point: patch
netbox_librenms_plugin.utils.find_matching_platform (or the corresponding import
used by import_utils/process handling) to return the mock_platform instead of
patching dcim.models.Platform; keep existing mocks for
DeviceConflictActionView.get_validated_device_with_selections and
render_device_row, and ensure the patched find_matching_platform is used by
DeviceConflictActionView.post when exercising the sync_platform path.
In `@netbox_librenms_plugin/utils.py`:
- Around line 49-53: The write to obj.custom_field_data["librenms_id"] assumes
cf_value is int or dict and can crash on other types; update the logic around
cf_value (the block that reads obj.custom_field_data.get("librenms_id") and
writes back using server_key and device_id) to defensively handle unexpected
types by: if cf_value is int convert to {"default": cf_value}, if cf_value is
dict use it, otherwise replace with a new dict (optionally preserving a
string/iterable by storing under "legacy" or discarding) before assigning
cf_value[server_key] = device_id and writing
obj.custom_field_data["librenms_id"] = cf_value; include a warning/log when
coercion occurs to aid debugging.
- Around line 56-69: The current find_by_librenms_id only checks the nested JSON
key custom_field_data__librenms_id__{server_key} and thus misses legacy records
that stored librenms_id as a top-level key (integer) in custom_field_data;
update find_by_librenms_id to perform an OR query using django.db.models.Q to
search both f"custom_field_data__librenms_id__{server_key}" == librenms_id and
"custom_field_data__librenms_id" == librenms_id (and coerce librenms_id to
int/str if needed), import Q, and return .first() of that combined queryset so
legacy integer records are matched.
In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Line 85: The current Device lookup (e.g., the expression using
Device.objects.get(**{f"custom_field_data__librenms_id__{server_key}":
remote_device_id})) only matches the JSON-per-server format and drops
compatibility with legacy integer librenms_id values; update all lookup sites
(the occurrences around the shown line and at the other locations called out) to
use a combined lookup that checks either the JSON path OR the legacy integer
form (for example using Q(...) | Q(...)) or, better, centralize this logic in a
helper and call LibreNMSAPI.get_librenms_id to resolve the mapping instead of
directly touching custom_field_data; ensure both device and interface resolution
code uses this helper/combined-Q pattern so partially migrated datasets still
match.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 897-899: The POST toggle reads (use_sysname and strip_domain)
currently default to False when the keys are absent, bypassing the earlier
naming-preference fallback; change the assignment so each toggle only overrides
the existing preference when the key is present in request.POST (e.g. if
"use-sysname-toggle" in request.POST: use_sysname =
request.POST.get("use-sysname-toggle") == "on", otherwise keep the
previously-determined use_sysname), and do the same for "strip-domain-toggle";
apply this fix to the other occurrences that set these flags (the blocks that
call _determine_device_name with use_sysname/strip_domain around the other
locations referenced).
- Around line 791-800: The code currently sets device_type_synced=True even when
librenms_hardware exists but match_librenms_hardware_to_device_type() returns no
match; update the logic in the block handling librenms_hardware (and the
analogous block at lines 802-813) to set device_type_synced=False when
hw_match.get("matched") is False or when librenms_hardware is present but
hw_match lacks a device_type, and only set librenms_device_type and leave
device_type_synced True when a valid hw_match["device_type"] is found and
matches existing_device.device_type.pk; use the existing identifiers
(librenms_hardware, hw_match, match_librenms_hardware_to_device_type,
librenms_device_type, existing_device.device_type, device_type_synced) so the
check reliably flips to False whenever hardware exists but no mapping is found.
- Around line 893-900: Validate that librenms_id (from
libre_device.get("device_id")) is present and a numeric string before calling
int(librenms_id); if it's missing or not an integer, handle it gracefully (e.g.,
raise a BadRequest/return a 400, log the invalid payload, or skip the link
action) instead of letting int() raise. Update all places that call
int(librenms_id) (the code that reads librenms_id and calls
set_librenms_device_id and any similar casts around the link/update flows) to
perform the check and only call set_librenms_device_id(existing_device,
int(librenms_id), self.librenms_api.server_key) when validation passes,
otherwise return or handle the error path consistently.
In `@netbox_librenms_plugin/views/sync/interfaces.py`:
- Line 238: The call to set_librenms_device_id is persisting a None port_id and
can overwrite existing mappings; change the code around
set_librenms_device_id(interface, librenms_interface.get("port_id"),
self.librenms_api.server_key) to first read port_id =
librenms_interface.get("port_id"), then only call set_librenms_device_id when
port_id is not None/empty (and otherwise skip or log/return) so you never
persist a null librenms_id for the given interface and server key.
---
Outside diff comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 501-507: Duplicate loops building libre_devices_cache by iterating
parsed_ids and calling fetch_device_with_cache should be consolidated: extract
the cache-building logic into a single helper (e.g.,
build_libre_devices_cache(parsed_ids, librenms_api)) or a small private method
used by both the background and synchronous branches, so both branches reuse the
same libre_devices_cache population instead of repeating the for-device_id ->
fetch_device_with_cache(...) loop; ensure the helper returns the
libre_devices_cache dict and replace the duplicated loops at the locations
around the existing libre_devices_cache usage (including the other occurrence
around lines 563-569) with calls to that helper.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (30)
netbox_librenms_plugin/import_utils.pynetbox_librenms_plugin/import_utils/__init__.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/permissions.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_librenms_api.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/views/imports/__init__.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/sync/interfaces.py
💤 Files with no reviewable changes (1)
- netbox_librenms_plugin/import_utils.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.13)
🧰 Additional context used
📓 Path-based instructions (6)
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/permissions.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/imports/__init__.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/import_utils/__init__.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/tests/test_librenms_api.pynetbox_librenms_plugin/tests/test_import_utils.py
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Sync pages should extend
librenms_sync_base.html.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/__init__.py
🧠 Learnings (25)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. 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_import.htmlnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/imports/__init__.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/import_utils/__init__.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to tables/**/*.py : Prefer updating the table renderer in `tables/*.py` rather than templates when changing row actions, since tables emit HTMX-enabled columns and buttons
Applied to files:
netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Tables drive most UIs via `tables/*.py` renderers that emit HTMX-enabled columns and buttons. Prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.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/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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/device_operations.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/__init__.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/import_utils/__init__.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/urls.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/**/*.py : Views should follow layered structure: extend the closest base class from `views/base/` and compose mixins like `LibreNMSAPIMixin` and `CacheMixin`
Applied to files:
netbox_librenms_plugin/views/__init__.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Applied to files:
netbox_librenms_plugin/librenms_api.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Update modal messages based on RQ status values: 'Job queued...', 'Processing...', 'Job completed!' with explicit handling for all RQ status values to avoid infinite polling
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances
Applied to files:
netbox_librenms_plugin/tests/test_librenms_api.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
🧬 Code graph analysis (15)
netbox_librenms_plugin/tables/device_status.py (1)
netbox_librenms_plugin/views/imports/actions.py (2)
get(707-727)get(733-757)
netbox_librenms_plugin/tables/interfaces.py (1)
netbox_librenms_plugin/utils.py (2)
get_librenms_device_id(12-35)get_interface_name_field(221-249)
netbox_librenms_plugin/views/sync/device_fields.py (1)
netbox_librenms_plugin/librenms_api.py (2)
get_librenms_id(173-231)get_device_info(314-337)
netbox_librenms_plugin/utils.py (1)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)
netbox_librenms_plugin/import_utils/bulk_import.py (5)
netbox_librenms_plugin/librenms_api.py (2)
LibreNMSAPI(15-1039)get_device_info(314-337)netbox_librenms_plugin/import_utils/cache.py (3)
get_cache_metadata_key(10-24)get_import_device_cache_key(139-158)get_validated_device_cache_key(112-136)netbox_librenms_plugin/import_utils/filters.py (1)
get_librenms_devices_for_import(43-213)netbox_librenms_plugin/import_utils/permissions.py (1)
require_permissions(31-48)netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
create_virtual_chassis_with_members(310-440)
netbox_librenms_plugin/views/base/cables_view.py (2)
netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)netbox_librenms_plugin/utils.py (1)
get_virtual_chassis_member(109-133)
netbox_librenms_plugin/views/object_sync/vms.py (2)
netbox_librenms_plugin/tables/interfaces.py (1)
LibreNMSVMInterfaceTable(592-616)netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)
netbox_librenms_plugin/urls.py (2)
netbox_librenms_plugin/views/imports/actions.py (1)
DeviceConflictActionView(855-1036)netbox_librenms_plugin/views/sync/device_fields.py (1)
UpdateDeviceNameView(12-56)
netbox_librenms_plugin/views/imports/__init__.py (1)
netbox_librenms_plugin/views/imports/actions.py (1)
DeviceConflictActionView(855-1036)
netbox_librenms_plugin/views/base/ip_addresses_view.py (2)
netbox_librenms_plugin/utils.py (1)
get_librenms_device_id(12-35)netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)
netbox_librenms_plugin/librenms_api.py (1)
netbox_librenms_plugin/utils.py (2)
get_librenms_device_id(12-35)set_librenms_device_id(38-53)
netbox_librenms_plugin/views/object_sync/devices.py (2)
netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)netbox_librenms_plugin/tables/interfaces.py (2)
VCInterfaceTable(510-589)LibreNMSInterfaceTable(25-507)
netbox_librenms_plugin/views/sync/interfaces.py (2)
netbox_librenms_plugin/utils.py (1)
set_librenms_device_id(38-53)netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)
netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
netbox_librenms_plugin/librenms_api.py (2)
get_device_info(314-337)get_inventory_filtered(726-803)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)
netbox_librenms_plugin/tests/test_import_utils.py (3)
netbox_librenms_plugin/import_utils/device_operations.py (1)
validate_device_for_import(79-579)netbox_librenms_plugin/views/imports/actions.py (10)
get(707-727)get(733-757)DeviceConflictActionView(855-1036)post(248-397)post(424-701)post(819-826)post(832-839)post(845-852)post(858-1036)post(1048-1062)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)
🔇 Additional comments (20)
netbox_librenms_plugin/tables/device_status.py (1)
447-488: Good contextual action-state mapping for existing objects.The added NetBox link plus conflict/details state derivation is clear and keeps the existing HTMX modal workflow consistent.
netbox_librenms_plugin/views/object_sync/vms.py (1)
48-50: Looks good:server_keypropagation is correctly wired.Passing
self.librenms_api.server_keyin Line 49 keeps VM interface table lookups aligned with the active LibreNMS server context.netbox_librenms_plugin/tests/test_permissions.py (1)
636-637: Patch targets are correctly updated to source modules.The new patch paths are consistent with the refactor split and should keep mocks attached to the symbols actually used at runtime.
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: 661-662, 685-685, 706-706, 732-732, 751-751, 876-877
netbox_librenms_plugin/tests/test_librenms_api.py (1)
646-646: Good test adjustment for body-driven duplicate detection.Using HTTP 200 on Line 646 with an error payload reflects the real API behavior this client path is expected to handle.
netbox_librenms_plugin/librenms_api.py (1)
193-196: Nice refactor: centralizedlibrenms_idread/write handling.Routing both retrieval and storage through shared utils reduces format drift and keeps per-server behavior consistent.
Also applies to: 259-262
netbox_librenms_plugin/views/base/ip_addresses_view.py (1)
14-14: Good update: per-server interface matching now uses shared helper.The
server_key-aware lookup in Lines 107-111 is consistent with the new JSONlibrenms_idformat and keeps IP enrichment aligned with active server context.Also applies to: 107-112
netbox_librenms_plugin/tables/interfaces.py (2)
50-55: LGTM on server_key propagation and librenms_id lookup.The changes correctly add
server_keyparameter toLibreNMSInterfaceTable.__init__and useget_librenms_device_id(netbox_interface, self.server_key)for per-server librenms_id resolution. This aligns with the coding guidelines to use the centralized ID retrieval helper.Also applies to: 365-365
523-527: LGTM - server_key propagates via**kwargs.The
VCInterfaceTable.__init__signature doesn't explicitly listserver_key, but it's passed through**kwargsto the parent class, which correctly accepts and stores it.netbox_librenms_plugin/views/object_sync/devices.py (1)
82-98: LGTM - server_key correctly propagated to interface tables.The view retrieves
server_keyfromself.librenms_api.server_keyand passes it to bothVCInterfaceTableandLibreNMSInterfaceTableconstructors, enabling per-server librenms_id resolution.netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html (1)
402-402: LGTM - modal width increased for richer content.The change from
modal-lgtomodal-xlaccommodates the expanded validation details and conflict resolution UI introduced in this PR. The HTMX patterns remain correct withhx-target="#htmx-modal-content"andhx-swap="innerHTML".netbox_librenms_plugin/views/imports/__init__.py (1)
7-7: LGTM - DeviceConflictActionView properly exported.The new view is correctly imported from
actions.pyand added to__all__, following the established pattern for import workflow views.Also applies to: 20-20
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (1)
245-271: LGTM - Name sync row follows existing patterns.The new Name row correctly implements:
- Conditional rendering when
sysNamediffers fromobject.name- Standard form POST with CSRF protection (consistent with other field sync buttons)
- Checkmark display when names already match
netbox_librenms_plugin/views/sync/device_fields.py (1)
12-56: LGTM - UpdateDeviceNameView follows established patterns.The implementation correctly:
- Uses
self.librenms_api.get_librenms_id()per coding guidelines- Reuses the LibreNMS client via
self.librenms_api.get_device_info()- Follows the same permission, validation, and error-handling patterns as sibling views (
UpdateDeviceSerialView, etc.)netbox_librenms_plugin/import_utils/permissions.py (1)
10-48: LGTM - Clean permission helper design.The two-tier approach (
check_user_permissionsfor testable checks,require_permissionsfor raise-on-failure) provides good flexibility. Error messages are descriptive and include the specific missing permissions.netbox_librenms_plugin/urls.py (1)
13-13: LGTM - URL routes properly added.The new endpoints follow established conventions:
devices/<int:pk>/update-name/grouped with other device field update routesdevice-import/conflict-action/<str:device_id>/grouped with other import action routesImport statements are correctly placed alphabetically.
Also applies to: 47-47, 193-198, 269-273
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js (1)
323-330: Good consolidation of modal teardown paths.Routing all terminal states through
hideModal(filterModal)is a solid cleanup and avoids divergent close behavior across job outcomes.Also applies to: 351-352, 416-418, 459-461, 477-479, 491-493, 507-509, 589-591, 600-602, 607-609, 618-620, 1018-1020
netbox_librenms_plugin/views/__init__.py (1)
4-6: Re-export intent is clearly documented.The docstring plus targeted
# noqa: F401usage makes this module’s public surface explicit forurls.py/import consumers.Also applies to: 14-65
netbox_librenms_plugin/import_utils/vm_operations.py (1)
146-168: Good reuse of shared import utilities in VM flow.Using
fetch_device_with_cache,validate_device_for_import, and_determine_device_namekeeps VM imports aligned with device import behavior and avoids duplicated logic.Also applies to: 201-214
netbox_librenms_plugin/import_utils/device_operations.py (1)
418-431: Matching path is correctly centralized through shared utilities.Site/platform/device-type matching goes through the expected helper functions, which keeps import validation behavior consistent.
As per coding guidelines: "Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions
find_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py."Also applies to: 498-499
netbox_librenms_plugin/import_utils/__init__.py (1)
17-52: Re-export surface looks clean and backward-compatible.The grouping is clear and preserves existing
from netbox_librenms_plugin.import_utils import Xcall sites.
| except (BrokenPipeError, ConnectionError, IOError) as e: | ||
| if request: | ||
| logger.info(f"Client disconnected during VC prefetch: {e}") | ||
| return [] |
There was a problem hiding this comment.
process_device_filters() breaks its return contract on early exits.
When return_cache_status=True, these branches return [] instead of (devices, from_cache). Callers expecting tuple unpacking can fail during cancellation/disconnect paths.
💡 Proposed fix
def process_device_filters(
@@
) -> List[dict] | tuple[List[dict], bool]:
@@
- # Validate each device
+ def _empty_result():
+ return ([], from_cache) if return_cache_status else []
+
+ # Validate each device
@@
- return []
+ return _empty_result()
@@
- return []
+ return _empty_result()
@@
- return []
+ return _empty_result()
@@
- return []
+ return _empty_result()
@@
- return []
+ return _empty_result()
@@
- return []
+ return _empty_result()Also applies to: 401-404, 407-409, 428-433, 436-438, 444-446, 490-493
🤖 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 380 - 383,
process_device_filters currently returns [] on several early-exit
exception/interrupt paths which breaks callers that expect the (devices,
from_cache) tuple when return_cache_status=True; update every early-return
branch within process_device_filters (the except/if request blocks that log
client disconnects and cancellations) to return an appropriate two-tuple: an
empty devices list and the from_cache boolean (e.g., ([], False) or ([],
from_cache) depending on context) so callers always receive (devices,
from_cache); ensure the change covers all similar branches referencing request
and logger inside process_device_filters.
| device_type_synced = True | ||
| librenms_device_type = None | ||
| if librenms_hardware and librenms_hardware != "-": | ||
| from netbox_librenms_plugin.utils import match_librenms_hardware_to_device_type | ||
|
|
||
| hw_match = match_librenms_hardware_to_device_type(librenms_hardware) | ||
| if hw_match.get("matched"): | ||
| librenms_device_type = hw_match["device_type"] | ||
| if not existing_device.device_type or existing_device.device_type.pk != librenms_device_type.pk: | ||
| device_type_synced = False |
There was a problem hiding this comment.
device_type_synced can be incorrectly reported as synced.
If LibreNMS hardware is present but no matching NetBox device type is found, device_type_synced remains True. That can make all_synced true for a non-synced record.
💡 Proposed fix
- device_type_synced = True
+ device_type_synced = librenms_hardware in ("", "-")
librenms_device_type = None
if librenms_hardware and librenms_hardware != "-":
@@
hw_match = match_librenms_hardware_to_device_type(librenms_hardware)
if hw_match.get("matched"):
librenms_device_type = hw_match["device_type"]
- if not existing_device.device_type or existing_device.device_type.pk != librenms_device_type.pk:
- device_type_synced = False
+ device_type_synced = bool(
+ existing_device.device_type and existing_device.device_type.pk == librenms_device_type.pk
+ )
+ else:
+ device_type_synced = FalseAlso applies to: 802-813
🤖 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 791 - 800, The
code currently sets device_type_synced=True even when librenms_hardware exists
but match_librenms_hardware_to_device_type() returns no match; update the logic
in the block handling librenms_hardware (and the analogous block at lines
802-813) to set device_type_synced=False when hw_match.get("matched") is False
or when librenms_hardware is present but hw_match lacks a device_type, and only
set librenms_device_type and leave device_type_synced True when a valid
hw_match["device_type"] is found and matches existing_device.device_type.pk; use
the existing identifiers (librenms_hardware, hw_match,
match_librenms_hardware_to_device_type, librenms_device_type,
existing_device.device_type, device_type_synced) so the check reliably flips to
False whenever hardware exists but no mapping is found.
7d4a2af to
585dc7d
Compare
…ort_utils package Port 4 commits from feat/serial-matching-and-conflict-resolution: - 097ea42: Fix validation readiness (VMs require cluster.found), hostname match always sets has_actions, guard platform sync forms for Device only, use _resolve_naming_preferences in BulkImportConfirmView, add else branch for unmatched hardware in _build_sync_info, reuse resolved_name in DeviceConflictActionView actions, fix test patch targets - c0d7bfc: Block import when issues present (can_import/is_ready require no issues), modal title uses resolved_name, dedup LibreNMSSettings DB query in _resolve_naming_preferences, guard hostname form for VMs - 13bafe1: Compare existing device/VM name against resolved_name instead of raw sysName, scope mismatch force gate to link/update/update_serial/ update_type only, add librenms_id collision check before linking - dad0d22: Update device_role dict in-place during refresh to preserve schema keys (available_roles etc.)
The librenms_id conflict check added in the port requires Device.objects.filter().exclude().first() to be mocked for all tests using link/update/update_serial actions.
- utils.py set_librenms_device_id: defensively handle unexpected types (non-int, non-dict) by resetting to empty dict with a warning log - utils.py find_by_librenms_id: add OR query to also match legacy records where librenms_id is stored as a bare integer - cables_view.py: update all 6 librenms_id lookup sites to use Q()|Q() so legacy integer records are matched alongside new JSON format - actions.py: validate librenms_id (device_id) before int() cast and return 400 if missing/non-numeric; use pre-validated int throughout the link/update/update_serial action blocks - interfaces.py: guard against None port_id before set_librenms_device_id to avoid clobbering an existing server-key mapping
find_by_librenms_id now calls .filter(Q(...)|Q(...)) passing a positional Q object. The three TestSerialNumberMatching mocks only accepted **kwargs, causing TypeError which was silently caught by the except block in device_operations.py, making existing_device None. Update all three device_filter side_effects to accept *args and check str(arg) for 'librenms_id' so Q-based lookups are detected correctly.
585dc7d to
16dfb65
Compare
- Add migrate_legacy_librenms_id(obj, server_key) helper in utils.py
Converts a bare-integer librenms_id to {server_key: int_value}.
Returns True if migration happened, False if already JSON or absent.
Does not call save() — caller is responsible.
- Detect legacy int format in validate_device_for_import()
When find_by_librenms_id matches a Device or VM whose librenms_id CF
is still a bare integer, set result['librenms_id_needs_migration']=True
so the import page can surface a migration action.
- Add 'migrate_librenms_id' action in DeviceConflictActionView
Verifies CF is still an int, requires serial_confirmed or force checkbox,
calls migrate_legacy_librenms_id() + save().
- Fix collision check to use find_by_librenms_id instead of raw queryset
The old Device.objects.filter(custom_field_data__librenms_id=int(...))
only matched the legacy integer format; the new helper matches both
the JSON dict format and the legacy format.
- Show 'Legacy ID format' badge + 'Migrate ID format' button in template
Appears in the existing_match_type=='librenms_id' section when
librenms_id_needs_migration is set. Button is disabled (requires force
checkbox) when serial not confirmed.
- Add TestLegacyLibreNMSIdMigration test class (6 tests)
- Update TestDeviceConflictActionView mocks for new filter().first() chain
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
netbox_librenms_plugin/import_utils/bulk_import.py (1)
383-387:⚠️ Potential issue | 🟠 MajorKeep
process_device_filters()return shape consistent on early exits.When
return_cache_status=True, these branches still return[]instead of(devices, from_cache), which can break tuple-unpack callers.🔧 Proposed fix
def process_device_filters( @@ ) -> List[dict] | tuple[List[dict], bool]: @@ libre_devices, from_cache = get_librenms_devices_for_import( api, filters=filters, force_refresh=clear_cache, return_cache_status=True, ) + + def _empty_result(): + return ([], from_cache) if return_cache_status else [] @@ except (BrokenPipeError, ConnectionError, IOError) as e: if request: logger.info(f"Client disconnected during VC prefetch: {e}") - return [] + return _empty_result() raise @@ if rq_job.is_failed or rq_job.is_stopped: job.logger.warning("Job was already stopped before validation started") - return [] + return _empty_result() @@ if job.job.status == JobStatusChoices.STATUS_FAILED: job.logger.warning("Job was stopped before validation started") - return [] + return _empty_result() @@ if rq_job.is_failed or rq_job.is_stopped: job.logger.info( f"Job stopped at device {idx}/{total} (RQ status: {rq_job.get_status()}). Exiting gracefully." ) - return [] + return _empty_result() @@ if job.job.status == JobStatusChoices.STATUS_FAILED: job.logger.info(f"Job stopped at device {idx}/{total}. Exiting gracefully.") - return [] + return _empty_result() @@ except (BrokenPipeError, ConnectionError, IOError): logger.info(f"Client disconnected during validation at device {idx}") - return [] + return _empty_result() @@ except (BrokenPipeError, ConnectionError, IOError) as e: if request: logger.info(f"Client disconnected during device validation: {e}") - return [] + return _empty_result() raiseAlso applies to: 404-407, 410-413, 431-436, 439-442, 447-450, 493-497
🤖 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 383 - 387, The early-exit branches in process_device_filters are returning [] on exceptions/early exits which breaks callers expecting a (devices, from_cache) tuple when return_cache_status=True; update each early return (including the except blocks and other early-return spots noted) to inspect the return_cache_status parameter and return either [] (when False) or ([], False) (or the appropriate from_cache value) so the return shape stays consistent for callers that unpack the tuple.netbox_librenms_plugin/tables/device_status.py (1)
483-497:⚠️ Potential issue | 🟡 MinorAdd
aria-labelfor accessibility when button is icon-only.When
btn_labelis empty (Line 485), the button becomes icon-only. Thetitleattribute alone is not a reliable accessible name; screen readers needaria-label.♿ Proposed fix
buttons.append( f'<button type="button" ' f'class="btn btn-sm {btn_class}" ' f'hx-get="{details_url}" ' f'hx-include="[name=cluster_{device_id}], [name=role_{device_id}], [name=rack_{device_id}]" ' f'hx-target="#htmx-modal-content" ' f'hx-swap="innerHTML" ' - f'title="{btn_title}">' + f'title="{btn_title}" ' + f'aria-label="{btn_title}">' f'<i class="mdi {btn_icon}"></i>{btn_label}</button>' )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tables/device_status.py` around lines 483 - 497, In the buttons.append block in device_status.py where btn_label may be empty (icon-only button), add an aria-label attribute when btn_label == "" so the button has an accessible name; use btn_title (or a localized equivalent) as the aria-label and include it in the rendered button markup (referencing variables btn_label, btn_title, details_url, device_id and the buttons.append call).netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)
100-102:⚠️ Potential issue | 🟡 MinorAdd
rel="noopener noreferrer"to alltarget="_blank"links.Multiple anchor tags open links in new tabs without the security attribute, which can allow reverse-tabnabbing attacks.
Affected locations:
- Line 100: Name cell link
- Line 422: Hostname match alert link
- Line 482: Serial match alert link
- Line 520: IP match alert link
- Line 529: Exists alert link
- Lines 571-584: Footer "View in NetBox" and "Full Sync Page" links
🔒 Example fix pattern
-<a href="{{ existing_device_url }}" target="_blank"> +<a href="{{ existing_device_url }}" target="_blank" rel="noopener noreferrer">Apply this pattern to all
target="_blank"anchors in the template.Also applies to: 420-424, 480-484, 518-522, 527-530, 571-584
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html` around lines 100 - 102, Update every anchor element that uses target="_blank" in the template to include rel="noopener noreferrer"; specifically add this attribute to the link surrounding {{ validation.existing_device.name }} (href="{{ existing_device_url }}"), to the alert links for hostname/serial/IP/existence (the anchors referenced near the Hostname/Serial/IP/Exists alert blocks), and to the footer links ("View in NetBox" and "Full Sync Page") so all external-tab opens use rel="noopener noreferrer". Ensure each anchor tag that has target="_blank" is updated consistently to include rel="noopener noreferrer".
🤖 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`:
- Around line 633-656: The code uses the literal "default" when server_key is
None, causing cross-server mismatches; change to derive an effective_server_key
from the LibreNMSAPI instance and use it everywhere instead of hardcoding
"default". Specifically, after creating api = LibreNMSAPI(server_key=server_key)
determine effective_server_key = server_key or getattr(api, "server_key", None)
or "default" and pass effective_server_key into validate_device_for_import (call
site in validate_device_for_import(..., server_key=...)) and use the same
effective_server_key when persisting or constructing librenms_id later (the code
that currently uses the string "default"), ensuring all references consistently
use the resolved value.
- Around line 438-457: The code overwrites result["device_type"] with dt_match
which removes the expected schema (keys like "found") when dt_match["matched"]
is False, causing later readiness checks to fail; instead, preserve the
result["device_type"] schema and set/rename keys explicitly: always ensure
result["device_type"]["found"] is set (use dt_match["matched"]), set
result["device_type"]["device_type"] and result["device_type"]["match_type"]
when present, and when unmatched populate result["device_type"]["suggestions"]
using DeviceType.objects.all()[:10] rather than replacing the whole dict; apply
the same change to the similar block referenced around lines 568-573 so
unmatched hardware keeps the expected keys.
In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 1765-1774: The helper _create_view currently constructs the view
via object.__new__(DeviceConflictActionView), bypassing __init__; replace this
with a real instantiation (call DeviceConflictActionView()) and then attach the
mocked dependencies (set view._librenms_api with MagicMock and configure
view.request and user permissions) or, if the view __init__ requires external
services, patch/mock those services before calling DeviceConflictActionView() so
the real initializer runs; ensure you still set view._librenms_api.server_key
and view.request.user.has_perm as in the original test.
- Around line 2364-2395: The _setup_no_existing helper currently indexes into
the variadic mocks tuple (mocks[-1], mocks[-2], etc.), which is fragile; change
it to explicitly unpack the mocks tuple into named variables (e.g., mock_vm,
mock_device, mock_find_site, mock_find_platform, mock_match_type, mock_role,
mock_rack, mock_site_model) in the documented order that matches the `@patch`
decorators, then use those named variables where the code currently uses
mocks[-N]; alternatively refactor _setup_no_existing to accept named keyword
arguments matching the patched target names so callers/passengers can pass mocks
by name instead of positional indexing.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 383-387: The early-exit branches in process_device_filters are
returning [] on exceptions/early exits which breaks callers expecting a
(devices, from_cache) tuple when return_cache_status=True; update each early
return (including the except blocks and other early-return spots noted) to
inspect the return_cache_status parameter and return either [] (when False) or
([], False) (or the appropriate from_cache value) so the return shape stays
consistent for callers that unpack the tuple.
In `@netbox_librenms_plugin/tables/device_status.py`:
- Around line 483-497: In the buttons.append block in device_status.py where
btn_label may be empty (icon-only button), add an aria-label attribute when
btn_label == "" so the button has an accessible name; use btn_title (or a
localized equivalent) as the aria-label and include it in the rendered button
markup (referencing variables btn_label, btn_title, details_url, device_id and
the buttons.append call).
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 100-102: Update every anchor element that uses target="_blank" in
the template to include rel="noopener noreferrer"; specifically add this
attribute to the link surrounding {{ validation.existing_device.name }}
(href="{{ existing_device_url }}"), to the alert links for
hostname/serial/IP/existence (the anchors referenced near the
Hostname/Serial/IP/Exists alert blocks), and to the footer links ("View in
NetBox" and "Full Sync Page") so all external-tab opens use rel="noopener
noreferrer". Ensure each anchor tag that has target="_blank" is updated
consistently to include rel="noopener noreferrer".
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (15)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/sync/interfaces.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_import_utils.py
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (22)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. 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/htmx/device_validation_details.htmlnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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/device_operations.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Applied to files:
netbox_librenms_plugin/librenms_api.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Tables drive most UIs via `tables/*.py` renderers that emit HTMX-enabled columns and buttons. Prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to tables/**/*.py : Prefer updating the table renderer in `tables/*.py` rather than templates when changing row actions, since tables emit HTMX-enabled columns and buttons
Applied to files:
netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.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/bulk_import.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.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_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
🧬 Code graph analysis (9)
netbox_librenms_plugin/views/base/cables_view.py (2)
netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)netbox_librenms_plugin/utils.py (1)
get_virtual_chassis_member(162-186)
netbox_librenms_plugin/librenms_api.py (1)
netbox_librenms_plugin/utils.py (2)
get_librenms_device_id(19-42)set_librenms_device_id(45-67)
netbox_librenms_plugin/tables/interfaces.py (1)
netbox_librenms_plugin/utils.py (2)
get_librenms_device_id(19-42)get_interface_name_field(274-302)
netbox_librenms_plugin/tables/device_status.py (1)
netbox_librenms_plugin/views/imports/actions.py (2)
get(707-727)get(733-757)
netbox_librenms_plugin/utils.py (2)
netbox_librenms_plugin/views/imports/actions.py (2)
get(707-727)get(733-757)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)
netbox_librenms_plugin/views/imports/actions.py (8)
netbox_librenms_plugin/utils.py (7)
get_user_pref(258-262)save_user_pref(265-271)set_librenms_device_id(45-67)find_matching_platform(390-420)match_librenms_hardware_to_device_type(305-353)find_by_librenms_id(70-89)migrate_legacy_librenms_id(92-122)netbox_librenms_plugin/views/mixins.py (2)
LibreNMSAPIMixin(196-282)librenms_api(217-231)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)netbox_librenms_plugin/views/settings_views.py (3)
get(26-43)post(45-122)post(131-194)netbox_librenms_plugin/views/imports/list.py (1)
get(124-343)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)netbox_librenms_plugin/import_utils/device_operations.py (2)
validate_device_for_import(79-586)_determine_device_name(23-76)netbox_librenms_plugin/import_utils/cache.py (1)
get_import_device_cache_key(139-158)
netbox_librenms_plugin/import_utils/bulk_import.py (6)
netbox_librenms_plugin/librenms_api.py (2)
LibreNMSAPI(15-1039)get_device_info(314-337)netbox_librenms_plugin/import_utils/cache.py (3)
get_cache_metadata_key(10-24)get_import_device_cache_key(139-158)get_validated_device_cache_key(112-136)netbox_librenms_plugin/import_utils/device_operations.py (2)
import_single_device(589-808)validate_device_for_import(79-586)netbox_librenms_plugin/import_utils/filters.py (1)
get_librenms_devices_for_import(43-213)netbox_librenms_plugin/import_utils/permissions.py (1)
require_permissions(31-48)netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
create_virtual_chassis_with_members(310-440)empty_virtual_chassis_data(15-23)
netbox_librenms_plugin/views/sync/interfaces.py (3)
netbox_librenms_plugin/utils.py (1)
set_librenms_device_id(45-67)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)
netbox_librenms_plugin/tests/test_import_utils.py (2)
netbox_librenms_plugin/import_utils/device_operations.py (1)
validate_device_for_import(79-586)netbox_librenms_plugin/views/imports/actions.py (12)
get(707-727)get(733-757)DeviceConflictActionView(857-1100)post(249-397)post(424-701)post(821-828)post(834-841)post(847-854)post(860-1100)post(1112-1126)DeviceValidationDetailsView(730-815)_build_sync_info(760-815)
🔇 Additional comments (27)
netbox_librenms_plugin/tests/test_import_utils.py (10)
14-66: LGTM!Cache key generation tests are well-structured and verify key format/content correctly.
73-142: LGTM!Comprehensive test coverage for device name determination including edge cases like IP addresses, empty sysName, and domain stripping.
152-153: LGTM!Patch targets correctly updated to reference the new submodule paths (
filters.cache,filters.LibreNMSAPI,virtual_chassis.cache) following the refactored module structure. Based on learnings: "Patch deferred/inline imports at their source module".Also applies to: 174-174, 192-192, 212-212, 228-228, 282-282
1104-1146: LGTM!Good test structure using
setup_method/teardown_methodwith centralizedSERIAL_PATCHESlist. The helper methods_start_patches,_stop_patches, and_setup_no_match_mocksreduce boilerplate and improve maintainability across the serial number matching tests.
1617-1760: LGTM!Comprehensive tests for legacy librenms_id migration, covering both detection during validation and the migration helper function. Tests correctly verify the int-to-dict format conversion (
42→{"server_key": 42}).
2215-2229: Patch target for sync_platform action.This test patches
dcim.models.Platform, but previous feedback suggestedDeviceConflictActionView.post()may resolve platforms viafind_matching_platform. Since the previous review marked this as "✅ Addressed", the implementation may legitimately usePlatform.objects.get()for the sync operation. Verify the view implementation matches this test's assumptions.
2246-2260: LGTM!The
sync_device_typetest correctly patchesnetbox_librenms_plugin.utils.match_librenms_hardware_to_device_typeat its source module location.
2263-2358: LGTM!
_build_sync_infotests correctly patch utility functions at their source module (netbox_librenms_plugin.utils) and comprehensively verify theall_syncedflag logic across different sync scenarios.
2480-2500: LGTM!Good test verifying that duplicate detection uses the resolved name (after applying
use_sysnamepreference) rather than raw hostname. This ensures consistency between validation and import behavior.
301-309: The patch targets are correctly implemented and follow the guideline.VirtualMachineis imported inline within functions (lines 218, 224, 301 ofdevice_operations.py), so it should be patched at its source module (virtualization.models.VirtualMachine).Deviceis imported at the module level, so it should be patched at the consuming module (netbox_librenms_plugin.import_utils.device_operations.Device). No changes are needed.Likely an incorrect or invalid review comment.
netbox_librenms_plugin/utils.py (1)
56-67: Defensivelibrenms_idcoercion is well implemented.This safely handles malformed legacy values before write-back and prevents type-related sync crashes.
netbox_librenms_plugin/librenms_api.py (1)
193-196: Good centralization of LibreNMS ID handling.Delegating both lookup and storage to shared helpers keeps behavior consistent across legacy and multi-server formats.
Also applies to: 259-262
netbox_librenms_plugin/views/base/cables_view.py (1)
82-89: Multi-format lookup handling is consistent across cable resolution paths.The
Q(json_path) | Q(legacy_int)pattern is applied uniformly for device and interface resolution.Also applies to: 131-145, 170-184, 389-391
netbox_librenms_plugin/import_utils/vm_operations.py (1)
146-168: VM bulk-import flow is cleanly integrated with shared cache/validation helpers.The path from cached fetch → validation → mapped creation is consistent and keeps server-key propagation intact.
Also applies to: 212-214
netbox_librenms_plugin/views/object_sync/devices.py (1)
82-98: Good server-key propagation into interface table constructors.This keeps per-server ID resolution aligned throughout the device interface sync UI.
netbox_librenms_plugin/views/base/ip_addresses_view.py (1)
14-15: Nice move to centralized server-key-aware ID lookup in IP prefetch.This improves consistency with the new multi-server
librenms_idmodel.Also applies to: 107-112
netbox_librenms_plugin/views/sync/interfaces.py (1)
12-12: LGTM! Proper use of centralized ID helper with null guard.The refactor correctly uses
set_librenms_device_idhelper instead of direct field manipulation, and the null guard onport_idprevents clobbering existing mappings. This aligns with the coding guideline to use helpers for LibreNMS ID access.Also applies to: 237-240
netbox_librenms_plugin/tables/device_status.py (1)
461-486: LGTM! Clear conditional styling based on validation state.The branching logic for button appearance is well-structured with descriptive boolean flags (
has_mismatch,has_actions,has_name_sync,has_sync_needed). Button classes correctly convey severity levels.netbox_librenms_plugin/views/object_sync/vms.py (1)
46-50: LGTM! Consistent server_key propagation to VM interface table.The
server_keyparameter is correctly passed fromself.librenms_api.server_keytoLibreNMSVMInterfaceTable, ensuring server-aware ID handling for VM interfaces.netbox_librenms_plugin/tables/interfaces.py (1)
16-16: LGTM! Proper use of centralized ID getter with server_key support.The refactor correctly:
- Imports
get_librenms_device_idhelper- Accepts
server_keyparameter in constructor (defaulting to "default")- Uses the helper in
render_librenms_idinstead of directcustom_field_dataaccessThis enables multi-server support and aligns with the coding guideline to use helpers for LibreNMS ID access.
Also applies to: 50-55, 365-365
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)
104-114: LGTM! Well-structured inline forms with proper CSRF protection.The inline action forms correctly:
- Include
{% csrf_token %}for security- Use
hx-swap="none"since row updates happen via OOB swaps- Implement force checkbox patterns with JavaScript-enabled button state management
- Cover comprehensive validation states (name sync, device type, serial, platform, migration)
Also applies to: 151-175, 206-216, 385-404
netbox_librenms_plugin/views/imports/actions.py (6)
38-65: LGTM! Robust naming preference resolution with proper fallback chain.The
_resolve_naming_preferenceshelper correctly implements the priority:
- POST data (explicit toggle)
- User preferences
- Plugin settings default
This addresses the previous concern about conflict actions bypassing naming preference fallback logic.
790-815: LGTM! Correcteddevice_type_syncedlogic.The
_build_sync_infomethod now correctly handles all cases:
- Sets
device_type_synced = Falsewhenhw_match.get("matched")fails (line 801-802)- Sets
device_type_synced = Falsewhen types don't match (line 799-800)This addresses the previous review concern about
device_type_syncedincorrectly remainingTruewhen no matching device type is found.
1062-1084: LGTM! Migration action with proper safety checks.The
migrate_librenms_idaction correctly:
- Validates the current value is a bare integer before migrating (line 1069)
- Requires either serial confirmation or explicit force flag (lines 1074-1078)
- Uses the
migrate_legacy_librenms_idhelper for the actual migration- Logs the migration for audit purposes
914-931: LGTM! Link action with proper name resolution.The
linkaction correctly:
- Prioritizes
validation["resolved_name"]which is populated via_resolve_naming_preferences- Falls back to
_determine_device_namewith POST toggles (defensive path)- Uses
set_librenms_device_idhelper for ID assignment- Handles optional device type update when force is used with mismatch
1089-1100: LGTM! Proper cache invalidation and HTMX response handling.After conflict resolution actions:
- Cache is cleared to ensure fresh validation on next access
- Device is re-validated and row is re-rendered with updated state
HX-Trigger: closeModalheader correctly signals the UI to close the modal
903-912: The import path is correct.find_by_librenms_idis defined innetbox_librenms_plugin/utils.pyat line 70, and importing it fromnetbox_librenms_plugin.utilsat line 904 is the proper usage.
… accessibility - device_operations.py: preserve device_type schema dict instead of replacing with raw dt_match; 'found' key is now always set regardless of match result - device_operations.py: use api.server_key instead of server_key or 'default' to consistently use the resolved server key after LibreNMSAPI instantiation - device_operations.py: move Q import to module level; remove inline 'from dcim.models import Rack' so test mock patching works correctly - bulk_import.py: fix 7 early-return sites to return ([], False) when return_cache_status=True, keeping the return shape consistent for callers - device_status.py: add 'Legacy ID' warning badge when librenms_id_needs_migration is set on an existing device matched by librenms_id - device_status.py: add aria-label to icon-only buttons for accessibility - device_validation_details.html: add rel='noopener noreferrer' to all target='_blank' anchors (8 links) - test_import_utils.py: add DeviceType to SERIAL_PATCHES so suggestions path is mocked; fix _create_view to use real instantiation; fix _setup_no_existing to use named variables instead of fragile negative indexing; remove dead mock_rack.objects.filter.return_value = [] lines (inline import bypass meant these were never effective); fix device_type assertions to use 'found' key
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
netbox_librenms_plugin/tests/test_import_utils.py (2)
2469-2474: 🧹 Nitpick | 🔵 TrivialAvoid positional mock indexing in this test path.
mocks[-2]is still fragile against decorator-order edits; explicit unpacking keeps the test stable and self-documenting.♻️ Suggested cleanup
- mock_device = mocks[-2] # Device + ( + _mock_site_model, + _mock_rack, + _mock_cluster, + _mock_role, + _mock_match_type, + _mock_find_platform, + _mock_find_site, + mock_device, + _mock_vm, + ) = mocks#!/bin/bash set -euo pipefail rg -nP 'mocks\[-\d+\]' netbox_librenms_plugin/tests/test_import_utils.pyExpected verification result: no remaining positional
mocks[-N]access in this file.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_import_utils.py` around lines 2469 - 2474, The test uses a fragile positional access mocks[-2] to get mock_device; replace this with explicit unpacking or named assignment from the mocks list to make the test stable (e.g., assign mock_device = mocks[<index_name>] by destructuring like device_mock, other_mocks = mocks or explicitly pull out the device mock from the returned fixtures), then keep the rest of the setup (existing = MagicMock(); existing.name = "core-switch"; existing.serial = ""; mock_device.objects.filter.return_value.first.side_effect = [None, existing]) unchanged; update any other occurrences of mocks[-N] in this test to use named variables instead.
2198-2206:⚠️ Potential issue | 🟠 MajorPatch target for
sync_platformlooks misaligned with runtime resolution path.This test may bypass the real platform-resolution logic by patching
dcim.models.Platformdirectly, which can make the assertion pass without exercising the intended branch.#!/bin/bash set -euo pipefail actions_file="$(fd '^actions\.py$' netbox_librenms_plugin/views/imports | head -n1)" echo "Inspecting: ${actions_file}" rg -n -C4 'sync_platform|find_matching_platform|Platform' "${actions_file}"Expected verification result: confirm whether
DeviceConflictActionView.post()resolves platforms viafind_matching_platform(...)(or via a module-local alias) vsPlatform.objects.get(...), then patch the exact symbol in that source module.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".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_import_utils.py` around lines 2198 - 2206, The test patches dcim.models.Platform directly, but DeviceConflictActionView.post resolves platforms via the module-local symbol used by the view (e.g., find_matching_platform or a locally imported Platform alias) so the patch is misaligned; update the test to patch the exact symbol the view uses (patch the view module's find_matching_platform or its module-local Platform alias used by DeviceConflictActionView.post/sync_platform) instead of dcim.models.Platform, and ensure mock return values are wired to DeviceConflictActionView.get_validated_device_with_selections, sync_platform/find_matching_platform, and any module-local Platform lookup so the real resolution branch is exercised.
🤖 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`:
- Around line 234-235: After you set result["import_as_vm"] when an existing VM
is detected, downstream validation branches still read the original import_as_vm
parameter causing incorrect device-only validation; fix this by switching those
branches to use the effective import mode stored in result["import_as_vm"] (or
assign import_as_vm = result["import_as_vm"] immediately after detection) so all
subsequent checks use the updated mode. Update every occurrence where the
original import_as_vm argument is used after existing-object detection
(including the other blocks noted around the ranges 403-423, 512-513, 559-561)
to reference result["import_as_vm"] (or the reassigned local variable) instead.
Ensure the logic that sets result["can_import"] still respects the effective
import mode.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 111-113: The icon-only sync button in the template (the <button
... title="Sync name to {{ validation.suggested_name }}"> that wraps the <i
class="mdi mdi-sync">) lacks an explicit accessible name; add an aria-label
attribute (e.g., aria-label="Sync name to {{ validation.suggested_name }}") to
that button so screen-readers can announce its purpose, and apply the same
change to the other icon-only sync buttons in this modal (the buttons at the
blocks around the occurrences referenced by validation.suggested_name and
similar sync buttons at the other listed locations).
---
Duplicate comments:
In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 2469-2474: The test uses a fragile positional access mocks[-2] to
get mock_device; replace this with explicit unpacking or named assignment from
the mocks list to make the test stable (e.g., assign mock_device =
mocks[<index_name>] by destructuring like device_mock, other_mocks = mocks or
explicitly pull out the device mock from the returned fixtures), then keep the
rest of the setup (existing = MagicMock(); existing.name = "core-switch";
existing.serial = ""; mock_device.objects.filter.return_value.first.side_effect
= [None, existing]) unchanged; update any other occurrences of mocks[-N] in this
test to use named variables instead.
- Around line 2198-2206: The test patches dcim.models.Platform directly, but
DeviceConflictActionView.post resolves platforms via the module-local symbol
used by the view (e.g., find_matching_platform or a locally imported Platform
alias) so the patch is misaligned; update the test to patch the exact symbol the
view uses (patch the view module's find_matching_platform or its module-local
Platform alias used by DeviceConflictActionView.post/sync_platform) instead of
dcim.models.Platform, and ensure mock return values are wired to
DeviceConflictActionView.get_validated_device_with_selections,
sync_platform/find_matching_platform, and any module-local Platform lookup so
the real resolution branch is exercised.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_import_utils.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tests/test_import_utils.py
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
🧠 Learnings (18)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. 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/htmx/device_validation_details.htmlnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.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_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
🧬 Code graph analysis (2)
netbox_librenms_plugin/import_utils/device_operations.py (4)
netbox_librenms_plugin/librenms_api.py (2)
LibreNMSAPI(15-1039)get_device_info(314-337)netbox_librenms_plugin/utils.py (1)
find_by_librenms_id(70-89)netbox_librenms_plugin/import_utils/cache.py (1)
get_import_device_cache_key(139-158)netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
empty_virtual_chassis_data(15-23)get_virtual_chassis_data(60-80)
netbox_librenms_plugin/tables/device_status.py (2)
netbox_librenms_plugin/views/imports/actions.py (2)
get(707-727)get(733-757)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)
🔇 Additional comments (2)
netbox_librenms_plugin/import_utils/bulk_import.py (1)
386-387: Nice return-contract hardening on early exits.The cancellation/disconnect paths now consistently honor
return_cache_status=Trueby returning( [], False ), which removes tuple-unpack hazards for callers.Also applies to: 406-407, 412-413, 435-436, 441-442, 449-450, 496-497
netbox_librenms_plugin/tables/device_status.py (1)
491-503: Good accessibility improvement for icon-only state.Adding conditional
aria-labelwhen the details button has no visible text is a solid fix for assistive-tech naming.
- import_utils/cache.py: guard datetime.fromisoformat with try/except fallback; replace hash() with deterministic sha256 for cross-process stable cache keys - import_utils/filters.py: replace hash() with deterministic sha256 for librenms_devices_import cache key; add hashlib/json imports - import_utils/permissions.py: remove unused logger and logging import - import_utils/virtual_chassis.py: fix blank serial skip logic - only skip member when serial is non-empty AND matches master serial; same fix in expected_members count - device_validation_details.html: add aria-label to 6 icon-only sync buttons for screen-reader accessibility - test_import_utils.py: replace fragile mocks[-2] indexing with named unpacking - views/imports/actions.py: import escape(); HTML-escape untrusted values (id_conflict.name, librenms_os, hardware, action) in HttpResponse f-strings - views/sync/interfaces.py: guard handle_mac_address with is_device_interface to avoid setting MAC on VMInterface objects - import_utils/device_operations.py: reassign import_as_vm from result dict after existing-object detection so downstream branches use effective VM mode
When an existing device is deleted between cache and refresh, recompute can_import and is_ready to match validate_device_for_import semantics: - can_import = not bool(issues) (not unconditionally True) - VMs: is_ready = can_import AND cluster.found (not site+cluster+device_role) - Devices: is_ready = can_import AND site+device_type+device_role
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (5)
netbox_librenms_plugin/import_utils/cache.py (1)
67-70:⚠️ Potential issue | 🟠 MajorLocation-choice cache key should be server-scoped.
Using a global key can cross-contaminate location labels between LibreNMS servers.
🔧 Proposed fix
- location_cache_key = "librenms_locations_choices" + location_cache_key = f"librenms_locations_choices_{server_key}"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/cache.py` around lines 67 - 70, The cache key "librenms_locations_choices" is global and can mix labels between LibreNMS servers; update the code that builds location_cache_key (used with cache.get and to set location_choices) to include a server-specific identifier (e.g., server id, hostname, or instance PK available in the surrounding context) so each server has a unique key (for example "librenms_locations_choices:{server_id}"), and ensure any cache.set/invalidations use the same server-scoped key.netbox_librenms_plugin/import_utils/filters.py (1)
77-79:⚠️ Potential issue | 🟠 MajorUse the resolved API server key when building cache keys.
When
apiis provided andserver_keyarg isNone, keys collapse underNoneand can mix cache data across servers.🔧 Proposed fix
if api is None: api = LibreNMSAPI(server_key=server_key) + effective_server_key = server_key or getattr(api, "server_key", "default") @@ - cache_key = f"librenms_devices_import_{server_key}_{_hash(api_filters)}_{_hash(client_filters)}" + cache_key = ( + f"librenms_devices_import_{effective_server_key}_{_hash(api_filters)}_{_hash(client_filters)}" + )Also applies to: 181-181
🤖 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 77 - 79, The cache keys are being built with the raw server_key argument which can be None when an api instance is supplied, causing keys to collide; update the code to resolve the actual server key before building keys by using the api's configured key (e.g., read api.server_key or api.get_server_key() immediately after the api is set in the function) and then use that resolved_server_key for all cache key construction (apply the same change where cache keys are built around the code referencing server_key at the other spot near line 181).netbox_librenms_plugin/views/imports/actions.py (1)
923-925:⚠️ Potential issue | 🟡 MinorFallback naming path still bypasses
_resolve_naming_preferences().The direct checkbox reads in these fallback branches can still diverge from the preference-resolution logic.
🔧 Proposed fix
+ use_sysname_pref, strip_domain_pref = _resolve_naming_preferences(request) @@ - use_sysname=request.POST.get("use-sysname-toggle") == "on", - strip_domain=request.POST.get("strip-domain-toggle") == "on", + use_sysname=use_sysname_pref, + strip_domain=strip_domain_pref,Also applies to: 943-945, 995-997
🤖 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 923 - 925, The fallback branches are reading checkboxes directly from request.POST (e.g., passing use_sysname=request.POST.get("use-sysname-toggle") == "on" and strip_domain=request.POST.get("strip-domain-toggle") == "on") which bypasses the shared naming preference logic in _resolve_naming_preferences(); update those fallback branches to call and use _resolve_naming_preferences(...) instead of reading request.POST directly (replace the direct use_sysname/strip_domain reads with the values returned by _resolve_naming_preferences), and apply the same change to the other occurrences mentioned (the blocks around the code at the other similar spots).netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
178-179:⚠️ Potential issue | 🟡 MinorUse an explicit
Nonecheck forparent_index.
if not parent_indexcan incorrectly reject a valid0index.🔧 Proposed fix
- if not parent_index: + if parent_index is None: return None🤖 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 178 - 179, Replace the falsy check on parent_index with an explicit None check: change the conditional "if not parent_index:" to "if parent_index is None:" so a valid index of 0 is not treated as missing; keep the existing return None behavior and adjust any surrounding logic in the same function that relies on parent_index's presence.
257-263: 🧹 Nitpick | 🔵 TrivialAvoid querying settings on every VC member-name generation call.
LibreNMSSettings.objects.first()in this path adds repeated DB overhead during VC processing.♻️ Refactor sketch
+from functools import lru_cache + +@lru_cache(maxsize=1) +def _get_vc_member_name_pattern() -> str: + from ..models import LibreNMSSettings + try: + settings = LibreNMSSettings.objects.first() + return settings.vc_member_name_pattern if settings else "-M{position}" + except Exception: + return "-M{position}" @@ - try: - settings = LibreNMSSettings.objects.first() - pattern = 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.") - pattern = "-M{position}" + pattern = _get_vc_member_name_pattern()🤖 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 257 - 263, The code repeatedly queries LibreNMSSettings.objects.first() inside the VC member-name generation path (the try block that sets pattern from LibreNMSSettings.vc_member_name_pattern), causing DB overhead; change this to read and cache the pattern once (e.g., at module import or when the importer is initialized) and reuse the cached value in the member-name generation function instead of calling LibreNMSSettings.objects.first() each time, falling back to "-M{position}" if no setting or on error; update references to pattern retrieval (the code that assigns pattern) to use the cached_pattern variable or an injected setting accessor rather than the live DB query.
🤖 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 443-450: The current try/except around the no-op lookup of
request.META['wsgi.input'] cannot catch BrokenPipeError and should be removed or
replaced; remove the entire clause that performs only hasattr(request, "META")
and request.META.get("wsgi.input") with a pass, or if you need an actual probe
replace it with a real I/O probe on request.META['wsgi.input'] (e.g., attempt a
non-blocking peek/read on the wsgi.input stream and catch
BrokenPipeError/ConnectionError/IOError), and keep the existing
logger.info(f"Client disconnected during validation at device {idx}") and return
logic (return [], False if return_cache_status else []) intact.
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 25-26: The cache metadata key builder drops valid falsy filter
values because the comprehension uses "if v" which filters out 0/False; update
the filter_parts computation (the generator that builds filter_parts) to include
falsy-but-valid values by changing the predicate to "if v is not None" (and
ensure values are stringified, e.g., f"{k}={v}") so 0 and False are preserved
while still excluding None; keep the return string format that references
server_key and vc_enabled as before.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 950-953: The HttpResponse messages leak unescaped user/device
values (incoming_serial and conflict_device.name); fix by applying the same
escaping pattern already used elsewhere: import and use django.utils.html.escape
(or the project's existing escape helper) to escape incoming_serial and
conflict_device.name before interpolating into the HttpResponse body for all
three serial-conflict responses (the ones that construct messages with
incoming_serial and conflict_device.name and return HttpResponse with status
409) so the returned HTML is safe from reflected XSS.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 67-70: The cache key "librenms_locations_choices" is global and
can mix labels between LibreNMS servers; update the code that builds
location_cache_key (used with cache.get and to set location_choices) to include
a server-specific identifier (e.g., server id, hostname, or instance PK
available in the surrounding context) so each server has a unique key (for
example "librenms_locations_choices:{server_id}"), and ensure any
cache.set/invalidations use the same server-scoped key.
In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 77-79: The cache keys are being built with the raw server_key
argument which can be None when an api instance is supplied, causing keys to
collide; update the code to resolve the actual server key before building keys
by using the api's configured key (e.g., read api.server_key or
api.get_server_key() immediately after the api is set in the function) and then
use that resolved_server_key for all cache key construction (apply the same
change where cache keys are built around the code referencing server_key at the
other spot near line 181).
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 178-179: Replace the falsy check on parent_index with an explicit
None check: change the conditional "if not parent_index:" to "if parent_index is
None:" so a valid index of 0 is not treated as missing; keep the existing return
None behavior and adjust any surrounding logic in the same function that relies
on parent_index's presence.
- Around line 257-263: The code repeatedly queries
LibreNMSSettings.objects.first() inside the VC member-name generation path (the
try block that sets pattern from LibreNMSSettings.vc_member_name_pattern),
causing DB overhead; change this to read and cache the pattern once (e.g., at
module import or when the importer is initialized) and reuse the cached value in
the member-name generation function instead of calling
LibreNMSSettings.objects.first() each time, falling back to "-M{position}" if no
setting or on error; update references to pattern retrieval (the code that
assigns pattern) to use the cached_pattern variable or an injected setting
accessor rather than the live DB query.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 923-925: The fallback branches are reading checkboxes directly
from request.POST (e.g., passing
use_sysname=request.POST.get("use-sysname-toggle") == "on" and
strip_domain=request.POST.get("strip-domain-toggle") == "on") which bypasses the
shared naming preference logic in _resolve_naming_preferences(); update those
fallback branches to call and use _resolve_naming_preferences(...) instead of
reading request.POST directly (replace the direct use_sysname/strip_domain reads
with the values returned by _resolve_naming_preferences), and apply the same
change to the other occurrences mentioned (the blocks around the code at the
other similar spots).
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (10)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/permissions.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/interfaces.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/import_utils/permissions.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/import_utils/device_operations.py
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (21)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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/filters.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.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.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.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/filters.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. 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/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/device_operations.py
🧬 Code graph analysis (4)
netbox_librenms_plugin/views/sync/interfaces.py (2)
netbox_librenms_plugin/utils.py (1)
set_librenms_device_id(45-67)netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)
netbox_librenms_plugin/import_utils/bulk_import.py (6)
netbox_librenms_plugin/librenms_api.py (2)
LibreNMSAPI(15-1039)get_device_info(314-337)netbox_librenms_plugin/import_utils/cache.py (3)
get_cache_metadata_key(12-26)get_import_device_cache_key(147-166)get_validated_device_cache_key(120-144)netbox_librenms_plugin/import_utils/device_operations.py (2)
import_single_device(589-808)validate_device_for_import(80-586)netbox_librenms_plugin/import_utils/filters.py (1)
get_librenms_devices_for_import(45-220)netbox_librenms_plugin/import_utils/permissions.py (1)
require_permissions(27-44)netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
create_virtual_chassis_with_members(310-442)empty_virtual_chassis_data(15-23)prefetch_vc_data_for_devices(83-117)
netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)netbox_librenms_plugin/librenms_api.py (3)
LibreNMSAPI(15-1039)get_device_info(314-337)get_inventory_filtered(726-803)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)
netbox_librenms_plugin/import_utils/device_operations.py (4)
netbox_librenms_plugin/librenms_api.py (1)
LibreNMSAPI(15-1039)netbox_librenms_plugin/utils.py (3)
find_matching_platform(390-420)find_matching_site(356-387)match_librenms_hardware_to_device_type(305-353)netbox_librenms_plugin/import_utils/cache.py (1)
get_import_device_cache_key(147-166)netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
empty_virtual_chassis_data(15-23)get_virtual_chassis_data(60-80)
🔇 Additional comments (9)
netbox_librenms_plugin/views/sync/interfaces.py (1)
237-240: Good guardrails on interface attribute syncing.The
port_id is not Nonecheck andis_device_interfacegate prevent accidental ID/MAC corruption during sync.Also applies to: 250-252
netbox_librenms_plugin/import_utils/permissions.py (1)
6-44: Permission helper extraction looks solid.This is a clean, reusable permission layer for import operations.
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)
100-112: Nice accessibility and link-hardening follow-through.
aria-labelon icon-only controls andrel="noopener noreferrer"on new-tab links are correctly applied.Also applies to: 159-174, 212-214, 266-268, 290-292, 422-423, 482-483, 520-530, 571-583
netbox_librenms_plugin/views/imports/actions.py (1)
39-67: The naming-preference resolution cleanup is a good improvement.Using POST → user prefs → plugin defaults consistently reduces drift across HTMX flows.
Also applies to: 147-158, 263-305
netbox_librenms_plugin/import_utils/device_operations.py (1)
403-406: Validation mode handoff anddevice_typeschema handling are now stable.Using effective
import_as_vmand preservingresult["device_type"]keys avoids downstream state/schema drift.Also applies to: 442-447
netbox_librenms_plugin/tests/test_import_utils.py (1)
1-7: LGTM — Test file properly updated for new module structure.The test patches correctly target the new submodule surfaces (
device_operations,filters,virtual_chassis) rather than legacy paths, aligning with the guideline to patch at source modules. The explicit tuple unpacking inTestSerialNumberMatching,TestLegacyLibreNMSIdMigration, andTestDeviceNamingPreferencesaddresses prior fragility concerns about index-based mock access.netbox_librenms_plugin/import_utils/bulk_import.py (3)
23-219: LGTM — Bulk import orchestration is well-structured.Good design decisions:
- Permission check at entry point (lines 74-79)
- Race condition prevention for VC creation using
processed_vc_domainsset (lines 174-175)- Proper error isolation per device with aggregated results
- Efficient API client reuse with single instantiation (line 89)
270-309: LGTM — Refresh logic properly handles deleted devices.The recomputation of
can_importandis_readywhen an existing device has been deleted (lines 289-306) correctly mirrors the logic invalidate_device_for_import, ensuring cache staleness doesn't lead to incorrect UI states.
380-388: Return contract fix verified.Early exits now correctly return
([], False)whenreturn_cache_status=True, addressing the previously flagged issue about broken return contracts.
- virtual_chassis.py: load VC member name pattern once before loops to avoid N DB queries per member creation/update - virtual_chassis.py: fix parent_index check (0 is valid, use 'is None') - actions.py: use _resolve_naming_preferences() in link/update/sync_name actions instead of reading POST toggles directly (respects user prefs) - actions.py: HTML-escape incoming_serial and conflict_device.name in all serial-conflict HttpResponse messages (XSS fix) - actions.py: verify existing_device_id matches validated conflict target before mutation (only enforced when validation has existing_device set) - actions.py: add comment explaining collision check is functionally correct - bulk_import.py: remove dead wsgi.input try/except (no-op, cannot catch BrokenPipeError as written) - cache.py: fix filter_parts to use 'if v is not None' (preserves 0/False) - cache.py: normalize naive datetime to UTC before age_seconds subtraction - cache.py: update docstring example to reflect SHA-256 hex prefix format - filters.py: use api.server_key (always resolved) in cache key construction - device_operations.py: add comments explaining direct CF access is needed for legacy integer format detection (not a bug) - test_import_utils.py: add comment explaining dcim.models.Platform patch is correct (inline import at call time) - test_import_utils.py: fix _create_request to always include both toggle keys to prevent DB access in _resolve_naming_preferences fallback
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
netbox_librenms_plugin/import_utils/cache.py (1)
68-69:⚠️ Potential issue | 🟠 MajorScope location-choice cache key by server.
Line 68 uses a global key (
librenms_locations_choices), so active-search filter labels can bleed across LibreNMS servers when IDs overlap.🔧 Proposed fix
- location_cache_key = "librenms_locations_choices" + location_cache_key = f"librenms_locations_choices_{server_key}"#!/bin/bash # Verify all reads/writes of location choices cache key are consistently server-scoped. rg -n "librenms_locations_choices" netbox_librenms_plugin🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/cache.py` around lines 68 - 69, The cache key "librenms_locations_choices" is global and must be scoped per server to avoid cross-server label leakage; update the code that sets and reads location_cache_key (the variable location_cache_key and calls like cache.get and cache.set) to include a server-unique identifier (e.g., server.id, server.pk, or server.hostname/slug) when building the key so both reads and writes use the same per-server key (for example append or format the identifier into the key string) and update any other occurrences of "librenms_locations_choices" to the new scoped key.netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
215-218: 🧹 Nitpick | 🔵 TrivialStill querying VC naming pattern per member during detection.
detect_virtual_chassis_from_inventory()calls_generate_vc_member_name()without a preloaded pattern, so settings lookup can still happen once per member in this loop.🔧 Proposed fix
- # Step 5: Extract member info + # Step 5: Extract member info + vc_pattern = _load_vc_member_name_pattern() members = [] for idx, chassis in enumerate(chassis_items): @@ # Generate suggested name if we have master name if master_name: - member_data["suggested_name"] = _generate_vc_member_name(master_name, position + 1) + member_data["suggested_name"] = _generate_vc_member_name( + master_name, position + 1, pattern=vc_pattern + ) else: member_data["suggested_name"] = f"Member-{position + 1}"🤖 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 215 - 218, detect_virtual_chassis_from_inventory is calling _generate_vc_member_name inside a per-member loop without a preloaded naming pattern, causing a settings lookup per member; fix by loading the VC naming pattern once before the loop (or change _generate_vc_member_name to accept a precomputed pattern/cached value) and pass that cached pattern into the function when creating member_data["suggested_name"] (i.e., compute pattern once, then call _generate_vc_member_name(master_name, position + 1, pattern) or supply pattern via closure/cached variable to avoid repeated settings lookups).
🤖 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`:
- Line 387: Introduce a small helper function (e.g., def
_empty_return(return_cache_status): return ([], False) if return_cache_status
else []) in the module and replace each repeated occurrence of "([], False) if
return_cache_status else []" with a call to _empty_return(return_cache_status);
this centralizes the empty-return contract, reduces duplication, and makes
maintenance safer — update all spots in bulk_import.py that currently return
"([], False) if return_cache_status else []" to call _empty_return instead.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 886-889: The handler currently proceeds when
validation.get("existing_device") is missing; update the logic in the
conflict-action code path (the variables validation, validated_existing,
existing_device) to require a validated conflict target before applying any
conflict actions: if validation is falsy or validation.get("existing_device") is
None, return a 400 error (e.g., "Missing validated conflict target") and do not
mutate existing_device; keep the existing mismatch check (validated_existing.pk
!= existing_device.pk) but only run it after confirmed validated_existing is
present.
- Around line 1067-1078: The migration currently reads
existing_device.custom_field_data["librenms_id"] directly and may overwrite a
mapping incorrectly; replace that direct access by calling
self.librenms_api.get_librenms_id(existing_device) to fetch the current mapping,
verify the returned value is an int and equals the LibreNMS payload device_id
(or the expected device identifier) before calling
migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key), and
ensure you only proceed with migration when get_librenms_id confirms the legacy
id matches the active device_id (otherwise return the 400 response).
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 68-69: The cache key "librenms_locations_choices" is global and
must be scoped per server to avoid cross-server label leakage; update the code
that sets and reads location_cache_key (the variable location_cache_key and
calls like cache.get and cache.set) to include a server-unique identifier (e.g.,
server.id, server.pk, or server.hostname/slug) when building the key so both
reads and writes use the same per-server key (for example append or format the
identifier into the key string) and update any other occurrences of
"librenms_locations_choices" to the new scoped key.
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 215-218: detect_virtual_chassis_from_inventory is calling
_generate_vc_member_name inside a per-member loop without a preloaded naming
pattern, causing a settings lookup per member; fix by loading the VC naming
pattern once before the loop (or change _generate_vc_member_name to accept a
precomputed pattern/cached value) and pass that cached pattern into the function
when creating member_data["suggested_name"] (i.e., compute pattern once, then
call _generate_vc_member_name(master_name, position + 1, pattern) or supply
pattern via closure/cached variable to avoid repeated settings lookups).
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (7)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/cache.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.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/bulk_import.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.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_import_utils.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
🧬 Code graph analysis (3)
netbox_librenms_plugin/tests/test_import_utils.py (8)
netbox_librenms_plugin/import_utils/device_operations.py (1)
validate_device_for_import(80-592)netbox_librenms_plugin/views/imports/actions.py (11)
get(708-728)get(734-758)post(250-398)post(425-702)post(822-829)post(835-842)post(848-855)post(861-1099)post(1111-1125)DeviceValidationDetailsView(731-816)_build_sync_info(761-816)netbox_librenms_plugin/views/imports/list.py (1)
get(124-343)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)netbox_librenms_plugin/utils.py (1)
migrate_legacy_librenms_id(92-122)netbox_librenms_plugin/views/sync/interfaces.py (2)
post(33-80)post(304-381)netbox_librenms_plugin/views/base/cables_view.py (2)
post(323-341)post(349-465)netbox_librenms_plugin/views/base/ip_addresses_view.py (2)
post(283-302)post(402-490)
netbox_librenms_plugin/import_utils/filters.py (1)
netbox_librenms_plugin/librenms_api.py (2)
LibreNMSAPI(15-1039)list_devices(805-870)
netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)netbox_librenms_plugin/librenms_api.py (3)
LibreNMSAPI(15-1039)get_device_info(314-337)get_inventory_filtered(726-803)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)
🔇 Additional comments (4)
netbox_librenms_plugin/import_utils/device_operations.py (2)
449-453: Good schema-preserving update for device-type validation.Line 449-Line 453 keeps
result["device_type"]shape intact while mappingmatched→found, which avoids downstream key-shape regressions.
655-662: Resolved server key usage is consistent in validation and persisted mapping.Line 661-Line 662 and Line 747 correctly use
api.server_key, which keepslibrenms_idlinkage aligned with the effective LibreNMS server.Also applies to: 740-748
netbox_librenms_plugin/import_utils/cache.py (1)
79-89: Nice hardening on cache timestamp parsing and deterministic key hashing.The naive→UTC normalization and SHA-256 keying here are solid stability improvements.
Also applies to: 146-148
netbox_librenms_plugin/import_utils/filters.py (1)
174-183: Deterministic, server-scoped cache key construction looks good.Line 182 correctly keys by
api.server_keyplus stable digests, which improves cross-worker cache hit consistency.
| 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 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) |
There was a problem hiding this comment.
Legacy librenms_id migration can rewrite the wrong mapping and bypasses the mapping accessor contract.
The migration branch directly reads custom_field_data["librenms_id"] and does not verify that the legacy integer equals the current LibreNMS payload device_id before rewriting. That can migrate an unrelated legacy value to the active server key.
🔧 Proposed fix
- cf_value = existing_device.custom_field_data.get("librenms_id")
- if not isinstance(cf_value, int):
+ mapped_id = self.librenms_api.get_librenms_id(existing_device)
+ if mapped_id is not None:
return HttpResponse(
"Device librenms_id is already in JSON format; no migration needed.",
status=400,
)
+ cf_value = existing_device.custom_field_data.get("librenms_id")
+ if type(cf_value) is not int:
+ return HttpResponse("Device librenms_id has unsupported legacy format.", status=400)
+ if cf_value != librenms_id and not force:
+ return HttpResponse(
+ "Legacy librenms_id does not match current LibreNMS device_id. "
+ "Use force to migrate anyway.",
+ 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,
)🤖 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 1067 - 1078,
The migration currently reads existing_device.custom_field_data["librenms_id"]
directly and may overwrite a mapping incorrectly; replace that direct access by
calling self.librenms_api.get_librenms_id(existing_device) to fetch the current
mapping, verify the returned value is an int and equals the LibreNMS payload
device_id (or the expected device identifier) before calling
migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key), and
ensure you only proceed with migration when get_librenms_id confirms the legacy
id matches the active device_id (otherwise return the 400 response).
- bulk_import.py: add _empty_return() helper, replace 5 inline '([], False) if return_cache_status else []' with calls to it - cache.py: fix docstring example (spurious '-' before hash digest) - filters.py: normalize status filter value to int before comparison so string '1'/'0' from form fields is handled correctly - virtual_chassis.py: preserve discovered SNMP vc_position when available; fall back to sequential counter only when position is absent - actions.py: require validated_existing to be non-None for conflict actions (missing target now returns 400 instead of proceeding) - actions.py: replace find_by_librenms_id collision check with an exclusion-aware queryset (.exclude(pk=).exists()) for cleaner intent - actions.py: verify legacy cf_value matches librenms_id before migration to prevent migrating stale/incorrect associations - test_import_utils.py: add existing_device to all conflict-action test validation dicts to match the new required-target enforcement - test_import_utils.py: patch find_matching_platform at utility module level instead of dcim.models.Platform
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (1)
netbox_librenms_plugin/import_utils/cache.py (1)
68-69:⚠️ Potential issue | 🟠 MajorLocation-choice cache key is not server-scoped.
Using a global key can cross-contaminate location labels between LibreNMS servers.
Proposed fix
- location_cache_key = "librenms_locations_choices" + location_cache_key = f"librenms_locations_choices_{server_key}"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/cache.py` around lines 68 - 69, The cache key "location_cache_key" is global and can mix location labels across LibreNMS servers; change the key to include a server-specific identifier (e.g. server ID, hostname, base_url or API token fingerprint) when setting/reading the cache so entries are namespaced per server. Update places that use location_cache_key and cache.get/cache.set (the variables location_cache_key and cached_locations in import_utils/cache.py) to compose the key like "librenms_locations_choices:{server_identifier}" and ensure the same identifier source (passed into the import function or read from the server config) is used for both cache reads and writes.
🤖 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 367-368: The disabled-device filtering uses a strict int
comparison which drops devices whose status comes back as a string; update the
filter around show_disabled and libre_devices to treat status values robustly by
normalizing d.get("status") before comparing (e.g., accept both 1 and "1" or
attempt an int() conversion with a safe fallback) so active devices aren't
incorrectly filtered out; target the filtering expression that references
show_disabled, libre_devices and d.get("status") and implement
normalization/try-except conversion to ensure both int and string statuses are
handled.
In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 39-40: The filter currently checks device activity with
d.get("status") == 1 which fails when status is serialized as a string; update
the list comprehension that filters devices (the devices = [d for d in devices
if ...] block used when show_disabled is False) to compare a normalized value,
e.g. coerce the status to string or int safely (for example use
str(d.get("status")) == "1" or wrap int conversion with a try/except) so both
"1" and 1 are treated as active.
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 201-205: The fallback for entPhysicalParentRelPos is using a
zero-based idx which yields position=0 for the first member; change the fallback
to be 1-based so vc_position is correct: in the block that parses raw_position
(variables raw_position, position, idx) replace the fallback assignment position
= idx with position = idx + 1 (and keep the int(...) parsing and the except
(TypeError, ValueError) branch), ensuring the computed position (and ultimately
vc_position) is 1-based.
- Around line 410-414: The current logic may let later fallbacks reuse an
already-assigned discovered position because position isn’t moved ahead when
discovered_pos is used; change the handling around chosen_pos so that after
computing chosen_pos = discovered_pos if discovered_pos is not None else
position you also advance the sequential counter: if discovered_pos is None then
position += 1 else set position = max(position, discovered_pos + 1) (or
otherwise bump position past discovered_pos) so the fallback counter always
stays ahead of any discovered_pos; update references in this block (chosen_pos,
discovered_pos, position) accordingly.
In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 1949-1957: The test is hitting the "missing validated conflict
target" 400 because mock of
DeviceConflictActionView.get_validated_device_with_selections returns an empty
validation dict; to exercise the unknown-action branch return a validation that
includes a validated conflict target (e.g., set validation to include the
expected key like "conflict_target" pointing to the existing_device or its
identifier) while keeping the action value unknown, so
mock_validate.return_value = (libre_device, {"conflict_target":
existing_device}, {}) and then assert the response.status_code == 400 to
validate the unknown-action handling in
DeviceConflictActionView.get_validated_device_with_selections.
- Around line 1216-1219: The assertion expects the old warning text
"reinstalled" but the code now emits "hostname differs"; update the test in
tests/test_import_utils.py to assert the new warning text by replacing the check
assert "reinstalled" in result["warnings"][0] with assert "hostname differs" in
result["warnings"][0] (reference: result["serial_action"],
result["existing_match_type"], result["warnings"]).
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 893-899: Hoist the _FORCE_REQUIRED_ACTIONS set out of the request
handler to module scope (defined near the top of the file after imports) so it
is not recreated per request; replace the inline definition in the function with
a reference to that module-level constant (use an immutable frozenset for the
module-level constant, e.g., _FORCE_REQUIRED_ACTIONS = frozenset({"link",
"update", "update_serial", "update_type"}), and keep the existing logic that
checks action in _FORCE_REQUIRED_ACTIONS and request.POST.get("force") == "on").
- Line 312: The assignment device_name = validation["resolved_name"] is brittle
and can raise KeyError if validate_device_for_import didn't set resolved_name;
change it to use defensive access (e.g., device_name =
validation.get("resolved_name") with a sensible fallback such as
validation.get("name") or None) and add a clear handling path (raise a
descriptive error or skip/log) so functions like validate_device_for_import and
any downstream code handle the missing value safely.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 68-69: The cache key "location_cache_key" is global and can mix
location labels across LibreNMS servers; change the key to include a
server-specific identifier (e.g. server ID, hostname, base_url or API token
fingerprint) when setting/reading the cache so entries are namespaced per
server. Update places that use location_cache_key and cache.get/cache.set (the
variables location_cache_key and cached_locations in import_utils/cache.py) to
compose the key like "librenms_locations_choices:{server_identifier}" and ensure
the same identifier source (passed into the import function or read from the
server config) is used for both cache reads and writes.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (6)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/views/imports/actions.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.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/cache.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
🧬 Code graph analysis (3)
netbox_librenms_plugin/tests/test_import_utils.py (2)
netbox_librenms_plugin/views/imports/actions.py (11)
get(708-728)get(734-758)post(250-398)post(425-702)post(822-829)post(835-842)post(848-855)post(861-1117)post(1129-1143)DeviceValidationDetailsView(731-816)_build_sync_info(761-816)netbox_librenms_plugin/utils.py (1)
migrate_legacy_librenms_id(92-122)
netbox_librenms_plugin/import_utils/bulk_import.py (6)
netbox_librenms_plugin/librenms_api.py (2)
LibreNMSAPI(15-1039)get_device_info(314-337)netbox_librenms_plugin/import_utils/cache.py (3)
get_cache_metadata_key(12-27)get_import_device_cache_key(151-170)get_validated_device_cache_key(124-148)netbox_librenms_plugin/import_utils/device_operations.py (2)
import_single_device(595-814)validate_device_for_import(80-592)netbox_librenms_plugin/import_utils/filters.py (1)
get_librenms_devices_for_import(45-226)netbox_librenms_plugin/import_utils/permissions.py (1)
require_permissions(27-44)netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
create_virtual_chassis_with_members(321-464)prefetch_vc_data_for_devices(83-117)
netbox_librenms_plugin/import_utils/virtual_chassis.py (4)
netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)netbox_librenms_plugin/librenms_api.py (3)
LibreNMSAPI(15-1039)get_device_info(314-337)get_inventory_filtered(726-803)netbox_librenms_plugin/views/imports/actions.py (2)
get(708-728)get(734-758)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)
🔇 Additional comments (8)
netbox_librenms_plugin/views/imports/actions.py (8)
39-66: LGTM!The naming preference resolution logic correctly implements the POST → user pref → plugin settings fallback chain. Reusing the
settingsvariable avoids duplicate DB queries when both preferences need the settings fallback.
147-158: LGTM!Clean integration of naming preferences through the helper function, with proper server_key propagation for multi-server support.
760-816: LGTM!The sync comparison logic correctly handles all cases. The
device_type_syncednow properly returnsFalsewhen hardware is present but no matching device type is found (lines 802-803), addressing the previous review concern.
886-890: Validated conflict target requirement implemented.The code now correctly requires
validated_existingbefore proceeding with conflict actions, addressing the previous review concern about bypassing validation context.
932-1066: LGTM!The action handlers correctly:
- Use
set_librenms_device_idper coding guidelines instead of direct field access- Apply
escape()to user-controlled values in error responses (addressing previous XSS concern)- Use
_resolve_naming_preferencesfor consistent naming fallback behavior- Use
find_matching_platformandmatch_librenms_hardware_to_device_typefor exact matching
1068-1101: Legacy ID verification implemented correctly.The migration now verifies that the stored legacy
librenms_idmatches the active LibreNMS device ID (lines 1085-1090), preventing incorrect associations. The directcustom_field_dataaccess is appropriately justified in the comment—the accessor normalizes return values, but migration requires detecting the raw format.
1106-1117: LGTM!The post-action flow correctly clears the cache, re-validates the device with updated state, and triggers the modal close via HTMX header.
670-671: LGTM!Server key propagation to
validate_device_for_importmaintains consistency with multi-server configuration support.
- bulk_import.py: normalize status (int/str) in show_disabled filter
- bulk_import.py: use api.server_key (resolved) in import_single_device call
- filters.py: normalize status in count_librenms_devices show_disabled filter
- virtual_chassis.py: fix 0-based fallback for vc_position (idx+1 not idx)
- virtual_chassis.py: advance sequential counter past discovered_pos; normalize pos=0 to absent
- actions.py: add django.db.transaction import
- actions.py: module-level _FORCE_REQUIRED_ACTIONS frozenset
- actions.py: defensive .get('resolved_name') instead of [] access
- actions.py: add NetBoxObjectPermissionMixin to DeviceConflictActionView
- actions.py: wrap librenms_id collision check+write in transaction.atomic()
- tests: fix unknown-action test to include existing_device in validation dict
- tests: update warning text assertion to 'hostname differs'
- tests: add transaction mock to 6 link/update/update_serial tests
- tests: add TestProcessDeviceFilters with 5 new tests for status norm + api.server_key
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
netbox_librenms_plugin/views/imports/actions.py (1)
1093-1099:⚠️ Potential issue | 🟠 MajorAvoid direct
custom_field_dataaccess for LibreNMS mapping reads.This migration path reads
librenms_iddirectly fromexisting_device.custom_field_data. Please move legacy-format detection behind a LibreNMS API/helper method and consume that method here to keep mapping access contract-consistent.As per coding guidelines, "Always call
LibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly".🤖 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 1093 - 1099, The code reads the legacy librenms_id directly from existing_device.custom_field_data to detect integer vs JSON formats; instead call the helper LibreNMSAPI.get_librenms_id(existing_device) (or the equivalent API method used elsewhere) to obtain the mapping and perform legacy-format detection inside that helper, then consume its result here; remove direct access to existing_device.custom_field_data.get("librenms_id") and update the early-return branch to use the helper's output (e.g., treat non-legacy/JSON result as "no migration needed") so mapping access contract remains consistent.
🤖 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 201-222: The suggested VC member name is off-by-one because
position is already 1-based but the code calls
_generate_vc_member_name(master_name, position + 1) and likewise builds the
fallback f"Member-{position + 1}"; change both calls to use position (not
position + 1) so the generated suggested_name uses the normalized 1-based
position; update all occurrences (e.g., the block populating member_data where
suggested_name is set and the similar code around the other occurrence
mentioned) to pass position directly to _generate_vc_member_name and to format
f"Member-{position}".
- Around line 35-40: The code in virtual_chassis.py assigns a zero-based
fallback position (idx) when parsing member positions, which can produce
position 0; update the fallback to be 1-based by using idx+1 in the except
branch so member_copy["position"] = idx+1 instead of idx. Locate the block that
reads raw_position, tries int(raw_position), and appends member_copy (variables:
raw_position, member_copy["position"], members.append) and change the fallback
assignment to idx+1.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 791-793: platform_synced can end up as None because the chained
expression (netbox_platform and matching_platform and netbox_platform.pk ==
matching_platform.pk) can return a non-boolean; update the assignment for
platform_synced to ensure a boolean is produced by casting that comparison
branch with bool(...) so platform_synced = librenms_os == "-" or
bool(netbox_platform and matching_platform and netbox_platform.pk ==
matching_platform.pk), and apply the same boolean cast to the analogous
expression referenced around the matching lines (809-810) to keep all sync flags
type-stable.
---
Duplicate comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1093-1099: The code reads the legacy librenms_id directly from
existing_device.custom_field_data to detect integer vs JSON formats; instead
call the helper LibreNMSAPI.get_librenms_id(existing_device) (or the equivalent
API method used elsewhere) to obtain the mapping and perform legacy-format
detection inside that helper, then consume its result here; remove direct access
to existing_device.custom_field_data.get("librenms_id") and update the
early-return branch to use the helper's output (e.g., treat non-legacy/JSON
result as "no migration needed") so mapping access contract remains consistent.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/virtual_chassis.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/virtual_chassis.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.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/bulk_import.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.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/filters.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
🧬 Code graph analysis (3)
netbox_librenms_plugin/import_utils/bulk_import.py (6)
netbox_librenms_plugin/librenms_api.py (2)
LibreNMSAPI(15-1039)get_device_info(314-337)netbox_librenms_plugin/import_utils/cache.py (3)
get_cache_metadata_key(12-27)get_import_device_cache_key(151-170)get_validated_device_cache_key(124-148)netbox_librenms_plugin/import_utils/device_operations.py (2)
import_single_device(595-814)validate_device_for_import(80-592)netbox_librenms_plugin/import_utils/filters.py (1)
get_librenms_devices_for_import(53-234)netbox_librenms_plugin/import_utils/permissions.py (1)
require_permissions(27-44)netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
create_virtual_chassis_with_members(323-473)empty_virtual_chassis_data(15-23)prefetch_vc_data_for_devices(83-117)
netbox_librenms_plugin/views/imports/actions.py (11)
netbox_librenms_plugin/utils.py (3)
get_user_pref(258-262)save_user_pref(265-271)set_librenms_device_id(45-67)netbox_librenms_plugin/views/mixins.py (2)
LibreNMSAPIMixin(196-282)librenms_api(217-231)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)netbox_librenms_plugin/views/settings_views.py (3)
get(26-43)post(45-122)post(131-194)netbox_librenms_plugin/views/imports/list.py (1)
get(124-343)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)netbox_librenms_plugin/import_utils/device_operations.py (2)
validate_device_for_import(80-592)_determine_device_name(24-77)netbox_librenms_plugin/views/sync/interfaces.py (2)
post(33-80)post(304-381)netbox_librenms_plugin/views/object_sync/devices.py (4)
post(106-146)post(158-245)post(284-320)post(334-362)netbox_librenms_plugin/views/sync/device_fields.py (6)
post(19-56)post(66-110)post(120-171)post(181-238)post(251-308)post(318-384)netbox_librenms_plugin/import_utils/cache.py (1)
get_import_device_cache_key(151-170)
netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
netbox_librenms_plugin/librenms_api.py (2)
get_device_info(314-337)get_inventory_filtered(726-803)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)
🔇 Additional comments (5)
netbox_librenms_plugin/import_utils/bulk_import.py (1)
366-376: Good status normalization for active-device filtering.This safely handles both
1and"1"responses and avoids dropping valid active devices.netbox_librenms_plugin/import_utils/filters.py (2)
38-49: Active-status filtering is now robust.The conversion-based check correctly handles mixed LibreNMS status types without false exclusions.
190-196: Deterministic cache-key construction looks solid.Using a stable digest for API/client filter payloads is a good reliability improvement for cache reuse.
netbox_librenms_plugin/views/imports/actions.py (1)
922-941: Nice hardening of the conflict check + write path.Grouping the LibreNMS ID collision check and mutation in a single transaction is a meaningful safety improvement.
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
415-423: Good fix for sequential fallback position advancement.Advancing the counter past discovered positions prevents accidental position reuse for later fallback members.
…m_synced bool - bulk_import_devices_shared: check RQ/Redis job state first (is_stopped/is_failed) before falling back to DB status, matching pattern in process_device_filters(); also check on idx==1 (first iteration) not just idx%5==0 - virtual_chassis._clone_virtual_chassis_data: position fallback now idx+1 (1-based) instead of idx (0-based) so position 0 is never produced - virtual_chassis.detect_virtual_chassis_from_inventory: suggested_name uses position directly (not position+1) since position is already 1-based after batch 6 fallback fix - actions._build_sync_info: wrap platform_synced expression in bool() so lazy and/or chain never returns None when matching_platform is None or netbox_platform is None; all sync flag types are now stable booleans - tests: add TestVCPositionHandling (4 tests) and TestBulkImportCancellation (4 tests); add 2 TestBuildSyncInfo tests for platform_synced=False type-stability
- Template: cluster/rack badge → text+tick icon (0a24a51 completion) - Template: naming preference badges in Device Information card header (d1c015f) - Template: keep tooltip on device_type match (existing librenms_id feature) - actions.py: _resolve_naming_preferences gains GET support for HTMX hx-include (d1c015f) - actions.py: DeviceValidationDetailsView passes use_sysname/strip_domain to template - actions.py: add _save_device() helper with full_clean()+ValidationError+IntegrityError handling - actions.py: replace all existing_device.save() with _save_device() in conflict actions - device_operations.py: add naming_criteria dict to validate_device_for_import result - device_operations.py: VC-aware name comparison for librenms_id-matched devices - device_operations.py: call update_vc_member_suggested_names when VC is a stack - bulk_import.py: add use_sysname/strip_domain params to process_device_filters - jobs.py: add use_sysname/strip_domain params to FilterDevicesJob.run() - list.py: resolve naming prefs and pass use_sysname/strip_domain to process_device_filters - list.py: pass use_sysname/strip_domain to FilterDevicesJob enqueue - tables/device_status.py: add #use-sysname-toggle, #strip-domain-toggle to all 4 Details hx-include - librenms_import.js: re-init Bootstrap tooltips after modal HTMX content swap (d1c015f) - tests: add TestNameMatchesWithNamingPreferences (8 tests: VC-aware name matching) - tests: fix existing MagicMock devices to set virtual_chassis=None/vc_position=None
) Replace bare 'except Exception: pass' with logger.exception() so database and config problems are visible in logs rather than silently swallowed. Addresses Copilot review comment on bonzo81#227.
Without this, re-validation after import always uses defaults (use_sysname=True, strip_domain=False) instead of the user's actual choices, causing incorrect name-match status in the row.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
309-317:⚠️ Potential issue | 🟠 MajorFix 1-based VC position handling in suggested-name regeneration.
Line 310 defaults to
idx(0-based), and Line 315 applies+1, which shifts suggested names and can desync stored/displayed positions.🔧 Proposed fix
- for idx, member in enumerate(vc_data.get("members", [])): - raw_position = member.get("position", idx) + for idx, member in enumerate(vc_data.get("members", [])): + raw_position = member.get("position", idx + 1) 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 + base_position = idx + 1 + if base_position < 1: + base_position = idx + 1 + position = base_position + member["position"] = position member["suggested_name"] = _generate_vc_member_name( master_name, position, serial=member.get("serial"), pattern=vc_pattern )🤖 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 309 - 317, The loop currently defaults raw_position to idx (0-based) and then adds +1 to create position, causing a mismatch between stored member["position"] (base_position) and the suggested-name position; change the default to member.get("position", idx + 1) so missing positions default to 1-based, compute base_position = int(raw_position) as before, set position = base_position (remove the +1 adjustment), and store the 1-based value into member["position"]; update usage of position passed to _generate_vc_member_name to use this 1-based position so suggested names and stored/displayed positions stay in sync for vc_data members.
🤖 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 482-487: The cache key created by get_validated_device_cache_key
does not incorporate the naming flags (use_sysname and strip_domain), causing
cached validation results to be reused when those preferences change; update the
cache key construction to include the relevant naming preference flags
(use_sysname and strip_domain) so that the key is unique per naming
configuration, and apply the same change to the other callsite(s) that invoke
get_validated_device_cache_key (the calls around the second occurrence noted) to
avoid returning stale validation results.
- Around line 441-446: The DB fallback currently only treats
JobStatusChoices.STATUS_FAILED as terminal; update the checks that call
job.job.refresh_from_db() and then inspect job.job.status (the blocks that
return _empty_return(return_cache_status)) to treat both
JobStatusChoices.STATUS_FAILED and JobStatusChoices.STATUS_ERRORED as terminal
states, i.e., check for status in {JobStatusChoices.STATUS_FAILED,
JobStatusChoices.STATUS_ERRORED} and return _empty_return(return_cache_status)
when matched; apply this change to the occurrence around the try/except that
logs "Job was stopped before validation started" and the similar fallback block
later in the file.
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Line 770: The mapping incorrectly treats only integer 1 as active because it
compares libre_device.get("status") == 1; normalize the LibreNMS status first
(e.g., coerce to int safely or compare against both "1" and 1) before mapping to
the NetBox "status" field so string "1" is treated as active; update the
expression that builds the dict (the call using libre_device.get("status")) to
perform the normalization (handle None/invalid values with a safe default) and
then set "status": "active" if normalized_status == 1 else "offline".
In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js`:
- Around line 1098-1102: The current code calls
modalContent.querySelectorAll(...) without verifying modalContent is non-null,
which can throw; update the block that re-initializes tooltips (the
bootstrap.Tooltip section referencing modalContent and tooltipEls) to first
check that modalContent is truthy (e.g., if (!modalContent) return or skip)
before calling querySelectorAll, and only proceed to collect tooltipEls and
instantiate new bootstrap.Tooltip(el) when modalContent exists.
In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 262-273: The code currently ignores submitted form toggles when
user prefs are unset; update the assignment of _use_sysname and _strip_domain so
they first use the saved preference (_use_sysname_pref / _strip_domain_pref) if
not None, otherwise use the submitted form value (e.g.
request.POST.get('use_sysname') and request.POST.get('strip_domain') or the
form's cleaned_data equivalents) and only fall back to getattr(settings,
"use_sysname_default", True) / getattr(settings, "strip_domain_default", False)
if no form value is provided; apply the same fix for the duplicate logic around
the variables at the later block (the code referenced at lines ~442-451).
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 309-317: The loop currently defaults raw_position to idx (0-based)
and then adds +1 to create position, causing a mismatch between stored
member["position"] (base_position) and the suggested-name position; change the
default to member.get("position", idx + 1) so missing positions default to
1-based, compute base_position = int(raw_position) as before, set position =
base_position (remove the +1 adjustment), and store the 1-based value into
member["position"]; update usage of position passed to _generate_vc_member_name
to use this 1-based position so suggested names and stored/displayed positions
stay in sync for vc_data members.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (10)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/jobs.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/jobs.pynetbox_librenms_plugin/views/imports/actions.py
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/imports/actions.py
**/jobs.py
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/jobs.py: Background jobs must use NetBox'sJobRunnerbase class (netbox.jobs.JobRunner) for long-running operations like device filtering with VC detection
Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
RQ status values must be:queued,started,finished,stopped,failed(NOTcompleted)
Database Job status values must be:pending,scheduled,running,completed,failed,errored(NOcancelledstatus exists)
Files:
netbox_librenms_plugin/jobs.py
🧠 Learnings (18)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Tables drive most UIs via `tables/*.py` renderers that emit HTMX-enabled columns and buttons. Prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to tables/**/*.py : Prefer updating the table renderer in `tables/*.py` rather than templates when changing row actions, since tables emit HTMX-enabled columns and buttons
Applied to files:
netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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/list.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.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/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection
Applied to files:
netbox_librenms_plugin/jobs.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Update modal messages based on RQ status values: 'Job queued...', 'Processing...', 'Job completed!' with explicit handling for all RQ status values to avoid infinite polling
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
🧬 Code graph analysis (3)
netbox_librenms_plugin/tables/device_status.py (3)
netbox_librenms_plugin/views/imports/actions.py (2)
get(736-756)get(762-790)netbox_librenms_plugin/views/imports/list.py (1)
get(124-366)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)
netbox_librenms_plugin/import_utils/virtual_chassis.py (4)
netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)netbox_librenms_plugin/librenms_api.py (3)
LibreNMSAPI(15-1039)get_device_info(314-337)get_inventory_filtered(726-803)netbox_librenms_plugin/views/imports/actions.py (2)
get(736-756)get(762-790)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)
netbox_librenms_plugin/views/imports/actions.py (3)
netbox_librenms_plugin/utils.py (5)
get_user_pref(258-262)save_user_pref(265-271)find_matching_platform(390-420)match_librenms_hardware_to_device_type(305-353)migrate_legacy_librenms_id(92-122)netbox_librenms_plugin/views/mixins.py (4)
LibreNMSAPIMixin(196-282)NetBoxObjectPermissionMixin(85-193)librenms_api(217-231)require_object_permissions(123-147)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)
🔇 Additional comments (5)
netbox_librenms_plugin/tables/device_status.py (2)
447-503: Good conflict/details action-state handling and icon-only accessibility fallback.The state prioritization is clear, and the
aria-labelfallback for icon-only button rendering is a solid improvement.
520-556: Nice propagation of naming-preference toggles into modal detail requests.Including
#use-sysname-toggleand#strip-domain-togglein all details/review HTMX actions keeps validation details consistent with the active naming mode.Based on learnings: REST endpoints for imports should keep server responses and HTMX targets in sync.
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js (2)
313-328: Good fallback layering inhideModal.Recovering an existing modal instance before manual fallback improves close reliability across environments with/without Bootstrap helpers.
Based on learnings: Modals use Tabler (Bootstrap-like) but without
bootstrap.Modalhelpers.
351-351: Nice consolidation of modal teardown paths.Using one
filterModalreference andhideModal(...)consistently removes duplicated modal-close logic and makes status-path behavior more predictable.Also applies to: 416-418, 459-461, 477-479, 491-493, 507-509, 589-591, 600-602, 607-609, 618-620, 1018-1020
netbox_librenms_plugin/jobs.py (1)
49-50: Good propagation of naming prefs into background filtering.This cleanly threads
use_sysname/strip_domaininto the shared filter pipeline, keeping async behavior aligned with synchronous flows.Also applies to: 95-97
| cache_key = get_validated_device_cache_key( | ||
| server_key=api.server_key, | ||
| filters=filters, | ||
| device_id=device_id, | ||
| vc_enabled=vc_detection_enabled, | ||
| ) |
There was a problem hiding this comment.
Scope validated-cache keys by naming preferences.
Validation output depends on use_sysname/strip_domain, but current cache keys don’t include those flags. Reusing cached entries across toggle changes can yield stale matching and wrong conflict actions.
🔧 Proposed fix
+ cache_scoped_filters = {
+ **filters,
+ "__use_sysname": bool(use_sysname),
+ "__strip_domain": bool(strip_domain),
+ }
+
cache_key = get_validated_device_cache_key(
server_key=api.server_key,
- filters=filters,
+ filters=cache_scoped_filters,
device_id=device_id,
vc_enabled=vc_detection_enabled,
)
@@
cache_metadata_key = get_cache_metadata_key(
- server_key=api.server_key, filters=filters, vc_enabled=vc_detection_enabled
+ server_key=api.server_key, filters=cache_scoped_filters, vc_enabled=vc_detection_enabled
)Also applies to: 557-559
🤖 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 482 - 487,
The cache key created by get_validated_device_cache_key does not incorporate the
naming flags (use_sysname and strip_domain), causing cached validation results
to be reused when those preferences change; update the cache key construction to
include the relevant naming preference flags (use_sysname and strip_domain) so
that the key is unique per naming configuration, and apply the same change to
the other callsite(s) that invoke get_validated_device_cache_key (the calls
around the second occurrence noted) to avoid returning stale validation results.
| "site": site, | ||
| "device_type": device_type, | ||
| "role": device_role, | ||
| "status": "active" if libre_device.get("status") == 1 else "offline", |
There was a problem hiding this comment.
Normalize LibreNMS status before mapping to NetBox status.
Line 770 treats only integer 1 as up. If LibreNMS returns "1" (string), active devices are imported as offline.
🔧 Proposed fix
- device_data = {
+ status_raw = libre_device.get("status")
+ is_up = str(status_raw).strip() == "1"
+
+ device_data = {
"name": device_name,
"site": site,
"device_type": device_type,
"role": device_role,
- "status": "active" if libre_device.get("status") == 1 else "offline",
+ "status": "active" if is_up 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)}},
}🤖 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` at line 770, The
mapping incorrectly treats only integer 1 as active because it compares
libre_device.get("status") == 1; normalize the LibreNMS status first (e.g.,
coerce to int safely or compare against both "1" and 1) before mapping to the
NetBox "status" field so string "1" is treated as active; update the expression
that builds the dict (the call using libre_device.get("status")) to perform the
normalization (handle None/invalid values with a safe default) and then set
"status": "active" if normalized_status == 1 else "offline".
| // Re-initialize tooltips for newly swapped modal content | ||
| if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) { | ||
| const tooltipEls = modalContent.querySelectorAll('[data-bs-toggle="tooltip"]'); | ||
| [...tooltipEls].map(el => new bootstrap.Tooltip(el)); | ||
| } |
There was a problem hiding this comment.
Guard modalContent before tooltip query to prevent runtime errors.
At Line 1100, modalContent.querySelectorAll(...) can throw if modalContent is null in pages/flows where the container is not present at initialization time.
✅ Minimal fix
- if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
+ if (modalContent && typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
const tooltipEls = modalContent.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipEls].map(el => new bootstrap.Tooltip(el));
}📝 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.
| // Re-initialize tooltips for newly swapped modal content | |
| if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) { | |
| const tooltipEls = modalContent.querySelectorAll('[data-bs-toggle="tooltip"]'); | |
| [...tooltipEls].map(el => new bootstrap.Tooltip(el)); | |
| } | |
| // Re-initialize tooltips for newly swapped modal content | |
| if (modalContent && typeof bootstrap !== 'undefined' && bootstrap.Tooltip) { | |
| const tooltipEls = modalContent.querySelectorAll('[data-bs-toggle="tooltip"]'); | |
| [...tooltipEls].map(el => new bootstrap.Tooltip(el)); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js`
around lines 1098 - 1102, The current code calls
modalContent.querySelectorAll(...) without verifying modalContent is non-null,
which can throw; update the block that re-initializes tooltips (the
bootstrap.Tooltip section referencing modalContent and tooltipEls) to first
check that modalContent is truthy (e.g., if (!modalContent) return or skip)
before calling querySelectorAll, and only proceed to collect tooltipEls and
instantiate new bootstrap.Tooltip(el) when modalContent exists.
…inventory - _refresh_existing_device: extend with re-lookup path when existing_device was None at cache time (imports since caching are now detected); new signature (validation, libre_device=None, server_key='default'); add find_by_librenms_id import; update call site to pass libre_device=device, server_key=api.server_key - Validation modal: show server name(s) alongside librenms_id badge (DeviceValidationDetailsView._build_id_server_info); falls back to plain ID for legacy bare-int format - Sync page: show 'LibreNMS Connections' table card with per-server entries; configured servers get display name + clickable ID link; orphaned/stale server entries show in red with a 'Remove' button (RemoveServerMappingView, POST devices/<pk>/remove-server-mapping/); legacy bare-int falls back to old card - Tests: TestBuildAllServerMappings (5 cases) and TestBuildIdServerInfo (5 cases) added; all 135 affected tests pass Ported from inventory branch commits b3fed51 and 2d26019.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (3)
netbox_librenms_plugin/import_utils/bulk_import.py (2)
489-492:⚠️ Potential issue | 🟠 MajorInclude
erroredin DB fallback termination checks.Both DB fallback branches only stop on
failed, soerroredjobs can continue processing unexpectedly.🔧 Suggested fix
- if job.job.status == JobStatusChoices.STATUS_FAILED: + job_status = job.job.status + status_value = job_status.value if hasattr(job_status, "value") else job_status + if status_value in ( + JobStatusChoices.STATUS_FAILED, + JobStatusChoices.STATUS_ERRORED, + "failed", + "errored", + ): job.logger.warning("Job was stopped before validation started") return _empty_return(return_cache_status) @@ - if job.job.status == JobStatusChoices.STATUS_FAILED: + job_status = job.job.status + status_value = job_status.value if hasattr(job_status, "value") else job_status + if status_value in ( + JobStatusChoices.STATUS_FAILED, + JobStatusChoices.STATUS_ERRORED, + "failed", + "errored", + ): job.logger.info(f"Job stopped at device {idx}/{total}. Exiting gracefully.") return _empty_return(return_cache_status)Based on learnings: Database Job status values include
erroredas a terminal state and should be treated as stopped for this flow.Also applies to: 518-521
🤖 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 489 - 492, The termination checks currently only treat JobStatusChoices.STATUS_FAILED as a terminal state; update both places that call job.job.refresh_from_db() and check job.job.status (the block using JobStatusChoices.STATUS_FAILED and returning _empty_return(return_cache_status)) to also treat JobStatusChoices.STATUS_ERRORED (or the enum member representing "errored") as a terminal state so errored jobs are stopped the same as failed ones; apply the same change to the other occurrence around the block referenced (the second refresh/check at the later location).
528-533:⚠️ Potential issue | 🟠 MajorScope validation cache keys by naming preferences.
Validation output depends on
use_sysnameandstrip_domain, but the cache key/metadata key currently ignore both flags. That can reuse stale validation across preference toggles.🔧 Suggested fix
- cache_key = get_validated_device_cache_key( + cache_scoped_filters = { + **filters, + "__use_sysname": bool(use_sysname), + "__strip_domain": bool(strip_domain), + } + cache_key = get_validated_device_cache_key( server_key=api.server_key, - filters=filters, + filters=cache_scoped_filters, device_id=device_id, vc_enabled=vc_detection_enabled, ) @@ cache_metadata_key = get_cache_metadata_key( - server_key=api.server_key, filters=filters, vc_enabled=vc_detection_enabled + server_key=api.server_key, + filters=cache_scoped_filters, + vc_enabled=vc_detection_enabled, ) @@ - "filters": filters, + "filters": cache_scoped_filters,Also applies to: 603-605
🤖 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 528 - 533, The cache key generation for device validation (calls to get_validated_device_cache_key) ignores naming preference flags use_sysname and strip_domain, causing stale cache reuse; update the get_validated_device_cache_key signature and its callers (the usages at the shown call near get_validated_device_cache_key and the other call around lines 603-605) to include the use_sysname and strip_domain flags, and ensure the function incorporates those two booleans into both the cache key and any metadata key computation so validation is scoped by naming preferences.netbox_librenms_plugin/views/imports/actions.py (1)
861-863:⚠️ Potential issue | 🟠 MajorAvoid direct
custom_field_data["librenms_id"]access in mapping paths.Line 861 and Line 1156 bypass the mapping accessor contract by reading the custom field directly. Please route through
LibreNMSAPImapping accessors/helpers for both display and migration decisions.As per coding guidelines: "Always call
LibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly".Also applies to: 1153-1158
🤖 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 861 - 863, Replace direct reads of existing_device.custom_field_data.get("librenms_id") with the LibreNMSAPI mapping accessor: call LibreNMSAPI.get_librenms_id(...) to retrieve the mapping object and base your dict/type checks and migration/display decisions on its return value; update both the check around cf_value in imports/actions.py (function handling existing_device) and the similar block at lines ~1153-1158 to use LibreNMSAPI.get_librenms_id(existing_device) (or the VM variant if applicable) instead of touching custom_field_data directly.
🤖 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 363-367: The current logic only attempts the sys_name lookup when
hostname is truthy, which misses matches when hostname is empty; change the flow
in the block that sets new_device so that after trying hostname
(Model.objects.filter(name__iexact=hostname).first()) you independently attempt
sys_name when new_device is still falsy (if not new_device and sys_name:
new_device = Model.objects.filter(name__iexact=sys_name).first()), i.e., remove
the hostname truthiness gating around the sys_name lookup so sys_name is used as
a fallback regardless of hostname presence.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 50-55: The template's comparisons against libre_device.status only
match integers 1/0 and mark string values like "1"/"0" as Unknown; update the
conditional checks to normalize the value before comparing (e.g., coerce
libre_device.status to an integer using the template filter or convert to string
and check both forms) so that both numeric and string responses from the API are
handled; modify the three conditionals that reference libre_device.status (the
Up/Down/Unknown badge branches) to perform the normalized comparison (e.g., use
libero_device.status|int == 1 and == 0 or equivalent).
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 997-1004: Extract the repeated resolved_name fallback logic into a
single helper (e.g., _resolve_hostname_for_import(request, validation,
libre_device)) that returns the final hostname; implement it to check
validation.get("resolved_name") and return that if present, otherwise call
_resolve_naming_preferences(request) to get use_sysname and strip_domain and
then call _determine_device_name(libre_device, use_sysname=use_sysname,
strip_domain=strip_domain). Replace the duplicated blocks that reference
resolved_name and call _determine_device_name (including the occurrences around
the existing resolved_name usage and the locations noted) to call the new helper
instead so all branches use the same canonical resolution path.
- Around line 979-988: The uniqueness check using transaction.atomic() + the
conflict_exists read is still race-prone; instead serialize on (server_key,
librenms_id) by introducing and using a DB-enforced mapping or lock before
mutating Device: inside transaction.atomic() create or lock a dedicated mapping
row (e.g., LibrenmsIdMapping with unique_together=('server_key','librenms_id'))
or SELECT ... FOR UPDATE on that mapping, then use that locked mapping to
decide/update the Device (replace the current conflict_exists flow tied to
server_key and librenms_id); alternatively add a unique constraint on the new
mapping model and catch IntegrityError to handle concurrent assignments. Ensure
references to transaction.atomic, server_key, conflict_exists, and Device are
replaced by this serialized mapping/lock logic wherever the check occurs (also
update the other occurrences noted).
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 405-418: The removal currently deletes any server_key present in
device.custom_field_data["librenms_id"]; change the logic in the view around
cf_value / cf / server_key (and inside the transaction where device_locked is
used) to only allow deletion when the existing mapping for server_key is
explicitly stale/orphaned: fetch mapping =
device.custom_field_data.get("librenms_id", {}).get(server_key) and validate a
stale indicator before deleting (e.g., mapping.get("stale") is True or
mapping.get("status") == "orphan" or mapping lacks a live identifier like
"device_id"); if the mapping is not stale, add messages.warning(request, ...)
and redirect without modifying device_locked or saving. Ensure the check is
applied both before acquiring the lock and re-checked after select_for_update()
on device_locked to avoid races.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 489-492: The termination checks currently only treat
JobStatusChoices.STATUS_FAILED as a terminal state; update both places that call
job.job.refresh_from_db() and check job.job.status (the block using
JobStatusChoices.STATUS_FAILED and returning _empty_return(return_cache_status))
to also treat JobStatusChoices.STATUS_ERRORED (or the enum member representing
"errored") as a terminal state so errored jobs are stopped the same as failed
ones; apply the same change to the other occurrence around the block referenced
(the second refresh/check at the later location).
- Around line 528-533: The cache key generation for device validation (calls to
get_validated_device_cache_key) ignores naming preference flags use_sysname and
strip_domain, causing stale cache reuse; update the
get_validated_device_cache_key signature and its callers (the usages at the
shown call near get_validated_device_cache_key and the other call around lines
603-605) to include the use_sysname and strip_domain flags, and ensure the
function incorporates those two booleans into both the cache key and any
metadata key computation so validation is scoped by naming preferences.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 861-863: Replace direct reads of
existing_device.custom_field_data.get("librenms_id") with the LibreNMSAPI
mapping accessor: call LibreNMSAPI.get_librenms_id(...) to retrieve the mapping
object and base your dict/type checks and migration/display decisions on its
return value; update both the check around cf_value in imports/actions.py
(function handling existing_device) and the similar block at lines ~1153-1158 to
use LibreNMSAPI.get_librenms_id(existing_device) (or the VM variant if
applicable) instead of touching custom_field_data directly.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (10)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Sync pages should extend
librenms_sync_base.html.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/imports/actions.py
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (25)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/urls.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_sync_view_mismatch.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. 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/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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/__init__.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/**/*.py : Views should follow layered structure: extend the closest base class from `views/base/` and compose mixins like `LibreNMSAPIMixin` and `CacheMixin`
Applied to files:
netbox_librenms_plugin/views/__init__.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/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/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Database Job status values must be: `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : RQ status values must be: `queued`, `started`, `finished`, `stopped`, `failed` (NOT `completed`)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database after stopping RQ job
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.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/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
🧬 Code graph analysis (2)
netbox_librenms_plugin/views/sync/device_fields.py (4)
netbox_librenms_plugin/utils.py (1)
match_librenms_hardware_to_device_type(305-353)netbox_librenms_plugin/views/mixins.py (3)
LibreNMSAPIMixin(196-282)require_all_permissions(167-179)librenms_api(217-231)netbox_librenms_plugin/views/sync/cables.py (1)
post(126-146)netbox_librenms_plugin/librenms_api.py (2)
get_librenms_id(173-231)get_device_info(314-337)
netbox_librenms_plugin/urls.py (2)
netbox_librenms_plugin/views/imports/actions.py (1)
DeviceConflictActionView(914-1197)netbox_librenms_plugin/views/sync/device_fields.py (2)
RemoveServerMappingView(387-422)UpdateDeviceNameView(12-56)
🔇 Additional comments (1)
netbox_librenms_plugin/views/imports/actions.py (1)
949-973: Good hardening on conflict safety paths.Requiring a validated conflict target, validating
device_idbefore conversion, and escaping serial-conflict response values materially improves safety in this endpoint.Also applies to: 1029-1033, 1054-1058, 1105-1108
| cf_value = device.custom_field_data.get("librenms_id") | ||
| if not isinstance(cf_value, dict) or server_key not in cf_value: | ||
| messages.warning(request, f"No mapping found for server '{server_key}'.") | ||
| return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk) | ||
|
|
||
| with transaction.atomic(): | ||
| device_locked = Device.objects.select_for_update().get(pk=pk) | ||
| cf = device_locked.custom_field_data.get("librenms_id", {}) | ||
| if isinstance(cf, dict) and server_key in cf: | ||
| del cf[server_key] | ||
| device_locked.custom_field_data["librenms_id"] = cf if cf else None | ||
| device_locked.full_clean() | ||
| device_locked.save() | ||
| messages.success(request, f"Removed LibreNMS mapping for server '{server_key}'.") |
There was a problem hiding this comment.
Enforce stale-only mapping removal in the backend.
This endpoint currently removes any server_key present in librenms_id, including configured/active mappings. The UI only offers removal for orphaned mappings, so the server should enforce the same constraint.
🔧 Suggested guard
def post(self, request, pk):
@@
- with transaction.atomic():
+ from netbox.plugins import get_plugin_config
+ servers_cfg = get_plugin_config("netbox_librenms_plugin", "servers") or {}
+ if server_key in servers_cfg:
+ messages.error(request, f"Server '{server_key}' is configured and cannot be removed here.")
+ return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
+
+ with transaction.atomic():
device_locked = Device.objects.select_for_update().get(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 405 - 418,
The removal currently deletes any server_key present in
device.custom_field_data["librenms_id"]; change the logic in the view around
cf_value / cf / server_key (and inside the transaction where device_locked is
used) to only allow deletion when the existing mapping for server_key is
explicitly stale/orphaned: fetch mapping =
device.custom_field_data.get("librenms_id", {}).get(server_key) and validate a
stale indicator before deleting (e.g., mapping.get("stale") is True or
mapping.get("status") == "orphan" or mapping lacks a live identifier like
"device_id"); if the mapping is not stale, add messages.warning(request, ...)
and redirect without modifying device_locked or saving. Ensure the check is
applied both before acquiring the lock and re-checked after select_for_update()
on device_locked to avoid races.
…nation, sysname fallback, hostname helper, configured-server guard
- cache.py: add use_sysname/strip_domain params to get_cache_metadata_key and
get_validated_device_cache_key so validation is scoped by naming preferences
- bulk_import.py: treat STATUS_ERRORED same as STATUS_FAILED in DB fallback
termination checks; fix sys_name lookup to run independently of hostname
(was gated inside 'if hostname:' block, missing matches when hostname empty)
- jobs.py: store use_sysname/strip_domain in FilterDevicesJob.data so
_load_job_results can reconstruct correct cache keys for background jobs
- list.py: read use_sysname/strip_domain from job data; pass to cache key calls
- actions.py: extract _get_hostname_for_action helper to remove 3x duplication
of resolved_name fallback logic across link/update/sync_name action handlers
- device_fields.py: RemoveServerMappingView now refuses to remove mappings for
servers still present in PLUGINS_CONFIG; guard applied before lock and inside
transaction for race-free enforcement
- device_validation_details.html: normalise libre_device.status comparisons to
handle both int and string values from LibreNMS API ('1'/'0' vs 1/0)
- tests: add TestCacheKeyGeneration naming-mode isolation tests,
TestBulkImportCancellation STATUS_ERRORED test,
TestRefreshExistingDeviceSysNameFallback, TestGetHostnameForAction;
update TestLoadJobResults assertions to include new cache key params
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
netbox_librenms_plugin/import_utils/cache.py (1)
72-73:⚠️ Potential issue | 🟠 MajorScope location-choice cache entries by
server_key.At Line 72, using a global
"librenms_locations_choices"key can cross-contaminate location labels between servers.Proposed fix
- location_cache_key = "librenms_locations_choices" + location_cache_key = f"librenms_locations_choices_{server_key}"Also update the corresponding cache writer(s) to use the same server-scoped key.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/cache.py` around lines 72 - 73, The cache key "librenms_locations_choices" is global and can mix location labels across servers; change the getter to scope by server_key (e.g. compute location_cache_key = f"librenms_locations_choices:{server_key}" before calling cache.get in the code that references location_cache_key and cached_locations) and update any cache writer(s) that set this same key to use the identical server-scoped key format so reads and writes consistently use "librenms_locations_choices:{server_key}" (ensure you locate usages around the location_cache_key variable, cached_locations, and any cache.set/write calls).netbox_librenms_plugin/views/imports/list.py (1)
266-277:⚠️ Potential issue | 🟠 MajorHonor submitted naming toggles when user prefs are unset.
At Line 268 and Line 446, toggle values from the current form request are skipped whenever
*_pref is None, so users without saved prefs get defaults instead of their submitted toggles.Proposed fix
- _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) - ) + form_use_sysname = self._filter_form_data.get("use_sysname_toggle") + form_strip_domain = self._filter_form_data.get("strip_domain_toggle") + _use_sysname = ( + form_use_sysname + if form_use_sysname 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) + ) + ) + _strip_domain = ( + form_strip_domain + if form_strip_domain 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) + ) + )Apply the same precedence pattern in
_get_import_queryset()foruse_sysname/strip_domain.Also applies to: 446-455
🤖 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 266 - 277, The current logic uses _use_sysname_pref/_strip_domain_pref and then falls back to settings defaults when prefs are None, which ignores the submitted form toggles; update the precedence so that when get_user_pref(...) returns None you first check the current request/form values (e.g., request.POST.get("use_sysname") / request.POST.get("strip_domain") or the view's submitted parameters) and parse them into booleans before falling back to getattr(settings, "..._default", ...); apply the same change inside _get_import_queryset() so the use_sysname and strip_domain variables honor submitted toggles when user prefs are unset (reference symbols: _use_sysname_pref, _strip_domain_pref, get_user_pref, _get_import_queryset, use_sysname, strip_domain).netbox_librenms_plugin/views/imports/actions.py (1)
993-1002:⚠️ Potential issue | 🟠 Major
librenms_iduniqueness enforcement is still race-prone.At Line 995, the conflict check is a read-before-write pattern without a DB-enforced unique key (or equivalent lock) for
(server_key, librenms_id). Concurrent requests can both pass and assign duplicates.Also applies to: 1009-1064
🤖 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 993 - 1002, The read-before-write check using Device.objects.filter(... custom_field_data__librenms_id ...) inside the transaction (where server_key = self.librenms_api.server_key and librenms_id is being assigned) is race-prone; replace it with a DB-enforced uniqueness strategy and handle IntegrityError: add a dedicated DB-backed uniqueness (e.g., a new model like DeviceLibrenmsID with fields server_key and librenms_id and a unique_together constraint) or create a DB unique index that represents the (server_key, librenms_id) pair, then change the create/update flow in the code paths around Device, transaction.atomic, and any update logic in the block (including the code referenced at 1009-1064) to perform the insert/update and catch django.db.IntegrityError to detect duplicates instead of relying on the filter-exclude existence check; alternatively, if adding a schema change is not possible immediately, acquire a row-level lock by selecting the Device table row(s) with select_for_update() keyed by server_key/librenms_id before writing to ensure only one transaction proceeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 424-433: Wrap the device_locked.full_clean() and
device_locked.save() calls in a try/except that catches
django.core.exceptions.ValidationError and a broad Exception, call
transaction.set_rollback(True) in the except block, add a messages.error(...)
containing the validation messages or exception string, and perform the same
redirect/return flow used on success instead of allowing the exception to
bubble; locate the block around device_locked.full_clean(),
device_locked.save(), device_locked.custom_field_data and messages.success to
implement this change.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 72-73: The cache key "librenms_locations_choices" is global and
can mix location labels across servers; change the getter to scope by server_key
(e.g. compute location_cache_key = f"librenms_locations_choices:{server_key}"
before calling cache.get in the code that references location_cache_key and
cached_locations) and update any cache writer(s) that set this same key to use
the identical server-scoped key format so reads and writes consistently use
"librenms_locations_choices:{server_key}" (ensure you locate usages around the
location_cache_key variable, cached_locations, and any cache.set/write calls).
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 993-1002: The read-before-write check using
Device.objects.filter(... custom_field_data__librenms_id ...) inside the
transaction (where server_key = self.librenms_api.server_key and librenms_id is
being assigned) is race-prone; replace it with a DB-enforced uniqueness strategy
and handle IntegrityError: add a dedicated DB-backed uniqueness (e.g., a new
model like DeviceLibrenmsID with fields server_key and librenms_id and a
unique_together constraint) or create a DB unique index that represents the
(server_key, librenms_id) pair, then change the create/update flow in the code
paths around Device, transaction.atomic, and any update logic in the block
(including the code referenced at 1009-1064) to perform the insert/update and
catch django.db.IntegrityError to detect duplicates instead of relying on the
filter-exclude existence check; alternatively, if adding a schema change is not
possible immediately, acquire a row-level lock by selecting the Device table
row(s) with select_for_update() keyed by server_key/librenms_id before writing
to ensure only one transaction proceeds.
In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 266-277: The current logic uses
_use_sysname_pref/_strip_domain_pref and then falls back to settings defaults
when prefs are None, which ignores the submitted form toggles; update the
precedence so that when get_user_pref(...) returns None you first check the
current request/form values (e.g., request.POST.get("use_sysname") /
request.POST.get("strip_domain") or the view's submitted parameters) and parse
them into booleans before falling back to getattr(settings, "..._default", ...);
apply the same change inside _get_import_queryset() so the use_sysname and
strip_domain variables honor submitted toggles when user prefs are unset
(reference symbols: _use_sysname_pref, _strip_domain_pref, get_user_pref,
_get_import_queryset, use_sysname, strip_domain).
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (9)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/jobs.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/jobs.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/views/imports/list.py
**/jobs.py
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/jobs.py: Background jobs must use NetBox'sJobRunnerbase class (netbox.jobs.JobRunner) for long-running operations like device filtering with VC detection
Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
RQ status values must be:queued,started,finished,stopped,failed(NOTcompleted)
Database Job status values must be:pending,scheduled,running,completed,failed,errored(NOcancelledstatus exists)
Files:
netbox_librenms_plugin/jobs.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.py
🧠 Learnings (25)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. 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/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection
Applied to files:
netbox_librenms_plugin/jobs.pynetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.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_background_jobs.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.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_background_jobs.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_background_jobs.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Instantiate `JobRunner` subclasses using `object.__new__(JobClass)` to bypass `__init__`, then set `job.job = MagicMock()` and `job.logger = MagicMock()`
Applied to files:
netbox_librenms_plugin/tests/test_background_jobs.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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_background_jobs.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/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/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Database Job status values must be: `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : RQ status values must be: `queued`, `started`, `finished`, `stopped`, `failed` (NOT `completed`)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database after stopping RQ job
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
🧬 Code graph analysis (1)
netbox_librenms_plugin/views/imports/list.py (3)
netbox_librenms_plugin/views/imports/actions.py (2)
get(750-770)get(776-805)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)netbox_librenms_plugin/utils.py (1)
get_user_pref(258-262)
🔇 Additional comments (4)
netbox_librenms_plugin/tests/test_background_jobs.py (1)
689-723: Solid coverage for naming-aware cache-key loading paths.These assertions correctly lock in
use_sysname/strip_domainpropagation through_load_job_results().Also applies to: 743-761
netbox_librenms_plugin/import_utils/bulk_import.py (1)
530-537: Good cache-key scoping for naming preferences.Including
use_sysnameandstrip_domainin both validated-device and metadata keys prevents stale cross-mode cache reuse.Also applies to: 607-613
netbox_librenms_plugin/jobs.py (1)
49-50: Naming-preference propagation is correctly wired through the job lifecycle.Run signature, processing call, and stored
job.dataall stay aligned foruse_sysnameandstrip_domain.Also applies to: 95-97, 116-117
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)
50-56: Accessibility and status-handling updates look good.The int/string status normalization and the added
aria-label+rel="noopener noreferrer"harden both UX and safety in this modal.Also applies to: 123-125, 172-174, 185-187, 228-230, 283-285, 308-310, 446-447, 506-507, 544-545, 553-554, 596-607
… handling - actions.py: check both hyphenated and underscore toggle key variants in _resolve_naming_preferences (e.g. 'use-sysname-toggle' and 'use_sysname-toggle') - actions.py: add select_for_update() row lock inside link/update/update_serial transaction block; document DB-unique-constraint limitation - actions.py sync_serial: wrap conflict-check-and-write in transaction.atomic() with select_for_update() to prevent concurrent serial assignment race - device_fields.py RemoveServerMappingView: wrap full_clean()/save() in try/except for ValidationError and Exception; call transaction.set_rollback(), messages.error(), and redirect on failure instead of bubbling as 500 - bulk_import.py: fix vc_domain dedup key to use sorted member serials instead of per-device device_id, preventing duplicate VC creation for stack members - list.py: add comment explaining why POST toggles are not re-read (they are saved via savePref AJAX before filter form submission) - cache.py: add TODO comment noting location cache key should be server-scoped in a future PR - tests: add TestResolveNamingPreferencesKeys, TestVCDomainStackDedup, and TestRemoveServerMappingViewErrorHandling; update DeviceConflictActionView tests to mock select_for_update() and transaction.atomic()
Primary IP is informational only and not imported, so the green check-circle icons were misleading.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
netbox_librenms_plugin/import_utils/cache.py (1)
71-76:⚠️ Potential issue | 🟠 MajorServer-agnostic location choices cache key still risks cross-server label bleed.
Line 75 still uses a global key (
"librenms_locations_choices"), so cached location labels can be reused across different servers and render incorrect filter summaries.🔧 Proposed fix
- location_cache_key = "librenms_locations_choices" + location_cache_key = f"librenms_locations_choices:{server_key}" cached_locations = cache.get(location_cache_key)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/cache.py` around lines 71 - 76, The cache key for location choices is currently global (location_cache_key = "librenms_locations_choices") which allows labels to bleed across servers; change the key to include the server identifier (e.g., location_cache_key = f"librenms_locations_choices:{server_key}") wherever location_cache_key is defined/used (including the cache.get(...) and any cache.set(...) calls) and ensure server_key is available in that scope before constructing the key so each server has its own cached location choices.netbox_librenms_plugin/views/imports/list.py (1)
451-460:⚠️ Potential issue | 🟠 MajorNaming-preference precedence still drops submitted toggle values when user prefs are unset.
At Line 451 and Line 457, the ternary is reversed:
data_sourcetoggle values are only considered when*_pref is not None. When pref isNone, form-provided toggles are bypassed and defaults are used.🔧 Proposed fix
- use_sysname = ( - data_source.get("use_sysname_toggle", use_sysname_pref) - if use_sysname_pref is not None - else (getattr(_settings, "use_sysname_default", True) if _settings else True) - ) - strip_domain = ( - data_source.get("strip_domain_toggle", strip_domain_pref) - if strip_domain_pref is not None - else (getattr(_settings, "strip_domain_default", False) if _settings else False) - ) + form_use_sysname = data_source.get("use_sysname_toggle") + form_strip_domain = data_source.get("strip_domain_toggle") + use_sysname = ( + form_use_sysname + if form_use_sysname 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) + ) + ) + strip_domain = ( + form_strip_domain + if form_strip_domain 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) + ) + )🤖 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 451 - 460, The ternary conditions for use_sysname and strip_domain are reversed so submitted toggles in data_source get ignored when user prefs are None; change the logic in the use_sysname and strip_domain assignments to prefer data_source.get("use_sysname_toggle"/"strip_domain_toggle") when it exists, otherwise fall back to use_sysname_pref/strip_domain_pref if not None, and only then to getattr(_settings, "use_sysname_default"/"strip_domain_default", <default>). Update the expressions referencing use_sysname_pref, strip_domain_pref, data_source, and _settings (and the attributes use_sysname_default/strip_domain_default) accordingly so the precedence is: submitted toggle → user pref → settings default.netbox_librenms_plugin/views/imports/actions.py (1)
1177-1186:⚠️ Potential issue | 🟠 MajorUse the mapping accessor before legacy migration checks.
This branch still reads the
librenms_idcustom field directly for mapping validation. Please resolve the current mapping viaself.librenms_api.get_librenms_id(existing_device)first, and keep raw custom-field access only for legacy-format detection/migration gating.♻️ Suggested adjustment
+ mapped_id = self.librenms_api.get_librenms_id(existing_device) + if mapped_id is None: + return HttpResponse("Device has no LibreNMS mapping to migrate.", status=400) + if mapped_id != librenms_id: + return HttpResponse( + f"Mapped LibreNMS ID ({mapped_id}) does not match active device ID ({librenms_id}).", + status=400, + ) 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, )As per coding guidelines: "Always call
LibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly".🤖 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 1177 - 1186, Call self.librenms_api.get_librenms_id(existing_device) first and use its returned value for the mapping validation instead of reading existing_device.custom_field_data["librenms_id"] directly; keep direct access to existing_device.custom_field_data only to detect the legacy JSON format (i.e., to gate migration when the stored value is not an int) and then perform the legacy migration to update the mapping; update the checks around cf_value, the comparison to librenms_id, and the HttpResponse branches to rely on the resolved mapping from get_librenms_id (use variables like existing_device, cf_value, librenms_id, and self.librenms_api.get_librenms_id to locate the code).
🤖 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 127-129: The progress log is emitted before the current device is
actually imported (off-by-one); move the job.logger.info(... f"Imported device
{idx} of {total}") so it runs only after a successful import of that device
(i.e., inside the success path of the import loop), and ensure the reported
count matches completed items (use idx+1 if the loop index is zero-based, or
keep idx if it is already 1-based); reference the job.logger call and the loop
variables idx and total when making the change.
In `@netbox_librenms_plugin/tests/test_permissions.py`:
- Around line 961-1038: Add a positive-path assertion to the existing tests to
catch regression where the wrong custom-field key is mutated: in the successful
removal case (use RemoveServerMappingView.post and the mock Device returned by
Device.objects.select_for_update.get), set up a mock_device with
custom_field_data containing the target librenms_id entry, call
view.post(request, pk=...), then assert that
mock_device.custom_field_data["librenms_id"] no longer contains the removed
server_key (or has been updated appropriately) and that mock_device.save() was
called to persist the change; this ensures the view mutates and saves
custom_field_data["librenms_id"] correctly on success.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1046-1049: The serial-conflict check around incoming_serial /
conflict_device (the block that queries
Device.objects.filter(serial=incoming_serial).exclude(pk=existing_device.pk).first())
is still race-prone; modify the flow to either (A) enforce a DB-level unique
constraint on the Device.serial column and wrap the create/update in
transaction.atomic while catching IntegrityError to handle duplicate-serial
failures, or (B) serialize attempts on the serial value (e.g., PostgreSQL
advisory lock or a SELECT ... FOR UPDATE on a row representing that serial)
around the checks and the subsequent save so concurrent requests cannot both
pass the check; apply the same change for the other occurrences you noted (the
blocks around lines where incoming_serial is checked at 1072-1074 and 1116-1120)
and ensure you handle and surface IntegrityError or lock-failure cases to the
caller.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 71-76: The cache key for location choices is currently global
(location_cache_key = "librenms_locations_choices") which allows labels to bleed
across servers; change the key to include the server identifier (e.g.,
location_cache_key = f"librenms_locations_choices:{server_key}") wherever
location_cache_key is defined/used (including the cache.get(...) and any
cache.set(...) calls) and ensure server_key is available in that scope before
constructing the key so each server has its own cached location choices.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1177-1186: Call self.librenms_api.get_librenms_id(existing_device)
first and use its returned value for the mapping validation instead of reading
existing_device.custom_field_data["librenms_id"] directly; keep direct access to
existing_device.custom_field_data only to detect the legacy JSON format (i.e.,
to gate migration when the stored value is not an int) and then perform the
legacy migration to update the mapping; update the checks around cf_value, the
comparison to librenms_id, and the HttpResponse branches to rely on the resolved
mapping from get_librenms_id (use variables like existing_device, cf_value,
librenms_id, and self.librenms_api.get_librenms_id to locate the code).
In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 451-460: The ternary conditions for use_sysname and strip_domain
are reversed so submitted toggles in data_source get ignored when user prefs are
None; change the logic in the use_sysname and strip_domain assignments to prefer
data_source.get("use_sysname_toggle"/"strip_domain_toggle") when it exists,
otherwise fall back to use_sysname_pref/strip_domain_pref if not None, and only
then to getattr(_settings, "use_sysname_default"/"strip_domain_default",
<default>). Update the expressions referencing use_sysname_pref,
strip_domain_pref, data_source, and _settings (and the attributes
use_sysname_default/strip_domain_default) accordingly so the precedence is:
submitted toggle → user pref → settings default.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (8)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.py
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.py
🧠 Learnings (24)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. 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/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
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.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_permissions.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/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/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Database Job status values must be: `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : RQ status values must be: `queued`, `started`, `finished`, `stopped`, `failed` (NOT `completed`)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database after stopping RQ job
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.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/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_permissions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_permissions.py
🧬 Code graph analysis (5)
netbox_librenms_plugin/views/sync/device_fields.py (3)
netbox_librenms_plugin/utils.py (1)
match_librenms_hardware_to_device_type(305-353)netbox_librenms_plugin/views/mixins.py (2)
LibreNMSAPIMixin(196-282)librenms_api(217-231)netbox_librenms_plugin/librenms_api.py (2)
get_librenms_id(173-231)get_device_info(314-337)
netbox_librenms_plugin/import_utils/bulk_import.py (5)
netbox_librenms_plugin/utils.py (1)
find_by_librenms_id(70-89)netbox_librenms_plugin/import_utils/cache.py (3)
get_cache_metadata_key(12-31)get_import_device_cache_key(169-188)get_validated_device_cache_key(131-166)netbox_librenms_plugin/import_utils/filters.py (1)
get_librenms_devices_for_import(53-234)netbox_librenms_plugin/import_utils/permissions.py (1)
require_permissions(27-44)netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
create_virtual_chassis_with_members(324-474)empty_virtual_chassis_data(15-23)prefetch_vc_data_for_devices(83-117)
netbox_librenms_plugin/import_utils/cache.py (1)
netbox_librenms_plugin/views/imports/list.py (1)
get(128-375)
netbox_librenms_plugin/tests/test_permissions.py (1)
netbox_librenms_plugin/views/sync/device_fields.py (8)
RemoveServerMappingView(387-446)post(19-56)post(66-110)post(120-171)post(181-238)post(251-308)post(318-384)post(394-446)
netbox_librenms_plugin/views/imports/list.py (3)
netbox_librenms_plugin/views/imports/actions.py (2)
get(761-781)get(787-816)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(7-48)netbox_librenms_plugin/utils.py (1)
get_user_pref(258-262)
🔇 Additional comments (7)
netbox_librenms_plugin/import_utils/bulk_import.py (1)
494-531: Good RQ-first cancellation detection with DB fallback.The termination checks in Line 494 and Line 529 correctly prioritize Redis/RQ state and gracefully fall back to DB status checks.
netbox_librenms_plugin/views/imports/list.py (1)
309-316: Correct UUID/PK separation for background job polling and result loading.This correctly uses
job.job_idfor/api/core/background-tasks/{uuid}/and keepsjob.pkfor DB-backed result loading.As per coding guidelines: "Use Job UUID (
job.job_id) for RQ API endpoints ... Use Job PK (job.pk) for database endpoints and result loading."netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)
112-124: Nice hardening on external-link safety and icon-button accessibility.The
rel="noopener noreferrer"additions and explicitaria-labelattributes are correctly applied in the modal actions.Also applies to: 594-605
netbox_librenms_plugin/views/sync/device_fields.py (2)
12-56: Good addition of guarded name-sync flow.Permission checks, LibreNMS lookup, validation handling, and user-facing redirects/messages are consistently handled here.
424-446: Transaction and failure handling in mapping removal looks solid.Re-checking under
select_for_update()and returning controlled messages on validation/runtime failures prevents partial updates and 500s.netbox_librenms_plugin/views/imports/actions.py (2)
43-87: Naming preference resolution fallback chain is well-implemented.Supporting both hyphenated/underscored keys and falling back through POST/GET → user prefs → plugin defaults is a solid hardening step.
845-863: Sync-state boolean logic is now consistent and safer.
platform_syncedremains type-stable, and device-type sync correctly flips false when hardware exists but no mapping is found.
- cache.py + forms.py: scope location choices cache key by server_key
(f"librenms_locations_choices:{server_key}") so labels from different
LibreNMS servers don't bleed into each other's filter summaries
- list.py ~245: include exc_info=True in swallowed cache-check exception log
- list.py ~337: log user.username scalar instead of full user object to avoid
serialising sensitive/large data in exception handler
- list.py ~451: fix reversed ternary for use_sysname/strip_domain — submitted
form toggle now correctly takes priority over user pref which takes priority
over settings default (was: pref check gated the toggle, ignoring toggle when
pref was None)
- actions.py ~52: add plain "use_sysname" / "strip_domain" canonical keys to
_USE_SYSNAME_KEYS / _STRIP_DOMAIN_KEYS so preview and import use the same
resolution logic regardless of which key name the form sends
- actions.py ~1016 / ~1136: wrap select_for_update().get() in try/except
Device.DoesNotExist and return 409 instead of propagating a 500
- actions.py ~1054 / ~1084: add select_for_update() to serial-conflict filter
query inside transaction to reduce concurrent-assignment race window
- device_fields.py ~416: also protect "default" server_key when legacy
single-server librenms_url is configured (was only checking servers dict)
- device_fields.py ~430: wrap select_for_update().get(pk=pk) in try/except
Device.DoesNotExist and redirect with error message instead of 500
- bulk_import.py ~127: move progress log inside success path after the import
completes; use idx+1 so the count reflects completed items
- tests: add positive-path removal test; update DeviceConflictActionView mocks
to cover select_for_update().filter() chain
…latform errors, CSV forms
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
netbox_librenms_plugin/views/imports/actions.py (2)
584-587: 🧹 Nitpick | 🔵 TrivialUse lazy string formatting for logger calls.
The f-string is evaluated regardless of whether the log level is enabled. Use
%-style formatting for logger efficiency.♻️ Suggested fix
- logger.info( - f"Enqueued ImportDevicesJob {job.pk} (UUID: {job.job_id}) for user {request.user} - {total_import_count} devices/VMs" - ) + logger.info( + "Enqueued ImportDevicesJob %s (UUID: %s) for user %s - %d devices/VMs", + job.pk, job.job_id, request.user, total_import_count + )🤖 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 584 - 587, The logger call in ImportDevicesJob enqueue uses an f-string which will be evaluated even if the log level is disabled; change the logger invocation in the block that logs "Enqueued ImportDevicesJob" (the logger.info call referencing job.pk, job.job_id, request.user and total_import_count) to use lazy %-style formatting and pass the values as separate parameters (e.g. logger.info("Enqueued ImportDevicesJob %s (UUID: %s) for user %s - %d devices/VMs", job.pk, job.job_id, request.user, total_import_count)) so interpolation happens only when the message will actually be emitted.
594-606: 🧹 Nitpick | 🔵 TrivialConsider using
reverse()for URL generation.Hardcoded URLs are fragile and may break if URL patterns change. Django's
reverse()provides safer URL generation.♻️ Suggested fix
+ from django.urls import reverse + messages.info( request, mark_safe( f"Import job started for {total_import_count} device{'s' if total_import_count != 1 else ''}. " - f'You can monitor progress in the <a href="/core/jobs/{job.pk}/">Jobs interface</a>.' + f'You can monitor progress in the <a href="{reverse("core:job", kwargs={"pk": job.pk})}">Jobs interface</a>.' ), ) if request.headers.get("HX-Request"): - # For HTMX requests, redirect to clean import page (no filters) - # This matches the "Clear" button behavior return HttpResponse( "", - headers={"HX-Redirect": "/plugins/librenms_plugin/librenms-import/"}, + headers={"HX-Redirect": reverse("plugins:netbox_librenms_plugin:librenms_import")}, )🤖 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 594 - 606, The code currently uses hardcoded URLs for the job link and the HTMX redirect (e.g., the f'/core/jobs/{job.pk}/' string and '/plugins/librenms_plugin/librenms-import/') which is fragile; update the view to build both URLs with Django's reverse() (or reverse_lazy) instead: generate the job URL with reverse('admin:core_job_change', args=[job.pk]) or the appropriate named route and use reverse('librenms_plugin:librenms-import') (or the actual URL names used in your URLconf) for the HX-Redirect, then substitute those reversed URLs into the mark_safe message and the HttpResponse headers that check request.headers.get("HX-Request"); ensure imports include django.urls.reverse and adjust any tests or templates that relied on the previous hardcoded strings.
♻️ Duplicate comments (1)
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
309-318:⚠️ Potential issue | 🟠 MajorOff-by-one error in position handling and 0-based fallback.
Two issues in this loop:
Line 310: Fallback is
idx(0-based), inconsistent withdetect_virtual_chassis_from_inventoryand_clone_virtual_chassis_datawhich useidx + 1.Lines 315-316:
position = base_position + 1assumes positions are 0-based, but data fromdetect_virtual_chassis_from_inventoryalready uses 1-based positions. This causes position 1 to generate names as if it were position 2.Trace for a member with
position=1:
base_position = 1,position = 2→ generatesMember-2instead ofMember-1🐛 Proposed fix
for idx, member in enumerate(vc_data.get("members", [])): - raw_position = member.get("position", idx) + raw_position = member.get("position", idx + 1) 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 + base_position = idx + 1 + # Positions are already 1-based from detect_virtual_chassis_from_inventory + if base_position < 1: + base_position = idx + 1 + member["position"] = base_position member["suggested_name"] = _generate_vc_member_name( - master_name, position, serial=member.get("serial"), pattern=vc_pattern + master_name, base_position, serial=member.get("serial"), pattern=vc_pattern )🤖 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 309 - 318, The loop in import_utils.virtual_chassis.py mis-handles 0/1-based positions: parse raw_position into an int and treat it as 1-based (if parsed value is >= 1 use it), otherwise fall back to idx + 1 (not idx), set member["position"] to that 1-based value (base_position), and pass that same 1-based value to _generate_vc_member_name (do not add +1); this ensures consistency with detect_virtual_chassis_from_inventory and _clone_virtual_chassis_data and prevents off-by-one name generation.
🤖 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/filters.py`:
- Around line 41-43: The current filter uses int(d.get("disabled", 0)) which can
raise on None/empty/non-numeric values; change the comprehension to first
normalize/parse the disabled field with a safe conversion (e.g., val =
d.get("disabled"); try: disabled = int(val) except (TypeError, ValueError):
disabled = 0) and then filter with disabled != 1 (replace the existing list
comprehension in the devices filtering block that uses show_disabled). Apply the
same safe-parsing logic to the corresponding devices filtering in bulk_import.py
(the code that currently calls int(d.get("disabled", 0)) around line 446) so
both places use the same normalized disabled value before comparison.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Line 1051: Replace f-string logger calls with lazy `%`-style logging to avoid
eager evaluation; e.g., change logger.info(f"Linked device
'{existing_device.name}' to LibreNMS ID {librenms_id}") to logger.info("Linked
device '%s' to LibreNMS ID %s", existing_device.name, librenms_id) and apply the
same pattern for the other logger.info/debug/error calls referenced (lines
around the blocks that use variables like existing_device, librenms_id, and any
message templates at 1080-1083, 1110-1113, 1121, 1129, 1162, 1177, 1193,
1228-1231) so all logging defers string interpolation until the message is
emitted.
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 46-55: The device name update path currently calls
device.full_clean() and device.save() without wrapping the write inside a
database transaction; wrap the mutation in transaction.atomic() so the revert on
exception is atomic and consistent. Update the view handling the POST (the block
that sets device.name, calls device.full_clean() and device.save()) to perform
the read of selected items via request.POST.getlist('select'), load any cached
data with CacheMixin.get_cache_key(), and then run the full_clean()/save() (and
the device.name rollback on exception) inside a transaction.atomic() context;
ensure the view still uses LibreNMSPermissionMixin and
NetBoxObjectPermissionMixin for permission checks and ends by redirecting to the
sync tab with ?tab=<resource> on error/success.
---
Outside diff comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 584-587: The logger call in ImportDevicesJob enqueue uses an
f-string which will be evaluated even if the log level is disabled; change the
logger invocation in the block that logs "Enqueued ImportDevicesJob" (the
logger.info call referencing job.pk, job.job_id, request.user and
total_import_count) to use lazy %-style formatting and pass the values as
separate parameters (e.g. logger.info("Enqueued ImportDevicesJob %s (UUID: %s)
for user %s - %d devices/VMs", job.pk, job.job_id, request.user,
total_import_count)) so interpolation happens only when the message will
actually be emitted.
- Around line 594-606: The code currently uses hardcoded URLs for the job link
and the HTMX redirect (e.g., the f'/core/jobs/{job.pk}/' string and
'/plugins/librenms_plugin/librenms-import/') which is fragile; update the view
to build both URLs with Django's reverse() (or reverse_lazy) instead: generate
the job URL with reverse('admin:core_job_change', args=[job.pk]) or the
appropriate named route and use reverse('librenms_plugin:librenms-import') (or
the actual URL names used in your URLconf) for the HX-Redirect, then substitute
those reversed URLs into the mark_safe message and the HttpResponse headers that
check request.headers.get("HX-Request"); ensure imports include
django.urls.reverse and adjust any tests or templates that relied on the
previous hardcoded strings.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 309-318: The loop in import_utils.virtual_chassis.py mis-handles
0/1-based positions: parse raw_position into an int and treat it as 1-based (if
parsed value is >= 1 use it), otherwise fall back to idx + 1 (not idx), set
member["position"] to that 1-based value (base_position), and pass that same
1-based value to _generate_vc_member_name (do not add +1); this ensures
consistency with detect_virtual_chassis_from_inventory and
_clone_virtual_chassis_data and prevents off-by-one name generation.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (11)
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/sync/device_fields.py
| # Device was deleted since caching — recompute readiness to match | ||
| # validate_device_for_import logic. | ||
| validation["existing_device"] = None | ||
| validation["existing_match_type"] = None | ||
| can_import = not bool(validation.get("issues")) | ||
| if validation.get("import_as_vm"): | ||
| # VMs only require a cluster (site/role not mandatory) | ||
| is_ready = can_import and bool(validation.get("cluster", {}).get("found")) | ||
| else: | ||
| is_ready = ( | ||
| can_import | ||
| and bool(validation.get("site", {}).get("found")) | ||
| and bool(validation.get("device_type", {}).get("found")) | ||
| and bool(validation.get("device_role", {}).get("found")) | ||
| ) | ||
| validation["can_import"] = can_import | ||
| validation["is_ready"] = is_ready |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Avoid in-place status recomputation here; route through import_validation_helpers.py.
This block manually recalculates can_import/is_ready and mutates validation state inline. That logic should be centralized to prevent behavior drift with the rest of import validation flows.
As per coding guidelines: "Centralize validation state mutation during import using import_validation_helpers.py for role/cluster/rack assignment, issue removal, and status recalculation."
| if not show_disabled: | ||
| devices = [d for d in devices if int(d.get("disabled", 0)) != 1] | ||
|
|
There was a problem hiding this comment.
Guard disabled-flag parsing to prevent filter crashes.
At Line 42, int(d.get("disabled", 0)) can raise on None, empty strings, or unexpected payloads and abort filtering.
🔧 Proposed fix
+def _is_disabled(value) -> bool:
+ try:
+ return int(str(value).strip()) == 1
+ except (TypeError, ValueError):
+ return False
+
...
if not show_disabled:
- devices = [d for d in devices if int(d.get("disabled", 0)) != 1]
+ devices = [d for d in devices if not _is_disabled(d.get("disabled", 0))]Please apply the same normalization in netbox_librenms_plugin/import_utils/bulk_import.py (Line 446) to keep behavior consistent.
🤖 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/non-numeric values; change the comprehension to first normalize/parse
the disabled field with a safe conversion (e.g., val = d.get("disabled"); try:
disabled = int(val) except (TypeError, ValueError): disabled = 0) and then
filter with disabled != 1 (replace the existing list comprehension in the
devices filtering block that uses show_disabled). Apply the same safe-parsing
logic to the corresponding devices filtering in bulk_import.py (the code that
currently calls int(d.get("disabled", 0)) around line 446) so both places use
the same normalized disabled value before comparison.
| existing_device.device_type = librenms_device_type | ||
| if err := _save_device(existing_device): | ||
| return err | ||
| logger.info(f"Linked device '{existing_device.name}' to LibreNMS ID {librenms_id}") |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider using lazy string formatting in logger calls.
Multiple logger.info() calls use f-strings which are evaluated regardless of log level. While not a functional issue, using %-style formatting is more efficient.
♻️ Example fix (apply similar pattern to other logger calls)
- logger.info(f"Linked device '{existing_device.name}' to LibreNMS ID {librenms_id}")
+ logger.info("Linked device '%s' to LibreNMS ID %s", existing_device.name, librenms_id)Also applies to: 1080-1083, 1110-1113, 1121-1121, 1129-1129, 1162-1162, 1177-1177, 1193-1193, 1228-1231
🤖 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` at line 1051, Replace
f-string logger calls with lazy `%`-style logging to avoid eager evaluation;
e.g., change logger.info(f"Linked device '{existing_device.name}' to LibreNMS ID
{librenms_id}") to logger.info("Linked device '%s' to LibreNMS ID %s",
existing_device.name, librenms_id) and apply the same pattern for the other
logger.info/debug/error calls referenced (lines around the blocks that use
variables like existing_device, librenms_id, and any message templates at
1080-1083, 1110-1113, 1121, 1129, 1162, 1177, 1193, 1228-1231) so all logging
defers string interpolation until the message is emitted.
| old_name = device.name | ||
| device.name = sys_name | ||
| try: | ||
| device.full_clean() | ||
| device.save() | ||
| except (ValidationError, IntegrityError) as e: | ||
| device.name = old_name | ||
| error_msg = e.message_dict if hasattr(e, "message_dict") else str(e) | ||
| messages.error(request, f"Failed to update device name to '{sys_name}': {error_msg}") | ||
| return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Wrap name update write path in transaction.atomic().
This sync POST action writes model state but does not wrap the mutation in transaction.atomic().
🔧 Proposed fix
old_name = device.name
- device.name = sys_name
try:
- device.full_clean()
- device.save()
+ with transaction.atomic():
+ device.name = sys_name
+ device.full_clean()
+ device.save()
except (ValidationError, IntegrityError) as e:
device.name = old_name
error_msg = e.message_dict if hasattr(e, "message_dict") else str(e)As per coding guidelines: "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>."
🤖 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 46 - 55, The
device name update path currently calls device.full_clean() and device.save()
without wrapping the write inside a database transaction; wrap the mutation in
transaction.atomic() so the revert on exception is atomic and consistent. Update
the view handling the POST (the block that sets device.name, calls
device.full_clean() and device.save()) to perform the read of selected items via
request.POST.getlist('select'), load any cached data with
CacheMixin.get_cache_key(), and then run the full_clean()/save() (and the
device.name rollback on exception) inside a transaction.atomic() context; ensure
the view still uses LibreNMSPermissionMixin and NetBoxObjectPermissionMixin for
permission checks and ends by redirecting to the sync tab with ?tab=<resource>
on error/success.
- #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)
- utils.set_librenms_device_id: use coerce_librenms_id so float-like IDs (1.9) and bools are rejected instead of silently truncated (#3). - migrate.MoveInterfaceToWinnerView: lock + re-read the donor interface row inside the txn and use its locked name for the collision check, closing a concurrent-rename TOCTOU (the IP-move flow already did this) (#5). - device_operations: populate existing_librenms_link on the primary-IP match path so an already-linked device no longer renders as 'not linked' (#6). - _oob_interface_select.html: add a label for the new-interface name input (#8). - ip_addresses_view: resolve the management IP once on the fresh-fetch path and cache it; flagging no longer makes a live LibreNMS call on cached renders (#12). - sync/ip_addresses: rebuild the API client scoped to the POST server_key so the management-IP lookup hits the same server the cached rows came from (#13). - _dt_mapping_form.html: drop the unneeded CSRF header from the read-only GET (#1). - tests: stale promote_to_host clearing regression (#2); assert same-name interface lookup args (#9); distinct locked-row mocks in OOB transfer (#10). Skipped: #4 (obsolete - auto_create_ipam removed), #7 (hx-include targets the wrapper span which still includes the -cb checkbox), #11 (no VC-with-OOB config in practice).
- utils.set_librenms_device_id: use coerce_librenms_id so float-like IDs (1.9) and bools are rejected instead of silently truncated (#3). - migrate.MoveInterfaceToWinnerView: lock + re-read the donor interface row inside the txn and use its locked name for the collision check, closing a concurrent-rename TOCTOU (the IP-move flow already did this) (#5). - device_operations: populate existing_librenms_link on the primary-IP match path so an already-linked device no longer renders as 'not linked' (#6). - _oob_interface_select.html: add a label for the new-interface name input (#8). - ip_addresses_view: resolve the management IP once on the fresh-fetch path and cache it; flagging no longer makes a live LibreNMS call on cached renders (#12). - sync/ip_addresses: rebuild the API client scoped to the POST server_key so the management-IP lookup hits the same server the cached rows came from (#13). - _dt_mapping_form.html: drop the unneeded CSRF header from the read-only GET (#1). - tests: stale promote_to_host clearing regression (#2); assert same-name interface lookup args (#9); distinct locked-row mocks in OOB transfer (#10). Skipped: #4 (obsolete - auto_create_ipam removed), #7 (hx-include targets the wrapper span which still includes the -cb checkbox), #11 (no VC-with-OOB config in practice).
- utils.set_librenms_device_id: use coerce_librenms_id so float-like IDs (1.9) and bools are rejected instead of silently truncated (#3). - migrate.MoveInterfaceToWinnerView: lock + re-read the donor interface row inside the txn and use its locked name for the collision check, closing a concurrent-rename TOCTOU (the IP-move flow already did this) (#5). - device_operations: populate existing_librenms_link on the primary-IP match path so an already-linked device no longer renders as 'not linked' (#6). - _oob_interface_select.html: add a label for the new-interface name input (#8). - ip_addresses_view: resolve the management IP once on the fresh-fetch path and cache it; flagging no longer makes a live LibreNMS call on cached renders (#12). - sync/ip_addresses: rebuild the API client scoped to the POST server_key so the management-IP lookup hits the same server the cached rows came from (#13). - _dt_mapping_form.html: drop the unneeded CSRF header from the read-only GET (#1). - tests: stale promote_to_host clearing regression (#2); assert same-name interface lookup args (#9); distinct locked-row mocks in OOB transfer (#10). Skipped: #4 (obsolete - auto_create_ipam removed), #7 (hx-include targets the wrapper span which still includes the -cb checkbox), #11 (no VC-with-OOB config in practice).
- utils.set_librenms_device_id: use coerce_librenms_id so float-like IDs (1.9) and bools are rejected instead of silently truncated (#3). - migrate.MoveInterfaceToWinnerView: lock + re-read the donor interface row inside the txn and use its locked name for the collision check, closing a concurrent-rename TOCTOU (the IP-move flow already did this) (#5). - device_operations: populate existing_librenms_link on the primary-IP match path so an already-linked device no longer renders as 'not linked' (#6). - _oob_interface_select.html: add a label for the new-interface name input (#8). - ip_addresses_view: resolve the management IP once on the fresh-fetch path and cache it; flagging no longer makes a live LibreNMS call on cached renders (#12). - sync/ip_addresses: rebuild the API client scoped to the POST server_key so the management-IP lookup hits the same server the cached rows came from (#13). - _dt_mapping_form.html: drop the unneeded CSRF header from the read-only GET (#1). - tests: stale promote_to_host clearing regression (#2); assert same-name interface lookup args (#9); distinct locked-row mocks in OOB transfer (#10). Skipped: #4 (obsolete - auto_create_ipam removed), #7 (hx-include targets the wrapper span which still includes the -cb checkbox), #11 (no VC-with-OOB config in practice).
- utils.set_librenms_device_id: use coerce_librenms_id so float-like IDs (1.9) and bools are rejected instead of silently truncated (#3). - migrate.MoveInterfaceToWinnerView: lock + re-read the donor interface row inside the txn and use its locked name for the collision check, closing a concurrent-rename TOCTOU (the IP-move flow already did this) (#5). - device_operations: populate existing_librenms_link on the primary-IP match path so an already-linked device no longer renders as 'not linked' (#6). - _oob_interface_select.html: add a label for the new-interface name input (#8). - ip_addresses_view: resolve the management IP once on the fresh-fetch path and cache it; flagging no longer makes a live LibreNMS call on cached renders (#12). - sync/ip_addresses: rebuild the API client scoped to the POST server_key so the management-IP lookup hits the same server the cached rows came from (#13). - _dt_mapping_form.html: drop the unneeded CSRF header from the read-only GET (#1). - tests: stale promote_to_host clearing regression (#2); assert same-name interface lookup args (#9); distinct locked-row mocks in OOB transfer (#10). Skipped: #4 (obsolete - auto_create_ipam removed), #7 (hx-include targets the wrapper span which still includes the -cb checkbox), #11 (no VC-with-OOB config in practice).
- AddAsOOBView: gate Interface/IPAddress perms inside the OOB-IP sub-flow (top-level only authorizes change Device); skip IP-set with a warning when the perm is missing so the link still commits (#14) - modules_view: skip interface matching for OOB-sourced inventory rows — only the main device's interfaces are indexed, so a name match is a false positive (#13) - device_operations: describe existing host/OOB linkage in the primary-IP collision warning instead of always saying 'not linked to LibreNMS' (#9) - guard posted server_key: LibreNMSAPI() raises KeyError for an unknown key, so a stale/tampered POST turned the IP-sync and OOB-attach/promote actions into a 500. Add build_librenms_api() helper and surface a user-facing error instead (#7) - _platform_mapping_form: drop CSRF header from the read-only platform-list GET (#10) - tests: tie migrate move-update assertion to a dedicated queryset (#6); assert find_by_librenms_id call args in the OOB-refresh test (#12); add regression tests for the OOB-perm gate, OOB-row match skip, and unknown-server_key guard
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?
Link any related issues if applicable.
Scope of Change
Tick all that apply:
How Was This Tested?
Tick all that apply and describe briefly.
Manual Test Steps (if applicable)
Risk Assessment
Explain briefly.
Backwards Compatibility
Other Notes
Anything the maintainer(s) should pay particular attention to?
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Tests