feat: librenms_id JSON migration with multi-server support and PR rev… - #15
feat: librenms_id JSON migration with multi-server support and PR rev…#15marcinpsk wants to merge 13 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds per-server scoping for LibreNMS IDs and cache keys; propagates server_key, use_sysname, and strip_domain through import/validation/cache/VC flows; introduces librenms_id utilities and migration, RemoveServerMapping endpoint/UI, VC naming refinements, deterministic cache keys, and related UI/accessibility updates. (50 words) Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Browser as "User / Browser"
participant View as "Sync/Import View"
participant Job as "Background Job (RQ)"
participant API as "LibreNMS API Client"
participant Cache as "Cache (Redis)"
participant DB as "NetBox DB"
rect rgba(200,200,255,0.5)
Browser->>View: trigger import/list (server_key, use_sysname, strip_domain)
View->>Job: enqueue job with metadata (server_key, use_sysname, strip_domain)
end
rect rgba(200,255,200,0.5)
Job->>Cache: check validated_device cache key (includes server_key, use_sysname, strip_domain)
alt cache miss
Job->>API: fetch devices (api.server_key)
API-->>Job: libre_devices
Job->>DB: find_by_librenms_id / existing device lookups (server-scoped)
DB-->>Job: existing device info
Job->>Cache: write validated entries and metadata (server-scoped keys)
else cache hit
Cache-->>Job: return cached results
end
Job->>View: store job results / notify completion
View->>Browser: render results (all_server_mappings, validation UI)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 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: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
netbox_librenms_plugin/views/sync/interfaces.py (1)
21-22:⚠️ Potential issue | 🔴 CriticalAdd
LibreNMSAPIMixinbefore dereferencingself.librenms_api.
update_interface_attributes()now readsself.librenms_api.server_key, but this view class does not declareLibreNMSAPIMixinin its MRO. That can raiseAttributeErrorduring sync.As per coding guidelines: New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`).🔧 Proposed fix
from netbox_librenms_plugin.views.mixins import ( CacheMixin, + LibreNMSAPIMixin, LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, VlanAssignmentMixin, ) @@ -class SyncInterfacesView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, VlanAssignmentMixin, CacheMixin, View): +class SyncInterfacesView( + LibreNMSPermissionMixin, + NetBoxObjectPermissionMixin, + LibreNMSAPIMixin, + VlanAssignmentMixin, + CacheMixin, + View, +):Also applies to: 240-240
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/interfaces.py` around lines 21 - 22, The SyncInterfacesView class lacks LibreNMSAPIMixin in its MRO but update_interface_attributes() accesses self.librenms_api.server_key, so add LibreNMSAPIMixin into the inheritance list for SyncInterfacesView (placing it before any code that dereferences self.librenms_api) following the recommended mixin order (LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, CacheMixin, VlanAssignmentMixin) so self.librenms_api is defined and update_interface_attributes() can safely use server_key.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/__init__.py`:
- Line 5: The release line in netbox_librenms_plugin/__init__.py incorrectly
sets __version__ = "0.4.2", causing a version rollback; update the __version__
symbol to the correct, non-regressive release string (e.g., the intended release
>= the previously published 0.4.3 or the new bump such as "0.4.4") so package
resolution will upgrade cleanly.
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 319-320: The current branch that runs when hasattr(refreshed,
"role") and refreshed.role replaces validation["device_role"] with a partial
dict and drops other keys like available_roles; instead, ensure you preserve the
existing device_role schema by making validation["device_role"] a dict if not
already (e.g., set to {} when missing or not a dict) and then update it with the
refreshed values (e.g., set "found": True and "role": refreshed.role via
dict.update) so available_roles and other keys remain intact; apply this change
in the block that currently assigns validation["device_role"] = {"found": True,
"role": refreshed.role}.
- Line 446: The list comprehension that filters disabled devices (libre_devices
= [d for d in libre_devices if int(d.get("disabled", 0)) != 1]) can raise on
non-numeric or None values; update the filtering to parse the "disabled" field
defensively in the bulk_import logic by reading d.get("disabled"), handling
None/empty/"-" etc., converting safely to an int inside a try/except (defaulting
to 0 on failure) or by normalizing string values before numeric conversion, and
then use that safe value to decide inclusion in libre_devices; locate the
comprehension that assigns to libre_devices and replace it with a tolerant check
that references the same "disabled" key.
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 772-773: The code currently sets "custom_field_data":
{"librenms_id": {api.server_key: int(device_id)}} directly; replace this direct
assignment by using the centralized helper set_librenms_device_id to produce the
proper custom_field_data/formatting. Locate the create path that builds the
payload (the dict containing "custom_field_data") and remove the inline
librenms_id construction, calling set_librenms_device_id(payload_or_account_obj,
int(device_id)) (or whatever signature set_librenms_device_id expects) to
inject/return the normalized custom_field_data so formatting/normalization is
consistent across create/update flows.
In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 41-43: The list-comprehension that filters based on disabled uses
int(d.get("disabled", 0)) which can raise on None/""/unexpected strings; update
the filtering logic around the show_disabled check to safely coerce the disabled
value instead of calling int() directly: extract val = d.get("disabled", 0),
handle TypeError/ValueError or non-numeric strings by mapping common truthy
strings ("1","true","yes","on") to 1 and everything else to 0, then filter
devices by comparing the safe integer (e.g., disabled != 1); adjust the
comprehension that references devices so it uses this safe-parsed disabled
value.
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 201-207: Ensure VC member positions are strictly 1-based by
validating parsed values: when you parse raw_position into position (the
variables raw_position and position in virtual_chassis.py), coerce to int and
then enforce position = max(1, parsed_value) (or fall back to idx+1 if
parsed_value < 1) instead of allowing 0; also apply the same validation in the
second parsing block around lines 309-319. This prevents
update_vc_member_suggested_names (referenced by name) from adding +1 to
already-correct values and avoids off-by-one suggested names.
In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py`:
- Around line 331-334: The test defines a local helper _make_obj that builds a
MagicMock with custom_field_data; remove this ad-hoc builder and use the shared
fixture provided in tests/conftest.py instead: stop creating _make_obj, accept
or call the appropriate fixture (e.g., the object/device factory defined in
conftest) in your test and set or parametrize its custom_field_data to
{"librenms_id": cf_librenms_id} as needed, or extend the conftest fixture to
accept an id argument so tests call the fixture with cf_librenms_id rather than
constructing a new MagicMock.
In `@netbox_librenms_plugin/urls.py`:
- Around line 225-229: The new route registers RemoveServerMappingView but the
view writes the wrong custom-field key due to a typo: it assigns to
custom_field_data["librenrenns_id"] (or "librenrenms_id") instead of the correct
"librenms_id", which leaves the real mapping intact and corrupts CF state;
update the handler inside RemoveServerMappingView to use the exact key
"librenms_id" when removing/clearing the mapping and ensure any save()/update
call persists that corrected key (also add a small unit/check to confirm the key
exists before and after mutation to prevent regressions).
In `@netbox_librenms_plugin/utils.py`:
- Around line 45-67: The function set_librenms_device_id currently stores
device_id without normalization which allows strings to be persisted and later
break integer-based lookups like find_by_librenms_id; coerce/validate device_id
to an int before storing (e.g., attempt int(device_id) and handle conversion
errors), ensure only an integer is written into
obj.custom_field_data["librenms_id"][server_key], and if conversion fails raise
or log a clear error so callers cannot accidentally persist non-integer IDs.
In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 86-89: Extract the duplicated Q(...) OR Q(...) predicate into a
single helper function (e.g. add a method on LibreNMSAPI like
get_librenms_id_predicate(server_key, remote_id) that returns a Django Q object)
and use it wherever Devices/VMs are being looked up (replace direct usages in
Device.objects.get and similar queries in cables_view.py — locations around the
current Device lookup and the other occurrences called out in the comment). Use
LibreNMSAPI.get_librenms_id() (or the new helper) instead of directly
referencing custom_field_data__librenms_id to build the predicate so all lookups
go through the centralized logic and then pass that Q into the existing
.get/.filter calls.
In `@netbox_librenms_plugin/views/base/ip_addresses_view.py`:
- Around line 108-112: The dict comprehension in interfaces_by_librenms_id calls
get_librenms_device_id(interface, server_key) twice per iteration; compute the
id once into a local variable (e.g., id = get_librenms_device_id(interface,
server_key)) inside the comprehension/loop and use that variable in both the key
and the conditional so each interface invokes get_librenms_device_id only once;
update the comprehension using the unique symbols interfaces_by_librenms_id,
all_interfaces and get_librenms_device_id to locate the change.
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 146-155: _build_all_server_mappings currently only checks
servers_config and marks the legacy "default" mapping as unconfigured; change
the logic so that if srv_cfg is None and sk == "default" you also inspect
plugins_cfg.get("librenms_url") (and optional plugins_cfg.get("display_name"))
and treat that as an active configuration: set is_configured True, librenms_url
from plugins_cfg["librenms_url"], display_name from plugins_cfg["display_name"]
or sk, and build device_url from that librenms_url; update the branches that
compute srv_cfg, is_configured, librenms_url, display_name, and device_url
accordingly in _build_all_server_mappings (use the existing variables
servers_config, plugins_cfg, srv_cfg, librenms_url, display_name, device_url to
locate the change).
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1203-1227: Protect the read-check-write sequence by acquiring a DB
row lock on the device before inspecting or mutating custom_field_data: wrap the
logic in a transaction.atomic() block and re-query the device with
select_for_update() (use the Device PK from existing_device) to get a locked
instance, perform the isinstance/int checks against locked.custom_field_data,
verify IDs and serial_confirmed, call migrate_legacy_librenms_id on the locked
instance and then call _save_device(locked); this prevents concurrent updates
from being lost by ensuring the migration operates on a locked row.
In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 270-281: The user preference values returned by get_user_pref
(e.g., _use_sysname_pref and _strip_domain_pref) may be strings like "false"
which are truthy; coerce/normalize these persisted prefs to strict booleans
before using them in _use_sysname and _strip_domain (and the same block around
lines 445–467). Implement normalization logic (or a small helper) to treat
values like "false", "0", "no" (case-insensitive) as False and "true", "1",
"yes" as True, handle actual booleans and None unchanged, then use the
normalized boolean when applying the settings fallback via getattr(settings,
"..._default", ...).
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 460-467: The except blocks in device_fields.py that catch
ValidationError and Exception currently include raw exception text in
messages.error (the handlers around messages.error(request, f"...{exc}")) —
change these to log the exception server-side (use logger.exception() or similar
referencing the same except blocks) and replace the user-facing messages.error
text with a generic, non-sensitive message (e.g., "Validation error removing
mapping." and "Error removing mapping for server.") while preserving
transaction.set_rollback(True) and the
redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk); keep
server_key in logs only, not in the flash message.
---
Outside diff comments:
In `@netbox_librenms_plugin/views/sync/interfaces.py`:
- Around line 21-22: The SyncInterfacesView class lacks LibreNMSAPIMixin in its
MRO but update_interface_attributes() accesses self.librenms_api.server_key, so
add LibreNMSAPIMixin into the inheritance list for SyncInterfacesView (placing
it before any code that dereferences self.librenms_api) following the
recommended mixin order (LibreNMSPermissionMixin, NetBoxObjectPermissionMixin,
LibreNMSAPIMixin, CacheMixin, VlanAssignmentMixin) so self.librenms_api is
defined and update_interface_attributes() can safely use server_key.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (33)
docs/changelog.mdnetbox_librenms_plugin/__init__.pynetbox_librenms_plugin/forms.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/virtual_chassis.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/jobs.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.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.pypyproject.toml
💤 Files with no reviewable changes (1)
- docs/changelog.md
|
|
||
| __author__ = "Andy Norwood" | ||
| __version__ = "0.4.3" | ||
| __version__ = "0.4.2" |
There was a problem hiding this comment.
Avoid version rollback for a feature-bearing release.
Line 5 downgrades to 0.4.2. If any deployment already has 0.4.3, this release will not upgrade cleanly via normal package resolution.
📌 Suggested fix
-__version__ = "0.4.2"
+__version__ = "0.4.4"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| __version__ = "0.4.2" | |
| __version__ = "0.4.4" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/__init__.py` at line 5, The release line in
netbox_librenms_plugin/__init__.py incorrectly sets __version__ = "0.4.2",
causing a version rollback; update the __version__ symbol to the correct,
non-regressive release string (e.g., the intended release >= the previously
published 0.4.3 or the new bump such as "0.4.4") so package resolution will
upgrade cleanly.
| _use_sysname_pref = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname") | ||
| _strip_domain_pref = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain") | ||
| _use_sysname = ( | ||
| _use_sysname_pref | ||
| if _use_sysname_pref is not None | ||
| else (getattr(settings, "use_sysname_default", True) if settings else True) | ||
| ) | ||
| _strip_domain = ( | ||
| _strip_domain_pref | ||
| if _strip_domain_pref is not None | ||
| else (getattr(settings, "strip_domain_default", False) if settings else False) | ||
| ) |
There was a problem hiding this comment.
Normalize persisted naming preferences to strict booleans before using them.
Line [270] and Line [445] consume get_user_pref(...) values directly. If a stored value is "false" (string), it is truthy and will be treated as enabled, causing wrong naming behavior and cache-key mismatches.
Proposed fix
+def _coerce_bool(value, default: bool) -> bool:
+ if value is None:
+ return default
+ if isinstance(value, bool):
+ return value
+ if isinstance(value, str):
+ return value.strip().lower() in {"1", "true", "on", "yes"}
+ return bool(value)
...
- _use_sysname = (
- _use_sysname_pref
- if _use_sysname_pref is not None
- else (getattr(settings, "use_sysname_default", True) if settings else True)
- )
- _strip_domain = (
- _strip_domain_pref
- if _strip_domain_pref is not None
- else (getattr(settings, "strip_domain_default", False) if settings else False)
- )
+ _use_sysname = _coerce_bool(
+ _use_sysname_pref,
+ getattr(settings, "use_sysname_default", True) if settings else True,
+ )
+ _strip_domain = _coerce_bool(
+ _strip_domain_pref,
+ getattr(settings, "strip_domain_default", False) if settings else False,
+ )
...
- use_sysname = (
- _use_sysname_toggle
- if _use_sysname_toggle is not None
- else use_sysname_pref
- if use_sysname_pref is not None
- else (getattr(_settings, "use_sysname_default", True) if _settings else True)
- )
+ use_sysname = _coerce_bool(
+ _use_sysname_toggle if _use_sysname_toggle is not None else use_sysname_pref,
+ getattr(_settings, "use_sysname_default", True) if _settings else True,
+ )
...
- strip_domain = (
- _strip_domain_toggle
- if _strip_domain_toggle is not None
- else strip_domain_pref
- if strip_domain_pref is not None
- else (getattr(_settings, "strip_domain_default", False) if _settings else False)
- )
+ strip_domain = _coerce_bool(
+ _strip_domain_toggle if _strip_domain_toggle is not None else strip_domain_pref,
+ getattr(_settings, "strip_domain_default", False) if _settings else False,
+ )Also applies to: 445-467
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/views/imports/list.py` around lines 270 - 281, The
user preference values returned by get_user_pref (e.g., _use_sysname_pref and
_strip_domain_pref) may be strings like "false" which are truthy;
coerce/normalize these persisted prefs to strict booleans before using them in
_use_sysname and _strip_domain (and the same block around lines 445–467).
Implement normalization logic (or a small helper) to treat values like "false",
"0", "no" (case-insensitive) as False and "true", "1", "yes" as True, handle
actual booleans and None unchanged, then use the normalized boolean when
applying the settings fallback via getattr(settings, "..._default", ...).
7a25806 to
27eac04
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
netbox_librenms_plugin/views/imports/actions.py (1)
68-70:⚠️ Potential issue | 🟠 MajorPersisted user preferences should be coerced to strict booleans here too.
If
get_user_pref()returns"false"/"0"as strings, these branches treat them as truthy and can produce wrong hostname decisions during conflict actions.💡 Proposed fix
_TRUTHY = frozenset({"on", "true", "1"}) + _FALSY = frozenset({"off", "false", "0", "no"}) def _is_truthy(val): - return val.lower() in _TRUTHY if val is not None else False + if isinstance(val, bool): + return val + if val is None: + return False + return str(val).strip().lower() in _TRUTHY + + def _coerce_bool(val, default: bool) -> bool: + if val is None: + return default + if isinstance(val, bool): + return val + lowered = str(val).strip().lower() + if lowered in _TRUTHY: + return True + if lowered in _FALSY: + return False + return default ... pref = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname") - if pref is not None: - use_sysname = pref - else: - settings = LibreNMSSettings.objects.first() - use_sysname = getattr(settings, "use_sysname_default", True) if settings else True + settings = LibreNMSSettings.objects.first() + use_sysname = _coerce_bool( + pref, + getattr(settings, "use_sysname_default", True) if settings else True, + ) ... pref = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain") - if pref is not None: - strip_domain = pref - else: - if settings is None: - settings = LibreNMSSettings.objects.first() - strip_domain = getattr(settings, "strip_domain_default", False) if settings else False + if settings is None: + settings = LibreNMSSettings.objects.first() + strip_domain = _coerce_bool( + pref, + getattr(settings, "strip_domain_default", False) if settings else False, + )Also applies to: 83-86
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/imports/actions.py` around lines 68 - 70, The user preference retrieved via get_user_pref is being assigned directly to use_sysname (and a similar variable later) which treats string values like "false" or "0" as truthy; change the assignment to coerce pref to a strict boolean (e.g., if pref is already a bool keep it, otherwise normalize with str(pref).lower() in ("true","1") or similar) before setting use_sysname; update both occurrences where get_user_pref is checked (the branches that set use_sysname and the later identical block around lines 83-86) so the decision logic uses the normalized boolean value.
♻️ Duplicate comments (8)
netbox_librenms_plugin/views/base/ip_addresses_view.py (1)
108-112: 🧹 Nitpick | 🔵 TrivialAvoid calling
get_librenms_device_id()twice per interface.At Line 109 and Line 111, the same lookup runs twice for each item; compute once and reuse.
♻️ Proposed refactor
- interfaces_by_librenms_id = { - get_librenms_device_id(interface, server_key): interface - for interface in all_interfaces - if get_librenms_device_id(interface, server_key) - } + interfaces_by_librenms_id = {} + for interface in all_interfaces: + mapped_id = get_librenms_device_id(interface, server_key) + if mapped_id: + interfaces_by_librenms_id[mapped_id] = interface🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/ip_addresses_view.py` around lines 108 - 112, The dict comprehension in interfaces_by_librenms_id repeatedly calls get_librenms_device_id(interface, server_key) twice; compute the id once per iteration by assigning it to a local variable (e.g., lib_id) inside the comprehension or rewrite as a simple for-loop, then use that variable both as the key and in the truthiness filter; update references to get_librenms_device_id in the comprehension to use lib_id and keep the same behavior with all_interfaces and server_key.netbox_librenms_plugin/import_utils/filters.py (1)
41-43:⚠️ Potential issue | 🟠 MajorHarden disabled filtering to avoid runtime crashes.
At Line 42,
int(d.get("disabled", 0))can raise forNone, empty strings, or non-numeric flags and abort the count path.🔧 Proposed fix
- if not show_disabled: - devices = [d for d in devices if int(d.get("disabled", 0)) != 1] + if not show_disabled: + def _is_disabled(value) -> bool: + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return int(value) == 1 + return str(value).strip().lower() in {"1", "true", "yes", "on"} + + devices = [d for d in devices if not _is_disabled(d.get("disabled", 0))]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/filters.py` around lines 41 - 43, The current filter uses int(d.get("disabled", 0)) which can raise on None, empty or non-numeric values; change the filtering to defensively parse the disabled flag for each item in devices (when show_disabled is False) by reading d.get("disabled", 0), converting to string, trimming and lowercasing, then treating "1", "true", "yes" as disabled or otherwise fall back to 0 (or wrap int(...) in try/except and treat parsing errors as not-disabled). Update the list comprehension that builds devices (and/or add a small helper like is_disabled(device)) so it won't raise on bad values from the "disabled" key.netbox_librenms_plugin/tests/test_sync_view_mismatch.py (1)
331-334:⚠️ Potential issue | 🟡 MinorUse shared
conftest.pyfixtures instead of a local_make_objhelper.Line 331 introduces the same ad-hoc builder pattern previously flagged; please switch this setup to the shared test fixtures to keep mocks consistent across the suite.
Based on learnings: "Reuse fixtures from
tests/conftest.pyinstead of creating ad-hoc mocks..."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py` around lines 331 - 334, Replace the ad-hoc builder function _make_obj (which creates a MagicMock with custom_field_data {"librenms_id": ...}) with the shared fixture provided in tests/conftest.py; locate usages of _make_obj in test_sync_view_mismatch.py and switch them to the appropriate conftest fixture (the one that supplies a mock object with custom_field_data/librenms_id), removing the local _make_obj helper and updating test parameters to accept the fixture instead.netbox_librenms_plugin/views/base/cables_view.py (1)
86-89: 🛠️ Refactor suggestion | 🟠 MajorCentralize the server-scoped
librenms_idpredicate instead of repeating raw JSON lookups.The same
Q(custom_field_data__librenms_id__<server_key>) | Q(custom_field_data__librenms_id=...)logic is duplicated across multiple paths, which is prone to lookup drift.As per coding guidelines: Use
LibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS.Also applies to: 131-134, 142-145, 170-172, 182-184, 389-390
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/cables_view.py` around lines 86 - 89, Replace the repeated raw JSON lookups for the server-scoped librenms_id with the central helper: instead of building Q(**{f"custom_field_data__librenms_id__{server_key}": ...}) | Q(custom_field_data__librenms_id=...), call LibreNMSAPI.get_librenms_id(remote_device_id, server_key) (or the equivalent signature provided by LibreNMSAPI) to produce the predicate/value used when querying Device/VM objects; update the Device.objects.get(...) and similar lookups in functions around the occurrences shown (e.g., the Device query at lines 86-89 and the other duplicated spots) to use the helper so the server-scoped key logic is centralized and consistent.netbox_librenms_plugin/views/imports/actions.py (1)
1203-1227:⚠️ Potential issue | 🟠 MajorProtect legacy-ID migration with a row lock.
This path still does an unlocked read-check-write on
custom_field_data; concurrent link/update actions can overwrite each other.💡 Proposed fix
elif action == "migrate_librenms_id": from netbox_librenms_plugin.utils import migrate_legacy_librenms_id - - cf_value = existing_device.custom_field_data.get("librenms_id") - if not isinstance(cf_value, int): - return HttpResponse( - "Device librenms_id is already in JSON format; no migration needed.", - status=400, - ) - if cf_value != librenms_id: - return HttpResponse( - f"Legacy librenms_id ({cf_value}) does not match the active device ID " - f"({librenms_id}); cannot migrate safely.", - status=400, - ) - if not validation.get("serial_confirmed") and not force: - return HttpResponse( - "Serial number not confirmed. Check the force checkbox to migrate without serial verification.", - status=400, - ) - migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key) - if err := _save_device(existing_device): - return err + with transaction.atomic(): + try: + locked_device = Device.objects.select_for_update().get(pk=existing_device.pk) + except Device.DoesNotExist: + return HttpResponse( + "Device no longer exists; it may have been deleted concurrently.", + status=409, + ) + + cf_value = locked_device.custom_field_data.get("librenms_id") + if not isinstance(cf_value, int): + return HttpResponse( + "Device librenms_id is already in JSON format; no migration needed.", + status=400, + ) + if cf_value != librenms_id: + return HttpResponse( + f"Legacy librenms_id ({cf_value}) does not match the active device ID " + f"({librenms_id}); cannot migrate safely.", + status=400, + ) + if not validation.get("serial_confirmed") and not force: + return HttpResponse( + "Serial number not confirmed. Check the force checkbox to migrate without serial verification.", + status=400, + ) + migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key) + if err := _save_device(locked_device): + return err🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/imports/actions.py` around lines 1203 - 1227, The legacy-ID migration performs an unlocked read-check-write on existing_device.custom_field_data; wrap the entire check-and-migrate sequence in a DB transaction and acquire a row lock on the device before reading/updating custom_field_data (use a select_for_update() fetch of the Device record for the same PK inside a transaction.atomic block), then re-check the locked record's custom_field_data, perform migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key) and call _save_device(existing_device) while still holding the lock to prevent concurrent link/update races.netbox_librenms_plugin/views/sync/device_fields.py (1)
460-467:⚠️ Potential issue | 🟠 MajorDo not expose internal exception details in mapping-removal flash messages.
These handlers currently leak raw exception text to users; log server-side and return a generic failure message instead.
💡 Proposed fix
- except ValidationError as exc: - transaction.set_rollback(True) - messages.error(request, f"Validation error removing mapping: {exc}") + except ValidationError: + logger.exception( + "Validation error removing LibreNMS mapping for server '%s' on device pk=%s", + server_key, + pk, + ) + messages.error(request, "Failed to remove mapping. Please contact an administrator.") return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk) - except Exception as exc: - transaction.set_rollback(True) - messages.error(request, f"Error removing mapping for server '{server_key}': {exc}") + except Exception: + logger.exception( + "Unexpected error removing LibreNMS mapping for server '%s' on device pk=%s", + server_key, + pk, + ) + messages.error(request, "Failed to remove mapping. Please contact an administrator.") return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/device_fields.py` around lines 460 - 467, The except blocks catching ValidationError and the generic Exception should not include raw exception text in the user-facing messages; instead, log the full exception server-side (use logger.exception(...) or logger.error(..., exc_info=True)) referencing the except handlers for ValidationError and the generic Exception, then call transaction.set_rollback(True) and call messages.error(request, "...generic failure message...") without including exc or server_key details, and finally return the existing redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk) as before.netbox_librenms_plugin/views/imports/list.py (1)
95-97:⚠️ Potential issue | 🟠 MajorPersisted naming preferences still need strict boolean normalization.
String values (for example
"false") can remain truthy and will produce wrong naming behavior and cache-key mismatches in both load and job-result paths.💡 Proposed fix
+def _coerce_bool(value, default: bool) -> bool: + if value is None: + return default + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "on", "yes"} + return bool(value) ... - use_sysname = job_data.get("use_sysname", True) - strip_domain = job_data.get("strip_domain", False) + use_sysname = _coerce_bool(job_data.get("use_sysname"), True) + strip_domain = _coerce_bool(job_data.get("strip_domain"), False) ... - _use_sysname = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname") - _strip_domain = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain") - if _use_sysname is None: - _use_sysname = getattr(settings_obj, "use_sysname_default", True) if settings_obj else True - if _strip_domain is None: - _strip_domain = getattr(settings_obj, "strip_domain_default", False) if settings_obj else False + _use_sysname_pref = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname") + _strip_domain_pref = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain") + _use_sysname = _coerce_bool( + _use_sysname_pref, + getattr(settings_obj, "use_sysname_default", True) if settings_obj else True, + ) + _strip_domain = _coerce_bool( + _strip_domain_pref, + getattr(settings_obj, "strip_domain_default", False) if settings_obj else False, + )Also applies to: 153-159
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/imports/list.py` around lines 95 - 97, job_data values like use_sysname and strip_domain are read as-is and string values such as "false" remain truthy; normalize them to strict booleans by parsing the job_data entries (e.g., replace use_sysname = job_data.get("use_sysname", True) and strip_domain = job_data.get("strip_domain", False) with explicit boolean normalization such as checking type and comparing lowercased strings or using a helper like parse_bool(value) so that "false"/"0"/"" become False and True-like values become True); apply the same strict boolean normalization to the other similar variables in the same file (the block around the other job_data.get usages mentioned in the review) so cache keys and naming behavior are consistent.netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
146-155:⚠️ Potential issue | 🟠 MajorLegacy
defaultmappings are still misclassified as unconfigured.
_build_all_server_mappings()only checksserversconfig. In legacy config mode (librenms_urlwithoutservers.default), activedefaultlinks are still marked orphaned.💡 Proposed fix
- plugins_cfg = django_settings.PLUGINS_CONFIG.get("netbox_librenms_plugin", {}) + plugins_cfg = getattr(django_settings, "PLUGINS_CONFIG", {}).get("netbox_librenms_plugin", {}) servers_config = plugins_cfg.get("servers", {}) + legacy_url = plugins_cfg.get("librenms_url") + legacy_display_name = plugins_cfg.get("display_name") result = [] for sk, did in cf_value.items(): srv_cfg = servers_config.get(sk) - is_configured = srv_cfg is not None - librenms_url = srv_cfg.get("librenms_url") if srv_cfg else None - display_name = (srv_cfg.get("display_name") or sk) if srv_cfg else sk + is_legacy_default = sk == "default" and srv_cfg is None and bool(legacy_url) + is_configured = (srv_cfg is not None) or is_legacy_default + librenms_url = srv_cfg.get("librenms_url") if srv_cfg else (legacy_url if is_legacy_default else None) + display_name = ( + (srv_cfg.get("display_name") or sk) + if srv_cfg + else ((legacy_display_name or sk) if is_legacy_default else sk) + ) device_url = f"{librenms_url}/device/device={did}/" if librenms_url else None🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/librenms_sync_view.py` around lines 146 - 155, The code in _build_all_server_mappings currently treats entries as unconfigured if servers_config lacks a specific key, which misses legacy configs; update the loop that builds srv_cfg by falling back to a legacy/default mapping: if servers_config.get(sk) is None then try servers_config.get("default"), and if that is also missing, try plugins_cfg.get("librenms_url") as a legacy top-level default; use that fallback to populate librenms_url, display_name and device_url so legacy/default mappings are considered configured (adjust variables servers_config, plugins_cfg, srv_cfg, librenms_url, display_name and device_url in _build_all_server_mappings accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 375-378: The code sets match_type = "hostname" when a device is
matched via sys_name in the block that checks new_device and sys_name (where
new_device is assigned from
Model.objects.filter(name__iexact=sys_name).first()); change the assigned
match_type to a more accurate value such as "sysname" (or "name" if you prefer a
generic label) so the variable reflects the actual matching source.
In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 182-190: Replace the inline cache-key construction in
import_utils.filters (the local _hash helper and the cache_key assignment using
api.server_key, api_filters and client_filters) with a centralized helper: add a
get_import_search_cache_key(server_key, api_filters, client_filters) function in
the cache helpers module that encapsulates the hashing logic (the
sha256/json.dumps logic currently in _hash) and then call that helper from
filters to produce cache_key; ensure you stop hardcoding the
"librenms_devices_import_" format in filters and instead pass api.server_key,
api_filters and client_filters to the new get_import_search_cache_key function.
In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js`:
- Around line 1103-1108: The ensureModalVisible() routine currently initializes
tooltips twice on the same modalContent (first at lines ~1096–1100 and again at
~1104–1108), causing duplicate bootstrap.Tooltip instances; update the tooltip
initialization to a single, idempotent loop over
modalContent.querySelectorAll('[data-bs-toggle="tooltip"]') and call
bootstrap.Tooltip.getOrCreateInstance(el) for each element (refer to
modalContent and ensureModalVisible) so repeated HTMX swaps do not create
multiple tooltip instances or duplicate event listeners.
In `@netbox_librenms_plugin/tests/test_permissions.py`:
- Around line 996-1001: The tests patch `django.conf.settings` which can miss
the `settings` object imported inside the view module; update the patches to
target the view module's settings (patch
"netbox_librenms_plugin.views.sync.device_fields.settings") so the code under
test sees the mocked config. In the same with-blocks that already patch
`netbox_librenms_plugin.views.sync.device_fields.get_object_or_404`, `Device`
(mock_Device_cls), `messages`, and `redirect`, replace
patch("django.conf.settings") with
patch("netbox_librenms_plugin.views.sync.device_fields.settings") for each
occurrence to make the tests deterministic. Ensure all three occurrences in this
test are updated.
In `@netbox_librenms_plugin/views/object_sync/vms.py`:
- Around line 48-50: The returned table view needs the current request bound
before using server-scoped behavior (self.librenms_api.server_key): in
get_interface_context(), instantiate the concrete view (LibreNMSVMInterfaceTable
or VMInterfaceTableView instance), assign its request attribute (e.g.,
interface_sync_view.request = request), then set any required properties
(server_key from self.librenms_api) and call
interface_sync_view.get_context_data() (or the view's equivalent) to produce the
context to return; update the return path to return that context rather than
directly constructing the table without binding request.
---
Outside diff comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 68-70: The user preference retrieved via get_user_pref is being
assigned directly to use_sysname (and a similar variable later) which treats
string values like "false" or "0" as truthy; change the assignment to coerce
pref to a strict boolean (e.g., if pref is already a bool keep it, otherwise
normalize with str(pref).lower() in ("true","1") or similar) before setting
use_sysname; update both occurrences where get_user_pref is checked (the
branches that set use_sysname and the later identical block around lines 83-86)
so the decision logic uses the normalized boolean value.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 41-43: The current filter uses int(d.get("disabled", 0)) which can
raise on None, empty or non-numeric values; change the filtering to defensively
parse the disabled flag for each item in devices (when show_disabled is False)
by reading d.get("disabled", 0), converting to string, trimming and lowercasing,
then treating "1", "true", "yes" as disabled or otherwise fall back to 0 (or
wrap int(...) in try/except and treat parsing errors as not-disabled). Update
the list comprehension that builds devices (and/or add a small helper like
is_disabled(device)) so it won't raise on bad values from the "disabled" key.
In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py`:
- Around line 331-334: Replace the ad-hoc builder function _make_obj (which
creates a MagicMock with custom_field_data {"librenms_id": ...}) with the shared
fixture provided in tests/conftest.py; locate usages of _make_obj in
test_sync_view_mismatch.py and switch them to the appropriate conftest fixture
(the one that supplies a mock object with custom_field_data/librenms_id),
removing the local _make_obj helper and updating test parameters to accept the
fixture instead.
In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 86-89: Replace the repeated raw JSON lookups for the server-scoped
librenms_id with the central helper: instead of building
Q(**{f"custom_field_data__librenms_id__{server_key}": ...}) |
Q(custom_field_data__librenms_id=...), call
LibreNMSAPI.get_librenms_id(remote_device_id, server_key) (or the equivalent
signature provided by LibreNMSAPI) to produce the predicate/value used when
querying Device/VM objects; update the Device.objects.get(...) and similar
lookups in functions around the occurrences shown (e.g., the Device query at
lines 86-89 and the other duplicated spots) to use the helper so the
server-scoped key logic is centralized and consistent.
In `@netbox_librenms_plugin/views/base/ip_addresses_view.py`:
- Around line 108-112: The dict comprehension in interfaces_by_librenms_id
repeatedly calls get_librenms_device_id(interface, server_key) twice; compute
the id once per iteration by assigning it to a local variable (e.g., lib_id)
inside the comprehension or rewrite as a simple for-loop, then use that variable
both as the key and in the truthiness filter; update references to
get_librenms_device_id in the comprehension to use lib_id and keep the same
behavior with all_interfaces and server_key.
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 146-155: The code in _build_all_server_mappings currently treats
entries as unconfigured if servers_config lacks a specific key, which misses
legacy configs; update the loop that builds srv_cfg by falling back to a
legacy/default mapping: if servers_config.get(sk) is None then try
servers_config.get("default"), and if that is also missing, try
plugins_cfg.get("librenms_url") as a legacy top-level default; use that fallback
to populate librenms_url, display_name and device_url so legacy/default mappings
are considered configured (adjust variables servers_config, plugins_cfg,
srv_cfg, librenms_url, display_name and device_url in _build_all_server_mappings
accordingly).
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1203-1227: The legacy-ID migration performs an unlocked
read-check-write on existing_device.custom_field_data; wrap the entire
check-and-migrate sequence in a DB transaction and acquire a row lock on the
device before reading/updating custom_field_data (use a select_for_update()
fetch of the Device record for the same PK inside a transaction.atomic block),
then re-check the locked record's custom_field_data, perform
migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key) and
call _save_device(existing_device) while still holding the lock to prevent
concurrent link/update races.
In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 95-97: job_data values like use_sysname and strip_domain are read
as-is and string values such as "false" remain truthy; normalize them to strict
booleans by parsing the job_data entries (e.g., replace use_sysname =
job_data.get("use_sysname", True) and strip_domain =
job_data.get("strip_domain", False) with explicit boolean normalization such as
checking type and comparing lowercased strings or using a helper like
parse_bool(value) so that "false"/"0"/"" become False and True-like values
become True); apply the same strict boolean normalization to the other similar
variables in the same file (the block around the other job_data.get usages
mentioned in the review) so cache keys and naming behavior are consistent.
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 460-467: The except blocks catching ValidationError and the
generic Exception should not include raw exception text in the user-facing
messages; instead, log the full exception server-side (use logger.exception(...)
or logger.error(..., exc_info=True)) referencing the except handlers for
ValidationError and the generic Exception, then call
transaction.set_rollback(True) and call messages.error(request, "...generic
failure message...") without including exc or server_key details, and finally
return the existing
redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk) as
before.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5d6a8db4-820e-4e31-8f12-51e5facdba5e
📒 Files selected for processing (30)
netbox_librenms_plugin/forms.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/virtual_chassis.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/jobs.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.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
| with ( | ||
| patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), | ||
| patch("netbox_librenms_plugin.views.sync.device_fields.Device") as mock_Device_cls, | ||
| patch("django.conf.settings") as mock_settings, | ||
| patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_messages, | ||
| patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), |
There was a problem hiding this comment.
Patch settings at the view module source to make these tests deterministic.
At Line 999, Line 1030, and Line 1059, patching django.conf.settings can miss the object actually referenced inside netbox_librenms_plugin.views.sync.device_fields, causing config-dependent flakiness.
🛠️ Suggested fix
- patch("django.conf.settings") as mock_settings,
+ patch("netbox_librenms_plugin.views.sync.device_fields.settings") as mock_settings,
...
- patch("django.conf.settings") as mock_settings,
+ patch("netbox_librenms_plugin.views.sync.device_fields.settings") as mock_settings,
...
- patch("django.conf.settings") as mock_settings,
+ patch("netbox_librenms_plugin.views.sync.device_fields.settings") as mock_settings,Based on learnings: "Patch deferred/inline imports at their source module (e.g., netbox_librenms_plugin.import_utils.process_device_filters), not the consuming module."
Also applies to: 1028-1033, 1056-1061
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/tests/test_permissions.py` around lines 996 - 1001,
The tests patch `django.conf.settings` which can miss the `settings` object
imported inside the view module; update the patches to target the view module's
settings (patch "netbox_librenms_plugin.views.sync.device_fields.settings") so
the code under test sees the mocked config. In the same with-blocks that already
patch `netbox_librenms_plugin.views.sync.device_fields.get_object_or_404`,
`Device` (mock_Device_cls), `messages`, and `redirect`, replace
patch("django.conf.settings") with
patch("netbox_librenms_plugin.views.sync.device_fields.settings") for each
occurrence to make the tests deterministic. Ensure all three occurrences in this
test are updated.
| return LibreNMSVMInterfaceTable( | ||
| data, device=obj, vlan_groups=vlan_groups, server_key=self.librenms_api.server_key | ||
| ) |
There was a problem hiding this comment.
Bind request to VMInterfaceTableView before using self.librenms_api.server_key.
Line 49 now depends on self.librenms_api; ensure get_interface_context() sets interface_sync_view.request = request so server-scoped behavior remains correct.
🛠️ Suggested fix
def get_interface_context(self, request, obj):
"""Return interface sync context for the virtual machine."""
interface_name_field = get_interface_name_field(request)
interface_sync_view = VMInterfaceTableView()
+ interface_sync_view.request = request
return interface_sync_view.get_context_data(request, obj, interface_name_field)As per coding guidelines **/views/object_sync/**/*.py: Object sync view methods must create instances of concrete table views, copy the request object, and call get_context_data().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/views/object_sync/vms.py` around lines 48 - 50, The
returned table view needs the current request bound before using server-scoped
behavior (self.librenms_api.server_key): in get_interface_context(), instantiate
the concrete view (LibreNMSVMInterfaceTable or VMInterfaceTableView instance),
assign its request attribute (e.g., interface_sync_view.request = request), then
set any required properties (server_key from self.librenms_api) and call
interface_sync_view.get_context_data() (or the view's equivalent) to produce the
context to return; update the return path to return that context rather than
directly constructing the table without binding request.
There was a problem hiding this comment.
♻️ Duplicate comments (4)
netbox_librenms_plugin/import_utils/bulk_import.py (2)
312-316:⚠️ Potential issue | 🟡 MinorPreserve the
device_roleschema when refreshing cached validation.Line [315] replaces
validation["device_role"]with a partial object, which can drop keys expected downstream (for example,available_roles).Proposed fix
if refreshed: validation["existing_device"] = refreshed - if hasattr(refreshed, "role") and refreshed.role: - validation["device_role"] = {"found": True, "role": refreshed.role} + if hasattr(refreshed, "role"): + role_state = validation.get("device_role") + if not isinstance(role_state, dict): + role_state = {} + validation["device_role"] = role_state + role_state["found"] = bool(refreshed.role) + role_state["role"] = refreshed.role + role_state.setdefault("available_roles", [])Based on learnings: Centralize validation state mutation during import using
import_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 312 - 316, When refreshing cached validation in bulk_import.py, don't overwrite the entire validation["device_role"] dict (which can remove keys like "available_roles"); instead ensure validation.setdefault("device_role", {}) exists and only update the specific fields (e.g., set "found" and "role" from refreshed) rather than replacing the object. Locate the block that checks refreshed (the refreshed variable and validation dict) and change it to preserve existing keys, or better, delegate role/cluster/rack assignment and validation mutations to the centralized helpers in import_validation_helpers.py so all mutations remain consistent.
442-447:⚠️ Potential issue | 🟠 MajorMake disabled filtering tolerant to non-numeric API values.
Line [446] can raise
TypeError/ValueErrorwhendisabledis missing or non-numeric (None,"","-"), which aborts filter processing.Proposed fix
- if not show_disabled: - libre_devices = [d for d in libre_devices if int(d.get("disabled", 0)) != 1] + def _is_disabled(value) -> bool: + if isinstance(value, bool): + return value + if isinstance(value, str): + v = value.strip().lower() + if v in {"1", "true", "yes", "on"}: + return True + if v in {"0", "false", "no", "off", "", "-"}: + return False + try: + return int(value) == 1 + except (TypeError, ValueError): + return False + + if not show_disabled: + libre_devices = [d for d in libre_devices if not _is_disabled(d.get("disabled", 0))]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 442 - 447, The filter for disabled devices in bulk_import.py can raise TypeError/ValueError when a device's "disabled" value is None or non-numeric; update the list comprehension that builds libre_devices (the expression using int(d.get("disabled", 0)) != 1) to perform a safe numeric conversion: retrieve d.get("disabled"), attempt to coerce to int in a small try/except (or treat non-digit strings as 0) and then compare to 1, so non-numeric/missing values are treated as not-disabled instead of raising an exception.netbox_librenms_plugin/views/imports/actions.py (1)
1197-1227:⚠️ Potential issue | 🟠 MajorProtect legacy-ID migration with row locking.
Lines [1206]-[1227] still run a read-check-write sequence on
custom_field_datawithoutselect_for_update(). Concurrent requests can race and overwrite migration state.💡 Proposed fix
elif action == "migrate_librenms_id": @@ - cf_value = existing_device.custom_field_data.get("librenms_id") - if not isinstance(cf_value, int): - return HttpResponse( - "Device librenms_id is already in JSON format; no migration needed.", - status=400, - ) - # Verify the stored legacy ID matches the active LibreNMS device_id so we don't - # migrate a stale/incorrect association to the wrong server mapping. - if cf_value != librenms_id: - return HttpResponse( - f"Legacy librenms_id ({cf_value}) does not match the active device ID " - f"({librenms_id}); cannot migrate safely.", - status=400, - ) - if not validation.get("serial_confirmed") and not force: - return HttpResponse( - "Serial number not confirmed. Check the force checkbox to migrate without serial verification.", - status=400, - ) - migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key) - if err := _save_device(existing_device): - return err + with transaction.atomic(): + try: + locked_device = Device.objects.select_for_update().get(pk=existing_device.pk) + except Device.DoesNotExist: + return HttpResponse( + "Device no longer exists; it may have been deleted concurrently.", + status=409, + ) + + cf_value = locked_device.custom_field_data.get("librenms_id") + if not isinstance(cf_value, int): + return HttpResponse( + "Device librenms_id is already in JSON format; no migration needed.", + status=400, + ) + if cf_value != librenms_id: + return HttpResponse( + f"Legacy librenms_id ({cf_value}) does not match the active device ID " + f"({librenms_id}); cannot migrate safely.", + status=400, + ) + if not validation.get("serial_confirmed") and not force: + return HttpResponse( + "Serial number not confirmed. Check the force checkbox to migrate without serial verification.", + status=400, + ) + + migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key) + if err := _save_device(locked_device): + return err logger.info( - f"Migrated legacy librenms_id on '{existing_device.name}' " + f"Migrated legacy librenms_id on '{locked_device.name}' " f"to {{{self.librenms_api.server_key!r}: {cf_value}}}" )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/imports/actions.py` around lines 1197 - 1227, The migration path for action "migrate_librenms_id" performs a read-check-write on existing_device.custom_field_data without a DB row lock; wrap the logic that reads cf_value, validates cf_value==librenms_id, checks serial_confirmed/force, calls migrate_legacy_librenms_id(existing_device, ...) and _save_device(existing_device) inside a transaction.atomic() block and acquire a select_for_update() lock on the device row (e.g., re-query the Device with .select_for_update() into existing_device_locked) before reading or mutating custom_field_data to prevent concurrent races and ensure the migration is applied atomically.netbox_librenms_plugin/utils.py (1)
482-505:⚠️ Potential issue | 🟠 MajorEnforce integer normalization in LibreNMS ID write/read helpers.
Line [503] still persists
device_idas-is, and Lines [524]-[525] query using unnormalized input. Mixedstr/intvalues will miss matches and can produce duplicate associations.💡 Proposed fix
def set_librenms_device_id(obj, device_id, server_key: str = "default"): @@ - cf_value[server_key] = device_id + try: + normalized_device_id = int(device_id) + except (TypeError, ValueError): + raise ValueError(f"Invalid LibreNMS device_id: {device_id!r}") from None + + cf_value[server_key] = normalized_device_id obj.custom_field_data["librenms_id"] = cf_value def find_by_librenms_id(model, librenms_id, server_key: str = "default"): @@ - return model.objects.filter( - Q(**{f"custom_field_data__librenms_id__{server_key}": librenms_id}) - | Q(custom_field_data__librenms_id=librenms_id) + try: + normalized_id = int(librenms_id) + except (TypeError, ValueError): + return None + + return model.objects.filter( + Q(**{f"custom_field_data__librenms_id__{server_key}": normalized_id}) + | Q(custom_field_data__librenms_id=normalized_id) ).first()Also applies to: 507-526
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/utils.py` around lines 482 - 505, set_librenms_device_id and the corresponding read helpers are storing and comparing device IDs without normalizing types, causing mismatches between str/int values; cast/normalize device_id to an int on write (in set_librenms_device_id) before placing it into obj.custom_field_data["librenms_id"] (handle/ log ValueError if conversion fails) and update the read helper(s) (the functions that look up cf_value[server_key] around the 507-526 region, e.g., get_librenms_device_id or similar) to also normalize retrieved values to int (or safely compare by converting both sides to int) so stored and queried IDs always use the same integer type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 312-316: When refreshing cached validation in bulk_import.py,
don't overwrite the entire validation["device_role"] dict (which can remove keys
like "available_roles"); instead ensure validation.setdefault("device_role", {})
exists and only update the specific fields (e.g., set "found" and "role" from
refreshed) rather than replacing the object. Locate the block that checks
refreshed (the refreshed variable and validation dict) and change it to preserve
existing keys, or better, delegate role/cluster/rack assignment and validation
mutations to the centralized helpers in import_validation_helpers.py so all
mutations remain consistent.
- Around line 442-447: The filter for disabled devices in bulk_import.py can
raise TypeError/ValueError when a device's "disabled" value is None or
non-numeric; update the list comprehension that builds libre_devices (the
expression using int(d.get("disabled", 0)) != 1) to perform a safe numeric
conversion: retrieve d.get("disabled"), attempt to coerce to int in a small
try/except (or treat non-digit strings as 0) and then compare to 1, so
non-numeric/missing values are treated as not-disabled instead of raising an
exception.
In `@netbox_librenms_plugin/utils.py`:
- Around line 482-505: set_librenms_device_id and the corresponding read helpers
are storing and comparing device IDs without normalizing types, causing
mismatches between str/int values; cast/normalize device_id to an int on write
(in set_librenms_device_id) before placing it into
obj.custom_field_data["librenms_id"] (handle/ log ValueError if conversion
fails) and update the read helper(s) (the functions that look up
cf_value[server_key] around the 507-526 region, e.g., get_librenms_device_id or
similar) to also normalize retrieved values to int (or safely compare by
converting both sides to int) so stored and queried IDs always use the same
integer type.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1197-1227: The migration path for action "migrate_librenms_id"
performs a read-check-write on existing_device.custom_field_data without a DB
row lock; wrap the logic that reads cf_value, validates cf_value==librenms_id,
checks serial_confirmed/force, calls migrate_legacy_librenms_id(existing_device,
...) and _save_device(existing_device) inside a transaction.atomic() block and
acquire a select_for_update() lock on the device row (e.g., re-query the Device
with .select_for_update() into existing_device_locked) before reading or
mutating custom_field_data to prevent concurrent races and ensure the migration
is applied atomically.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: cf5dc356-cf1a-47b9-908d-97498b0779d9
📒 Files selected for processing (3)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
netbox_librenms_plugin/views/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/**/*.py: Views must follow a three-layer structure: Base views inviews/base/, Object sync views inviews/object_sync/, and Sync action views inviews/sync/with shared mixins fromviews/mixins.py
New views must extend the closest base class and compose mixins fromviews/mixins.py(LibreNMSPermissionMixin,NetBoxObjectPermissionMixin,LibreNMSAPIMixin,CacheMixin,VlanAssignmentMixin)
All views must inheritLibreNMSPermissionMixinfromviews/mixins.pywithpermission_required = PERM_VIEW_PLUGIN
Declarerequired_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request
Use_get_safe_redirect_url(request)to validate referrer URLs in permission checks to prevent open-redirect attacks
Files:
netbox_librenms_plugin/views/imports/actions.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Job cancellation flow: (1) Call/api/core/background-tasks/{uuid}/stop/to stop RQ job, (2) Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database, (3) Frontend polling detects status changes and redirects appropriately
Poll/api/core/background-tasks/{uuid}/for real-time RQ status, update modal messages based on status transitions (queued,started,finished,stopped,failed), handle all RQ status values explicitly to avoid infinite polling, and usecancelInProgressflag to prevent polling interference during cancellation
NetBox's/api/core/background-tasks/endpoint requires superuser (IsSuperuserinBaseRQViewSet); non-superuser users cannot poll job status and get 403 Forbidden. The plugin must automatically fall back to synchronous mode for non-superusers viashould_use_background_job()inlist.pyandactions.py
Custom sync endpointapi/views.py::sync_job_status()must sync database Job status with RQ job status, needed because NetBox worker doesn't always update DB when jobs stop before processing starts
Import page supports synchronous mode (callsprocess_device_filters()directly, renders results inline) and background mode (enqueuesFilterDevicesJob, returnsJsonResponsewithjob_id/job_pk/poll_url, frontend polls and redirects on completion)
Result loading via_load_job_results(job_id)must readjob.data['device_ids']and reconstruct devices from per-device cache usingget_validated_device_cache_key()
Import filter fields must include:librenms_location,librenms_type,librenms_os,librenms_hostname,librenms_sysname,librenms_hardware,enable_vc_detection,show_disabled,exclude_existing
DeviceImportHelperMixinmust provideget_validated_device_with_selections()andrender_device_row()for HTMX row rendering, shared by update views
BulkImportConfirmView(POST) must render confirmation modal with selected device...
Files:
netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (38)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Reuse fixtures from `tests/conftest.py` instead of creating ad-hoc mocks: Configuration (`mock_multi_server_config`, `mock_legacy_config`), API client (`mock_librenms_api`), NetBox objects (`mock_netbox_device`, `mock_netbox_vm`, `mock_netbox_site`, `mock_netbox_platform`, `mock_netbox_device_type`, `mock_netbox_device_role`, `mock_netbox_cluster`, `mock_netbox_rack`), HTTP responses (`mock_response_factory`, `mock_success_response`, `mock_device_response`, `mock_error_response`, `mock_auth_error_response`), and Import workflow (`sample_librenms_device`, `sample_librenms_device_minimal`, `sample_validation_state`, `sample_validation_state_vm`).
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `ImportDevicesJob` background job: imports devices and VMs. Must call `bulk_import_devices_shared()` for devices and `bulk_import_vms()` for VMs. `job.data` keys must include `imported_device_pks`, `imported_vm_pks`, `imported_libre_device_ids`, `imported_libre_vm_ids`, `server_key`, `total`, `success_count`, `failed_count`, `skipped_count`, `virtual_chassis_created`, `errors`, `completed`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `BulkImportDevicesView` (POST) must execute import: background mode enqueues `ImportDevicesJob`; sync mode calls `bulk_import_devices()` + `bulk_import_vms()` and returns OOB row swaps with `HX-Trigger: closeModal`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Job cancellation flow: (1) Call `/api/core/background-tasks/{uuid}/stop/` to stop RQ job, (2) Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database, (3) Frontend polling detects status changes and redirects appropriately
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/jobs.py : Follow background job conventions defined in `.github/instructions/background-jobs.instructions.md` for `jobs.py`, import views, and import utilities
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py,**/import_utils.py : Both synchronous and background modes must use `get_validated_device_cache_key()` from `import_utils.py` to generate cache keys, ensuring `_load_job_results()` in the list view can retrieve devices regardless of which mode produced them. Never hardcode cache key formats; always use the helper functions
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Result loading via `_load_job_results(job_id)` must read `job.data['device_ids']` and reconstruct devices from per-device cache using `get_validated_device_cache_key()`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/jobs.py : Background jobs must use standalone helpers from `import_utils.py` (`check_user_permissions`, `require_permissions`) instead of view mixins; non-superusers fall back to synchronous mode
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_devices_shared(devices, user, ...)` must be the shared implementation between sync and background import
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : The `pollJobStatus()` function polls `/api/core/background-tasks/{jobId}/` every 2s, updates progress messages, handles cancel button, and redirects on completion.
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_validation_helpers.py : Validation state mutation helpers in `import_validation_helpers.py` must include: `apply_role_to_validation()`, `apply_cluster_to_validation()`, `apply_rack_to_validation()` to update validation state when user selects role/cluster/rack
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*import*.html : Device import dropdowns rely on TomSelect decorators set up elsewhere. Keep `<select class="device-role-select">` markup stable to preserve JS hook-up.
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions in `utils.py` (`find_matching_site`, `match_librenms_hardware_to_device_type`, `find_matching_platform`)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `process_device_filters(filters, ...)` must fetch and validate devices from LibreNMS, returning a list
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `fetch_device_with_cache(device_id, ...)` must retrieve and cache individual device data
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`.
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py,**/jobs.py : Cache key generation must use helper functions: `get_validated_device_cache_key()`, `get_cache_metadata_key()`, `get_active_cached_searches()`, `get_import_device_cache_key()`. Never hardcode cache key formats
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `FilterDevicesJob` background job: filters devices with VC detection. `job.data` keys must include `device_ids`, `total_processed`, `filters`, `server_key`, `vc_detection_enabled`, `cache_timeout`, `cached_at`, `completed`. Devices are cached individually via shared cache keys from `get_validated_device_cache_key()`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Import page supports synchronous mode (calls `process_device_filters()` directly, renders results inline) and background mode (enqueues `FilterDevicesJob`, returns `JsonResponse` with `job_id`/`job_pk`/`poll_url`, frontend polls and redirects on completion)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `validate_device_for_import(device, ...)` is the core validation function that produces validation state dict
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Hook into NetBox Django 5 plugin APIs via `navigation.py`, `urls.py`, and `api/` modules under `netbox_librenms_plugin/`; respect NetBox plugin conventions
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Keep REST endpoint server responses and HTMX targets in sync; implement REST endpoints in `views/imports/actions.py` and HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*.html : HTMX fragments live in `templates/netbox_librenms_plugin/htmx/` and include: `device_import_row.html` (individual import row updates), `device_validation_details.html` (expandable validation details), `device_vc_details.html` (virtual chassis member details), `bulk_import_confirm.html` (import confirmation modal content). Keep server responses and HTMX targets in sync when modifying these fragments.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/sync/**/*.py : Sync action views must follow the pattern: check permissions with `LibreNMSPermissionMixin` and `NetBoxObjectPermissionMixin`, read selected items from `request.POST.getlist('select')`, load cached data using `CacheMixin.get_cache_key()`, apply changes inside `transaction.atomic()`, and redirect to the sync tab with `?tab=<resource>`.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Follow sync conventions defined in `.github/instructions/sync.instructions.md` for sync views, base views, tables, and sync JavaScript
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Sync POST handlers must call `require_all_permissions()` (not just `require_write_permission()`) and return early if it returns a response; use `require_all_permissions_json()` for AJAX/JSON endpoints
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/views.py : API endpoints must use `LibreNMSPluginPermission` class in `api/views.py` where GET checks `view_librenmssettings` and other methods check `change_librenmssettings`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,tables}/**/*.{html,py} : Templates live in `templates/netbox_librenms_plugin/`; reuse/includes under `inc/`. Sync pages extend `librenms_sync_base.html`. Tables emit HTMX-enabled columns and buttons (`tables/*.py`), so prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`)
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `DeviceImportHelperMixin` must provide `get_validated_device_with_selections()` and `render_device_row()` for HTMX row rendering, shared by update views
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_validation_helpers.py : Validation helpers must include: `fetch_model_by_id()`, `extract_device_selections()` for reading form data
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Per-device dropdown update views (`DeviceRoleUpdateView`, `DeviceClusterUpdateView`, `DeviceRackUpdateView`) must apply selection to validation state and return re-rendered row via `render_device_row()`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
🔇 Additional comments (3)
netbox_librenms_plugin/import_utils/bulk_import.py (1)
532-539: Good cache-key scoping for naming preferences.Including
use_sysnameandstrip_domainin both validated-device keys and metadata keys correctly prevents cache collisions across different naming modes.Also applies to: 609-615
netbox_librenms_plugin/utils.py (1)
529-559: Good separation of migration vs persistence responsibility.
migrate_legacy_librenms_id()mutates in-memory state and leaves persistence to callers; that keeps transaction boundaries explicit.netbox_librenms_plugin/views/imports/actions.py (1)
204-212: Good fix:server_keyis now threaded through validation paths.This closes cross-server ambiguity during import validation and post-import row revalidation.
Also applies to: 352-359, 721-729
There was a problem hiding this comment.
♻️ Duplicate comments (1)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
146-155:⚠️ Potential issue | 🟠 MajorLegacy
defaultmapping is misclassified as unconfigured.When using the legacy single-server configuration (where
librenms_urlis set at the plugin config root level rather than underservers), adefaultkey in thelibrenms_idcustom field will havesrv_cfg = Noneand thusis_configured = False. This is incorrect because the legacy config is still an active, valid configuration.Per
LibreNMSAPI.__init__, the legacy fallback readslibrenms_urldirectly fromplugins_cfg, not fromservers_config. The method should check for this case whensk == "default"andsrv_cfgisNone.,
🔧 Proposed fix
plugins_cfg = django_settings.PLUGINS_CONFIG.get("netbox_librenms_plugin", {}) servers_config = plugins_cfg.get("servers", {}) + legacy_url = plugins_cfg.get("librenms_url") result = [] for sk, did in cf_value.items(): srv_cfg = servers_config.get(sk) - is_configured = srv_cfg is not None - librenms_url = srv_cfg.get("librenms_url") if srv_cfg else None - display_name = (srv_cfg.get("display_name") or sk) if srv_cfg else sk + is_legacy_default = sk == "default" and not srv_cfg and bool(legacy_url) + is_configured = (srv_cfg is not None) or is_legacy_default + librenms_url = srv_cfg.get("librenms_url") if srv_cfg else (legacy_url if is_legacy_default else None) + display_name = (srv_cfg.get("display_name") or sk) if srv_cfg else sk device_url = f"{librenms_url}/device/device={did}/" if librenms_url else None🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/librenms_sync_view.py` around lines 146 - 155, The loop that builds per-server data treats a legacy "default" cf_value key as unconfigured because srv_cfg is None; update the loop in the block using plugins_cfg, servers_config and cf_value so that when sk == "default" and srv_cfg is None you read librenms_url and display_name from plugins_cfg (the legacy root-level keys), set is_configured = True, and compute device_url/display_name from those legacy values (mirroring LibreNMSAPI.__init__ fallback behavior) so the legacy single-server config is handled correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 146-155: The loop that builds per-server data treats a legacy
"default" cf_value key as unconfigured because srv_cfg is None; update the loop
in the block using plugins_cfg, servers_config and cf_value so that when sk ==
"default" and srv_cfg is None you read librenms_url and display_name from
plugins_cfg (the legacy root-level keys), set is_configured = True, and compute
device_url/display_name from those legacy values (mirroring LibreNMSAPI.__init__
fallback behavior) so the legacy single-server config is handled correctly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9145d377-7abf-43ae-a785-3438b2216dff
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (2)
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: test-netbox (3.12)
- GitHub Check: test-netbox (3.14)
🧰 Additional context used
📓 Path-based instructions (3)
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
netbox_librenms_plugin/views/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/**/*.py: Views must follow a three-layer structure: Base views inviews/base/, Object sync views inviews/object_sync/, and Sync action views inviews/sync/with shared mixins fromviews/mixins.py
New views must extend the closest base class and compose mixins fromviews/mixins.py(LibreNMSPermissionMixin,NetBoxObjectPermissionMixin,LibreNMSAPIMixin,CacheMixin,VlanAssignmentMixin)
All views must inheritLibreNMSPermissionMixinfromviews/mixins.pywithpermission_required = PERM_VIEW_PLUGIN
Declarerequired_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request
Use_get_safe_redirect_url(request)to validate referrer URLs in permission checks to prevent open-redirect attacks
Files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
**/views/base/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
**/views/base/**/*.py: Base view classes (BaseLibreNMSSyncView,BaseInterfaceTableView,BaseCableTableView,BaseIPAddressTableView,BaseVLANTableView) must implement the data pipeline pattern: fetch data from LibreNMS API, cache results withCacheMixinkeys likelibrenms_{data_type}_{model_name}_{pk}, compare against NetBox objects, and render a django-tables2 table in a partial template.
Base table view classes must implement resource-specific comparison logic: interface matching by name, IP matching by address/mask, VLAN matching by VID+group, and cables by matching remote devices and checking cable status.
VlanAssignmentMixinmust resolve VLAN group scope in order: Rack → Location → Site → SiteGroup → Region → Global, and must provide auto-selection of the most-specific VLAN group and lookup map building for interface and VLAN sync.
Files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
🧠 Learnings (10)
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Follow sync conventions defined in `.github/instructions/sync.instructions.md` for sync views, base views, tables, and sync JavaScript
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Views must follow a three-layer structure: Base views in `views/base/`, Object sync views in `views/object_sync/`, and Sync action views in `views/sync/` with shared mixins from `views/mixins.py`
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,tables}/**/*.{html,py} : Templates live in `templates/netbox_librenms_plugin/`; reuse/includes under `inc/`. Sync pages extend `librenms_sync_base.html`. Tables emit HTMX-enabled columns and buttons (`tables/*.py`), so prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py : Use `LibreNMSAPIMixin` to provide lazy-loaded `LibreNMSAPI` instance via `self.librenms_api` property and `get_server_info()` method for template context.
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Hook into NetBox Django 5 plugin APIs via `navigation.py`, `urls.py`, and `api/` modules under `netbox_librenms_plugin/`; respect NetBox plugin conventions
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/models.py : Coordinate schema changes through Django migrations in `migrations/` directory; update `models.py`, admin, and Pydantic representations accordingly
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Keep REST endpoint server responses and HTMX targets in sync; implement REST endpoints in `views/imports/actions.py` and HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
🔇 Additional comments (3)
netbox_librenms_plugin/views/base/librenms_sync_view.py (3)
3-3: LGTM!Clean import with aliasing to avoid naming conflicts with local settings objects.
118-118: LGTM!Correctly passes the NetBox object and active server key to the new method. Per
LibreNMSAPI.__init__,server_keyis guaranteed to be non-empty.
156-170: LGTM!The mapping structure, sorting logic, and return value handling are all correct. The sort order (active → configured → orphaned) provides intuitive UI display. Direct access to
custom_field_datais appropriate here since this method needs to enumerate all server mappings, not just the active one.
- Store librenms_id as {server_key: device_id} dict instead of bare int
- Add get_librenms_device_id/set_librenms_device_id/find_by_librenms_id/
migrate_legacy_librenms_id helpers in utils.py (at end of file)
- Thread server_key through import pipeline (filters, cache, bulk_import,
device_operations, vm_operations, virtual_chassis)
- Deterministic SHA256 cache keys, None-safe filter inclusion
- Fix disabled field filter (use 'disabled' flag, not 'status')
- Truthy string parsing for use_sysname/strip_domain ('on'/'true'/'1')
- migrate_librenms_id action in DeviceConflictActionView
- RemoveServerMappingView for per-server librenms_id removal
- New tests: test_permissions.py, test_sync_view_mismatch.py
- RQ-based job cancellation check in bulk import
Reduce cosmetic diff vs develop:
- _save_device restored to before _resolve_naming_preferences in actions.py
- _empty_return moved to just before process_device_filters in bulk_import.py
- New utils.py helpers at end of file with imports merged into main block
a312799 to
62f838c
Compare
- librenms_sync_view: fall back to legacy root-level librenms_url when
server_key=="default" and no matching servers config entry exists,
so single-server legacy configs are shown as configured
- bulk_import: preserve existing device_role dict keys on refresh using
.setdefault().update() instead of full replacement
- bulk_import/filters: replace int(d.get("disabled",0)) with _safe_disabled()
helper to tolerate None/non-numeric values without raising TypeError
- bulk_import: fix match_type for sysname-based re-check (was "hostname",
now "sysname") to reflect the actual matching source
- utils: cast device_id to int in set_librenms_device_id so stored values
always use integer type, preventing str/int comparison mismatches
- actions: wrap migrate_librenms_id in transaction.atomic() +
select_for_update() to prevent concurrent race on the same device
- librenms_import.js: deduplicate tooltip initialization in ensureModalVisible
using bootstrap.Tooltip.getOrCreateInstance() instead of new Tooltip()
- ip_addresses_view: compute get_librenms_device_id() once per iteration
instead of calling it twice in the dict comprehension
- device_fields: log exception server-side and show a generic error message
instead of exposing exc details and server_key to the user
- cache/filters: extract _hash + cache_key construction into
get_import_search_cache_key() in cache.py; filters.py now imports and
uses the helper, removing duplicate hashing logic
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
netbox_librenms_plugin/views/imports/actions.py (1)
83-85:⚠️ Potential issue | 🟠 MajorNormalize persisted preference fallbacks in
_resolve_naming_preferences().POST/GET values are normalized, but stored prefs are still used raw. If a stored value is
"false"/"0", it is truthy and can corrupt naming resolution and cache behavior.Proposed fix
- def _is_truthy(val): - return val.lower() in _TRUTHY if val is not None else False + def _is_truthy(val): + if val is None: + return False + if isinstance(val, bool): + return val + return str(val).strip().lower() in _TRUTHY + + def _coerce_bool(val, default: bool) -> bool: + if val is None: + return default + return _is_truthy(val) ... pref = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname") if pref is not None: - use_sysname = pref + use_sysname = _coerce_bool(pref, True) else: settings = LibreNMSSettings.objects.first() use_sysname = getattr(settings, "use_sysname_default", True) if settings else True ... pref = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain") if pref is not None: - strip_domain = pref + strip_domain = _coerce_bool(pref, False) else: if settings is None: settings = LibreNMSSettings.objects.first()Also applies to: 98-100
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/imports/actions.py` around lines 83 - 85, The stored user prefs retrieved via get_user_pref in _resolve_naming_preferences (e.g., when assigning use_sysname and prefer_name) are used raw and can be truthy strings like "false" or "0"; normalize those persisted values the same way POST/GET inputs are normalized (convert string booleans/"0"/"1" to actual booleans or apply the same normalization helper used elsewhere) before assigning to use_sysname and prefer_name so cached naming resolution behaves correctly; update the assignments around get_user_pref(...) to pass the retrieved value through the existing normalization routine (or implement a small normalize_pref utility) rather than assigning raw pref.
♻️ Duplicate comments (11)
netbox_librenms_plugin/import_utils/filters.py (1)
15-21:⚠️ Potential issue | 🟡 Minor
_safe_disabled()still misclassifies truthy string values.Line [20]-Line [21] maps non-numeric values to
0, so"true","yes", or"on"are treated as enabled and can bypassshow_disabled=False.🔧 Proposed fix
def _safe_disabled(device: dict) -> int: """Return 1 if the device is disabled, 0 otherwise. Tolerates None/non-numeric values.""" val = device.get("disabled", 0) - try: - return int(val) - except (TypeError, ValueError): - return 0 + if isinstance(val, bool): + return 1 if val else 0 + if isinstance(val, (int, float)): + return 1 if int(val) == 1 else 0 + text = str(val).strip().lower() + return 1 if text in {"1", "true", "yes", "on"} else 0🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/filters.py` around lines 15 - 21, _safe_disabled currently treats non-numeric truthy strings as 0, causing values like "true"/"yes"/"on" to be misclassified as enabled; update _safe_disabled to normalize string inputs from device.get("disabled", 0) (case-insensitive) and return 1 for common truthy tokens ("true","yes","on","1") and 0 for falsy tokens ("false","no","off","0"), while still attempting int conversion and catching TypeError/ValueError to default to 0 — keep the function name _safe_disabled and its contract (returns int 0/1).netbox_librenms_plugin/import_utils/bulk_import.py (1)
24-30:⚠️ Potential issue | 🟡 MinorUse the same tolerant disabled parsing here as well.
This helper has the same gap as the filter module: non-numeric truthy values (e.g.,
"true","yes") become0, which can incorrectly include disabled devices.🔧 Proposed fix
def _safe_disabled(device: dict) -> int: """Return 1 if the device is disabled, 0 otherwise. Tolerates None/non-numeric values.""" val = device.get("disabled", 0) - try: - return int(val) - except (TypeError, ValueError): - return 0 + if isinstance(val, bool): + return 1 if val else 0 + if isinstance(val, (int, float)): + return 1 if int(val) == 1 else 0 + text = str(val).strip().lower() + return 1 if text in {"1", "true", "yes", "on"} else 0🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 24 - 30, The _safe_disabled helper currently converts non-numeric truthy strings to 0; update _safe_disabled(device: dict) to be tolerant of boolean-like values by checking the retrieved val = device.get("disabled", 0) for booleans and common truthy strings before attempting int(): if val is a bool return int(val), if it's a str normalize (.strip().lower()) and return 1 for values like "true","yes","on" (and treat "1") and 0 for the rest, then fall back to the existing int() try/except to handle numeric strings and other types; touch the _safe_disabled function only.netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
35-39:⚠️ Potential issue | 🟠 MajorKeep VC member positions strictly 1-based end-to-end.
Line [315] still adds
+1to positions that are already 1-based, so suggested names can shift (e.g., position 1 renders as member 2). Also, Line [37] and Line [205] allow parsed0/negative positions through unchanged.🔧 Proposed fix
@@ - raw_position = member_copy.get("position", idx + 1) - try: - member_copy["position"] = int(raw_position) - except (TypeError, ValueError): - member_copy["position"] = idx + 1 # 1-based fallback; position 0 is invalid + raw_position = member_copy.get("position", idx + 1) + try: + parsed_position = int(raw_position) + except (TypeError, ValueError): + parsed_position = idx + 1 + member_copy["position"] = parsed_position if parsed_position >= 1 else idx + 1 @@ - raw_position = chassis.get("entPhysicalParentRelPos", idx + 1) - try: - position = int(raw_position) - except (TypeError, ValueError): - position = idx + 1 + raw_position = chassis.get("entPhysicalParentRelPos", idx + 1) + try: + parsed_position = int(raw_position) + except (TypeError, ValueError): + parsed_position = idx + 1 + position = parsed_position if parsed_position >= 1 else idx + 1 @@ - for idx, member in enumerate(vc_data.get("members", [])): - raw_position = member.get("position", idx) - try: - base_position = int(raw_position) - except (TypeError, ValueError): - base_position = idx - position = base_position + 1 # Convert to 1-based position - member["position"] = base_position + for idx, member in enumerate(vc_data.get("members", []), start=1): + raw_position = member.get("position", idx) + try: + parsed_position = int(raw_position) + except (TypeError, ValueError): + parsed_position = idx + position = parsed_position if parsed_position >= 1 else idx + member["position"] = position member["suggested_name"] = _generate_vc_member_name( master_name, position, serial=member.get("serial"), pattern=vc_pattern )Also applies to: 203-207, 309-319
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 35 - 39, The VC member position handling currently allows 0/negative positions and sometimes adds an extra +1, shifting already 1-based positions; update the parsing logic (where raw_position, member_copy and idx are used) to coerce int(raw_position) and then if the result is <= 0 replace it with idx + 1 (1-based fallback), assign that value to member_copy["position"], and remove any later code that adds an extra +1 to positions when building suggested names (inspect the other occurrences around the blocks referenced at lines ~203-207, ~309-319 and the suggestion code near ~315) so positions remain strictly 1-based end-to-end.netbox_librenms_plugin/views/object_sync/vms.py (1)
25-29:⚠️ Potential issue | 🟠 MajorBind
requestonto VM table view instances beforeget_context_data().Line 49 now depends on
self.librenms_api.server_key; without bindingrequeston the instantiated table views, server-scoped context can drift and this breaks the object-sync view contract.Suggested fix
def get_interface_context(self, request, obj): """Return interface sync context for the virtual machine.""" interface_name_field = get_interface_name_field(request) interface_sync_view = VMInterfaceTableView() + interface_sync_view.request = request return interface_sync_view.get_context_data(request, obj, interface_name_field) @@ def get_ip_context(self, request, obj): """Return IP address sync context for the virtual machine.""" ipaddress_sync_view = VMIPAddressTableView() + ipaddress_sync_view.request = request return ipaddress_sync_view.get_context_data(request, obj)As per coding guidelines
**/views/object_sync/**/*.py: Object sync view methods must create instances of concrete table views, copy therequestobject, and callget_context_data().Also applies to: 35-38, 48-50
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/object_sync/vms.py` around lines 25 - 29, The VM object-sync method get_interface_context creates a VMInterfaceTableView and calls get_context_data without binding the request to the view instance, which allows server-scoped state (self.librenms_api.server_key) to drift; instantiate the concrete table view (VMInterfaceTableView), set its request attribute to a shallow copy of the incoming request (e.g., copy.copy(request)) before calling get_context_data, and then call get_context_data(request, obj, interface_name_field); apply the same pattern to the other object-sync methods in this file (the other table-view instances referenced around lines 35-38 and 48-50) so every table view instance has request bound/copied prior to get_context_data.netbox_librenms_plugin/tests/test_permissions.py (1)
999-1000:⚠️ Potential issue | 🟠 MajorPatch
settingsat the view module source, notdjango.conf.settings.These tests should patch
netbox_librenms_plugin.views.sync.device_fields.settingsso the code under test sees the mocked config deterministically.🛠️ Suggested fix
- patch("django.conf.settings") as mock_settings, + patch("netbox_librenms_plugin.views.sync.device_fields.settings") as mock_settings,Based on learnings: "Applies to tests/**/*.py : Patch deferred/inline imports at their source module ... not the consuming module."
Also applies to: 1030-1031, 1059-1060
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_permissions.py` around lines 999 - 1000, Replace patches that mock Django settings at the global import ("django.conf.settings") with patches that target the module where settings is actually imported by the code under test: use "netbox_librenms_plugin.views.sync.device_fields.settings" instead of "django.conf.settings" (and keep the existing message patch target "netbox_librenms_plugin.views.sync.device_fields.messages"); update the other similar occurrences referenced (the patches around the blocks at the later occurrences noted) so all tests patch settings at the source module the view uses.netbox_librenms_plugin/tests/test_sync_view_mismatch.py (1)
331-334: 🧹 Nitpick | 🔵 TrivialUse shared fixture(s) instead of a local ad-hoc
_make_objbuilder.This helper reintroduces custom mock shape drift; please switch to the existing
tests/conftest.pyfixture path for NetBox objects and override only the neededcustom_field_data.Based on learnings: "Applies to tests/**/*.py : Reuse fixtures from
tests/conftest.pyinstead of creating ad-hoc mocks..."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py` around lines 331 - 334, Replace the local helper _make_obj in tests/test_sync_view_mismatch.py with the shared NetBox object fixture defined in tests/conftest.py: stop creating an ad-hoc MagicMock, instead grab the conftest fixture for the NetBox object (the fixture name used in conftest.py) and only override its custom_field_data to {"librenms_id": cf_librenms_id} in the test that needs it; remove the _make_obj function and update tests to use the fixture instance (or a shallow copy of it) with the modified custom_field_data to avoid custom mock shape drift.netbox_librenms_plugin/utils.py (1)
503-512:⚠️ Potential issue | 🟠 MajorDo not persist non-integer
librenms_idvalues.Current fallback stores invalid IDs as-is, which can silently break server-scoped lookups and matching logic later.
🛠️ Suggested fix
- try: - cf_value[server_key] = int(device_id) - except (TypeError, ValueError): - logger.warning( - "librenms_id device_id %r is not a valid integer on %r; storing as-is.", - device_id, - obj, - ) - cf_value[server_key] = device_id + try: + cf_value[server_key] = int(device_id) + except (TypeError, ValueError): + raise ValueError(f"Invalid LibreNMS device_id: {device_id!r}") from None🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/utils.py` around lines 503 - 512, The code currently writes non-integer device_id into cf_value[server_key] and persists it via obj.custom_field_data["librenms_id"]; instead, only assign an int to cf_value[server_key] (convert and set when int(device_id) succeeds) and on TypeError/ValueError do not store the original device_id — just log the warning and leave cf_value unmodified (or remove the server_key entry if it already exists), then only write obj.custom_field_data["librenms_id"] when cf_value contains valid integer entries; refer to cf_value, server_key, device_id and obj.custom_field_data["librenms_id"] when making the change.netbox_librenms_plugin/views/base/cables_view.py (1)
86-89: 🛠️ Refactor suggestion | 🟠 MajorCentralize the server-scoped
librenms_idlookup predicate.The same JSON/legacy
Q(...) | Q(...)filter is repeated across several paths; extract one helper (e.g.,build_librenms_id_q(server_key, value)) and reuse it everywhere.As per coding guidelines: "Use
LibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS."Also applies to: 131-134, 142-145, 170-172, 182-184, 389-390
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/cables_view.py` around lines 86 - 89, Extract the repeated JSON/legacy lookup Q(...) | Q(...) into a single helper function (e.g., build_librenms_id_q(server_key, value)) and replace all instances where Device/VM lookups use the duplicated predicate (for example the Device.objects.get(...) call that builds Q(**{f"custom_field_data__librenms_id__{server_key}": remote_device_id}) | Q(custom_field_data__librenms_id=remote_device_id)) to call this helper instead; implement the helper to return the combined Q object and reuse it at the other mentioned locations (the other similar Device/VM queries). Additionally, stop accessing the custom field directly and use LibreNMSAPI.get_librenms_id() wherever the code maps Devices/VMs to LibreNMS IDs so mapping logic calls LibreNMSAPI.get_librenms_id() rather than reading custom_field_data__librenms_id directly. Ensure all replacements keep the same semantics and update imports/usages accordingly.netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
146-147:⚠️ Potential issue | 🟡 MinorGuard
PLUGINS_CONFIGaccess to avoid hard failures in minimal settings contexts.Use a safe fallback (
getattr) before.get(...)so this helper doesn’t raiseAttributeErrorwhenPLUGINS_CONFIGis absent.🛠️ Suggested fix
- plugins_cfg = django_settings.PLUGINS_CONFIG.get("netbox_librenms_plugin", {}) + plugins_cfg = getattr(django_settings, "PLUGINS_CONFIG", {}).get("netbox_librenms_plugin", {})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/librenms_sync_view.py` around lines 146 - 147, The current lookup assumes django_settings.PLUGINS_CONFIG exists and will raise AttributeError in minimal settings; update the access to guard against a missing attribute by using getattr(django_settings, "PLUGINS_CONFIG", {}) before calling .get(...), e.g., compute plugins_cfg from getattr(django_settings, "PLUGINS_CONFIG", {}) and then derive servers_config = plugins_cfg.get("servers", {}); update the code paths in librenms_sync_view.py that reference plugins_cfg and servers_config (variables named plugins_cfg, servers_config and the django_settings reference) so they use this safe fallback.netbox_librenms_plugin/import_utils/device_operations.py (1)
772-773: 🛠️ Refactor suggestion | 🟠 MajorUse
set_librenms_device_id()instead of inlinecustom_field_dataassignment.Inline mapping construction duplicates ID-format logic and can drift from the central normalization path.
Proposed refactor
from ..utils import ( find_matching_platform, find_matching_site, match_librenms_hardware_to_device_type, + set_librenms_device_id, ) ... device_data = { "name": device_name, "site": site, "device_type": device_type, "role": device_role, "status": "active" if libre_device.get("status") == 1 else "offline", "comments": f"Imported from LibreNMS by netbox-librenms-plugin on {import_time}", - "custom_field_data": {"librenms_id": {api.server_key: int(device_id)}}, } ... device = Device(**device_data) + set_librenms_device_id(device, int(device_id), api.server_key) device.full_clean() device.save()netbox_librenms_plugin/views/imports/list.py (1)
95-96:⚠️ Potential issue | 🟠 MajorNormalize naming flags to strict booleans before deriving cache keys.
use_sysname/strip_domaincan arrive as persisted strings (for example"false"), and passing them through raw makes them truthy in Python. That can skew naming mode and produce cache key mismatches between producer/consumer paths.Proposed fix
+def _coerce_bool(value, default: bool) -> bool: + if value is None: + return default + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "on", "yes"} + return bool(value) ... - use_sysname = job_data.get("use_sysname", True) - strip_domain = job_data.get("strip_domain", False) + use_sysname = _coerce_bool(job_data.get("use_sysname"), True) + strip_domain = _coerce_bool(job_data.get("strip_domain"), False) ... - _use_sysname = get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname") - _strip_domain = get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain") - if _use_sysname is None: - _use_sysname = getattr(settings_obj, "use_sysname_default", True) if settings_obj else True - if _strip_domain is None: - _strip_domain = getattr(settings_obj, "strip_domain_default", False) if settings_obj else False + _use_sysname = _coerce_bool( + get_user_pref(request, "plugins.netbox_librenms_plugin.use_sysname"), + getattr(settings_obj, "use_sysname_default", True) if settings_obj else True, + ) + _strip_domain = _coerce_bool( + get_user_pref(request, "plugins.netbox_librenms_plugin.strip_domain"), + getattr(settings_obj, "strip_domain_default", False) if settings_obj else False, + )Also applies to: 114-115, 447-448
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/imports/list.py` around lines 95 - 96, Normalize use_sysname and strip_domain to strict booleans before using them for naming/caching: read the raw values from job_data (the variables use_sysname and strip_domain in netbox_librenms_plugin/views/imports/list.py and the same occurrences around the other noted spots) and coerce string values like "false"/"true"/"0"/"1"/"yes"/"no" to proper booleans (e.g. lowercasing and comparing to accepted true tokens), otherwise fall back to bool() for non-string types, then use those normalized booleans when deriving cache keys and determining naming mode.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Line 224: The logic that sets naming_criteria["source"] can be wrong when
use_sysname is false but hostname is empty: in _determine_device_name() ensure
the source reflects the actual field used — if hostname is falsy and you fall
back to libre_device.get("sysName"), set naming_criteria["source"] to "sysname"
(or "sysName" consistent with your naming) instead of leaving it as "hostname";
update the ternary/if that assigns naming_criteria["source"] to check the actual
chosen name (hostname present ? "hostname" : libre_device.get("sysName") ?
"sysname" : whatever other fallback) so the recorded source matches the value
returned.
In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js`:
- Around line 1096-1102: The forEach callback currently uses a concise arrow
expression that implicitly returns the Tooltip instance (unused); update the
tooltip initialization in the modal swap code (the block that queries tooltipEls
and calls bootstrap.Tooltip.getOrCreateInstance) to use an explicit block-bodied
arrow or a for...of loop so the callback does not return a value (e.g., change
tooltipEls.forEach(el => bootstrap.Tooltip.getOrCreateInstance(el)) to a
block-bodied form or iterate with for (const el of tooltipEls) {
bootstrap.Tooltip.getOrCreateInstance(el); }) to make the intent clear and avoid
implicit returns.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 193-196: The span that shows the matched device type uses
Bootstrap's data-bs-toggle attribute (see the template block referencing
validation.device_type.match_type, validation.device_type.chassis_model, and
validation.device_type.device_type); remove the data-bs-toggle="tooltip"
entirely from that span so the fragment does not reintroduce Bootstrap toggle
helpers into the modal HTMX flow, and keep the title attribute (or replace with
a non-Bootstrap tooltip initialization if needed) so the explanatory text
remains available without using bootstrap.Modal helpers.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`:
- Around line 78-80: The inline onsubmit handler injects mapping.server_key
directly into a JS string which allows quote-breaking input to execute script;
update the template to JS-escape mapping.server_key before insertion (use
Django's escapejs filter on mapping.server_key inside the onsubmit attribute),
e.g. replace occurrences of {{ mapping.server_key }} in the onsubmit with {{
mapping.server_key|escapejs }} so the value is safely quoted in the confirm()
call and cannot break out into executable code.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1233-1241: Inside the lock, you must re-check that the current
legacy ID matches the expected librenms_id before migrating: after reading
cf_locked = locked_device.custom_field_data.get("librenms_id") ensure cf_locked
is an int and equals the librenms_id you intended to migrate (the same value
checked pre-lock, e.g. cf_value or librenms_id) and only then call
migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key) and
_save_device(locked_device); if cf_locked is not an int keep the existing
JSON-format response, and if cf_locked is an int but does not equal the expected
librenms_id return/skip migration to avoid migrating the wrong value.
- Around line 1139-1149: The serial conflict check in the sync flow (inside the
transaction block where you lock the target Device row via select_for_update and
use variables like locked_device and conflict_device) still has a race because
other transactions can concurrently insert/update different Device rows with the
same serial; add a database-level uniqueness constraint or unique index on the
Device.serial column (handling NULLs as needed) to enforce global uniqueness,
create the corresponding migration for the Device model, and update the
import/update code to catch and handle IntegrityError on save (or UPSERT) so the
UI returns a 409/meaningful message when the DB rejects a duplicate serial
instead of relying solely on the in-transaction conflict check.
---
Outside diff comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 83-85: The stored user prefs retrieved via get_user_pref in
_resolve_naming_preferences (e.g., when assigning use_sysname and prefer_name)
are used raw and can be truthy strings like "false" or "0"; normalize those
persisted values the same way POST/GET inputs are normalized (convert string
booleans/"0"/"1" to actual booleans or apply the same normalization helper used
elsewhere) before assigning to use_sysname and prefer_name so cached naming
resolution behaves correctly; update the assignments around get_user_pref(...)
to pass the retrieved value through the existing normalization routine (or
implement a small normalize_pref utility) rather than assigning raw pref.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 24-30: The _safe_disabled helper currently converts non-numeric
truthy strings to 0; update _safe_disabled(device: dict) to be tolerant of
boolean-like values by checking the retrieved val = device.get("disabled", 0)
for booleans and common truthy strings before attempting int(): if val is a bool
return int(val), if it's a str normalize (.strip().lower()) and return 1 for
values like "true","yes","on" (and treat "1") and 0 for the rest, then fall back
to the existing int() try/except to handle numeric strings and other types;
touch the _safe_disabled function only.
In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 15-21: _safe_disabled currently treats non-numeric truthy strings
as 0, causing values like "true"/"yes"/"on" to be misclassified as enabled;
update _safe_disabled to normalize string inputs from device.get("disabled", 0)
(case-insensitive) and return 1 for common truthy tokens ("true","yes","on","1")
and 0 for falsy tokens ("false","no","off","0"), while still attempting int
conversion and catching TypeError/ValueError to default to 0 — keep the function
name _safe_disabled and its contract (returns int 0/1).
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 35-39: The VC member position handling currently allows 0/negative
positions and sometimes adds an extra +1, shifting already 1-based positions;
update the parsing logic (where raw_position, member_copy and idx are used) to
coerce int(raw_position) and then if the result is <= 0 replace it with idx + 1
(1-based fallback), assign that value to member_copy["position"], and remove any
later code that adds an extra +1 to positions when building suggested names
(inspect the other occurrences around the blocks referenced at lines ~203-207,
~309-319 and the suggestion code near ~315) so positions remain strictly 1-based
end-to-end.
In `@netbox_librenms_plugin/tests/test_permissions.py`:
- Around line 999-1000: Replace patches that mock Django settings at the global
import ("django.conf.settings") with patches that target the module where
settings is actually imported by the code under test: use
"netbox_librenms_plugin.views.sync.device_fields.settings" instead of
"django.conf.settings" (and keep the existing message patch target
"netbox_librenms_plugin.views.sync.device_fields.messages"); update the other
similar occurrences referenced (the patches around the blocks at the later
occurrences noted) so all tests patch settings at the source module the view
uses.
In `@netbox_librenms_plugin/tests/test_sync_view_mismatch.py`:
- Around line 331-334: Replace the local helper _make_obj in
tests/test_sync_view_mismatch.py with the shared NetBox object fixture defined
in tests/conftest.py: stop creating an ad-hoc MagicMock, instead grab the
conftest fixture for the NetBox object (the fixture name used in conftest.py)
and only override its custom_field_data to {"librenms_id": cf_librenms_id} in
the test that needs it; remove the _make_obj function and update tests to use
the fixture instance (or a shallow copy of it) with the modified
custom_field_data to avoid custom mock shape drift.
In `@netbox_librenms_plugin/utils.py`:
- Around line 503-512: The code currently writes non-integer device_id into
cf_value[server_key] and persists it via obj.custom_field_data["librenms_id"];
instead, only assign an int to cf_value[server_key] (convert and set when
int(device_id) succeeds) and on TypeError/ValueError do not store the original
device_id — just log the warning and leave cf_value unmodified (or remove the
server_key entry if it already exists), then only write
obj.custom_field_data["librenms_id"] when cf_value contains valid integer
entries; refer to cf_value, server_key, device_id and
obj.custom_field_data["librenms_id"] when making the change.
In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 86-89: Extract the repeated JSON/legacy lookup Q(...) | Q(...)
into a single helper function (e.g., build_librenms_id_q(server_key, value)) and
replace all instances where Device/VM lookups use the duplicated predicate (for
example the Device.objects.get(...) call that builds
Q(**{f"custom_field_data__librenms_id__{server_key}": remote_device_id}) |
Q(custom_field_data__librenms_id=remote_device_id)) to call this helper instead;
implement the helper to return the combined Q object and reuse it at the other
mentioned locations (the other similar Device/VM queries). Additionally, stop
accessing the custom field directly and use LibreNMSAPI.get_librenms_id()
wherever the code maps Devices/VMs to LibreNMS IDs so mapping logic calls
LibreNMSAPI.get_librenms_id() rather than reading custom_field_data__librenms_id
directly. Ensure all replacements keep the same semantics and update
imports/usages accordingly.
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 146-147: The current lookup assumes django_settings.PLUGINS_CONFIG
exists and will raise AttributeError in minimal settings; update the access to
guard against a missing attribute by using getattr(django_settings,
"PLUGINS_CONFIG", {}) before calling .get(...), e.g., compute plugins_cfg from
getattr(django_settings, "PLUGINS_CONFIG", {}) and then derive servers_config =
plugins_cfg.get("servers", {}); update the code paths in librenms_sync_view.py
that reference plugins_cfg and servers_config (variables named plugins_cfg,
servers_config and the django_settings reference) so they use this safe
fallback.
In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 95-96: Normalize use_sysname and strip_domain to strict booleans
before using them for naming/caching: read the raw values from job_data (the
variables use_sysname and strip_domain in
netbox_librenms_plugin/views/imports/list.py and the same occurrences around the
other noted spots) and coerce string values like
"false"/"true"/"0"/"1"/"yes"/"no" to proper booleans (e.g. lowercasing and
comparing to accepted true tokens), otherwise fall back to bool() for non-string
types, then use those normalized booleans when deriving cache keys and
determining naming mode.
In `@netbox_librenms_plugin/views/object_sync/vms.py`:
- Around line 25-29: The VM object-sync method get_interface_context creates a
VMInterfaceTableView and calls get_context_data without binding the request to
the view instance, which allows server-scoped state
(self.librenms_api.server_key) to drift; instantiate the concrete table view
(VMInterfaceTableView), set its request attribute to a shallow copy of the
incoming request (e.g., copy.copy(request)) before calling get_context_data, and
then call get_context_data(request, obj, interface_name_field); apply the same
pattern to the other object-sync methods in this file (the other table-view
instances referenced around lines 35-38 and 48-50) so every table view instance
has request bound/copied prior to get_context_data.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 82c218dc-44b4-4356-946a-b3dea6584e8e
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (31)
netbox_librenms_plugin/forms.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/virtual_chassis.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/jobs.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/tests/test_sync_view_mismatch.pynetbox_librenms_plugin/urls.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.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
| # Re-check under lock — another request may have already migrated it | ||
| cf_locked = locked_device.custom_field_data.get("librenms_id") | ||
| if not isinstance(cf_locked, int): | ||
| return HttpResponse( | ||
| "Device librenms_id is already in JSON format; no migration needed.", | ||
| status=400, | ||
| ) | ||
| migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key) | ||
| if err := _save_device(locked_device): |
There was a problem hiding this comment.
Re-check legacy ID equality under lock before migrating.
The pre-lock cf_value == librenms_id check can become stale. Inside the lock you only verify type, so a changed integer can be migrated incorrectly.
Proposed fix
cf_locked = locked_device.custom_field_data.get("librenms_id")
if not isinstance(cf_locked, int):
return HttpResponse(
"Device librenms_id is already in JSON format; no migration needed.",
status=400,
)
+ if cf_locked != librenms_id:
+ return HttpResponse(
+ f"Legacy librenms_id ({cf_locked}) does not match the active device ID "
+ f"({librenms_id}); cannot migrate safely.",
+ status=409,
+ )
migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/views/imports/actions.py` around lines 1233 - 1241,
Inside the lock, you must re-check that the current legacy ID matches the
expected librenms_id before migrating: after reading cf_locked =
locked_device.custom_field_data.get("librenms_id") ensure cf_locked is an int
and equals the librenms_id you intended to migrate (the same value checked
pre-lock, e.g. cf_value or librenms_id) and only then call
migrate_legacy_librenms_id(locked_device, self.librenms_api.server_key) and
_save_device(locked_device); if cf_locked is not an int keep the existing
JSON-format response, and if cf_locked is an int but does not equal the expected
librenms_id return/skip migration to avoid migrating the wrong value.
- device_operations: fix naming_criteria["source"] to reflect actual field
used — when use_sysname=False and hostname is empty, fallback to sysName
was not reflected; source now mirrors _determine_device_name logic
- librenms_sync_base.html: escapejs on mapping.server_key in confirm()
to prevent quote injection/XSS from untrusted server key values
- actions.py: inside migrate_librenms_id lock verify cf_locked == librenms_id
before migrating, guarding against value drift under concurrent lock
- actions.py: add comment explaining why DB-level unique constraint on serial
is intentionally omitted (device moves allow temporary duplicates)
- _safe_disabled: handle bool values and common truthy/falsy string tokens
(true/yes/on/false/no/off) in both bulk_import.py and filters.py
- utils.py: on TypeError/ValueError in set_librenms_device_id return early
without persisting the invalid entry instead of storing as-is
- cables_view.py: extract _librenms_id_q(server_key, value) helper; replace
all five duplicated Q(**{...}) | Q(...) patterns with calls to it
- librenms_sync_view.py: use getattr(django_settings, "PLUGINS_CONFIG", {})
to safely handle environments where PLUGINS_CONFIG is absent
- virtual_chassis.py: enforce pos > 0 in _clone_virtual_chassis_data; in
update_vc_member_suggested_names remove spurious +1 that was
over-incrementing already-1-based entPhysicalParentRelPos positions
- librenms_import.js: use for..of instead of forEach to avoid implicit
return from getOrCreateInstance in ensureModalVisible tooltip init
- device_validation_details.html: remove data-bs-toggle from device type
span to avoid duplicate Bootstrap tooltip instances on HTMX modal swaps
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
205-208:⚠️ Potential issue | 🟡 MinorEnforce 1-based VC positions before building member data.
Line 206 can still preserve
0/negative values fromentPhysicalParentRelPos, which breaks the 1-based invariant and can produce invalid output likeMember-0on Line 223.🔧 Proposed fix
try: position = int(raw_position) + if position < 1: + position = idx + 1 except (TypeError, ValueError): position = idx + 1Also applies to: 221-223
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 205 - 208, The parsed VC position (raw_position / entPhysicalParentRelPos) can be zero or negative; after converting to int in the block around variable position, enforce a 1-based positive value by replacing any position < 1 with a sane default (e.g., idx + 1 or 1) before it is used to construct member data (e.g., Member-{position}); apply the same check to the later parsing branch that builds members (the block that produces Member-{position} around lines referenced) so no Member-0 or negative positions are emitted.netbox_librenms_plugin/import_utils/device_operations.py (1)
771-779: 🛠️ Refactor suggestion | 🟠 MajorUse the centralized librenms-id setter instead of inline
custom_field_dataconstruction.This path still writes the
librenms_idpayload directly. That bypasses shared normalization behavior and can drift from the rest of the code.♻️ Proposed refactor
device_data = { "name": device_name, "site": site, "device_type": device_type, "role": device_role, "status": "active" if libre_device.get("status") == 1 else "offline", "comments": f"Imported from LibreNMS by netbox-librenms-plugin on {import_time}", - "custom_field_data": {"librenms_id": {api.server_key: int(device_id)}}, } @@ # Create the device device = Device(**device_data) + from ..utils import set_librenms_device_id + set_librenms_device_id(device, int(device_id), api.server_key) device.full_clean() device.save()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/device_operations.py` around lines 771 - 779, The device_data construction is directly populating custom_field_data with {"librenms_id": {api.server_key: int(device_id)}} which bypasses the centralized librenms-id normalization; replace the inline construction in device_data with the shared helper (e.g., call the existing set_librenms_id or equivalent helper) to populate device_data['custom_field_data'] (pass api.server_key and device_id to the helper or build the custom fields via the helper and assign the result to device_data['custom_field_data']), ensuring you remove the direct use of api.server_key/int conversion here and rely on the centralized setter for consistent normalization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 213-216: The code builds vc_domain by joining member_serials but
member_serial values may not be strings, causing
f"...,{','.join(member_serials)}" to throw; update the creation of
member_serials (used with vc_data, member_serials, and vc_domain) to normalize
each serial to a string (e.g., map str or use a comprehension converting
m.get("serial") to str for non-None values) before sorting and joining so join()
always receives strings and the success/failure accounting remains correct.
- Around line 24-42: The function _safe_disabled currently may return integers
other than 0/1 (e.g., "2" → 2); change the final conversion to clamp to strict
binary output by parsing int(val) into a local (e.g., int_val = int(val)) and
returning 1 only when int_val == 1, otherwise return 0, leaving the existing
boolean and string branches intact so the function always yields exactly 0 or 1.
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Line 221: The member name generation currently calls
_generate_vc_member_name(master_name, position) per member which recomputes the
naming pattern each time; instead, resolve/load the VC naming pattern once at
the start of the surrounding loop (e.g., into a local variable named pattern)
and pass that pattern into _generate_vc_member_name (e.g.,
_generate_vc_member_name(master_name, position, pattern)) when assigning
member_data["suggested_name"], so the pattern is reused across members rather
than recalculated repeatedly.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 771-779: The device_data construction is directly populating
custom_field_data with {"librenms_id": {api.server_key: int(device_id)}} which
bypasses the centralized librenms-id normalization; replace the inline
construction in device_data with the shared helper (e.g., call the existing
set_librenms_id or equivalent helper) to populate
device_data['custom_field_data'] (pass api.server_key and device_id to the
helper or build the custom fields via the helper and assign the result to
device_data['custom_field_data']), ensuring you remove the direct use of
api.server_key/int conversion here and rely on the centralized setter for
consistent normalization.
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 205-208: The parsed VC position (raw_position /
entPhysicalParentRelPos) can be zero or negative; after converting to int in the
block around variable position, enforce a 1-based positive value by replacing
any position < 1 with a sane default (e.g., idx + 1 or 1) before it is used to
construct member data (e.g., Member-{position}); apply the same check to the
later parsing branch that builds members (the block that produces
Member-{position} around lines referenced) so no Member-0 or negative positions
are emitted.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: a5fab7df-8bb7-44b7-bc29-54cb7398cf4b
📒 Files selected for processing (11)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (10)
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Follow frontend conventions for templates and static files defined in
.github/instructions/frontend.instructions.md
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer for table row updates. Table row updates must return<tr hx-swap-oob="true">. AvoidouterHTMLswaps; use OOB (Out-of-Band) or targetedinnerHTMLswaps to keep table layout intact.
Styling assumes Tabler defaults. Removingtable-responsivewrappers was deliberate to prevent dropdown clipping—do not re-add them.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/{templates,static}/**/*.{html,js}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/{templates,static}/**/*.{html,js}: All HTMX requests andfetch()calls must include a CSRF token. The standard pattern isdocument.querySelector('[name=csrfmiddlewaretoken]').value(from a hidden form input). The import JS also usesgetCookie('csrftoken')as a fallback — prefer the hidden input approach for consistency.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers. Buttons target thehtmx-modal-contentelement and JavaScript inlibrenms_import.htmltoggles the wrapper. Do not reintroducedata-bs-toggleor duplicate modal IDs.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/{templates,tables}/**/*.{html,py}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates live in
templates/netbox_librenms_plugin/; reuse/includes underinc/. Sync pages extendlibrenms_sync_base.html. Tables emit HTMX-enabled columns and buttons (tables/*.py), so prefer updating the table renderer in Python rather than templates when changing row actions.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/static/**/*import*.js
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/static/**/*import*.js: The import page usesModalManagerclass andfilterModalManagerinstance—always use this reference in fetch callbacks, not undefinedmodalInstancevariables.
The import filter form uses fetch withAccept: application/json, text/html—JSON for background jobs, HTML for synchronous mode.
Import page JavaScript (librenms_import.js) is wrapped in an IIFE withwindow.LibreNMSImportInitializedguard to prevent re-initialization during HTMX swaps.
TheModalManagerclass wraps Bootstrap 5 modal show/hide with fallback.
ThepollJobStatus()function polls/api/core/background-tasks/{jobId}/every 2s, updates progress messages, handles cancel button, and redirects on completion.
ThecaptureSelectionState()andrestoreSelectionState()functions preserve checkbox state across HTMX content swaps.
TheinitializeFilterForm()function intercepts form submit, detects JSON response (background job), and starts polling.
CSRF token extraction should usegetCookie('csrftoken')(cookie-based) as the approach for the import page JavaScript.
Files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
netbox_librenms_plugin/static/**/*.js
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/static/**/*.js: Always checkresponse.okbefore processing fetch responses to catch HTTP errors. In catch blocks, showerror.messagefor debugging rather than generic messages.
ThecreateCacheCountdown()function is a generic countdown timer for cache expiration display.
Files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
netbox_librenms_plugin/templates/**/htmx/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments live in
templates/netbox_librenms_plugin/htmx/and include:device_import_row.html(individual import row updates),device_validation_details.html(expandable validation details),device_vc_details.html(virtual chassis member details),bulk_import_confirm.html(import confirmation modal content). Keep server responses and HTMX targets in sync when modifying these fragments.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/import_utils/filters.py
netbox_librenms_plugin/views/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/**/*.py: Views must follow a three-layer structure: Base views inviews/base/, Object sync views inviews/object_sync/, and Sync action views inviews/sync/with shared mixins fromviews/mixins.py
New views must extend the closest base class and compose mixins fromviews/mixins.py(LibreNMSPermissionMixin,NetBoxObjectPermissionMixin,LibreNMSAPIMixin,CacheMixin,VlanAssignmentMixin)
All views must inheritLibreNMSPermissionMixinfromviews/mixins.pywithpermission_required = PERM_VIEW_PLUGIN
Declarerequired_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request
Use_get_safe_redirect_url(request)to validate referrer URLs in permission checks to prevent open-redirect attacks
Files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/base/cables_view.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Job cancellation flow: (1) Call/api/core/background-tasks/{uuid}/stop/to stop RQ job, (2) Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database, (3) Frontend polling detects status changes and redirects appropriately
Poll/api/core/background-tasks/{uuid}/for real-time RQ status, update modal messages based on status transitions (queued,started,finished,stopped,failed), handle all RQ status values explicitly to avoid infinite polling, and usecancelInProgressflag to prevent polling interference during cancellation
NetBox's/api/core/background-tasks/endpoint requires superuser (IsSuperuserinBaseRQViewSet); non-superuser users cannot poll job status and get 403 Forbidden. The plugin must automatically fall back to synchronous mode for non-superusers viashould_use_background_job()inlist.pyandactions.py
Custom sync endpointapi/views.py::sync_job_status()must sync database Job status with RQ job status, needed because NetBox worker doesn't always update DB when jobs stop before processing starts
Import page supports synchronous mode (callsprocess_device_filters()directly, renders results inline) and background mode (enqueuesFilterDevicesJob, returnsJsonResponsewithjob_id/job_pk/poll_url, frontend polls and redirects on completion)
Result loading via_load_job_results(job_id)must readjob.data['device_ids']and reconstruct devices from per-device cache usingget_validated_device_cache_key()
Import filter fields must include:librenms_location,librenms_type,librenms_os,librenms_hostname,librenms_sysname,librenms_hardware,enable_vc_detection,show_disabled,exclude_existing
DeviceImportHelperMixinmust provideget_validated_device_with_selections()andrender_device_row()for HTMX row rendering, shared by update views
BulkImportConfirmView(POST) must render confirmation modal with selected device...
Files:
netbox_librenms_plugin/views/imports/actions.py
**/views/base/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
**/views/base/**/*.py: Base view classes (BaseLibreNMSSyncView,BaseInterfaceTableView,BaseCableTableView,BaseIPAddressTableView,BaseVLANTableView) must implement the data pipeline pattern: fetch data from LibreNMS API, cache results withCacheMixinkeys likelibrenms_{data_type}_{model_name}_{pk}, compare against NetBox objects, and render a django-tables2 table in a partial template.
Base table view classes must implement resource-specific comparison logic: interface matching by name, IP matching by address/mask, VLAN matching by VID+group, and cables by matching remote devices and checking cable status.
VlanAssignmentMixinmust resolve VLAN group scope in order: Rack → Location → Site → SiteGroup → Region → Global, and must provide auto-selection of the most-specific VLAN group and lookup map building for interface and VLAN sync.
Files:
netbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/base/cables_view.py
🧠 Learnings (62)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Import filter fields must include: `librenms_location`, `librenms_type`, `librenms_os`, `librenms_hostname`, `librenms_sysname`, `librenms_hardware`, `enable_vc_detection`, `show_disabled`, `exclude_existing`
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,tables}/**/*.{html,py} : Templates live in `templates/netbox_librenms_plugin/`; reuse/includes under `inc/`. Sync pages extend `librenms_sync_base.html`. Tables emit HTMX-enabled columns and buttons (`tables/*.py`), so prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*.html : HTMX fragments live in `templates/netbox_librenms_plugin/htmx/` and include: `device_import_row.html` (individual import row updates), `device_validation_details.html` (expandable validation details), `device_vc_details.html` (virtual chassis member details), `bulk_import_confirm.html` (import confirmation modal content). Keep server responses and HTMX targets in sync when modifying these fragments.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,static}/**/*.{html,js} : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers. Buttons target the `htmx-modal-content` element and JavaScript in `librenms_import.html` toggles the wrapper. Do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/base/cables_view.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/settings.html : `settings.html` uses a split-form pattern: two separate Django forms (`ServerConfigForm` + `ImportSettingsForm`) sharing one page, differentiated by a hidden `form_type` field (`"server_config"` or `"import_settings"`). The test-connection button is an HTMX POST to `TestLibreNMSConnectionView`, returning an inline alert fragment.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/_*_sync{,_content}.html : Each sync resource has two templates following a naming convention: `_<resource>_sync.html` (the tab wrapper, loaded once when the tab is selected) and `_<resource>_sync_content.html` (the HTMX-swappable inner fragment, refreshed on data changes without a full page reload). Current resources: `_interface_sync`, `_cable_sync`, `_ipaddress_sync`, `_vlan_sync`. When adding a new sync resource, create both the wrapper and content templates following this pattern.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/base/cables_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Follow sync conventions defined in `.github/instructions/sync.instructions.md` for sync views, base views, tables, and sync JavaScript
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Keep REST endpoint server responses and HTMX targets in sync; implement REST endpoints in `views/imports/actions.py` and HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript CSRF token must be extracted via `document.querySelector('[name=csrfmiddlewaretoken]').value` for all POST requests.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,static}/**/*.{html,js} : All HTMX requests and `fetch()` calls must include a CSRF token. The standard pattern is `document.querySelector('[name=csrfmiddlewaretoken]').value` (from a hidden form input). The import JS also uses `getCookie('csrftoken')` as a fallback — prefer the hidden input approach for consistency.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : Import page JavaScript (`librenms_import.js`) is wrapped in an IIFE with `window.LibreNMSImportInitialized` guard to prevent re-initialization during HTMX swaps.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : The `captureSelectionState()` and `restoreSelectionState()` functions preserve checkbox state across HTMX content swaps.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer for table row updates. Table row updates must return `<tr hx-swap-oob="true">`. Avoid `outerHTML` swaps; use OOB (Out-of-Band) or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : The import page uses `ModalManager` class and `filterModalManager` instance—always use this reference in fetch callbacks, not undefined `modalInstance` variables.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*import*.html : Device import dropdowns rely on TomSelect decorators set up elsewhere. Keep `<select class="device-role-select">` markup stable to preserve JS hook-up.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : The `ModalManager` class wraps Bootstrap 5 modal show/hide with fallback.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript TomSelect dropdown initialization must use a `TOMSELECT_INIT_DELAY_MS = 100` constant and implement delayed initialization after HTMX swaps. Required initializer functions: `initializeVCMemberSelect()`, `initializeVRFSelects()`, `initializeVlanGroupSelects()`, `initializeVlanSyncGroupSelects()`.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript in `librenms_sync.js` must not be wrapped in an IIFE and must use a master initializer `initializeScripts()` that runs on both `DOMContentLoaded` and `htmx:afterSwap` events.
Applied to files:
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `DeviceValidationDetailsView` (GET) must render expandable validation details via `htmx/device_validation_details.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Removing `table-responsive` wrappers was deliberate to prevent dropdown clipping—do not re-add them.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript verification functions must include `handleInterfaceChange()`, `handleCableChange()`, `handleVRFChange()` that POST to single-item verify endpoints to validate resource changes.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/tables/**/*.py : Table classes in `tables/` must use `ToggleColumn(attrs={'input': {'name': 'select'}})` for selection, accept contextual parameters in constructors (e.g., `device`, `interface_name_field`, `vlan_groups`), set `self.tab` and `self.prefix` for multi-table pagination, include `data-*` attributes in row attrs, and VLAN columns must use `render_vlans()` with hidden inputs and JSON data.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript VLAN modal functions must implement `openVlanDetailModal()`, `verifyVlanInGroup()`, `verifyVlanSyncGroup()` for per-interface VLAN detail editing.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `DeviceImportHelperMixin` must provide `get_validated_device_with_selections()` and `render_device_row()` for HTMX row rendering, shared by update views
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Per-device dropdown update views (`DeviceRoleUpdateView`, `DeviceClusterUpdateView`, `DeviceRackUpdateView`) must apply selection to validation state and return re-rendered row via `render_device_row()`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `BulkImportConfirmView` (POST) must render confirmation modal with selected device list, returning `htmx/bulk_import_confirm.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `ImportDevicesJob` background job: imports devices and VMs. Must call `bulk_import_devices_shared()` for devices and `bulk_import_vms()` for VMs. `job.data` keys must include `imported_device_pks`, `imported_vm_pks`, `imported_libre_device_ids`, `imported_libre_vm_ids`, `server_key`, `total`, `success_count`, `failed_count`, `skipped_count`, `virtual_chassis_created`, `errors`, `completed`
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `validate_device_for_import(device, ...)` is the core validation function that produces validation state dict
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions in `utils.py` (`find_matching_site`, `match_librenms_hardware_to_device_type`, `find_matching_platform`)
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/base/cables_view.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `process_device_filters(filters, ...)` must fetch and validate devices from LibreNMS, returning a list
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_validation_helpers.py : Validation helpers must include: `fetch_model_by_id()`, `extract_device_selections()` for reading form data
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `fetch_device_with_cache(device_id, ...)` must retrieve and cache individual device data
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Result loading via `_load_job_results(job_id)` must read `job.data['device_ids']` and reconstruct devices from per-device cache using `get_validated_device_cache_key()`
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_vms(vm_imports, user, ...)` must implement VM import
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Hook into NetBox Django 5 plugin APIs via `navigation.py`, `urls.py`, and `api/` modules under `netbox_librenms_plugin/`; respect NetBox plugin conventions
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_netbox_librenms_plugin.py : `test_netbox_librenms_plugin.py` is an empty placeholder — do not add tests there.
Applied to files:
netbox_librenms_plugin/utils.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/jobs.py : Follow background job conventions defined in `.github/instructions/background-jobs.instructions.md` for `jobs.py`, import views, and import utilities
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/base/cables_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/sync/**/*.py : Sync action views must follow the pattern: check permissions with `LibreNMSPermissionMixin` and `NetBoxObjectPermissionMixin`, read selected items from `request.POST.getlist('select')`, load cached data using `CacheMixin.get_cache_key()`, apply changes inside `transaction.atomic()`, and redirect to the sync tab with `?tab=<resource>`.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `BulkImportDevicesView` (POST) must execute import: background mode enqueues `ImportDevicesJob`; sync mode calls `bulk_import_devices()` + `bulk_import_vms()` and returns OOB row swaps with `HX-Trigger: closeModal`
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module.
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Sync POST handlers must call `require_all_permissions()` (not just `require_write_permission()`) and return early if it returns a response; use `require_all_permissions_json()` for AJAX/JSON endpoints
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/views.py : API endpoints must use `LibreNMSPluginPermission` class in `api/views.py` where GET checks `view_librenmssettings` and other methods check `change_librenmssettings`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/jobs.py : Background jobs must use standalone helpers from `import_utils.py` (`check_user_permissions`, `require_permissions`) instead of view mixins; non-superusers fall back to synchronous mode
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`)
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/base/cables_view.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_devices_shared(devices, user, ...)` must be the shared implementation between sync and background import
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_validation_helpers.py : Validation state mutation helpers in `import_validation_helpers.py` must include: `apply_role_to_validation()`, `apply_cluster_to_validation()`, `apply_rack_to_validation()` to update validation state when user selects role/cluster/rack
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`.
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py,**/jobs.py : Cache key generation must use helper functions: `get_validated_device_cache_key()`, `get_cache_metadata_key()`, `get_active_cached_searches()`, `get_import_device_cache_key()`. Never hardcode cache key formats
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py,**/import_utils.py : Both synchronous and background modes must use `get_validated_device_cache_key()` from `import_utils.py` to generate cache keys, ensuring `_load_job_results()` in the list view can retrieve devices regardless of which mode produced them. Never hardcode cache key formats; always use the helper functions
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `FilterDevicesJob` background job: filters devices with VC detection. `job.data` keys must include `device_ids`, `total_processed`, `filters`, `server_key`, `vc_detection_enabled`, `cache_timeout`, `cached_at`, `completed`. Devices are cached individually via shared cache keys from `get_validated_device_cache_key()`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Import page supports synchronous mode (calls `process_device_filters()` directly, renders results inline) and background mode (enqueues `FilterDevicesJob`, returns `JsonResponse` with `job_id`/`job_pk`/`poll_url`, frontend polls and redirects on completion)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py : Use `LibreNMSAPIMixin` to provide lazy-loaded `LibreNMSAPI` instance via `self.librenms_api` property and `get_server_info()` method for template context.
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.pynetbox_librenms_plugin/views/base/cables_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Views must follow a three-layer structure: Base views in `views/base/`, Object sync views in `views/object_sync/`, and Sync action views in `views/sync/` with shared mixins from `views/mixins.py`
Applied to files:
netbox_librenms_plugin/views/base/librenms_sync_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py : Base view classes (`BaseLibreNMSSyncView`, `BaseInterfaceTableView`, `BaseCableTableView`, `BaseIPAddressTableView`, `BaseVLANTableView`) must implement the data pipeline pattern: fetch data from LibreNMS API, cache results with `CacheMixin` keys like `librenms_{data_type}_{model_name}_{pk}`, compare against NetBox objects, and render a django-tables2 table in a partial template.
Applied to files:
netbox_librenms_plugin/views/base/cables_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py|**/views/sync/**/*.py : Cache keys for sync data must follow the format: `librenms_{data_type}_{model_name}_{pk}` for fetched data and `librenms_{data_type}_last_fetched_{model_name}_{pk}` for fetch timestamps. VLAN group overrides must use `get_vlan_overrides_key(obj)`.
Applied to files:
netbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`.
Applied to files:
netbox_librenms_plugin/import_utils/filters.py
🔇 Additional comments (2)
netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js (1)
1098-1099: Good idempotent tooltip initialization.Using
bootstrap.Tooltip.getOrCreateInstance()here prevents duplicate tooltip instances during repeated HTMX swaps.netbox_librenms_plugin/views/imports/actions.py (1)
1015-1040: Transactional lock + conflict re-check is a solid hardening improvement.Good change: the check-and-write path is now atomic and guarded by
select_for_update(), which materially reduces same-row race conditions during conflict actions.
…y-rebased Code fixes (matching inventory-rebased): - bulk_import.py: clamp _safe_disabled to strict 0/1 (int_val guard) - bulk_import.py: normalize member_serials to str before join - virtual_chassis.py: guard position <= 0 in detect_virtual_chassis_from_inventory - virtual_chassis.py: load vc_name_pattern once before member loop - device_operations.py: use set_librenms_device_id helper instead of inline CF construction Tests ported from inventory-rebased: - test_vm_operations.py: 21 tests, 100% coverage for vm_operations.py - test_import_utils.py: port extra TestVCPositionHandling tests - test_import_utils.py: add 11 new VC test classes covering detect, create, edge cases Coverage: virtual_chassis.py 100%, vm_operations.py 100% 512 tests pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
287-292:⚠️ Potential issue | 🟠 MajorHandle malformed VC naming patterns without aborting creation/detection flows.
Line 288 can raise
ValueError/IndexError(not onlyKeyError) for invalid format strings (e.g., unmatched braces or positional placeholders). Right now those escape and can fail the whole VC operation.🔧 Proposed fix
- except KeyError as e: + except (KeyError, ValueError, IndexError) as e: logger.error(f"Invalid placeholder in VC naming pattern '{pattern}': {e}. Using default.") return f"{master_name}-M{position}"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 287 - 292, The try/except around pattern.format(**format_vars) only catches KeyError but malformed format strings can raise ValueError or IndexError and currently bubble up; update the exception handling in the VC naming routine to catch KeyError, ValueError and IndexError (e.g., in the block using variables pattern, formatted_suffix, master_name, format_vars, position) and on any of those exceptions log the error and return the fallback f"{master_name}-M{position}" so creation/detection continues.
♻️ Duplicate comments (1)
netbox_librenms_plugin/import_utils/bulk_import.py (1)
214-221:⚠️ Potential issue | 🟠 MajorExclude placeholder serials when deriving VC dedup domain.
"-"values are currently treated as real member serials, which can collapse unrelated stacks into the samevc_domainand skip VC creation for later devices.Proposed fix
- member_serials = sorted( - str(m.get("serial")) - for m in vc_data.get("members", []) - if m.get("serial") is not None and m.get("serial") != "" - ) + member_serials = sorted( + serial + for m in vc_data.get("members", []) + for serial in [str(m.get("serial")).strip()] + if serial not in ("", "-") + ) vc_domain = ( f"librenms-stack-{','.join(member_serials)}" if member_serials else f"librenms-{device_id}" )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 214 - 221, The VC dedup domain currently includes placeholder serials like "-" because member_serials is built from vc_data.get("members") without excluding "-" values; update the filtering logic used to build member_serials (in the same block that references member_serials, vc_data, and device_id) to also exclude falsy/placeholder serials such as "-" (e.g., skip if m.get("serial") is None, empty, or equals "-"/other known placeholders), and ensure you trim/normalize serial strings before joining so vc_domain is only derived from valid serials.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 414-431: Discovered SNMP positions (discovered_pos) can collide if
multiple members report the same value, causing repeated chosen_pos and VC
position uniqueness failures; to fix, track used positions (e.g., a set
used_positions) while iterating members and when computing chosen_pos for
_generate_vc_member_name, if discovered_pos is not None but already in
used_positions, treat it as unavailable and pick the next free sequential
position (bumping position until it is not in used_positions), then add the
assigned chosen_pos to used_positions and update position = max(position,
chosen_pos + 1) so future assignments skip already-used slots; apply the same
logic where discovered_pos is None (consume next free slot) to ensure no
duplicates.
In `@netbox_librenms_plugin/tests/test_vm_operations.py`:
- Around line 23-33: The test repeatedly creates local mocks (mock_cluster,
mock_platform, validation, mock_vm) inline in test_vm_operations.py; replace
these ad-hoc setups with the shared pytest fixtures defined in tests/conftest.py
(e.g., use the existing cluster, platform, validation, and vm fixtures) and
update the tests that reference mock_cluster, mock_platform, validation, and
mock_vm to accept and use those fixture names instead, keeping only
behavior-specific overrides inside the test bodies; apply the same change
pattern for the other occurrences mentioned (lines ~180-195, ~310-324,
~475-486).
---
Outside diff comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 287-292: The try/except around pattern.format(**format_vars) only
catches KeyError but malformed format strings can raise ValueError or IndexError
and currently bubble up; update the exception handling in the VC naming routine
to catch KeyError, ValueError and IndexError (e.g., in the block using variables
pattern, formatted_suffix, master_name, format_vars, position) and on any of
those exceptions log the error and return the fallback
f"{master_name}-M{position}" so creation/detection continues.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 214-221: The VC dedup domain currently includes placeholder
serials like "-" because member_serials is built from vc_data.get("members")
without excluding "-" values; update the filtering logic used to build
member_serials (in the same block that references member_serials, vc_data, and
device_id) to also exclude falsy/placeholder serials such as "-" (e.g., skip if
m.get("serial") is None, empty, or equals "-"/other known placeholders), and
ensure you trim/normalize serial strings before joining so vc_domain is only
derived from valid serials.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2486ec65-51f6-47dc-bb91-e4581a4a381d
📒 Files selected for processing (5)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_vm_operations.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: test-netbox (3.12)
- GitHub Check: test-netbox (3.13)
- GitHub Check: test-netbox (3.14)
🧰 Additional context used
📓 Path-based instructions (1)
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/import_utils/device_operations.py
🧠 Learnings (27)
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_devices_shared(devices, user, ...)` must be the shared implementation between sync and background import
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `ImportDevicesJob` background job: imports devices and VMs. Must call `bulk_import_devices_shared()` for devices and `bulk_import_vms()` for VMs. `job.data` keys must include `imported_device_pks`, `imported_vm_pks`, `imported_libre_device_ids`, `imported_libre_vm_ids`, `server_key`, `total`, `success_count`, `failed_count`, `skipped_count`, `virtual_chassis_created`, `errors`, `completed`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `BulkImportDevicesView` (POST) must execute import: background mode enqueues `ImportDevicesJob`; sync mode calls `bulk_import_devices()` + `bulk_import_vms()` and returns OOB row swaps with `HX-Trigger: closeModal`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `process_device_filters(filters, ...)` must fetch and validate devices from LibreNMS, returning a list
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_validation_helpers.py : Validation state mutation helpers in `import_validation_helpers.py` must include: `apply_role_to_validation()`, `apply_cluster_to_validation()`, `apply_rack_to_validation()` to update validation state when user selects role/cluster/rack
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*import*.html : Device import dropdowns rely on TomSelect decorators set up elsewhere. Keep `<select class="device-role-select">` markup stable to preserve JS hook-up.
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions in `utils.py` (`find_matching_site`, `match_librenms_hardware_to_device_type`, `find_matching_platform`)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch 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_vm_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `fetch_device_with_cache(device_id, ...)` must retrieve and cache individual device data
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`.
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py,**/jobs.py : Cache key generation must use helper functions: `get_validated_device_cache_key()`, `get_cache_metadata_key()`, `get_active_cached_searches()`, `get_import_device_cache_key()`. Never hardcode cache key formats
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py,**/import_utils.py : Both synchronous and background modes must use `get_validated_device_cache_key()` from `import_utils.py` to generate cache keys, ensuring `_load_job_results()` in the list view can retrieve devices regardless of which mode produced them. Never hardcode cache key formats; always use the helper functions
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `FilterDevicesJob` background job: filters devices with VC detection. `job.data` keys must include `device_ids`, `total_processed`, `filters`, `server_key`, `vc_detection_enabled`, `cache_timeout`, `cached_at`, `completed`. Devices are cached individually via shared cache keys from `get_validated_device_cache_key()`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Result loading via `_load_job_results(job_id)` must read `job.data['device_ids']` and reconstruct devices from per-device cache using `get_validated_device_cache_key()`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Import page supports synchronous mode (calls `process_device_filters()` directly, renders results inline) and background mode (enqueues `FilterDevicesJob`, returns `JsonResponse` with `job_id`/`job_pk`/`poll_url`, frontend polls and redirects on completion)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `validate_device_for_import(device, ...)` is the core validation function that produces validation state dict
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_netbox_librenms_plugin.py : `test_netbox_librenms_plugin.py` is an empty placeholder — do not add tests there.
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Reuse fixtures from `tests/conftest.py` instead of creating ad-hoc mocks: Configuration (`mock_multi_server_config`, `mock_legacy_config`), API client (`mock_librenms_api`), NetBox objects (`mock_netbox_device`, `mock_netbox_vm`, `mock_netbox_site`, `mock_netbox_platform`, `mock_netbox_device_type`, `mock_netbox_device_role`, `mock_netbox_cluster`, `mock_netbox_rack`), HTTP responses (`mock_response_factory`, `mock_success_response`, `mock_device_response`, `mock_error_response`, `mock_auth_error_response`), and Import workflow (`sample_librenms_device`, `sample_librenms_device_minimal`, `sample_validation_state`, `sample_validation_state_vm`).
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_vms(vm_imports, user, ...)` must implement VM import
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances.
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`.
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*.html : HTMX fragments live in `templates/netbox_librenms_plugin/htmx/` and include: `device_import_row.html` (individual import row updates), `device_validation_details.html` (expandable validation details), `device_vc_details.html` (virtual chassis member details), `bulk_import_confirm.html` (import confirmation modal content). Keep server responses and HTMX targets in sync when modifying these fragments.
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_validation_helpers.py : Validation helpers must include: `fetch_model_by_id()`, `extract_device_selections()` for reading form data
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
🔇 Additional comments (5)
netbox_librenms_plugin/import_utils/device_operations.py (1)
781-783: Good centralization oflibrenms_idwrite formatting.Using
set_librenms_device_id()here is the right direction for consistent multi-server custom-field structure across import flows.netbox_librenms_plugin/import_utils/bulk_import.py (1)
24-43: Disabled-flag normalization is much safer now.This parser handles mixed LibreNMS payload types defensively and keeps filtering logic stable.
netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
35-40: Good 1-based VC position normalization across clone/detect/update paths.Line 35, Line 206, and Line 317 now consistently enforce valid
position >= 1with sane fallbacks, which closes prior off-by-one/zero-position edge cases.Also applies to: 204-212, 315-324
200-201: Pattern preloading is a solid performance improvement.Loading the VC naming pattern once per loop and passing it into
_generate_vc_member_name()avoids repeated settings lookups in hot paths.Also applies to: 225-225, 312-313, 325-327, 394-395, 431-431
472-477: Atomic rollback handling is correctly simplified.Relying on
transaction.atomic()rollback and removing manual restoration avoids broken-transaction side effects.
| mock_cluster = MagicMock() | ||
| mock_platform = MagicMock() | ||
| validation = { | ||
| "can_import": True, | ||
| "cluster": {"cluster": mock_cluster}, | ||
| "platform": {"platform": mock_platform}, | ||
| } | ||
| mock_vm = MagicMock() | ||
| mock_vm.name = "vm01-computed" | ||
| mock_vm.pk = 10 | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Consolidate repeated mock setup into shared pytest fixtures.
This file repeatedly recreates API/user/device/validation mocks inline, which increases maintenance cost and test drift risk. Please switch these repeated setups to existing fixtures from tests/conftest.py and keep tests focused on behavior deltas only.
Based on learnings: "Reuse fixtures from tests/conftest.py instead of creating ad-hoc mocks."
Also applies to: 180-195, 310-324, 475-486
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/tests/test_vm_operations.py` around lines 23 - 33, The
test repeatedly creates local mocks (mock_cluster, mock_platform, validation,
mock_vm) inline in test_vm_operations.py; replace these ad-hoc setups with the
shared pytest fixtures defined in tests/conftest.py (e.g., use the existing
cluster, platform, validation, and vm fixtures) and update the tests that
reference mock_cluster, mock_platform, validation, and mock_vm to accept and use
those fixture names instead, keeping only behavior-specific overrides inside the
test bodies; apply the same change pattern for the other occurrences mentioned
(lines ~180-195, ~310-324, ~475-486).
- SyncInterfacesView: add LibreNMSAPIMixin to MRO (fixes AttributeError on librenms_api.server_key) - virtual_chassis: prevent VC position collision when multiple members report the same SNMP position (track used_positions set); extend _generate_vc_member_name exception handling to also catch ValueError and IndexError - librenms_sync_base.html: guard remove_server_mapping form with device-only check to prevent wrong URL resolution on VM sync pages - ip_addresses_view: normalize interfaces_by_librenms_id keys to str to prevent type mismatch on port_id lookups - vm_operations: add server_key to create_vm_from_librenms docstring
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
365-397: 🧹 Nitpick | 🔵 TrivialLoad naming pattern once for both master and members in this transaction.
Master rename (Line 365) and member creation currently load pattern in two separate calls. Preloading once ensures a consistent snapshot and saves one DB read.
♻️ Proposed refactor
with transaction.atomic(): + vc_pattern = _load_vc_member_name_pattern() # Rename master device to include position 1 pattern - master_device_new_name = _generate_vc_member_name(original_master_name, 1, serial=master_device.serial) + master_device_new_name = _generate_vc_member_name( + original_master_name, 1, serial=master_device.serial, pattern=vc_pattern + ) @@ - # Load naming pattern once to avoid a DB query per member - vc_pattern = _load_vc_member_name_pattern() + # Reuse the same naming pattern snapshot for member creation🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 365 - 397, Preload the VC member name pattern once and reuse it for both the master rename and member creation to avoid duplicate DB reads and ensure consistency: call _load_vc_member_name_pattern() before invoking _generate_vc_member_name for the master (used when computing master_device_new_name) and pass/reuse that same vc_pattern when creating subsequent members (the loop that creates members and uses _generate_vc_member_name), leaving VirtualChassis creation, master_device assignment, and member position logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 243-253: The _load_vc_member_name_pattern function currently calls
LibreNMSSettings.objects.first() (nondeterministic) — change its signature to
accept a server_key (or server identifier), look up the LibreNMSSettings via the
server-aware accessor (resolve settings.selected_server or the same
server-scoped lookup pattern used by
get_librenms_device_id/find_by_librenms_id), and return
settings.vc_member_name_pattern for that server or the default "-M{position}" if
missing; update all callers to pass the appropriate server_key so VC pattern
resolution is deterministic in multi-server mode.
In `@netbox_librenms_plugin/import_utils/vm_operations.py`:
- Line 64: Replace the inline construction of custom_field_data["librenms_id"]
in vm_operations (the line building custom_field_data={"librenms_id":
{server_key: int(libre_device["device_id"])}}) with the centralized setter: call
set_librenms_device_id(...) on the NetBox object (or use the helper that returns
the properly formed payload) so the legacy-value migration and guardrails in
set_librenms_device_id() are applied; locate where the NetBox VM/device is
created or updated in this function and invoke set_librenms_device_id with the
correct server_key and device_id before persisting instead of manually
assembling the JSON.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`:
- Around line 36-40: The template currently checks
librenms_server_info.is_legacy inside the all_server_mappings branch which can
error or produce implicit behavior when librenms_server_info is missing; update
the conditional to explicitly guard existence and intent, e.g. make the anchor
conditional require both all_server_mappings and librenms_server_info to be
truthy before checking is_legacy (use a combined guard like "all_server_mappings
and librenms_server_info and not librenms_server_info.is_legacy"), or
alternatively base visibility solely on all_server_mappings by moving the
is_legacy check to a separate, well-guarded block; target the condition around
the anchor that references librenms_server_info/is_legacy to add the explicit
existence check.
---
Outside diff comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 365-397: Preload the VC member name pattern once and reuse it for
both the master rename and member creation to avoid duplicate DB reads and
ensure consistency: call _load_vc_member_name_pattern() before invoking
_generate_vc_member_name for the master (used when computing
master_device_new_name) and pass/reuse that same vc_pattern when creating
subsequent members (the loop that creates members and uses
_generate_vc_member_name), leaving VirtualChassis creation, master_device
assignment, and member position logic unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: ce2bc77c-040c-48f9-a2a8-3d8c5719920c
📒 Files selected for processing (5)
netbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/views/sync/interfaces.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: test-netbox (3.14)
- GitHub Check: test-netbox (3.12)
🧰 Additional context used
📓 Path-based instructions (8)
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Follow frontend conventions for templates and static files defined in
.github/instructions/frontend.instructions.md
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer for table row updates. Table row updates must return<tr hx-swap-oob="true">. AvoidouterHTMLswaps; use OOB (Out-of-Band) or targetedinnerHTMLswaps to keep table layout intact.
Styling assumes Tabler defaults. Removingtable-responsivewrappers was deliberate to prevent dropdown clipping—do not re-add them.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/{templates,static}/**/*.{html,js}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/{templates,static}/**/*.{html,js}: All HTMX requests andfetch()calls must include a CSRF token. The standard pattern isdocument.querySelector('[name=csrfmiddlewaretoken]').value(from a hidden form input). The import JS also usesgetCookie('csrftoken')as a fallback — prefer the hidden input approach for consistency.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers. Buttons target thehtmx-modal-contentelement and JavaScript inlibrenms_import.htmltoggles the wrapper. Do not reintroducedata-bs-toggleor duplicate modal IDs.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/{templates,tables}/**/*.{html,py}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates live in
templates/netbox_librenms_plugin/; reuse/includes underinc/. Sync pages extendlibrenms_sync_base.html. Tables emit HTMX-enabled columns and buttons (tables/*.py), so prefer updating the table renderer in Python rather than templates when changing row actions.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/import_utils/vm_operations.py
netbox_librenms_plugin/views/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/**/*.py: Views must follow a three-layer structure: Base views inviews/base/, Object sync views inviews/object_sync/, and Sync action views inviews/sync/with shared mixins fromviews/mixins.py
New views must extend the closest base class and compose mixins fromviews/mixins.py(LibreNMSPermissionMixin,NetBoxObjectPermissionMixin,LibreNMSAPIMixin,CacheMixin,VlanAssignmentMixin)
All views must inheritLibreNMSPermissionMixinfromviews/mixins.pywithpermission_required = PERM_VIEW_PLUGIN
Declarerequired_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request
Use_get_safe_redirect_url(request)to validate referrer URLs in permission checks to prevent open-redirect attacks
Files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
netbox_librenms_plugin/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/sync/**/*.py: Sync POST handlers must callrequire_all_permissions()(not justrequire_write_permission()) and return early if it returns a response; userequire_all_permissions_json()for AJAX/JSON endpoints
Follow sync conventions defined in.github/instructions/sync.instructions.mdfor sync views, base views, tables, and sync JavaScript
Files:
netbox_librenms_plugin/views/sync/interfaces.py
**/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
Sync action views must follow the pattern: check permissions with
LibreNMSPermissionMixinandNetBoxObjectPermissionMixin, read selected items fromrequest.POST.getlist('select'), load cached data usingCacheMixin.get_cache_key(), apply changes insidetransaction.atomic(), and redirect to the sync tab with?tab=<resource>.
Files:
netbox_librenms_plugin/views/sync/interfaces.py
**/views/base/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
**/views/base/**/*.py: Base view classes (BaseLibreNMSSyncView,BaseInterfaceTableView,BaseCableTableView,BaseIPAddressTableView,BaseVLANTableView) must implement the data pipeline pattern: fetch data from LibreNMS API, cache results withCacheMixinkeys likelibrenms_{data_type}_{model_name}_{pk}, compare against NetBox objects, and render a django-tables2 table in a partial template.
Base table view classes must implement resource-specific comparison logic: interface matching by name, IP matching by address/mask, VLAN matching by VID+group, and cables by matching remote devices and checking cable status.
VlanAssignmentMixinmust resolve VLAN group scope in order: Rack → Location → Site → SiteGroup → Region → Global, and must provide auto-selection of the most-specific VLAN group and lookup map building for interface and VLAN sync.
Files:
netbox_librenms_plugin/views/base/ip_addresses_view.py
🧠 Learnings (29)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,tables}/**/*.{html,py} : Templates live in `templates/netbox_librenms_plugin/`; reuse/includes under `inc/`. Sync pages extend `librenms_sync_base.html`. Tables emit HTMX-enabled columns and buttons (`tables/*.py`), so prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*.html : HTMX fragments live in `templates/netbox_librenms_plugin/htmx/` and include: `device_import_row.html` (individual import row updates), `device_validation_details.html` (expandable validation details), `device_vc_details.html` (virtual chassis member details), `bulk_import_confirm.html` (import confirmation modal content). Keep server responses and HTMX targets in sync when modifying these fragments.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/import_utils/vm_operations.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Follow sync conventions defined in `.github/instructions/sync.instructions.md` for sync views, base views, tables, and sync JavaScript
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/import_utils/vm_operations.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,static}/**/*.{html,js} : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers. Buttons target the `htmx-modal-content` element and JavaScript in `librenms_import.html` toggles the wrapper. Do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/settings.html : `settings.html` uses a split-form pattern: two separate Django forms (`ServerConfigForm` + `ImportSettingsForm`) sharing one page, differentiated by a hidden `form_type` field (`"server_config"` or `"import_settings"`). The test-connection button is an HTMX POST to `TestLibreNMSConnectionView`, returning an inline alert fragment.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/import_utils/vm_operations.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/_*_sync{,_content}.html : Each sync resource has two templates following a naming convention: `_<resource>_sync.html` (the tab wrapper, loaded once when the tab is selected) and `_<resource>_sync_content.html` (the HTMX-swappable inner fragment, refreshed on data changes without a full page reload). Current resources: `_interface_sync`, `_cable_sync`, `_ipaddress_sync`, `_vlan_sync`. When adding a new sync resource, create both the wrapper and content templates following this pattern.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript CSRF token must be extracted via `document.querySelector('[name=csrfmiddlewaretoken]').value` for all POST requests.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,static}/**/*.{html,js} : All HTMX requests and `fetch()` calls must include a CSRF token. The standard pattern is `document.querySelector('[name=csrfmiddlewaretoken]').value` (from a hidden form input). The import JS also uses `getCookie('csrftoken')` as a fallback — prefer the hidden input approach for consistency.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Hook into NetBox Django 5 plugin APIs via `navigation.py`, `urls.py`, and `api/` modules under `netbox_librenms_plugin/`; respect NetBox plugin conventions
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/sync/**/*.py : Sync action views must follow the pattern: check permissions with `LibreNMSPermissionMixin` and `NetBoxObjectPermissionMixin`, read selected items from `request.POST.getlist('select')`, load cached data using `CacheMixin.get_cache_key()`, apply changes inside `transaction.atomic()`, and redirect to the sync tab with `?tab=<resource>`.
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`)
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Views must follow a three-layer structure: Base views in `views/base/`, Object sync views in `views/object_sync/`, and Sync action views in `views/sync/` with shared mixins from `views/mixins.py`
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/views.py : API endpoints must use `LibreNMSPluginPermission` class in `api/views.py` where GET checks `view_librenmssettings` and other methods check `change_librenmssettings`
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py : Use `LibreNMSAPIMixin` to provide lazy-loaded `LibreNMSAPI` instance via `self.librenms_api` property and `get_server_info()` method for template context.
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/object_sync/**/*.py : Object sync view methods must create instances of concrete table views, copy the `request` object, and call `get_context_data()`. VMs must skip cables and VLANs by returning `None` from those `get_*_context()` methods.
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : The import page uses `ModalManager` class and `filterModalManager` instance—always use this reference in fetch callbacks, not undefined `modalInstance` variables.
Applied to files:
netbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : Import page JavaScript (`librenms_import.js`) is wrapped in an IIFE with `window.LibreNMSImportInitialized` guard to prevent re-initialization during HTMX swaps.
Applied to files:
netbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module.
Applied to files:
netbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_netbox_librenms_plugin.py : `test_netbox_librenms_plugin.py` is an empty placeholder — do not add tests there.
Applied to files:
netbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py : Base view classes (`BaseLibreNMSSyncView`, `BaseInterfaceTableView`, `BaseCableTableView`, `BaseIPAddressTableView`, `BaseVLANTableView`) must implement the data pipeline pattern: fetch data from LibreNMS API, cache results with `CacheMixin` keys like `librenms_{data_type}_{model_name}_{pk}`, compare against NetBox objects, and render a django-tables2 table in a partial template.
Applied to files:
netbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_vms(vm_imports, user, ...)` must implement VM import
Applied to files:
netbox_librenms_plugin/import_utils/vm_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `ImportDevicesJob` background job: imports devices and VMs. Must call `bulk_import_devices_shared()` for devices and `bulk_import_vms()` for VMs. `job.data` keys must include `imported_device_pks`, `imported_vm_pks`, `imported_libre_device_ids`, `imported_libre_vm_ids`, `server_key`, `total`, `success_count`, `failed_count`, `skipped_count`, `virtual_chassis_created`, `errors`, `completed`
Applied to files:
netbox_librenms_plugin/import_utils/vm_operations.py
🔇 Additional comments (15)
netbox_librenms_plugin/views/sync/interfaces.py (3)
12-24: Good fix: mixin wiring now supports server-aware API access.Adding
LibreNMSAPIMixintoSyncInterfacesViewand imports is the right change for consistentself.librenms_apiusage in this sync action.
240-243: Good change:librenms_idis now written through the multi-server helper.Using
set_librenms_device_id()withself.librenms_api.server_key(and guarding onport_id is not None) keeps the custom-field format consistent and avoids legacy direct writes.
253-255: Good guard: MAC sync is correctly limited to device interfaces.Restricting MAC handling to
Interfaceobjects avoids invalid relation handling for VM interfaces.netbox_librenms_plugin/views/base/ip_addresses_view.py (4)
14-14: LGTM!The import of
get_librenms_device_idaligns with the PR's centralized librenms_id utilities approach and supports server-scoped interface lookups.
107-112: LGTM!The refactored loop computes
get_librenms_device_idonce per interface (addressing the previous review feedback) and correctly threadsserver_keyfor multi-server scoping. Stringifying the ID ensures type-consistent dictionary keys for downstream lookups.
195-196: LGTM!The
str(port_id)conversion ensures type-consistent dictionary lookups, matching the stringified keys built in_prefetch_netbox_data.
211-212: LGTM!Consistent use of
str(port_id)for dictionary lookups, matching the pattern in_enrich_existing_ipand the stringified keys from_prefetch_netbox_data.netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (2)
45-71: Per-mapping status/link rendering looks solid.The configured/unconfigured/active states and conditional
device_urlfallback are clean and resilient.
77-87: Good hardening on stale-mapping removal flow.Line 80 correctly escapes
server_keyin JS confirm, and the POST form includes CSRF with the expectedserver_keypayload.As per coding guidelines, “All HTMX requests and
fetch()calls must include a CSRF token”; this form follows the same CSRF protection pattern for POST actions.netbox_librenms_plugin/import_utils/vm_operations.py (2)
1-1:server_keysignature and docs update look correctThis update cleanly exposes multi-server context at VM creation time and keeps backward compatibility via the
"default"fallback.Also applies to: 16-18, 27-27
168-169:server_keypropagation through import flow is consistentPassing
api.server_keyinto bothvalidate_device_for_import()andcreate_vm_from_librenms()correctly preserves per-server scoping end-to-end.Also applies to: 213-215
netbox_librenms_plugin/import_utils/virtual_chassis.py (4)
35-40: 1-based cache position normalization looks correct.This defensively prevents invalid cached
position=0values from re-entering downstream VC logic.
179-227: Parent detection and member position handling are now robust.Using
parent_index is Noneand enforcingposition >= 1removes previous false-negatives/off-by-one behavior in VC detection and suggested-name generation.
415-455:vc_positioncollision handling is solid now.Normalizing discovered positions, tracking
used_positions, and persistingchosen_poscloses duplicate-position edge cases during member creation.
462-464: Expected-member warning and rollback behavior are correctly simplified.The filtered expected-member count is clearer, and relying on
transaction.atomic()rollback avoids broken-transaction side effects from manual restoration.Also applies to: 479-483
| def _load_vc_member_name_pattern() -> str: | ||
| """Load the VC member name pattern from settings, with fallback to default.""" | ||
| from ..models import LibreNMSSettings | ||
|
|
||
| try: | ||
| settings = LibreNMSSettings.objects.first() | ||
| return settings.vc_member_name_pattern if settings else "-M{position}" | ||
| except Exception as e: | ||
| logger.warning(f"Could not load VC member name pattern from settings: {e}. Using default.") | ||
| return "-M{position}" | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Inspect LibreNMSSettings model definition =="
fd "models.py" netbox_librenms_plugin --exec cat -n {} \; | head -200
echo
echo "== Search for LibreNMSSettings class and related constraints =="
rg -n "class LibreNMSSettings|vc_member_name_pattern|server_key|UniqueConstraint|Meta:" netbox_librenms_plugin --max-count 50Repository: marcinpsk/netbox-librenms-plugin
Length of output: 49190
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check _load_vc_member_name_pattern context and usage =="
rg -n -B5 -A10 "_load_vc_member_name_pattern" netbox_librenms_plugin
echo
echo "== Check vc_member_name_pattern field definition =="
rg -n "vc_member_name_pattern" netbox_librenms_plugin -B2 -A2
echo
echo "== Check for server-related fields in models =="
rg -n "server|Server" netbox_librenms_plugin/models.py -B1 -A1 | head -80Repository: marcinpsk/netbox-librenms-plugin
Length of output: 50388
Pass server context to _load_vc_member_name_pattern() to ensure deterministic resolution in multi-server mode.
LibreNMSSettings.objects.first() at line 248 is nondeterministic. The model is not constrained to a singleton, and in multi-server configurations, retrieving the pattern without scoping to the active server risks applying the wrong naming convention. Update the function to accept server_key and resolve the pattern from the active LibreNMSSettings.selected_server context, or use a server-aware settings accessor pattern consistent with other server-keyed lookups in the codebase (e.g., get_librenms_device_id, find_by_librenms_id).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 243 -
253, The _load_vc_member_name_pattern function currently calls
LibreNMSSettings.objects.first() (nondeterministic) — change its signature to
accept a server_key (or server identifier), look up the LibreNMSSettings via the
server-aware accessor (resolve settings.selected_server or the same
server-scoped lookup pattern used by
get_librenms_device_id/find_by_librenms_id), and return
settings.vc_member_name_pattern for that server or the default "-M{position}" if
missing; update all callers to pass the appropriate server_key so VC pattern
resolution is deterministic in multi-server mode.
… guard, IP id check, migrate conflict, MAC sync
- virtual_chassis.py: fix 'librenms-None' domain, preload vc_pattern before
master rename, add order_by('pk') to _load_vc_member_name_pattern
- vm_operations.py: use set_librenms_device_id() instead of inline CF dict
- librenms_sync_base.html: guard librenms_server_info existence before .is_legacy
- ip_addresses_view.py: use 'if lib_id is not None:' instead of 'if lib_id:'
- actions.py: add conflict check for duplicate librenms_id in migrate_librenms_id
- interfaces.py: remove is_device_interface guard from MAC handling,
add hasattr check so VMInterface is handled safely
- tests: update mocks for order_by().first() chain; update VM creation test
- Check validation resolved_name before raw hostname/sysname so newly imported devices are detected even when naming options (use_sysname/strip_domain) changed the stored name
- test_view_wiring: smoke tests for mixin/MRO wiring on all sync views - test_librenms_id: get_librenms_device_id, find_by_librenms_id, migrate_legacy, roundtrip - test_mixins: LibreNMSAPIMixin lazy init + get_server_info, CacheMixin key generation - test_sync_interfaces: update_interface_attributes (all branches), handle_mac_address - test_sync_devices: AddDeviceToLibreNMSView, UpdateDeviceLocationView, field view wiring - mock_librenms_server: reusable HTTP mock for integration tests - test_integration_sync: end-to-end API call tests via mock HTTP server
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 (1)
netbox_librenms_plugin/import_utils/vm_operations.py (1)
58-69:⚠️ Potential issue | 🟠 MajorPrevent partially-created VMs when
device_idis invalid.Line 58 writes the VM before Line 68 does
int(libre_device["device_id"]). If conversion/key access fails, the function errors after persistence, leaving a created VM while upstream reports failure.🛠️ Proposed fix
def create_vm_from_librenms( libre_device: dict, validation: dict, use_sysname: bool = True, role=None, server_key: str = "default" ): @@ - # Create the VM with librenms_id custom field + try: + parsed_device_id = int(libre_device["device_id"]) + except (KeyError, TypeError, ValueError) as exc: + raise ValueError("Invalid or missing LibreNMS device_id for VM import") from exc + + # Create the VM first, then attach validated librenms_id mapping vm = VirtualMachine.objects.create( name=vm_name, cluster=cluster, role=role, # Optional VM role platform=platform, comments=f"Imported from LibreNMS by netbox-librenms-plugin on {import_time}", ) @@ - set_librenms_device_id(vm, int(libre_device["device_id"]), server_key) + set_librenms_device_id(vm, parsed_device_id, server_key=server_key) vm.save()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/vm_operations.py` around lines 58 - 69, The VM is created before validating/converting libre_device["device_id"], which can raise and leave a persisted VM; change the flow in vm_operations.py so you validate and convert libre_device["device_id"] (and verify the key exists) before calling VirtualMachine.objects.create or, alternatively, wrap the creation and set_librenms_device_id(vm, int(...), server_key) call in a database transaction (e.g., Django transaction.atomic) so failures roll back; specifically, perform the int(libre_device["device_id"]) conversion and any key existence checks prior to invoking VirtualMachine.objects.create and only call set_librenms_device_id and vm.save after successful conversion, or wrap create + set_librenms_device_id in an atomic block to ensure no partial persistence.
♻️ Duplicate comments (1)
netbox_librenms_plugin/tests/test_vm_operations.py (1)
23-33: 🧹 Nitpick | 🔵 TrivialConsolidate repeated inline mock setup into shared fixtures.
This file still repeats local mock construction patterns across many tests, which increases maintenance overhead and drift risk.
Based on learnings: "Reuse fixtures from
tests/conftest.pyinstead of creating ad-hoc mocks."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_vm_operations.py` around lines 23 - 33, Replace the repeated ad-hoc mocks in test_vm_operations.py (mock_cluster, mock_platform, validation dict, mock_vm with name/pk) by reusing shared fixtures defined in tests/conftest.py: create or use fixtures like vm_mock, platform_mock, cluster_mock and validation_payload (or similar names) and update tests to accept those fixtures instead of constructing MagicMock instances inline; ensure the fixture returns the same structure currently expected (validation containing "can_import", "cluster": {"cluster": cluster_mock}, "platform": {"platform": platform_mock}) and that vm_mock has the name and pk attributes so existing assertions in the tests continue to work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 214-221: The vc_domain generation uses member_serials built from
str(m.get("serial")) and currently allows placeholder tokens like "-" or
whitespace to slip through; update the member_serials generator in
bulk_import.py (the code that builds member_serials and vc_domain) to cast,
strip, and filter out empty or placeholder values (e.g., serial =
str(m.get("serial") or "").strip(); exclude if not serial or serial == "-")
before sorting/joining so vc_domain only includes real serials and dedup keys
are correct.
In `@netbox_librenms_plugin/tests/mock_librenms_server.py`:
- Around line 70-72: The stop method currently only calls
self._server.shutdown() which doesn't fully release the HTTP server socket or
wait for the background thread; update stop (the method on the mock server) to
call self._server.shutdown(), then self._server.server_close() to close the
listening socket, and finally join the worker thread (e.g.,
self._thread.join(timeout=...) or appropriate thread attribute) so the server
thread is cleaned up before teardown.
In `@netbox_librenms_plugin/tests/test_librenms_id.py`:
- Around line 71-83: Update the test_queries_server_key_and_legacy_integer test
to assert that mock_model.objects.filter was called with a query that includes
both the JSON server-key lookup and the legacy integer fallback: call
find_by_librenms_id(mock_model, 42, "default") and then inspect
mock_model.objects.filter.call_args (or use assert_called_once_with) to verify
the filter argument contains a Q or kwargs expressing custom_fields__server-key
== "42" (string) and librenms_id == 42; reference the find_by_librenms_id
function name and mock_model.objects.filter when locating where to change the
assertion.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1252-1257: The conflict check currently only queries the
JSON-scoped key using server_key and misses legacy integer-stored IDs; update
the check around server_key and conflict so it also looks for legacy integer
collisions (e.g. include an OR condition checking the legacy field name
"custom_field_data__librenms_id" against the integer form of cf_locked when
cf_locked is numeric). Ensure you still exclude locked_device.pk and use
.exists() as before; convert cf_locked to int safely (or guard non-numeric
values) and combine the two filters (JSON-scoped key and legacy integer field)
using a Q() OR so duplicate ownership is detected in both storage formats.
---
Outside diff comments:
In `@netbox_librenms_plugin/import_utils/vm_operations.py`:
- Around line 58-69: The VM is created before validating/converting
libre_device["device_id"], which can raise and leave a persisted VM; change the
flow in vm_operations.py so you validate and convert libre_device["device_id"]
(and verify the key exists) before calling VirtualMachine.objects.create or,
alternatively, wrap the creation and set_librenms_device_id(vm, int(...),
server_key) call in a database transaction (e.g., Django transaction.atomic) so
failures roll back; specifically, perform the int(libre_device["device_id"])
conversion and any key existence checks prior to invoking
VirtualMachine.objects.create and only call set_librenms_device_id and vm.save
after successful conversion, or wrap create + set_librenms_device_id in an
atomic block to ensure no partial persistence.
---
Duplicate comments:
In `@netbox_librenms_plugin/tests/test_vm_operations.py`:
- Around line 23-33: Replace the repeated ad-hoc mocks in test_vm_operations.py
(mock_cluster, mock_platform, validation dict, mock_vm with name/pk) by reusing
shared fixtures defined in tests/conftest.py: create or use fixtures like
vm_mock, platform_mock, cluster_mock and validation_payload (or similar names)
and update tests to accept those fixtures instead of constructing MagicMock
instances inline; ensure the fixture returns the same structure currently
expected (validation containing "can_import", "cluster": {"cluster":
cluster_mock}, "platform": {"platform": platform_mock}) and that vm_mock has the
name and pk attributes so existing assertions in the tests continue to work.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: be88727e-1cc5-47e5-99cf-4c9f051b9d60
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (19)
.github/dependabot.yml.pre-commit-config.yamlnetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/mock_librenms_server.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_sync_interfaces.pynetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/interfaces.pypyproject.toml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: test-netbox (3.13)
- GitHub Check: test-netbox (3.14)
🧰 Additional context used
📓 Path-based instructions (9)
netbox_librenms_plugin/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/**/*.py: Hook into NetBox Django 5 plugin APIs vianavigation.py,urls.py, andapi/modules undernetbox_librenms_plugin/; respect NetBox plugin conventions
UseLibreNMSAPI.get_librenms_id()instead of directly accessing thelibrenms_idcustom field when mapping Devices/VMs to LibreNMS
Reuselibrenms_api.pyclient for all LibreNMS communication instead of making directrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and caching
Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions inutils.py(find_matching_site,match_librenms_hardware_to_device_type,find_matching_platform)
Centralize validation state mutation during import usingimport_validation_helpers.pyfor role/cluster/rack assignment, issue removal, and status recalculation
Useget_virtual_chassis_member()for port-to-member mapping andget_librenms_sync_device()for VC priority-based device selection in virtual chassis operations
Files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/mock_librenms_server.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/tests/test_sync_interfaces.py
netbox_librenms_plugin/views/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/**/*.py: Views must follow a three-layer structure: Base views inviews/base/, Object sync views inviews/object_sync/, and Sync action views inviews/sync/with shared mixins fromviews/mixins.py
New views must extend the closest base class and compose mixins fromviews/mixins.py(LibreNMSPermissionMixin,NetBoxObjectPermissionMixin,LibreNMSAPIMixin,CacheMixin,VlanAssignmentMixin)
All views must inheritLibreNMSPermissionMixinfromviews/mixins.pywithpermission_required = PERM_VIEW_PLUGIN
Declarerequired_object_permissionsas a dict mapping HTTP methods to[(action, Model)]tuples for NetBox model operations; some views may set this dynamically per-request
Use_get_safe_redirect_url(request)to validate referrer URLs in permission checks to prevent open-redirect attacks
Files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Job cancellation flow: (1) Call/api/core/background-tasks/{uuid}/stop/to stop RQ job, (2) Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database, (3) Frontend polling detects status changes and redirects appropriately
Poll/api/core/background-tasks/{uuid}/for real-time RQ status, update modal messages based on status transitions (queued,started,finished,stopped,failed), handle all RQ status values explicitly to avoid infinite polling, and usecancelInProgressflag to prevent polling interference during cancellation
NetBox's/api/core/background-tasks/endpoint requires superuser (IsSuperuserinBaseRQViewSet); non-superuser users cannot poll job status and get 403 Forbidden. The plugin must automatically fall back to synchronous mode for non-superusers viashould_use_background_job()inlist.pyandactions.py
Custom sync endpointapi/views.py::sync_job_status()must sync database Job status with RQ job status, needed because NetBox worker doesn't always update DB when jobs stop before processing starts
Import page supports synchronous mode (callsprocess_device_filters()directly, renders results inline) and background mode (enqueuesFilterDevicesJob, returnsJsonResponsewithjob_id/job_pk/poll_url, frontend polls and redirects on completion)
Result loading via_load_job_results(job_id)must readjob.data['device_ids']and reconstruct devices from per-device cache usingget_validated_device_cache_key()
Import filter fields must include:librenms_location,librenms_type,librenms_os,librenms_hostname,librenms_sysname,librenms_hardware,enable_vc_detection,show_disabled,exclude_existing
DeviceImportHelperMixinmust provideget_validated_device_with_selections()andrender_device_row()for HTMX row rendering, shared by update views
BulkImportConfirmView(POST) must render confirmation modal with selected device...
Files:
netbox_librenms_plugin/views/imports/actions.py
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Follow frontend conventions for templates and static files defined in
.github/instructions/frontend.instructions.md
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer for table row updates. Table row updates must return<tr hx-swap-oob="true">. AvoidouterHTMLswaps; use OOB (Out-of-Band) or targetedinnerHTMLswaps to keep table layout intact.
Styling assumes Tabler defaults. Removingtable-responsivewrappers was deliberate to prevent dropdown clipping—do not re-add them.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/{templates,static}/**/*.{html,js}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/{templates,static}/**/*.{html,js}: All HTMX requests andfetch()calls must include a CSRF token. The standard pattern isdocument.querySelector('[name=csrfmiddlewaretoken]').value(from a hidden form input). The import JS also usesgetCookie('csrftoken')as a fallback — prefer the hidden input approach for consistency.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers. Buttons target thehtmx-modal-contentelement and JavaScript inlibrenms_import.htmltoggles the wrapper. Do not reintroducedata-bs-toggleor duplicate modal IDs.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/{templates,tables}/**/*.{html,py}
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates live in
templates/netbox_librenms_plugin/; reuse/includes underinc/. Sync pages extendlibrenms_sync_base.html. Tables emit HTMX-enabled columns and buttons (tables/*.py), so prefer updating the table renderer in Python rather than templates when changing row actions.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
netbox_librenms_plugin/views/sync/**/*.py: Sync POST handlers must callrequire_all_permissions()(not justrequire_write_permission()) and return early if it returns a response; userequire_all_permissions_json()for AJAX/JSON endpoints
Follow sync conventions defined in.github/instructions/sync.instructions.mdfor sync views, base views, tables, and sync JavaScript
Files:
netbox_librenms_plugin/views/sync/interfaces.py
**/views/sync/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
Sync action views must follow the pattern: check permissions with
LibreNMSPermissionMixinandNetBoxObjectPermissionMixin, read selected items fromrequest.POST.getlist('select'), load cached data usingCacheMixin.get_cache_key(), apply changes insidetransaction.atomic(), and redirect to the sync tab with?tab=<resource>.
Files:
netbox_librenms_plugin/views/sync/interfaces.py
**/views/base/**/*.py
📄 CodeRabbit inference engine (.github/instructions/sync.instructions.md)
**/views/base/**/*.py: Base view classes (BaseLibreNMSSyncView,BaseInterfaceTableView,BaseCableTableView,BaseIPAddressTableView,BaseVLANTableView) must implement the data pipeline pattern: fetch data from LibreNMS API, cache results withCacheMixinkeys likelibrenms_{data_type}_{model_name}_{pk}, compare against NetBox objects, and render a django-tables2 table in a partial template.
Base table view classes must implement resource-specific comparison logic: interface matching by name, IP matching by address/mask, VLAN matching by VID+group, and cables by matching remote devices and checking cable status.
VlanAssignmentMixinmust resolve VLAN group scope in order: Rack → Location → Site → SiteGroup → Region → Global, and must provide auto-selection of the most-specific VLAN group and lookup map building for interface and VLAN sync.
Files:
netbox_librenms_plugin/views/base/ip_addresses_view.py
🧠 Learnings (62)
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py : Use `LibreNMSAPIMixin` to provide lazy-loaded `LibreNMSAPI` instance via `self.librenms_api` property and `get_server_info()` method for template context.
Applied to files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`.
Applied to files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : New views must extend the closest base class and compose mixins from `views/mixins.py` (`LibreNMSPermissionMixin`, `NetBoxObjectPermissionMixin`, `LibreNMSAPIMixin`, `CacheMixin`, `VlanAssignmentMixin`)
Applied to files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/test_netbox_librenms_plugin.py : `test_netbox_librenms_plugin.py` is an empty placeholder — do not add tests there.
Applied to files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/mock_librenms_server.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_sync_interfaces.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Reuse fixtures from `tests/conftest.py` instead of creating ad-hoc mocks: Configuration (`mock_multi_server_config`, `mock_legacy_config`), API client (`mock_librenms_api`), NetBox objects (`mock_netbox_device`, `mock_netbox_vm`, `mock_netbox_site`, `mock_netbox_platform`, `mock_netbox_device_type`, `mock_netbox_device_role`, `mock_netbox_cluster`, `mock_netbox_rack`), HTTP responses (`mock_response_factory`, `mock_success_response`, `mock_device_response`, `mock_error_response`, `mock_auth_error_response`), and Import workflow (`sample_librenms_device`, `sample_librenms_device_minimal`, `sample_validation_state`, `sample_validation_state_vm`).
Applied to files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/mock_librenms_server.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_sync_interfaces.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : All views must inherit `LibreNMSPermissionMixin` from `views/mixins.py` with `permission_required = PERM_VIEW_PLUGIN`
Applied to files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Reuse `librenms_api.py` client for all LibreNMS communication instead of making direct `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and caching
Applied to files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/tests/mock_librenms_server.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/**/*.py : Update API serializers in `api/serializers.py` and `api/views.py` together to prevent contract drift between models and external API consumers
Applied to files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/tests/test_sync_interfaces.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module.
Applied to files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/tests/test_sync_interfaces.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py : Base view classes (`BaseLibreNMSSyncView`, `BaseInterfaceTableView`, `BaseCableTableView`, `BaseIPAddressTableView`, `BaseVLANTableView`) must implement the data pipeline pattern: fetch data from LibreNMS API, cache results with `CacheMixin` keys like `librenms_{data_type}_{model_name}_{pk}`, compare against NetBox objects, and render a django-tables2 table in a partial template.
Applied to files:
netbox_librenms_plugin/tests/test_mixins.pynetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py|**/views/object_sync/**/*.py|**/views/sync/**/*.py : Cache keys for sync data must follow the format: `librenms_{data_type}_{model_name}_{pk}` for fetched data and `librenms_{data_type}_last_fetched_{model_name}_{pk}` for fetch timestamps. VLAN group overrides must use `get_vlan_overrides_key(obj)`.
Applied to files:
netbox_librenms_plugin/tests/test_mixins.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Centralize validation state mutation during import using `import_validation_helpers.py` for role/cluster/rack assignment, issue removal, and status recalculation
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*.html : HTMX fragments live in `templates/netbox_librenms_plugin/htmx/` and include: `device_import_row.html` (individual import row updates), `device_validation_details.html` (expandable validation details), `device_vc_details.html` (virtual chassis member details), `bulk_import_confirm.html` (import confirmation modal content). Keep server responses and HTMX targets in sync when modifying these fragments.
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Keep REST endpoint server responses and HTMX targets in sync; implement REST endpoints in `views/imports/actions.py` and HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/sync/**/*.py : Sync action views must follow the pattern: check permissions with `LibreNMSPermissionMixin` and `NetBoxObjectPermissionMixin`, read selected items from `request.POST.getlist('select')`, load cached data using `CacheMixin.get_cache_key()`, apply changes inside `transaction.atomic()`, and redirect to the sync tab with `?tab=<resource>`.
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Follow sync conventions defined in `.github/instructions/sync.instructions.md` for sync views, base views, tables, and sync JavaScript
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_sync_interfaces.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `BulkImportDevicesView` (POST) must execute import: background mode enqueues `ImportDevicesJob`; sync mode calls `bulk_import_devices()` + `bulk_import_vms()` and returns OOB row swaps with `HX-Trigger: closeModal`
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `LibreNMSAPI.get_librenms_id()` instead of directly accessing the `librenms_id` custom field when mapping Devices/VMs to LibreNMS
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,tables}/**/*.{html,py} : Templates live in `templates/netbox_librenms_plugin/`; reuse/includes under `inc/`. Sync pages extend `librenms_sync_base.html`. Tables emit HTMX-enabled columns and buttons (`tables/*.py`), so prefer updating the table renderer in Python rather than templates when changing row actions.
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/sync/**/*.py : Sync POST handlers must call `require_all_permissions()` (not just `require_write_permission()`) and return early if it returns a response; use `require_all_permissions_json()` for AJAX/JSON endpoints
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/tests/test_sync_devices.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Use `get_virtual_chassis_member()` for port-to-member mapping and `get_librenms_sync_device()` for VC priority-based device selection in virtual chassis operations
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/views/base/ip_addresses_view.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/_*_sync{,_content}.html : Each sync resource has two templates following a naming convention: `_<resource>_sync.html` (the tab wrapper, loaded once when the tab is selected) and `_<resource>_sync_content.html` (the HTMX-swappable inner fragment, refreshed on data changes without a full page reload). Current resources: `_interface_sync`, `_cable_sync`, `_ipaddress_sync`, `_vlan_sync`. When adding a new sync resource, create both the wrapper and content templates following this pattern.
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Hook into NetBox Django 5 plugin APIs via `navigation.py`, `urls.py`, and `api/` modules under `netbox_librenms_plugin/`; respect NetBox plugin conventions
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/api/views.py : API endpoints must use `LibreNMSPluginPermission` class in `api/views.py` where GET checks `view_librenmssettings` and other methods check `change_librenmssettings`
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/jobs.py : Follow background job conventions defined in `.github/instructions/background-jobs.instructions.md` for `jobs.py`, import views, and import utilities
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/jobs.py : Background jobs must use standalone helpers from `import_utils.py` (`check_user_permissions`, `require_permissions`) instead of view mixins; non-superusers fall back to synchronous mode
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `validate_device_for_import(device, ...)` is the core validation function that produces validation state dict
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : `DeviceImportHelperMixin` must provide `get_validated_device_with_selections()` and `render_device_row()` for HTMX row rendering, shared by update views
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_validation_helpers.py : Validation helpers must include: `fetch_model_by_id()`, `extract_device_selections()` for reading form data
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Per-device dropdown update views (`DeviceRoleUpdateView`, `DeviceClusterUpdateView`, `DeviceRackUpdateView`) must apply selection to validation state and return re-rendered row via `render_device_row()`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/**/*.py : Always use exact-only matching (not fuzzy) for site, platform, device type, and role via functions in `utils.py` (`find_matching_site`, `match_librenms_hardware_to_device_type`, `find_matching_platform`)
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,static}/**/*.{html,js} : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers. Buttons target the `htmx-modal-content` element and JavaScript in `librenms_import.html` toggles the wrapper. Do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/settings.html : `settings.html` uses a split-form pattern: two separate Django forms (`ServerConfigForm` + `ImportSettingsForm`) sharing one page, differentiated by a hidden `form_type` field (`"server_config"` or `"import_settings"`). The test-connection button is an HTMX POST to `TestLibreNMSConnectionView`, returning an inline alert fragment.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript CSRF token must be extracted via `document.querySelector('[name=csrfmiddlewaretoken]').value` for all POST requests.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/{templates,static}/**/*.{html,js} : All HTMX requests and `fetch()` calls must include a CSRF token. The standard pattern is `document.querySelector('[name=csrfmiddlewaretoken]').value` (from a hidden form input). The import JS also uses `getCookie('csrftoken')` as a fallback — prefer the hidden input approach for consistency.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:23:33.731Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-03T13:23:33.731Z
Learning: Applies to netbox_librenms_plugin/views/**/*.py : Views must follow a three-layer structure: Base views in `views/base/`, Object sync views in `views/object_sync/`, and Sync action views in `views/sync/` with shared mixins from `views/mixins.py`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript URL and tab state management must implement `initializeTabs()`, `getDeviceIdFromUrl()`, and `setInterfaceNameFieldFromURL()` to maintain browser state and URL synchronization.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/inc/paginator.html : `inc/paginator.html` is a custom paginator that preserves tab state and `interface_name_field` in pagination URLs. Used across all sync tables.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/base/**/*.py : Base table view classes must implement resource-specific comparison logic: interface matching by name, IP matching by address/mask, VLAN matching by VID+group, and cables by matching remote devices and checking cable status.
Applied to files:
netbox_librenms_plugin/tests/test_view_wiring.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: All base view classes must follow the three-layer architecture: Base views define abstract data pipelines via `get_*_context()` methods, object sync views wire base views to NetBox models using `register_model_view()`, and sync action views handle POST requests with permissions checks and transactional updates.
Applied to files:
netbox_librenms_plugin/tests/test_view_wiring.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Test business logic via the utility modules (import_utils.py, import_validation_helpers.py, etc.) they call, not via HTTP requests, for views in `views/sync/`, `views/object_sync/`, and `views/imports/actions.py`.
Applied to files:
netbox_librenms_plugin/tests/test_view_wiring.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/tests/test_sync_devices.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_vms(vm_imports, user, ...)` must implement VM import
Applied to files:
netbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `ImportDevicesJob` background job: imports devices and VMs. Must call `bulk_import_devices_shared()` for devices and `bulk_import_vms()` for VMs. `job.data` keys must include `imported_device_pks`, `imported_vm_pks`, `imported_libre_device_ids`, `imported_libre_vm_ids`, `server_key`, `total`, `success_count`, `failed_count`, `skipped_count`, `virtual_chassis_created`, `errors`, `completed`
Applied to files:
netbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances.
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/tests/test_sync_devices.pynetbox_librenms_plugin/tests/mock_librenms_server.pynetbox_librenms_plugin/tests/test_integration_sync.pynetbox_librenms_plugin/tests/test_librenms_id.pynetbox_librenms_plugin/tests/test_sync_interfaces.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`.
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:51.364Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-03-03T13:24:51.364Z
Learning: Applies to tests/**/*.py : Never use `RequestFactory`—mock request objects directly or test method logic in isolation.
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Result loading via `_load_job_results(job_id)` must read `job.data['device_ids']` and reconstruct devices from per-device cache using `get_validated_device_cache_key()`
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py,**/import_utils.py : Both synchronous and background modes must use `get_validated_device_cache_key()` from `import_utils.py` to generate cache keys, ensuring `_load_job_results()` in the list view can retrieve devices regardless of which mode produced them. Never hardcode cache key formats; always use the helper functions
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/**,**/import_utils.py : Recognize database Job status values as `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py,**/views/imports/** : Check `rq_job.is_stopped` or `rq_job.is_failed` flags in Redis for cancellation detection, not database status
Applied to files:
netbox_librenms_plugin/tests/test_vm_operations.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/views/object_sync/**/*.py : Object sync view methods must create instances of concrete table views, copy the `request` object, and call `get_context_data()`. VMs must skip cables and VLANs by returning `None` from those `get_*_context()` methods.
Applied to files:
netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : The import page uses `ModalManager` class and `filterModalManager` instance—always use this reference in fetch callbacks, not undefined `modalInstance` variables.
Applied to files:
netbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/static/**/*import*.js : Import page JavaScript (`librenms_import.js`) is wrapped in an IIFE with `window.LibreNMSImportInitialized` guard to prevent re-initialization during HTMX swaps.
Applied to files:
netbox_librenms_plugin/views/base/ip_addresses_view.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `bulk_import_devices_shared(devices, user, ...)` must be the shared implementation between sync and background import
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `process_device_filters(filters, ...)` must fetch and validate devices from LibreNMS, returning a list
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_validation_helpers.py : Validation state mutation helpers in `import_validation_helpers.py` must include: `apply_role_to_validation()`, `apply_cluster_to_validation()`, `apply_rack_to_validation()` to update validation state when user selects role/cluster/rack
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:18.134Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-03-03T13:24:18.134Z
Learning: Applies to netbox_librenms_plugin/templates/**/htmx/*import*.html : Device import dropdowns rely on TomSelect decorators set up elsewhere. Keep `<select class="device-role-select">` markup stable to preserve JS hook-up.
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py : `fetch_device_with_cache(device_id, ...)` must retrieve and cache individual device data
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/import_utils.py,**/jobs.py : Cache key generation must use helper functions: `get_validated_device_cache_key()`, `get_cache_metadata_key()`, `get_active_cached_searches()`, `get_import_device_cache_key()`. Never hardcode cache key formats
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/jobs.py : `FilterDevicesJob` background job: filters devices with VC detection. `job.data` keys must include `device_ids`, `total_processed`, `filters`, `server_key`, `vc_detection_enabled`, `cache_timeout`, `cached_at`, `completed`. Devices are cached individually via shared cache keys from `get_validated_device_cache_key()`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:00.657Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-03-03T13:24:00.657Z
Learning: Applies to **/views/imports/** : Import page supports synchronous mode (calls `process_device_filters()` directly, renders results inline) and background mode (enqueues `FilterDevicesJob`, returns `JsonResponse` with `job_id`/`job_pk`/`poll_url`, frontend polls and redirects on completion)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-03-03T13:24:38.082Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/sync.instructions.md:0-0
Timestamp: 2026-03-03T13:24:38.082Z
Learning: Applies to **/librenms_sync.js : JavaScript verification functions must include `handleInterfaceChange()`, `handleCableChange()`, `handleVRFChange()` that POST to single-item verify endpoints to validate resource changes.
Applied to files:
netbox_librenms_plugin/tests/test_sync_interfaces.py
| member_serials = sorted( | ||
| str(m.get("serial")) | ||
| for m in vc_data.get("members", []) | ||
| if m.get("serial") is not None and m.get("serial") != "" | ||
| ) | ||
| vc_domain = ( | ||
| f"librenms-stack-{','.join(member_serials)}" if member_serials else f"librenms-{device_id}" | ||
| ) |
There was a problem hiding this comment.
Exclude placeholder/empty serial tokens from VC dedup keys.
Line 214 currently includes placeholder-like values (e.g., "-" or whitespace variants after casting). That can make unrelated stacks share the same vc_domain, causing later VCs to be skipped incorrectly.
🛠️ Proposed fix
- member_serials = sorted(
- str(m.get("serial"))
- for m in vc_data.get("members", [])
- if m.get("serial") is not None and m.get("serial") != ""
- )
+ member_serials = sorted(
+ s
+ for m in vc_data.get("members", [])
+ for s in [str(m.get("serial")).strip()]
+ if s not in {"", "-"}
+ )
vc_domain = (
f"librenms-stack-{','.join(member_serials)}" if member_serials else f"librenms-{device_id}"
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 214 - 221,
The vc_domain generation uses member_serials built from str(m.get("serial")) and
currently allows placeholder tokens like "-" or whitespace to slip through;
update the member_serials generator in bulk_import.py (the code that builds
member_serials and vc_domain) to cast, strip, and filter out empty or
placeholder values (e.g., serial = str(m.get("serial") or "").strip(); exclude
if not serial or serial == "-") before sorting/joining so vc_domain only
includes real serials and dedup keys are correct.
| def stop(self): | ||
| self._server.shutdown() | ||
|
|
There was a problem hiding this comment.
Close the HTTP server socket and join the worker thread on teardown.
shutdown() alone does not fully release resources. This can leave background threads/sockets around and create intermittent test instability.
🛠️ Proposed fix
def stop(self):
- self._server.shutdown()
+ self._server.shutdown()
+ self._server.server_close()
+ if self._thread.is_alive():
+ self._thread.join(timeout=1)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/tests/mock_librenms_server.py` around lines 70 - 72,
The stop method currently only calls self._server.shutdown() which doesn't fully
release the HTTP server socket or wait for the background thread; update stop
(the method on the mock server) to call self._server.shutdown(), then
self._server.server_close() to close the listening socket, and finally join the
worker thread (e.g., self._thread.join(timeout=...) or appropriate thread
attribute) so the server thread is cleaned up before teardown.
| def test_queries_server_key_and_legacy_integer(self): | ||
| from unittest.mock import MagicMock | ||
| from netbox_librenms_plugin.utils import find_by_librenms_id | ||
|
|
||
| mock_model = MagicMock() | ||
| mock_qs = MagicMock() | ||
| mock_model.objects.filter.return_value = mock_qs | ||
| mock_qs.first.return_value = None | ||
|
|
||
| find_by_librenms_id(mock_model, 42, "default") | ||
|
|
||
| mock_model.objects.filter.assert_called_once() | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Strengthen this assertion to match the test’s intent.
Right now this only proves filter() was called, not that it includes both server-key JSON lookup and legacy integer fallback.
🔍 Proposed test hardening
find_by_librenms_id(mock_model, 42, "default")
- mock_model.objects.filter.assert_called_once()
+ mock_model.objects.filter.assert_called_once()
+ args, kwargs = mock_model.objects.filter.call_args
+ assert not kwargs
+ # Verify query shape includes both server-key JSON lookup and legacy integer fallback
+ query_repr = str(args[0])
+ assert "custom_field_data__librenms_id__default" in query_repr
+ assert "custom_field_data__librenms_id" in query_repr🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/tests/test_librenms_id.py` around lines 71 - 83,
Update the test_queries_server_key_and_legacy_integer test to assert that
mock_model.objects.filter was called with a query that includes both the JSON
server-key lookup and the legacy integer fallback: call
find_by_librenms_id(mock_model, 42, "default") and then inspect
mock_model.objects.filter.call_args (or use assert_called_once_with) to verify
the filter argument contains a Q or kwargs expressing custom_fields__server-key
== "42" (string) and librenms_id == 42; reference the find_by_librenms_id
function name and mock_model.objects.filter when locating where to change the
assertion.
| server_key = self.librenms_api.server_key | ||
| conflict = ( | ||
| Device.objects.filter(**{f"custom_field_data__librenms_id__{server_key}": cf_locked}) | ||
| .exclude(pk=locked_device.pk) | ||
| .exists() | ||
| ) |
There was a problem hiding this comment.
Legacy-ID migration conflict check misses legacy integer collisions.
Line 1252 only checks JSON-scoped mappings. If another device still stores the same ID in legacy integer format, this migration can still create duplicate ownership.
✅ Proposed fix
- conflict = (
- Device.objects.filter(**{f"custom_field_data__librenms_id__{server_key}": cf_locked})
- .exclude(pk=locked_device.pk)
- .exists()
- )
+ from django.db.models import Q
+
+ conflict = (
+ Device.objects.filter(
+ Q(**{f"custom_field_data__librenms_id__{server_key}": cf_locked})
+ | Q(custom_field_data__librenms_id=cf_locked) # legacy bare-int records
+ )
+ .exclude(pk=locked_device.pk)
+ .exists()
+ )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/views/imports/actions.py` around lines 1252 - 1257,
The conflict check currently only queries the JSON-scoped key using server_key
and misses legacy integer-stored IDs; update the check around server_key and
conflict so it also looks for legacy integer collisions (e.g. include an OR
condition checking the legacy field name "custom_field_data__librenms_id"
against the integer form of cf_locked when cf_locked is numeric). Ensure you
still exclude locked_device.pk and use .exists() as before; convert cf_locked to
int safely (or guard non-numeric values) and combine the two filters
(JSON-scoped key and legacy integer field) using a Q() OR so duplicate ownership
is detected in both storage formats.
- #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)
…resolve perf Five findings from the second /code-review sweep: - _interface_sync_content.html: gate the migrated "Move" action on has_write_permission. A read-only user otherwise got a live HTMX POST button that can only fail at the permission gate; now show muted "read-only" text. - librenms_sync_view._build_all_server_mappings: coerce the host id (and the OOB fallback id) via coerce_librenms_id, so an entry whose "id" is a corrupt non-None value (0/blank) but whose "oob.id" is valid still surfaces as an OOB-only mapping instead of being dropped by the `<= 0` guard (the user could not otherwise see/remove it). - find_by_librenms_id: fast-path a single combined query for the common 0/1-match case (runs per-port during sync); fall through to the separate host/OOB predicates only when >=2 rows match, to classify and fail closed on the precise ambiguity. Behaviour unchanged. - interfaces_view / cables_view / modules_view: reuse the sync device the request handler already resolved (passed through get_context_data / get_links_data / _build_context) instead of re-resolving it a second time per refresh. - modules_view.get_context_data: rebind to the active server from the request query so the GET cache read + OOB fingerprint key on the same server post() cached under (a non-default-server tab otherwise cache-missed → empty table). Red->green tests for the move-button gate, OOB-mapping visibility, the single- query fast-path, and the GET rebind. (#15 is a transparent device-reuse passthrough covered by the existing context tests + the full suite.) Claude-Session: https://claude.ai/code/session_01RKRVyWizgrukFSHmTF168J
…resolve perf Five findings from the second /code-review sweep: - _interface_sync_content.html: gate the migrated "Move" action on has_write_permission. A read-only user otherwise got a live HTMX POST button that can only fail at the permission gate; now show muted "read-only" text. - librenms_sync_view._build_all_server_mappings: coerce the host id (and the OOB fallback id) via coerce_librenms_id, so an entry whose "id" is a corrupt non-None value (0/blank) but whose "oob.id" is valid still surfaces as an OOB-only mapping instead of being dropped by the `<= 0` guard (the user could not otherwise see/remove it). - find_by_librenms_id: fast-path a single combined query for the common 0/1-match case (runs per-port during sync); fall through to the separate host/OOB predicates only when >=2 rows match, to classify and fail closed on the precise ambiguity. Behaviour unchanged. - interfaces_view / cables_view / modules_view: reuse the sync device the request handler already resolved (passed through get_context_data / get_links_data / _build_context) instead of re-resolving it a second time per refresh. - modules_view.get_context_data: rebind to the active server from the request query so the GET cache read + OOB fingerprint key on the same server post() cached under (a non-default-server tab otherwise cache-missed → empty table). Red->green tests for the move-button gate, OOB-mapping visibility, the single- query fast-path, and the GET rebind. (#15 is a transparent device-reuse passthrough covered by the existing context tests + the full suite.)
…resolve perf Five findings from the second /code-review sweep: - _interface_sync_content.html: gate the migrated "Move" action on has_write_permission. A read-only user otherwise got a live HTMX POST button that can only fail at the permission gate; now show muted "read-only" text. - librenms_sync_view._build_all_server_mappings: coerce the host id (and the OOB fallback id) via coerce_librenms_id, so an entry whose "id" is a corrupt non-None value (0/blank) but whose "oob.id" is valid still surfaces as an OOB-only mapping instead of being dropped by the `<= 0` guard (the user could not otherwise see/remove it). - find_by_librenms_id: fast-path a single combined query for the common 0/1-match case (runs per-port during sync); fall through to the separate host/OOB predicates only when >=2 rows match, to classify and fail closed on the precise ambiguity. Behaviour unchanged. - interfaces_view / cables_view / modules_view: reuse the sync device the request handler already resolved (passed through get_context_data / get_links_data / _build_context) instead of re-resolving it a second time per refresh. - modules_view.get_context_data: rebind to the active server from the request query so the GET cache read + OOB fingerprint key on the same server post() cached under (a non-default-server tab otherwise cache-missed → empty table). Red->green tests for the move-button gate, OOB-mapping visibility, the single- query fast-path, and the GET rebind. (#15 is a transparent device-reuse passthrough covered by the existing context tests + the full suite.)
…iew fixes
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
Delete items that don’t apply:
How Was This Tested?
Delete items that don’t 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