Conversation
📝 WalkthroughWalkthroughAdds mapping and normalization models, CRUD APIs, module inventory sync/install flows, a refactored import_utils package (split into focused modules), per-server librenms_id helpers and migration, naming-preference propagation, many view/template/form/table/JS updates, and extensive unit and e2e tests. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as NetBox UI
participant View as BaseModuleTableView
participant API as LibreNMS API
participant Cache as Django Cache
participant DB as NetBox DB
participant Installer as InstallModuleView
User->>UI: Click "Refresh Modules" or "Install Module"
UI->>View: HTMX POST /devices/{id}/module-sync or /install-module
View->>API: GET /api/v0/devices/{id}/inventory
API-->>View: inventory
View->>API: GET /api/v0/devices/{id}/transceivers
API-->>View: transceivers
View->>Cache: lookup mappings / cached VC data
Cache-->>View: mapping/cache result
View->>DB: query ModuleBay / ModuleType and mappings
DB-->>View: bay/type records
View->>Installer: validate & request install
Installer->>DB: transactional create Module(s)
DB-->>Installer: created entries
Installer->>View: success result
View->>User: HTMX swap updates module table
sequenceDiagram
participant User
participant UI as NetBox UI
participant ValidationView as DeviceValidationDetailsView
participant Validator as validate_device_for_import
participant API as LibreNMS API
participant DB as NetBox DB
participant ConflictView as DeviceConflictActionView
User->>UI: Open device validation modal
UI->>ValidationView: GET details (HTMX)
ValidationView->>Validator: validate_device_for_import(libre_device)
Validator->>API: optional VC / inventory requests
API-->>Validator: VC/inventory
Validator->>DB: lookup existing device by librenms_id/serial/hostname
DB-->>Validator: existing device info
Validator-->>ValidationView: validation payload (issues/warnings)
ValidationView->>User: render conflict modal with actions
User->>UI: choose action (link/update/sync)
UI->>ConflictView: POST conflict-action
ConflictView->>DB: apply updates (set_librenms_device_id, change fields)
DB-->>ConflictView: commit
ConflictView->>ValidationView: refresh validation fragment
ValidationView->>User: updated modal content
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 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: 26
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (12)
.devcontainer/scripts/start-netbox.sh (2)
20-23:⚠️ Potential issue | 🟡 MinorDebug
echoleaks into local dev output unconditionally.The
🐛 Debug:message on line 22 is always printed for non-Codespaces environments, which clutters normal local startup output. Consider removing it or guarding it behind aDEBUG_VERBOSEflag.🔧 Proposed fix
else ACCESS_URL="http://localhost:8000" - echo "🐛 Debug: ACCESS_URL is set to: $ACCESS_URL" fi🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.devcontainer/scripts/start-netbox.sh around lines 20 - 23, The debug echo that prints "🐛 Debug: ACCESS_URL is set to: $ACCESS_URL" runs unconditionally and clutters normal local startup; either remove that echo or guard it behind a verbosity flag (e.g., check a DEBUG_VERBOSE or VERBOSE env var) before printing, so update start-netbox.sh to only print the debug line when the flag is set (referencing ACCESS_URL and the existing echo statement) and default to silence for normal runs.
27-40: 🧹 Nitpick | 🔵 TrivialOrphaned processes are sent
SIGKILLwithout a priorSIGTERM.Both orphan cleanup blocks (
rqworkerandrunserver) jump straight topkill -9, skipping a graceful shutdown attempt. This can corrupt in-flight data (e.g. open DB transactions, pending RQ jobs). Consider sendingSIGTERMfirst and only escalating toSIGKILLafter a brief wait.♻️ Proposed approach
-pkill -9 -f "python.*rqworker" 2>/dev/null -sleep 1 +pkill -TERM -f "python.*rqworker" 2>/dev/null +sleep 2 +pkill -9 -f "python.*rqworker" 2>/dev/null || trueApply the same pattern for
runserver.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.devcontainer/scripts/start-netbox.sh around lines 27 - 40, The orphan cleanup blocks currently kill processes immediately with pkill -9; modify both the rqworker and runserver sections (ORPHAN_RQ_PIDS and ORPHAN_NETBOX_PIDS) to first send a graceful SIGTERM (e.g. pkill -15 -f "python.*rqworker" and pkill -15 -f "python.*runserver.*8000") when pids are found, sleep briefly (e.g. 1-3s), then check whether the processes remain and only then escalate to pkill -9; ensure you reuse the existing pgrep checks and messages so the script still reports “Found orphaned …, killing…” but performs the two-step TERM->KILL escalation..devcontainer/scripts/setup.sh (2)
255-266:⚠️ Potential issue | 🟡 MinorSuperuser password logged in plaintext.
Line 263 prints
Created superuser: {username}/{password}to stdout. In GitHub Codespaces or CI environments these setup logs can be captured and retained. The password confirmation can be omitted or masked.🔧 Proposed fix
- print(f'Created superuser: {username}/{password}') + print(f'Created superuser: {username} (password set)')🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.devcontainer/scripts/setup.sh around lines 255 - 266, The script prints the newly created superuser password in the python manage.py shell block (the print call that outputs Created superuser: {username}/{password}), which leaks secrets; change the output to omit or mask the password (e.g., only print the username or "password set" message) inside that python block so the create_superuser call still confirms success but never logs the plaintext password; update the print statements referenced in the inline Python executed by python manage.py shell accordingly.
251-252:⚠️ Potential issue | 🟠 MajorMigration errors silently swallowed.
The
|| trueon line 252, combined withgrep -E (...)filtering, means anymanage.py migratefailure (e.g. DB unreachable, failed migration) exits 0 without surfacing the error. This can mask a broken environment silently.Consider removing
|| trueor at minimum capturing the exit code separately so a critical migration failure still causes the setup to fail visibly.🔧 Suggested approach
-python manage.py migrate 2>&1 | grep -E "(Operations to perform|Running migrations|Apply all migrations|No migrations to apply|\s+Applying|\s+OK)" || true +python manage.py migrate 2>&1 | tee /tmp/migrate.log | grep -E "(Operations to perform|Running migrations|Apply all migrations|No migrations to apply|\s+Applying|\s+OK)" || true +if grep -qiE "(error|traceback|exception)" /tmp/migrate.log 2>/dev/null; then + echo "❌ Migration errors detected — check /tmp/migrate.log"; exit 1 +fi🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.devcontainer/scripts/setup.sh around lines 251 - 252, The migrate step currently pipes manage.py migrate through grep and appends "|| true", which masks any failure; update the setup to run "python manage.py migrate" without the trailing "|| true" and without filtering its stderr through grep so real errors surface, or alternatively capture the migrate exit code (run manage.py migrate, save $? into a variable) and if non-zero emit the full migrate output/error via echo/process logger and exit with that non-zero code; locate the migrate invocation in the setup script (the line that calls python manage.py migrate) and remove the silent-fail pattern so critical migration failures cause the setup to fail visibly..devcontainer/scripts/welcome.sh (1)
47-47:⚠️ Potential issue | 🟡 MinorTypo in user-facing output: "into you browser".
🔧 Proposed fix
- echo " NetBox will be available at: http://localhost:8000 (paste into you browser)" + echo " NetBox will be available at: http://localhost:8000 (paste into your browser)"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.devcontainer/scripts/welcome.sh at line 47, Fix the typo in the user-facing echo string: update the echo command that prints "NetBox will be available at: http://localhost:8000 (paste into you browser)" to use "your" instead of "you" so the message reads "(paste into your browser)"; locate the echo statement in .devcontainer/scripts/welcome.sh and change the string accordingly..devcontainer/scripts/diagnose.sh (1)
39-49:⚠️ Potential issue | 🟡 MinorUnquoted
$PIDinkill -0check.If the PID file is empty or malformed,
kill -0 $PIDwithout quotes expands tokill -0(no argument), emitting a usage error before2>/dev/nullsilences it. Quote the variable for correctness and clarity.🔧 Proposed fix
- if kill -0 $PID 2>/dev/null; then + if kill -0 "$PID" 2>/dev/null; then🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.devcontainer/scripts/diagnose.sh around lines 39 - 49, The PID check in the diagnose.sh snippet uses an unquoted variable (PID) in the kill command which can produce a usage error if the PID file is empty or malformed; update the kill test to use the quoted variable (use "PID" in the kill invocation) and also guard against empty content by ensuring PID is non-empty before calling kill (e.g., test -n "$PID" or treat empty as not running) so the block around PID, kill -0, and the subsequent echo messages (references: variable PID and the kill -0 check) handles empty/malformed PID files safely..github/workflows/test.yaml (1)
52-56:⚠️ Potential issue | 🟠 MajorAlign the workflow's scope with its stated purpose or update the workflow title.
The workflow is titled "Test with all supported NetBox versions," but it only checks out the
mainbranch—no matrix or conditional logic tests multiple versions. NetBox maintains active releases (v4.4.5 through v4.5.3 and beyond), so either add a version matrix strategy to test against supported releases, or update the workflow title to reflect that it tests the development branch only.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/test.yaml around lines 52 - 56, The workflow title and its checkout step are inconsistent: it currently says "Test with all supported NetBox versions" but the checkout in the job uses actions/checkout@v4 with repository: "netbox-community/netbox" and ref: main, so update the workflow to either add a matrix strategy (e.g., a job-level matrix over NetBox refs/tags for the supported releases) and use the matrix value for the checkout ref instead of the hardcoded ref: main, or change the workflow name/title to indicate it only tests the main branch; locate the job that contains uses: actions/checkout@v4 and ref: main and modify it accordingly.netbox_librenms_plugin/views/sync/cables.py (2)
90-103:⚠️ Potential issue | 🟠 Major
netbox_remote_device_idis gated as required but never consumed — valid cables are silently blocked.
verify_cable_creation_requirementsrequiresnetbox_remote_device_id(line 95), buthandle_cable_creationonly fetchesnetbox_local_interface_idandnetbox_remote_interface_id(lines 106-107).netbox_remote_device_idis never read anywhere in the class.When cached link data has both interface IDs but no
netbox_remote_device_id, the prerequisite check returnsFalse, and the cable is never created. The resulting"invalid"status also surfaces the misleading message "No LibreNMS link data found for interfaces" even though the link data was found and both interface IDs are available.🐛 Proposed fix
def verify_cable_creation_requirements(self, link_data): """Return True if all required NetBox IDs are present in link data.""" required_fields = [ "netbox_local_interface_id", - "netbox_remote_device_id", "netbox_remote_interface_id", ] return all(link_data.get(field) for field in required_fields)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/cables.py` around lines 90 - 103, The prereq check in verify_cable_creation_requirements wrongly requires "netbox_remote_device_id" which is never consumed by handle_cable_creation, causing valid links (with only interface IDs) to be rejected; remove "netbox_remote_device_id" from the required_fields list in verify_cable_creation_requirements so it only requires "netbox_local_interface_id" and "netbox_remote_interface_id", and keep handle_cable_creation as-is (or, alternatively, if remote device id is actually needed, consume it inside handle_cable_creation where local/remote interface IDs are read).
50-59:⚠️ Potential issue | 🔴 Critical
DatabaseErrorswallowed insidetransaction.atomic()will triggerTransactionManagementErroron subsequent iterations.
Cable.objects.create()can raiseIntegrityError(e.g., a duplicate-termination constraint). The broadexcept Exceptionincreate_cablecatches it inside thetransaction.atomic()block. If you catch and handle exceptions inside an atomic block, you may hide from Django the fact that a problem has happened. This can result in unexpected behavior — mostly a concern forDatabaseErrorand its subclasses such asIntegrityError. After such an error, the transaction is broken and Django will perform a rollback at the end of the atomic block. If you attempt to run database queries before the rollback happens, Django will raise aTransactionManagementError.In practice: after the
exceptswallows theIntegrityError,process_interface_synccontinues to the next iteration and opens a newwith transaction.atomic()savepoint. Any DB query in that block raisesTransactionManagementError, crashing the entireposthandler for all remaining interfaces — the opposite of the per-interface isolation the docstring promises.The fix is to catch database errors around the
atomicblock, not inside it:🐛 Proposed fix
def create_cable(self, local_interface, remote_interface, request): """Create a cable between local and remote interfaces. Returns: True on success, False on failure. """ - try: - Cable.objects.create( - a_terminations=[local_interface], - b_terminations=[remote_interface], - status="connected", - ) - return True - except Exception as exc: # pragma: no cover - protects UX - messages.error(request, f"Failed to create cable: {str(exc)}") - return False + Cable.objects.create( + a_terminations=[local_interface], + b_terminations=[remote_interface], + status="connected", + ) + return TrueMove the error handling outside the atomic scope in
process_interface_sync:for interface in selected_interfaces: - with transaction.atomic(): - result = self.process_single_interface(interface, cached_links) - results[result["status"]].append(result.get("interface", "")) + try: + with transaction.atomic(): + result = self.process_single_interface(interface, cached_links) + except Exception as exc: # pragma: no cover - protects UX + iface_key = interface.get("interface", "") + messages.error(self.request, f"Failed to create cable: {str(exc)}") + result = {"status": "invalid", "interface": iface_key} + results[result["status"]].append(result.get("interface", ""))Also applies to: 127-130
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/cables.py` around lines 50 - 59, The current create_cable/process_interface_sync flow catches all Exceptions (including IntegrityError/DatabaseError) inside a transaction.atomic block which can break the transaction and cause TransactionManagementError on subsequent DB work; refactor so that database errors are not swallowed inside the atomic scope: remove the broad try/except that surrounds Cable.objects.create() within the atomic block in create_cable (or process_interface_sync), let DB exceptions propagate so the atomic block rolls back, and instead catch DatabaseError/IntegrityError outside the transaction.atomic in the calling code (process_interface_sync) to record a user-facing message (messages.error) and continue per-interface processing; reference the functions create_cable and process_interface_sync to locate the changes and ensure only non-database exceptions (if any) are handled inside the atomic block.netbox_librenms_plugin/views/sync/interfaces.py (1)
158-185: 🛠️ Refactor suggestion | 🟠 MajorDuplicate
enabledassignment — remove fromsync_interface.
update_interface_attributes(lines 252–258) now sets and savesinterface.enabled. The identical block at lines 166–175 insync_interfacere-sets the same field afterupdate_interface_attributesreturns, producing a redundant write. Remove the block fromsync_interface.♻️ Proposed fix
self.update_interface_attributes( interface, librenms_interface, netbox_type, exclude_columns, interface_name_field, ) - if "enabled" not in exclude_columns: - interface.enabled = ( - True - if librenms_interface.get("ifAdminStatus") is None - else ( - librenms_interface["ifAdminStatus"].lower() == "up" - if isinstance(librenms_interface["ifAdminStatus"], str) - else bool(librenms_interface["ifAdminStatus"]) - ) - ) - # Sync VLANs if not excluded🤖 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 158 - 185, The block in sync_interface that re-assigns interface.enabled is redundant because update_interface_attributes already sets and saves interface.enabled; remove the entire conditional that checks "enabled" in exclude_columns and the nested assignment to interface.enabled from sync_interface, leaving the VLAN sync and save logic (vlan_synced/_sync_interface_vlans/_update_interface_vlan_assignment) intact so no extra save occurs.netbox_librenms_plugin/tests/test_import_utils.py (1)
301-309:⚠️ Potential issue | 🟠 MajorPatch
import_utilssymbols directly to ensure mocks are used.These decorators patch
virtualization.models.VirtualMachine, butimport_utilsimports and uses the symbol; the mock won’t apply unless you patchnetbox_librenms_plugin.import_utils.VirtualMachine(and similarly for Cluster/Site/Rack). This can leak real model access.✅ Suggested fix (apply similarly elsewhere)
- `@patch`("virtualization.models.VirtualMachine") + `@patch`("netbox_librenms_plugin.import_utils.VirtualMachine")Based on learnings “Patch deferred/inline imports at their source module (e.g.,
netbox_librenms_plugin.import_utils.process_device_filters), not the consuming module”.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/tests/test_import_utils.py` around lines 301 - 309, The test patches target external modules (e.g., virtualization.models.VirtualMachine) but import_utils uses its own imported symbols, so replace those decorators to patch the symbols as they are imported into import_utils (patch netbox_librenms_plugin.import_utils.VirtualMachine, .Cluster, .Rack, .Site, .Device, .DeviceRole, .find_matching_site, .find_matching_platform, .match_librenms_hardware_to_device_type) so the mocks are applied where import_utils actually references them; update any similar tests to patch the symbols on netbox_librenms_plugin.import_utils rather than their original source modules.netbox_librenms_plugin/views/__init__.py (1)
5-90:⚠️ Potential issue | 🟠 MajorFix F401 lint failures for re-export imports.
Pyflakes treats these as unused; the lint job is already failing. Add
# noqa: F401to the import statements or define__all__to mark intended re-exports.🧹 Example fix (apply to all re-export imports)
-from .base.modules_view import BulkInstallModulesView, InstallBranchView, InstallModuleView +from .base.modules_view import BulkInstallModulesView, InstallBranchView, InstallModuleView # noqa: F401🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/__init__.py` around lines 5 - 90, Pyflakes reports F401 unused-imports because this package __init__.py re-exports many view classes; fix by marking those imports as intentional exports—either add “# noqa: F401” to each re-export import line (e.g., lines importing BaseCableTableView, SingleCableVerifyView, LibreNMSImportView, DeviceCableTableView, SyncCablesView, LibreNMSSettingsView, etc.) or define an explicit __all__ tuple listing the exported symbols (include names like BaseInterfaceTableView, BaseIPAddressTableView, BulkInstallModulesView, DeviceTypeMappingListView, NormalizationRuleView, DeviceLibreNMSSyncView, VMIPAddressTableView, TestLibreNMSConnectionView, etc.) so linters recognize them as used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/test.yaml:
- Around line 42-48: The workflow currently pins GitHub Actions to version tags
"actions/checkout@v4" and "actions/setup-python@v5"; replace those tag
references with the corresponding full commit SHAs (e.g.,
actions/checkout@<full-commit-sha> and actions/setup-python@<full-commit-sha>)
to make CI reproducible and immutable, locating and changing the two "uses:"
lines in the workflow to the verified SHAs from the respective action
repositories before committing.
In `@netbox_librenms_plugin/__init__.py`:
- Around line 117-127: Hoist the duplicate import logging by moving a single
import logging to the top of the module (or at the start of the try block) and
remove the two inline imports; update the existing calls
logging.getLogger("netbox_librenms_plugin").info(...) and
logging.getLogger("netbox_librenms_plugin").exception(...) to use that single
import so there is no repeated import in the try/except branches.
In `@netbox_librenms_plugin/filters.py`:
- Around line 46-53: The filter set relies on django-filters' implicit PK filter
for the ForeignKey manufacturer, causing inconsistency with the form; explicitly
declare a ModelChoiceFilter named manufacturer on NormalizationRuleFilterSet
that points to the NormalizationRule.manufacturer field, uses the Manufacturer
queryset, and sets the filter's field_class to DynamicModelChoiceField (or
otherwise matches the DynamicModelChoiceField used in
NormalizationRuleFilterForm); also import Manufacturer and
DynamicModelChoiceField and remove reliance on implicit generation so the filter
behavior is explicit and consistent with the form.
In `@netbox_librenms_plugin/forms.py`:
- Around line 72-96: The cache is global so switching servers returns wrong
poller-groups; scope the cache key by the active LibreNMS server. Instantiate
LibreNMSAPI (LibreNMSAPI) and derive a server-specific identifier from the
client (e.g. an active_server, server_name, or base_url property on the api
instance) and incorporate it into cache_key instead of the fixed
"librenms_poller_group_choices"; use cache.get(cache_key) and
cache.set(cache_key, choices, timeout=api.cache_timeout) as before so cached
results are server-scoped and still honor api.cache_timeout and the
get_poller_groups flow.
In `@netbox_librenms_plugin/import_utils.py`:
- Around line 825-869: This code directly queries
custom_field_data__librenms_id; instead call the plugin helper
LibreNMSAPI.get_librenms_id to locate a matching Device/VM. Replace
Device.objects.filter(custom_field_data__librenms_id=int(librenms_id)).first()
with a lookup that uses LibreNMSAPI.get_librenms_id(librenms_id) (or a shared
accessor that delegates to that method) to return the mapped object, then
proceed to set result["existing_device"], result["existing_match_type"], and the
serial/name checks as before; ensure you handle non-int librenms_id exceptions
the same way and apply the same refactor for any VM lookups in this module.
- Around line 2231-2311: When cached existing_device is found deleted in
_refresh_existing_device, the code currently sets validation["can_import"]=True,
computes VM readiness without checking cluster, and replaces
validation["device_role"] losing available_roles; instead, recompute can_import
from validation.get("issues") (e.g., True only if no blocking issues), compute
is_ready using cluster for VMs (validation.get("cluster",{}).get("found")) and
the usual device_type/role/site checks for devices, and update device_role
in-place (merge or set "found" and "role" fields without dropping
validation["device_role"].get("available_roles")). Ensure these changes occur in
the branch that handles refreshed is None (the block that currently sets
existing_device=None).
In `@netbox_librenms_plugin/migrations/0013_normalizationrule.py`:
- Around line 15-22: The helper function table_exists in the migration module is
defined but never used; remove the unused function definition (table_exists)
from the migration file to eliminate dead code, or alternatively replace its
usage placeholder by actually calling table_exists wherever a migration needs to
check for an existing table (e.g., before creating/dropping tables inside
functions like forwards/backwards); adjust any imports or tests accordingly so
there are no references to table_exists after its removal or ensure calls pass
the correct table name and database connection if you choose to keep it.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/_module_sync_content.html`:
- Around line 1-31: Move the module sync template into the HTMX fragments folder
and update rendering to reference it: relocate
templates/netbox_librenms_plugin/_module_sync_content.html to
templates/netbox_librenms_plugin/htmx/_module_sync_content.html, then update the
view that returns this partial (modules_view.py POST handler that renders the
fragment) to render "netbox_librenms_plugin/htmx/_module_sync_content.html"
instead of the old path; ensure any template includes or relative paths inside
the moved file still resolve (adjust include paths in the moved template if
needed) so the HTMX swap into `#module-sync-content` continues to work.
In `@netbox_librenms_plugin/templates/netbox_librenms_plugin/_module_sync.html`:
- Around line 1-27: The reusable include currently referenced as '{% include
'netbox_librenms_plugin/_module_sync_content.html' %}' should be moved into the
inc/ subdirectory and the include reference updated accordingly; relocate the
file to templates/netbox_librenms_plugin/inc/_module_sync_content.html and
change the include in _module_sync.html to reference
'netbox_librenms_plugin/inc/_module_sync_content.html' so the template follows
the project's reusable-include convention.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 23-24: Replace Bootstrap dismissal attributes on the modal close
buttons with the HTMX-targeted close behavior: remove data-bs-dismiss (and any
data-bs-toggle or duplicate modal IDs) and make the close button target the
`#htmx-modal-content` wrapper used by the HTMX/Tabler modal JavaScript so the
wrapper is toggled by the existing librenms_import.html script; locate the close
buttons in device_validation_details.html (the <button class="btn-close">
elements) and change their attributes to target `#htmx-modal-content` instead of
relying on Bootstrap dismissal.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`:
- Around line 575-579: Move the reusable template partial _module_sync.html into
the inc/ subdirectory under templates/netbox_librenms_plugin, then update the
include in librenms_sync_base.html so the include refers to
'netbox_librenms_plugin/inc/_module_sync.html' instead of the current path;
ensure the filename and leading underscore remain the same and test the template
rendering to confirm the new include path is resolved.
- Around line 252-263: The template shows a Sync button whenever sysName is
truthy even if it's the placeholder "-"; update the conditional around the form
(and the elif block) to only display the sync form when sysName is not the
placeholder (e.g., sysName != "-" and sysName != object.name). Specifically
change the template logic that checks sysName and object.name for the form that
posts to the 'plugins:netbox_librenms_plugin:update_device_name' action so it
gates submission on a non-placeholder sysName value.
In `@netbox_librenms_plugin/tests/test_init.py`:
- Around line 43-63: The test only verifies logging.getLogger was called but not
that an info-level message was emitted; update the assertion to check the logger
instance's info method was called like in test_no_log_when_field_already_exists:
use mock_get_logger.return_value.info.assert_called_once() (or
assert_called_once_with(<expected message>) if you want to validate the exact
message) after invoking _ensure_librenms_id_custom_field so the test confirms an
info log was emitted by the logger instance.
In `@netbox_librenms_plugin/urls.py`:
- Around line 31-34: Remove the unused import BulkInstallModulesView from the
import list (where BulkInstallModulesView, DeviceVLANTableView,
InstallBranchView, InstallModuleView are imported) because it is not referenced
in urlpatterns; update the import line to exclude BulkInstallModulesView so the
linter error F401 is resolved.
In `@netbox_librenms_plugin/utils.py`:
- Around line 544-559: Normalization can crash when a NormalizationRule has an
invalid regex because re.sub(rule.match_pattern, rule.replacement, value) raises
re.error; wrap every re.sub call in a try/except that catches re.error, logs the
failure (include rule.pk or rule.id and rule.match_pattern) and continues to the
next rule so a single bad rule doesn't stop processing; apply this change to
both the manufacturer-scoped loop (the loops over mfg_filter and rules) and the
unscoped branch, using the module logger (or logging.getLogger(__name__)) to
emit logger.error messages and leaving value unchanged when a rule is skipped.
- Around line 217-225: The current
DeviceTypeMapping.objects.get(librenms_hardware__iexact=hardware_name) call only
catches DoesNotExist but not DeviceTypeMapping.MultipleObjectsReturned; add an
except DeviceTypeMapping.MultipleObjectsReturned block (as used for the
part_number/model lookups) immediately after the DoesNotExist handler to
defensively handle multiple case-insensitive matches and treat it as no match
(i.e., fall through or return the same "no match" behavior as the other
lookups), optionally logging a debug message for visibility.
In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 58-61: The loop building local_ports_map can raise a KeyError when
port lacks the resolved interface_name_field; update the loop that iterates over
ports_data.get("ports", []) to safely access the name (use
port.get(interface_name_field) or check "if interface_name_field in port") and
skip or fallback when missing, e.g., continue and optionally log a warning so
local_ports_map only receives valid entries; make sure references to port_id
(str(port["port_id"])) remain guarded if port_id might also be missing.
In `@netbox_librenms_plugin/views/base/modules_view.py`:
- Around line 509-519: The code is comparing match.group(1) (a string) to
parent_bay.position (an int), which always fails; update the block that computes
expected_fpc to normalize it to an int before comparing: call int on
match.group(1) (e.g., expected_fpc = int(match.group(1))) and then compare
parent_bay.position == expected_fpc, catching ValueError and returning True
(preserving existing fallback behavior) so invalid numeric parses don't break
the check; adjust references in the function around candidate_name,
expected_fpc, bay.module, module.module_bay and parent_bay.position accordingly.
- Around line 785-793: The code reads parent_index from request.POST and casts
it with int(parent_index) which can raise ValueError for non-numeric input; wrap
the conversion in a try/except ValueError (or validate with str.isdigit()) and
on failure call messages.error(request, "Missing parent inventory index.") (or a
clearer message like "Invalid parent inventory index.") then build sync_url the
same way (reverse("plugins:netbox_librenms_plugin:device_librenms_sync",
kwargs={"pk": pk})) and return
redirect(f"{sync_url}?tab=modules#librenms-module-table"); update the logic
around the parent_index variable in the view (the block that sets parent_index
and then does int(parent_index)) to perform this validation before using the
integer value.
- Around line 1045-1050: The regex matching loop currently returns a bay from
BaseModuleTableView._lookup_regex_bay_mapping without applying the FPC
parent-slot validation used elsewhere, causing modules to be installed under the
wrong parent; modify the logic so that before returning a bay you apply the same
FPC parent-slot check used by BaseModuleTableView._fpc_slot_matches (either by
calling _fpc_slot_matches with the same parameters or adding that check inside
_lookup_regex_bay_mapping) so only matches that pass the FPC parent-slot
validation for the given candidate_names, phys_class and module_bays are
returned.
- Around line 81-85: The code currently assumes cache.ttl() exists (e.g., around
cache.set(...) in methods like get_cache_key/inventory caching in
modules_view.py and similar in interfaces_view.py, vlan_table_view.py,
ip_addresses_view.py, cables_view.py, devices.py), which breaks on backends that
don't implement ttl; change callers to first check getattr(cache, "ttl", None)
and if absent compute expiry from a stored cached-at timestamp (save a parallel
"<key>:cached_at" when setting cache) and use that fallback to determine
remaining TTL before rendering/sync; update all usages where cache.ttl() is
invoked to use this guarded lookup and fallback logic so non-Redis backends
don't raise AttributeError.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 752-763: The code currently leaves device_type_synced = True when
librenms_hardware exists but match_librenms_hardware_to_device_type() returns no
mapping; change the logic so that when librenms_hardware is present (and not
"-") and hw_match.get("matched") is falsy, you set device_type_synced = False
and keep librenms_device_type as None; ensure this behavior is implemented
around the match_librenms_hardware_to_device_type call and referenced variables
(device_type_synced, librenms_device_type, librenms_hardware, hw_match,
existing_device.device_type) so an unknown mapping is treated as not synced
rather than a false positive.
In `@tests/e2e/test_module_install.py`:
- Around line 48-69: The helper _netbox_shell should enforce a subprocess
timeout and surface non-zero exits: when calling subprocess.run in _netbox_shell
add a timeout parameter (e.g., a few seconds) and catch
subprocess.TimeoutExpired to raise/return a clear error; after the call, check
result.returncode and if non-zero raise an exception that includes result.stderr
and result.stdout so failures aren’t silently treated as empty output; ensure
the exception message references the container and original command for easier
debugging.
- Around line 129-139: Replace all fixed time.sleep calls in this test file with
Playwright waits: in _goto_modules_tab, remove time.sleep(2) and time.sleep(8)
and instead wait for the page and the button reliably (use
page.wait_for_load_state("networkidle") or
page.wait_for_selector('button:has-text("Refresh Modules")') before clicking,
then wait for the network response or for a DOM change that indicates refresh
completion using page.wait_for_response(...) or page.wait_for_selector(...) for
the updated modules element); apply the same pattern to the other 11 occurrences
in this file—replace each time.sleep(...) with a targeted wait_for_selector,
wait_for_response, or wait_for_load_state tied to the UI element or API call
that indicates the operation finished so the tests become deterministic (search
for all instances of time.sleep in the file and update the surrounding logic).
- Around line 109-121: The page fixture logs in but lacks a post-login
auto-retrying assertion; after pg.click("button[type=submit]") and
pg.wait_for_load_state("networkidle") add Playwright expect-based checks to
verify login success (use expect(pg).to_have_url(...) with a stable path or
regex to avoid transient query params and also expect(pg.locator("text=Log out")
or a site-specific logout/user-menu selector). Update the page fixture to import
and use Playwright's expect and perform both the URL expectation and a visible
UI signal check so the fixture reliably fails on bad credentials; reference the
fixture name page and the pg variable when making these changes.
- Around line 31-45: The _get_container function's subprocess.run call should be
hardened by adding a short timeout and explicit error handling: invoke
subprocess.run with a timeout (e.g., timeout=5) and either check=True or inspect
result.returncode, catch subprocess.TimeoutExpired and
subprocess.CalledProcessError, and surface a clear failure (use pytest.skip or
pytest.fail with the exception/output details) instead of silently proceeding;
update references to CONTAINER_NAME and the docker ps invocation so the error
path logs or fails with the command stderr/stdout to aid diagnosis.
---
Outside diff comments:
In @.devcontainer/scripts/diagnose.sh:
- Around line 39-49: The PID check in the diagnose.sh snippet uses an unquoted
variable (PID) in the kill command which can produce a usage error if the PID
file is empty or malformed; update the kill test to use the quoted variable (use
"PID" in the kill invocation) and also guard against empty content by ensuring
PID is non-empty before calling kill (e.g., test -n "$PID" or treat empty as not
running) so the block around PID, kill -0, and the subsequent echo messages
(references: variable PID and the kill -0 check) handles empty/malformed PID
files safely.
In @.devcontainer/scripts/setup.sh:
- Around line 255-266: The script prints the newly created superuser password in
the python manage.py shell block (the print call that outputs Created superuser:
{username}/{password}), which leaks secrets; change the output to omit or mask
the password (e.g., only print the username or "password set" message) inside
that python block so the create_superuser call still confirms success but never
logs the plaintext password; update the print statements referenced in the
inline Python executed by python manage.py shell accordingly.
- Around line 251-252: The migrate step currently pipes manage.py migrate
through grep and appends "|| true", which masks any failure; update the setup to
run "python manage.py migrate" without the trailing "|| true" and without
filtering its stderr through grep so real errors surface, or alternatively
capture the migrate exit code (run manage.py migrate, save $? into a variable)
and if non-zero emit the full migrate output/error via echo/process logger and
exit with that non-zero code; locate the migrate invocation in the setup script
(the line that calls python manage.py migrate) and remove the silent-fail
pattern so critical migration failures cause the setup to fail visibly.
In @.devcontainer/scripts/start-netbox.sh:
- Around line 20-23: The debug echo that prints "🐛 Debug: ACCESS_URL is set to:
$ACCESS_URL" runs unconditionally and clutters normal local startup; either
remove that echo or guard it behind a verbosity flag (e.g., check a
DEBUG_VERBOSE or VERBOSE env var) before printing, so update start-netbox.sh to
only print the debug line when the flag is set (referencing ACCESS_URL and the
existing echo statement) and default to silence for normal runs.
- Around line 27-40: The orphan cleanup blocks currently kill processes
immediately with pkill -9; modify both the rqworker and runserver sections
(ORPHAN_RQ_PIDS and ORPHAN_NETBOX_PIDS) to first send a graceful SIGTERM (e.g.
pkill -15 -f "python.*rqworker" and pkill -15 -f "python.*runserver.*8000") when
pids are found, sleep briefly (e.g. 1-3s), then check whether the processes
remain and only then escalate to pkill -9; ensure you reuse the existing pgrep
checks and messages so the script still reports “Found orphaned …, killing…” but
performs the two-step TERM->KILL escalation.
In @.devcontainer/scripts/welcome.sh:
- Line 47: Fix the typo in the user-facing echo string: update the echo command
that prints "NetBox will be available at: http://localhost:8000 (paste into you
browser)" to use "your" instead of "you" so the message reads "(paste into your
browser)"; locate the echo statement in .devcontainer/scripts/welcome.sh and
change the string accordingly.
In @.github/workflows/test.yaml:
- Around line 52-56: The workflow title and its checkout step are inconsistent:
it currently says "Test with all supported NetBox versions" but the checkout in
the job uses actions/checkout@v4 with repository: "netbox-community/netbox" and
ref: main, so update the workflow to either add a matrix strategy (e.g., a
job-level matrix over NetBox refs/tags for the supported releases) and use the
matrix value for the checkout ref instead of the hardcoded ref: main, or change
the workflow name/title to indicate it only tests the main branch; locate the
job that contains uses: actions/checkout@v4 and ref: main and modify it
accordingly.
In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 301-309: The test patches target external modules (e.g.,
virtualization.models.VirtualMachine) but import_utils uses its own imported
symbols, so replace those decorators to patch the symbols as they are imported
into import_utils (patch netbox_librenms_plugin.import_utils.VirtualMachine,
.Cluster, .Rack, .Site, .Device, .DeviceRole, .find_matching_site,
.find_matching_platform, .match_librenms_hardware_to_device_type) so the mocks
are applied where import_utils actually references them; update any similar
tests to patch the symbols on netbox_librenms_plugin.import_utils rather than
their original source modules.
In `@netbox_librenms_plugin/views/__init__.py`:
- Around line 5-90: Pyflakes reports F401 unused-imports because this package
__init__.py re-exports many view classes; fix by marking those imports as
intentional exports—either add “# noqa: F401” to each re-export import line
(e.g., lines importing BaseCableTableView, SingleCableVerifyView,
LibreNMSImportView, DeviceCableTableView, SyncCablesView, LibreNMSSettingsView,
etc.) or define an explicit __all__ tuple listing the exported symbols (include
names like BaseInterfaceTableView, BaseIPAddressTableView,
BulkInstallModulesView, DeviceTypeMappingListView, NormalizationRuleView,
DeviceLibreNMSSyncView, VMIPAddressTableView, TestLibreNMSConnectionView, etc.)
so linters recognize them as used.
In `@netbox_librenms_plugin/views/sync/cables.py`:
- Around line 90-103: The prereq check in verify_cable_creation_requirements
wrongly requires "netbox_remote_device_id" which is never consumed by
handle_cable_creation, causing valid links (with only interface IDs) to be
rejected; remove "netbox_remote_device_id" from the required_fields list in
verify_cable_creation_requirements so it only requires
"netbox_local_interface_id" and "netbox_remote_interface_id", and keep
handle_cable_creation as-is (or, alternatively, if remote device id is actually
needed, consume it inside handle_cable_creation where local/remote interface IDs
are read).
- Around line 50-59: The current create_cable/process_interface_sync flow
catches all Exceptions (including IntegrityError/DatabaseError) inside a
transaction.atomic block which can break the transaction and cause
TransactionManagementError on subsequent DB work; refactor so that database
errors are not swallowed inside the atomic scope: remove the broad try/except
that surrounds Cable.objects.create() within the atomic block in create_cable
(or process_interface_sync), let DB exceptions propagate so the atomic block
rolls back, and instead catch DatabaseError/IntegrityError outside the
transaction.atomic in the calling code (process_interface_sync) to record a
user-facing message (messages.error) and continue per-interface processing;
reference the functions create_cable and process_interface_sync to locate the
changes and ensure only non-database exceptions (if any) are handled inside the
atomic block.
In `@netbox_librenms_plugin/views/sync/interfaces.py`:
- Around line 158-185: The block in sync_interface that re-assigns
interface.enabled is redundant because update_interface_attributes already sets
and saves interface.enabled; remove the entire conditional that checks "enabled"
in exclude_columns and the nested assignment to interface.enabled from
sync_interface, leaving the VLAN sync and save logic
(vlan_synced/_sync_interface_vlans/_update_interface_vlan_assignment) intact so
no extra save occurs.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (74)
.devcontainer/README.md.devcontainer/scripts/diagnose.sh.devcontainer/scripts/load-aliases.sh.devcontainer/scripts/setup.sh.devcontainer/scripts/start-netbox.sh.devcontainer/scripts/welcome.sh.github/workflows/lint-format.yaml.github/workflows/test.yamlcontrib/README.mdcontrib/device_type_mappings.yamlcontrib/interface_name_rules.yamlcontrib/interface_type_mappings.yamlcontrib/module_bay_mappings.yamlcontrib/module_type_mappings.yamlcontrib/normalization_rules.yamldocs/usage_tips/custom_field.mddocs/usage_tips/permissions.mdnetbox_librenms_plugin/__init__.pynetbox_librenms_plugin/api/serializers.pynetbox_librenms_plugin/api/urls.pynetbox_librenms_plugin/api/views.pynetbox_librenms_plugin/filters.pynetbox_librenms_plugin/forms.pynetbox_librenms_plugin/import_utils.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/migrations/0009_add_devicetypemapping.pynetbox_librenms_plugin/migrations/0010_add_moduletypemapping.pynetbox_librenms_plugin/migrations/0011_modulebaymapping.pynetbox_librenms_plugin/migrations/0012_add_is_regex_to_modulebaymapping.pynetbox_librenms_plugin/migrations/0013_normalizationrule.pynetbox_librenms_plugin/models.pynetbox_librenms_plugin/navigation.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.jsnetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/mappings.pynetbox_librenms_plugin/tables/modules.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/_interface_sync_content.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/_module_sync.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/_module_sync_content.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/devicetypemapping.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/devicetypemapping_list.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/modulebaymapping.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/modulebaymapping_list.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/moduletypemapping.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/moduletypemapping_list.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/normalizationrule.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/normalizationrule_list.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_init.pynetbox_librenms_plugin/tests/test_librenms_api.pynetbox_librenms_plugin/tests/test_utils.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/librenms_sync_view.pynetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/views/imports/__init__.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/mapping_views.pynetbox_librenms_plugin/views/object_sync/__init__.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/sync/cables.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/sync/devices.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/sync/locations.pytests/e2e/__init__.pytests/e2e/conftest.pytests/e2e/test_module_install.py
💤 Files with no reviewable changes (2)
- netbox_librenms_plugin/templates/netbox_librenms_plugin/_interface_sync_content.html
- netbox_librenms_plugin/views/sync/locations.py
| class NormalizationRuleFilterSet(django_filters.FilterSet): | ||
| """Filter set for NormalizationRule model.""" | ||
|
|
||
| class Meta: | ||
| """Meta options for NormalizationRuleFilterSet.""" | ||
|
|
||
| model = NormalizationRule | ||
| fields = ["scope", "manufacturer"] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.py" | xargs grep -l "class NormalizationRule" | head -5Repository: marcinpsk/netbox-librenms-plugin
Length of output: 262
🏁 Script executed:
# Look for the NormalizationRule model definition
rg -n "class NormalizationRule" --type=py -A 15Repository: marcinpsk/netbox-librenms-plugin
Length of output: 13694
🏁 Script executed:
# Check the filters.py file to see the current setup
rg -n "NormalizationRuleFilterSet" --type=py -A 20Repository: marcinpsk/netbox-librenms-plugin
Length of output: 4154
🏁 Script executed:
cat -n netbox_librenms_plugin/models.py | sed -n '205,270p'Repository: marcinpsk/netbox-librenms-plugin
Length of output: 2823
manufacturer filter on NormalizationRuleFilterSet — clarify or add explicit filter configuration.
The manufacturer field is a ForeignKey (line 235 in models.py), so django-filters auto-generates a PK-based filter. This works but creates inconsistency: NormalizationRuleFilterForm uses manufacturer_id with DynamicModelChoiceField, suggesting the filter expects PK. Add an explicit ModelChoiceFilter to the FilterSet to make the expected behavior clear and consistent with the form.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/filters.py` around lines 46 - 53, The filter set
relies on django-filters' implicit PK filter for the ForeignKey manufacturer,
causing inconsistency with the form; explicitly declare a ModelChoiceFilter
named manufacturer on NormalizationRuleFilterSet that points to the
NormalizationRule.manufacturer field, uses the Manufacturer queryset, and sets
the filter's field_class to DynamicModelChoiceField (or otherwise matches the
DynamicModelChoiceField used in NormalizationRuleFilterForm); also import
Manufacturer and DynamicModelChoiceField and remove reliance on implicit
generation so the filter behavior is explicit and consistent with the form.
| if not result["existing_device"]: | ||
| try: | ||
| existing_device = Device.objects.filter(custom_field_data__librenms_id=int(librenms_id)).first() | ||
| except (ValueError, TypeError): | ||
| # librenms_id is not convertible to int; no match will be found | ||
| existing_device = None | ||
|
|
||
| if existing_device: | ||
| logger.info(f"Found existing device: {existing_device.name} (matched by librenms_id={librenms_id})") | ||
| result["existing_device"] = existing_device | ||
| result["existing_match_type"] = "librenms_id" | ||
| result["can_import"] = False | ||
|
|
||
| # Check if name matches sysName | ||
| sys_name = libre_device.get("sysName") or "" | ||
| if sys_name and existing_device.name == sys_name: | ||
| result["name_matches"] = True | ||
| elif sys_name and existing_device.name != sys_name: | ||
| result["name_sync_available"] = True | ||
| result["suggested_name"] = sys_name | ||
|
|
||
| # Check for serial drift on the linked device | ||
| incoming_serial = libre_device.get("serial") or "" | ||
| if incoming_serial and incoming_serial != "-": | ||
| if existing_device.serial and existing_device.serial == incoming_serial: | ||
| result["serial_confirmed"] = True | ||
| elif existing_device.serial and existing_device.serial != incoming_serial: | ||
| serial_conflict = ( | ||
| Device.objects.filter(serial=incoming_serial).exclude(pk=existing_device.pk).first() | ||
| ) | ||
| if serial_conflict: | ||
| result["serial_action"] = "conflict" | ||
| result["serial_duplicate"] = True | ||
| result["warnings"].append( | ||
| f"Serial conflict: incoming serial '{incoming_serial}' is already assigned to " | ||
| f"device '{serial_conflict.name}' (ID: {serial_conflict.pk}) in NetBox. " | ||
| f"Investigate which device should own this serial before updating." | ||
| ) | ||
| else: | ||
| result["serial_action"] = "update_serial" | ||
| result["warnings"].append( | ||
| f"Serial number differs (NetBox: '{existing_device.serial}', " | ||
| f"LibreNMS: '{incoming_serial}'). Hardware may have been replaced." | ||
| ) | ||
|
|
There was a problem hiding this comment.
Avoid direct custom_field_data__librenms_id lookups; use the helper.
These new lookups bypass the plugin’s LibreNMSAPI.get_librenms_id helper, which is the standard way to access the mapping and handle config nuances. Please refactor these checks to go through the helper (or a shared accessor that delegates to it) for both Devices and VMs.
As per coding guidelines, "Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/import_utils.py` around lines 825 - 869, This code
directly queries custom_field_data__librenms_id; instead call the plugin helper
LibreNMSAPI.get_librenms_id to locate a matching Device/VM. Replace
Device.objects.filter(custom_field_data__librenms_id=int(librenms_id)).first()
with a lookup that uses LibreNMSAPI.get_librenms_id(librenms_id) (or a shared
accessor that delegates to that method) to return the mapped object, then
proceed to set result["existing_device"], result["existing_match_type"], and the
serial/name checks as before; ensure you handle non-int librenms_id exceptions
the same way and apply the same refactor for any VM lookups in this module.
| def _get_container(): | ||
| """Find the devcontainer name.""" | ||
| global CONTAINER_NAME | ||
| if CONTAINER_NAME: | ||
| return CONTAINER_NAME | ||
| result = subprocess.run( | ||
| ["docker", "ps", "--format", "{{.Names}}"], | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
| for name in result.stdout.strip().split("\n"): | ||
| if "devcontainer-devcontainer" in name: | ||
| CONTAINER_NAME = name | ||
| return name | ||
| pytest.skip("No devcontainer found") |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🌐 Web query:
Python subprocess.run timeout best practices error handling
💡 Result:
Best practices for subprocess.run(..., timeout=...) + error handling
- Prefer
subprocess.run()overPopenwhen possible (simpler, safer defaults for common cases). It returns aCompletedProcesswithargs,returncode,stdout,stderr. [1] - Always set a
timeoutfor potentially-hanging commands. The timeout is passed toPopen.communicate(); if it expires, the child is killed, waited for, and thenTimeoutExpiredis raised. (Process creation itself may not be interruptible immediately on some platforms.) [1] - Use
check=Truewhen a non-zero exit code should be treated as failure. This raisesCalledProcessErrorand can include captured output. [1] - Capture output intentionally (and avoid pipe deadlocks). Use
capture_output=Trueorstdout=PIPE/stderr=PIPE(orstderr=STDOUTto merge).run()handles the “read pipes while waiting” pattern internally viacommunicate(). [1] - Decode output explicitly. Use
text=True(optionally withencoding=/errors=) to get strings instead of bytes. [1] - Handle “can’t execute” separately from “command failed.” Missing executable typically surfaces as
OSError/FileNotFoundError; a found executable that exits non-zero isCalledProcessError(ifcheck=True). [1] - Avoid
shell=Truewith untrusted input. If you invoke a shell, you must quote/escape to prevent shell-injection. Prefer passing an argument list withshell=False. [1] - Log/debug with context. On exceptions, include: the command (
exc.cmd/exc.args),returncode, and any capturedstdout/stderr(notingTimeoutExpired.output/stdout/stderrare bytes if captured). [1]
Robust pattern (drop-in helper)
import subprocess
def run_cmd(args, *, timeout=30, cwd=None, env=None):
try:
cp = subprocess.run(
args,
cwd=cwd,
env=env,
timeout=timeout,
check=True, # raise CalledProcessError on non-zero exit
text=True, # decode stdout/stderr to str
capture_output=True, # or use stdout=PIPE, stderr=STDOUT, etc.
)
return cp.stdout
except subprocess.TimeoutExpired as e:
# e.cmd, e.timeout; e.stdout/e.stderr may be present (often bytes)
raise RuntimeError(f"Timed out after {e.timeout}s: {e.cmd}") from e
except subprocess.CalledProcessError as e:
# e.returncode, e.stdout, e.stderr (if captured)
raise RuntimeError(
f"Command failed ({e.returncode}): {e.cmd}\nSTDERR:\n{e.stderr}"
) from e
except (FileNotFoundError, OSError) as e:
raise RuntimeError(f"Executable not found / could not start: {args}") from eSources: Python subprocess docs (run/timeout behavior, exceptions, output capture, security considerations). [1] Real-world exception patterns/examples. [2]
References:
[1] Python 3.14 subprocess documentation — https://docs.python.org/3/library/subprocess.html
[2] Real Python: “The subprocess Module: Wrapping Programs With Python” — https://realpython.com/python-subprocess/
🏁 Script executed:
python3 << 'EOF'
import subprocess
import sys
# Verify subprocess.run behavior without check=True
print("subprocess.run behavior without check=True:")
print("- By default, returncode is NOT raised as exception")
print("- returncode is stored in CompletedProcess object")
print("- Need explicit check or returncode handling")
print()
print("timeout parameter:")
print("- Valid parameter for subprocess.run")
print("- Raises TimeoutExpired exception if timeout exceeded")
print("- Common pattern: wrap in try-except")
print()
print("Current code issue:")
print("- No timeout: can hang indefinitely if docker stalls")
print("- No error handling: fails silently with pytest.skip if docker ps returns non-zero")
print("- Result: test unreliability in CI/flaky tests")
EOFRepository: marcinpsk/netbox-librenms-plugin
Length of output: 618
🏁 Script executed:
rg "subprocess.run" tests/e2e/ -A 5Repository: marcinpsk/netbox-librenms-plugin
Length of output: 784
🏁 Script executed:
rg "pytest.skip" tests/ -B 2 -A 2 | head -40Repository: marcinpsk/netbox-librenms-plugin
Length of output: 341
Harden docker discovery with timeout and error handling.
The subprocess.run() call lacks a timeout, which can cause the test to hang indefinitely if the Docker daemon becomes unresponsive. Additionally, non-zero exit codes from the docker command are silently ignored, making failures harder to diagnose. Add a short timeout and explicit error handling.
🔧 Suggested update
result = subprocess.run(
["docker", "ps", "--format", "{{.Names}}"],
capture_output=True,
text=True,
+ timeout=10,
)
+ if result.returncode != 0:
+ pytest.skip(f"Docker ps failed: {result.stderr.strip() or 'unknown error'}")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/test_module_install.py` around lines 31 - 45, The _get_container
function's subprocess.run call should be hardened by adding a short timeout and
explicit error handling: invoke subprocess.run with a timeout (e.g., timeout=5)
and either check=True or inspect result.returncode, catch
subprocess.TimeoutExpired and subprocess.CalledProcessError, and surface a clear
failure (use pytest.skip or pytest.fail with the exception/output details)
instead of silently proceeding; update references to CONTAINER_NAME and the
docker ps invocation so the error path logs or fails with the command
stderr/stdout to aid diagnosis.
| def _netbox_shell(code): | ||
| """Run Python code in NetBox's Django shell.""" | ||
| import shlex | ||
|
|
||
| container = _get_container() | ||
| escaped = shlex.quote(code) | ||
| result = subprocess.run( | ||
| [ | ||
| "docker", | ||
| "exec", | ||
| container, | ||
| "bash", | ||
| "-c", | ||
| f"cd /opt/netbox/netbox && python3 manage.py shell -c {escaped}", | ||
| ], | ||
| capture_output=True, | ||
| text=True, | ||
| env={"PATH": "/usr/bin:/bin", "HOME": "/root"}, | ||
| ) | ||
| # Filter out config loading lines | ||
| lines = [line for line in result.stdout.strip().split("\n") if not line.startswith(("🧬", "156 objects"))] | ||
| return "\n".join(lines).strip() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the actual test file to verify the current state
cd /
find . -name "test_module_install.py" -path "*/e2e/*" 2>/dev/null | head -1Repository: marcinpsk/netbox-librenms-plugin
Length of output: 128
🏁 Script executed:
# Find and read the test file
git ls-files | grep -E "tests/e2e.*test_module_install"Repository: marcinpsk/netbox-librenms-plugin
Length of output: 108
🏁 Script executed:
# Try to locate the file more broadly
find . -type f -name "test_module_install.py" 2>/dev/nullRepository: marcinpsk/netbox-librenms-plugin
Length of output: 110
🏁 Script executed:
# List test directory structure
git ls-files | grep "tests/e2e"Repository: marcinpsk/netbox-librenms-plugin
Length of output: 152
🏁 Script executed:
# Check if the tests directory exists
ls -la tests/ 2>/dev/null || echo "No tests dir at root"Repository: marcinpsk/netbox-librenms-plugin
Length of output: 239
🏁 Script executed:
# Read the test file
cat -n tests/e2e/test_module_install.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 15078
🏁 Script executed:
# Also check testing.instructions.md for e2e conventions
cat -n .github/instructions/testing.instructions.md 2>/dev/null || echo "File not found"Repository: marcinpsk/netbox-librenms-plugin
Length of output: 4315
Add timeout and error handling to _netbox_shell to prevent hangs and silent failures.
_netbox_shell is used by multiple test assertions; without a timeout and return-code check, docker exec failures go undetected and return empty output, causing misleading test failures or hangs.
Suggested fix
result = subprocess.run(
[
"docker",
"exec",
container,
"bash",
"-c",
f"cd /opt/netbox/netbox && python3 manage.py shell -c {escaped}",
],
capture_output=True,
text=True,
env={"PATH": "/usr/bin:/bin", "HOME": "/root"},
+ timeout=60,
)
+ if result.returncode != 0:
+ pytest.fail(f"Docker exec failed: {result.stderr.strip() or 'unknown error'}")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/test_module_install.py` around lines 48 - 69, The helper
_netbox_shell should enforce a subprocess timeout and surface non-zero exits:
when calling subprocess.run in _netbox_shell add a timeout parameter (e.g., a
few seconds) and catch subprocess.TimeoutExpired to raise/return a clear error;
after the call, check result.returncode and if non-zero raise an exception that
includes result.stderr and result.stdout so failures aren’t silently treated as
empty output; ensure the exception message references the container and original
command for easier debugging.
| @pytest.fixture | ||
| def page(browser): | ||
| """Create a new page and log in to NetBox.""" | ||
| ctx = browser.new_context(ignore_https_errors=True) | ||
| pg = ctx.new_page() | ||
|
|
||
| pg.goto(f"{NETBOX_URL}/login/", timeout=10000) | ||
| pg.fill("#id_username", NETBOX_USER) | ||
| pg.fill("#id_password", NETBOX_PASS) | ||
| pg.click("button[type=submit]") | ||
| pg.wait_for_load_state("networkidle") | ||
| yield pg | ||
| ctx.close() |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
find . -name "test_module_install.py" -type f | head -20Repository: marcinpsk/netbox-librenms-plugin
Length of output: 110
🏁 Script executed:
git ls-files tests/e2e/Repository: marcinpsk/netbox-librenms-plugin
Length of output: 152
🌐 Web query:
Playwright page.url assertion login verification best practices
💡 Result:
URL assertions after login (Playwright) — best practices
-
Prefer auto-retrying URL assertions over
page.url()comparisons- Use
await expect(page).toHaveURL(...)so Playwright waits for navigation/redirects instead of doing a one-time check (less flaky). [1]
await page.getByRole('button', { name: 'Log in' }).click(); await expect(page).toHaveURL(/\/dashboard(?:\?|$)/);
Playwright’s web-first assertions auto-retry until they pass or time out. [1]
- Use
-
Match only the stable part of the URL
- Use regex or partial matching to avoid failures from transient query params (
?next=...,?utm=...) or dynamic segments. [1]
await expect(page).toHaveURL(/\/account\/settings/);
- Use regex or partial matching to avoid failures from transient query params (
-
Don’t rely on URL alone to prove you’re logged in
- Also assert a post-login UI signal (e.g., avatar/menu, “Log out” button, user name, or a dashboard heading) using locators—these are resilient and auto-wait. [4]
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); await expect(page.getByRole('button', { name: /log out/i })).toBeVisible();
-
Lean on Playwright’s auto-waiting; avoid manual sleeps
- Actions and many assertions already wait for the page to be in an actionable/ready state;
waitForTimeout()usually adds flakiness and slowdowns. [2]
- Actions and many assertions already wait for the page to be in an actionable/ready state;
-
For test suites: don’t UI-login in every test
- Do one authenticated “setup” login, save
storageState, and reuse it for other tests to speed up runs and reduce login flake. [3] - Keep at least one end-to-end test that performs the real UI login flow. [3]
- Do one authenticated “setup” login, save
Sources: Playwright Assertions & auto-retry behavior [1], auto-waiting/actionability [2], authentication & storageState guidance [3], locator-based best practices [4].
🏁 Script executed:
cat -n tests/e2e/test_module_install.py | sed -n '100,130p'Repository: marcinpsk/netbox-librenms-plugin
Length of output: 1113
Verify login success using Playwright best practices.
The fixture lacks a post-login assertion, which can cause false positives if credentials fail. However, avoid simple page.url checks, which don't auto-retry and can be flaky with transient query params. Instead, use Playwright's auto-retrying assertion and combine it with a UI signal check:
🔧 Suggested update
pg.click("button[type=submit]")
pg.wait_for_load_state("networkidle")
+ from playwright.sync_api import expect
+ expect(pg).to_have_url("/admin/") # or the post-login landing page
+ expect(pg.get_by_role("button", name="Log out")).to_be_visible() # verify UI signal
yield pg🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/test_module_install.py` around lines 109 - 121, The page fixture
logs in but lacks a post-login auto-retrying assertion; after
pg.click("button[type=submit]") and pg.wait_for_load_state("networkidle") add
Playwright expect-based checks to verify login success (use
expect(pg).to_have_url(...) with a stable path or regex to avoid transient query
params and also expect(pg.locator("text=Log out") or a site-specific
logout/user-menu selector). Update the page fixture to import and use
Playwright's expect and perform both the URL expectation and a visible UI signal
check so the fixture reliably fails on bad credentials; reference the fixture
name page and the pg variable when making these changes.
| def _goto_modules_tab(self, page): | ||
| """Navigate to the modules sync tab and refresh data.""" | ||
| page.goto(f"{NETBOX_URL}/dcim/devices/{self.DEVICE_ID}/librenms-sync/?tab=modules") | ||
| page.wait_for_load_state("networkidle") | ||
| time.sleep(2) | ||
|
|
||
| # Click Refresh Modules | ||
| btn = page.query_selector('button:has-text("Refresh Modules")') | ||
| assert btn is not None, "Refresh Modules button not found" | ||
| btn.click() | ||
| time.sleep(8) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
# First, let's read the test file to see the full context and all sleep calls
cat -n tests/e2e/test_module_install.py | head -200Repository: marcinpsk/netbox-librenms-plugin
Length of output: 8419
🏁 Script executed:
# Count all time.sleep calls in the file
rg "time\.sleep" tests/e2e/test_module_install.py -nRepository: marcinpsk/netbox-librenms-plugin
Length of output: 451
🏁 Script executed:
# Check if the selectors are used in templates or views
rg "#modules table" --type html --type pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 58
🏁 Script executed:
# Look for how Playwright is typically used in other test files
find tests -name "*.py" -type f | head -10Repository: marcinpsk/netbox-librenms-plugin
Length of output: 152
Replace fixed sleeps with Playwright waits.
Static sleeps are flaky and slow. Prefer waiting on selectors or network idle. The pattern applies to all 12 time.sleep() calls in this file (lines 133, 139, 188, 215, 217, 247, 249, 286, 297, 301, 312, 316), not just the ones in _goto_modules_tab.
🔧 Suggested update for _goto_modules_tab
page.goto(f"{NETBOX_URL}/dcim/devices/{self.DEVICE_ID}/librenms-sync/?tab=modules")
page.wait_for_load_state("networkidle")
- time.sleep(2)
+ page.wait_for_selector("#modules table")
# Click Refresh Modules
btn = page.query_selector('button:has-text("Refresh Modules")')
assert btn is not None, "Refresh Modules button not found"
btn.click()
- time.sleep(8)
+ page.wait_for_load_state("networkidle")
+ page.wait_for_selector("#modules table tr")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/test_module_install.py` around lines 129 - 139, Replace all fixed
time.sleep calls in this test file with Playwright waits: in _goto_modules_tab,
remove time.sleep(2) and time.sleep(8) and instead wait for the page and the
button reliably (use page.wait_for_load_state("networkidle") or
page.wait_for_selector('button:has-text("Refresh Modules")') before clicking,
then wait for the network response or for a DOM change that indicates refresh
completion using page.wait_for_response(...) or page.wait_for_selector(...) for
the updated modules element); apply the same pattern to the other 11 occurrences
in this file—replace each time.sleep(...) with a targeted wait_for_selector,
wait_for_response, or wait_for_load_state tied to the UI element or API call
that indicates the operation finished so the tests become deterministic (search
for all instances of time.sleep in the file and update the surrounding logic).
left a comment
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 (2)
netbox_librenms_plugin/views/sync/interfaces.py (1)
172-174: 🧹 Nitpick | 🔵 TrivialRedundant save when VLANs are excluded.
When
"vlans"is inexclude_columns,vlan_syncedremainsFalse, triggering the save at line 174. However,update_interface_attributesalready callsinterface.save()at line 251. This results in an unnecessary database write.Consider removing the save from
update_interface_attributes(line 251) and relying solely on the conditional save here, or removing this conditional save entirely sinceupdate_interface_attributesalways saves.♻️ Option 1: Remove redundant conditional save
# Sync VLANs if not excluded - vlan_synced = False if "vlans" not in exclude_columns: self._sync_interface_vlans(interface, librenms_interface, interface_name) - vlan_synced = True - - # Skip redundant save when _sync_interface_vlans already saved (via _update_interface_vlan_assignment) - if not vlan_synced: - interface.save()🤖 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 172 - 174, Remove the redundant conditional save here: delete the block that checks vlan_synced and calls interface.save() (the "if not vlan_synced: interface.save()" around vlan_synced) because update_interface_attributes already performs the persistent save; ensure update_interface_attributes remains responsible for persisting changes and that exclude_columns/"vlans" handling still avoids double-writing by relying on vlan_synced logic only for VLAN-specific ops rather than an extra save.netbox_librenms_plugin/forms.py (1)
778-806:⚠️ Potential issue | 🟠 MajorScope location cache key by LibreNMS server.
Similar to the fix applied in
_get_librenms_poller_group_choices, the location cache key should be server-scoped to avoid returning incorrect data when switching between LibreNMS servers.🛠️ Suggested fix
def _populate_librenms_locations(self): """Fetch and populate LibreNMS locations in the dropdown.""" from django.core.cache import cache from netbox_librenms_plugin.librenms_api import LibreNMSAPI try: - # Use caching to avoid repeated API calls - cache_key = "librenms_locations_choices" + # Fetch locations from LibreNMS + api = LibreNMSAPI() + # Use server-scoped caching to avoid incorrect data when switching servers + server_id = api.librenms_url.rstrip("/") + cache_key = f"librenms_locations_choices_{server_id}" cached_choices = cache.get(cache_key) if cached_choices: self.fields["librenms_location"].choices = cached_choices return - # Fetch locations from LibreNMS - api = LibreNMSAPI() success, locations = api.get_locations()As per coding guidelines "Reuse the
librenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/forms.py` around lines 778 - 806, The locations cache key is global and must be server-scoped; instead of the static "librenms_locations_choices" build the key using the LibreNMS client instance so it is unique per server (e.g. after creating api = LibreNMSAPI(), compute cache_key = f"librenms_locations_choices:{api.server_key_or_host_or_id}" where server_key_or_host_or_id is the server-identifying attribute available on LibreNMSAPI such as server, host, base_url or id), then use that cache_key for cache.get and cache.set and continue to use api.cache_timeout; mirror the pattern used in _get_librenms_poller_group_choices and ensure you reuse the LibreNMSAPI() instance rather than creating raw requests.
♻️ Duplicate comments (3)
netbox_librenms_plugin/views/base/modules_view.py (2)
240-241:⚠️ Potential issue | 🟠 MajorGuard
cache.ttl()for non-Redis cache backends.Line 240 assumes
ttl()exists on the active cache backend. On backends withoutttl, this raisesAttributeErrorand breaks module sync rendering.#!/bin/bash # Verify unguarded cache.ttl usage in views rg -n "cache\.ttl\(" netbox_librenms_plugin/views --type py rg -n -C2 "cache\.ttl\(" netbox_librenms_plugin/views/base/modules_view.py🔧 Proposed fix
- cache_ttl = cache.ttl(self.get_cache_key(obj, "inventory")) - cache_expiry = timezone.now() + timezone.timedelta(seconds=cache_ttl) if cache_ttl is not None else None + cache_expiry = None + ttl_fn = getattr(cache, "ttl", None) + if callable(ttl_fn): + cache_ttl = ttl_fn(self.get_cache_key(obj, "inventory")) + if cache_ttl is not None: + cache_expiry = timezone.now() + timezone.timedelta(seconds=cache_ttl)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/modules_view.py` around lines 240 - 241, The code calls cache.ttl(...) unguarded which will raise on backends lacking ttl; update the logic around cache_ttl = cache.ttl(self.get_cache_key(obj, "inventory")) in the modules view to first check for the existence of ttl (e.g., use hasattr(cache, "ttl") or getattr(cache, "ttl", None)) and only call it when present, falling back to None otherwise, then compute cache_expiry = timezone.now() + timezone.timedelta(seconds=cache_ttl) if cache_ttl is not None else None; refer to the cache_ttl variable and the get_cache_key(self, obj, "inventory") call to locate where to add the guard.
512-519:⚠️ Potential issue | 🟠 MajorNormalize the captured FPC slot to
intbefore comparing bay positions.Line 512 keeps
expected_fpcas string, then Line 519 compares it toparent_bay.position(numeric), causing false mismatches.#!/bin/bash # Verify the current string-vs-position comparison in _fpc_slot_matches rg -n -C2 "expected_fpc = match.group\(1\)|parent_bay.position == expected_fpc" netbox_librenms_plugin/views/base/modules_view.py🐛 Proposed fix
- expected_fpc = match.group(1) + try: + expected_fpc = int(match.group(1)) + except ValueError: + return True ... return parent_bay.position == expected_fpc🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/modules_view.py` around lines 512 - 519, The captured slot value expected_fpc (from match.group(1)) is currently a string and is being compared to parent_bay.position (numeric), causing mismatches; update the _fpc_slot_matches logic so that after obtaining expected_fpc you coerce it to int (e.g., parse int(match.group(1))) and handle parse errors by returning False (or falling back appropriately) before comparing to parent_bay.position; locate the code around expected_fpc, bay, module, and parent_bay (module.module_bay) to apply this change.netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (1)
575-579: 🛠️ Refactor suggestion | 🟠 MajorMove
_module_sync.htmlinclude underinc/.Line 578 still references a reusable partial outside
inc/.📁 Proposed fix
- {% include 'netbox_librenms_plugin/_module_sync.html' %} + {% include 'netbox_librenms_plugin/inc/_module_sync.html' %}As per coding guidelines: Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html` around lines 575 - 579, Update the include in librenms_sync_base.html that currently references '_module_sync.html' so it points to the reusable partial under the inc subfolder (i.e., change the include from 'netbox_librenms_plugin/_module_sync.html' to 'netbox_librenms_plugin/inc/_module_sync.html'), and move the '_module_sync.html' partial file into the inc/ directory so the template loader finds it; ensure any other references use the new inc/_module_sync.html path.
🤖 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`:
- Around line 86-125: The try/except can raise NameError because logging is
imported inside the try; move the logging import out of the try block (or ensure
logging is imported in the except) so
logging.getLogger("netbox_librenms_plugin").exception(...) always has logging
defined; locate the block using CustomField, ContentType and the models Device,
VirtualMachine, Interface, VMInterface (and the cf.object_types operations) and
import logging before that try so the except can safely log errors.
In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 59-62: The code is converting port.get("port_id") to a string up
front which turns None into "None" and bypasses the falsy check; change the
logic in the loop that builds local_ports_map so you first retrieve raw_port_id
= port.get("port_id") and validate it (skip if raw_port_id is None or empty),
only then convert with port_id = str(raw_port_id) and ensure
interface_name_field lookup (port.get(interface_name_field)) is still validated;
ensure local_ports_map keys are created from the validated str(port_id) and that
lookups using link.get("local_port_id") also convert/validate consistently to
avoid matching the bogus "None" key.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 857-865: The code casts libre_device.get("device_id") to int
without validation (used when setting
existing_device.custom_field_data["librenms_id"] and elsewhere), which can raise
a 500 if device_id is missing or non-numeric; update the handler around the
librenms_id usage to validate that libre_device contains a present, numeric
"device_id" before calling int(), e.g., check libre_device.get("device_id") is
not None and str(librenms_id).isdigit() (or try/except ValueError around int()),
and if invalid return a controlled error (HttpResponseBadRequest or a validation
error) or skip linking; apply the same guard to the other int(...) casts
referenced near lines with existing_device.custom_field_data["librenms_id"] and
the other occurrences at the same function handling actions "link" (and the
other two spots noted).
- Around line 819-840: The post handler in DeviceConflictActionView currently
trusts existing_device_id from POST and only checks global write permission;
instead bind conflict operations to the already-validated target (use the
libre_device returned by get_validated_device_with_selections) and perform
object-level authorization on any existing_device before mutating it: resolve
existing_device via the validated selections or ensure existing_device.pk
matches the validated libre_device.pk (or call the appropriate object-permission
check for the target Device) and reject mismatches or unauthorized access with a
403; update any subsequent logic that uses existing_device to operate only on
the authorized/validated Device instance (references:
DeviceConflictActionView.post, existing_device_id, existing_device,
libre_device, get_validated_device_with_selections).
In `@netbox_librenms_plugin/views/sync/cables.py`:
- Around line 126-129: The loop over selected_interfaces uses a single
transaction.atomic() around process_single_interface which allows exceptions
from process_single_interface to abort the whole sync; wrap each interface
processing in its own try/except inside the transaction so one failure doesn't
stop the loop: for each interface call transaction.atomic(), call
self.process_single_interface(interface, cached_links) inside a try block, on
exception catch/log the error and append a failure result into results["failed"]
(or appropriate status) using the interface identifier, and continue to the next
interface; reference functions/variables: process_single_interface,
selected_interfaces, cached_links, results, and transaction.atomic.
---
Outside diff comments:
In `@netbox_librenms_plugin/forms.py`:
- Around line 778-806: The locations cache key is global and must be
server-scoped; instead of the static "librenms_locations_choices" build the key
using the LibreNMS client instance so it is unique per server (e.g. after
creating api = LibreNMSAPI(), compute cache_key =
f"librenms_locations_choices:{api.server_key_or_host_or_id}" where
server_key_or_host_or_id is the server-identifying attribute available on
LibreNMSAPI such as server, host, base_url or id), then use that cache_key for
cache.get and cache.set and continue to use api.cache_timeout; mirror the
pattern used in _get_librenms_poller_group_choices and ensure you reuse the
LibreNMSAPI() instance rather than creating raw requests.
In `@netbox_librenms_plugin/views/sync/interfaces.py`:
- Around line 172-174: Remove the redundant conditional save here: delete the
block that checks vlan_synced and calls interface.save() (the "if not
vlan_synced: interface.save()" around vlan_synced) because
update_interface_attributes already performs the persistent save; ensure
update_interface_attributes remains responsible for persisting changes and that
exclude_columns/"vlans" handling still avoids double-writing by relying on
vlan_synced logic only for VLAN-specific ops rather than an extra save.
---
Duplicate comments:
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html`:
- Around line 575-579: Update the include in librenms_sync_base.html that
currently references '_module_sync.html' so it points to the reusable partial
under the inc subfolder (i.e., change the include from
'netbox_librenms_plugin/_module_sync.html' to
'netbox_librenms_plugin/inc/_module_sync.html'), and move the
'_module_sync.html' partial file into the inc/ directory so the template loader
finds it; ensure any other references use the new inc/_module_sync.html path.
In `@netbox_librenms_plugin/views/base/modules_view.py`:
- Around line 240-241: The code calls cache.ttl(...) unguarded which will raise
on backends lacking ttl; update the logic around cache_ttl =
cache.ttl(self.get_cache_key(obj, "inventory")) in the modules view to first
check for the existence of ttl (e.g., use hasattr(cache, "ttl") or
getattr(cache, "ttl", None)) and only call it when present, falling back to None
otherwise, then compute cache_expiry = timezone.now() +
timezone.timedelta(seconds=cache_ttl) if cache_ttl is not None else None; refer
to the cache_ttl variable and the get_cache_key(self, obj, "inventory") call to
locate where to add the guard.
- Around line 512-519: The captured slot value expected_fpc (from
match.group(1)) is currently a string and is being compared to
parent_bay.position (numeric), causing mismatches; update the _fpc_slot_matches
logic so that after obtaining expected_fpc you coerce it to int (e.g., parse
int(match.group(1))) and handle parse errors by returning False (or falling back
appropriately) before comparing to parent_bay.position; locate the code around
expected_fpc, bay, module, and parent_bay (module.module_bay) to apply this
change.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (19)
.devcontainer/scripts/diagnose.sh.devcontainer/scripts/setup.sh.devcontainer/scripts/start-netbox.sh.devcontainer/scripts/welcome.sh.github/workflows/test.yamlnetbox_librenms_plugin/__init__.pynetbox_librenms_plugin/forms.pynetbox_librenms_plugin/migrations/0013_normalizationrule.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/tests/test_init.pynetbox_librenms_plugin/tests/test_utils.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/modules_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/cables.pynetbox_librenms_plugin/views/sync/interfaces.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/sync/cables.pynetbox_librenms_plugin/__init__.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/tests/test_utils.pynetbox_librenms_plugin/tests/test_init.pynetbox_librenms_plugin/forms.pynetbox_librenms_plugin/migrations/0013_normalizationrule.pynetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/urls.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.py
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Sync pages should extend
librenms_sync_base.html.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
🧠 Learnings (25)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Use devcontainer commands (`netbox-run`, `netbox-run-bg`, `netbox-reload`, `netbox-logs`) to manage NetBox + plugin reloading during development
Applied to files:
.devcontainer/scripts/diagnose.sh.devcontainer/scripts/start-netbox.sh.devcontainer/scripts/welcome.sh.devcontainer/scripts/setup.sh
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/forms.pynetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/urls.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/forms.pynetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/tests/test_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/__init__.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Applied to files:
netbox_librenms_plugin/__init__.pynetbox_librenms_plugin/tests/test_utils.pynetbox_librenms_plugin/tests/test_init.pynetbox_librenms_plugin/urls.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances
Applied to files:
netbox_librenms_plugin/tests/test_utils.pynetbox_librenms_plugin/tests/test_init.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_utils.pynetbox_librenms_plugin/tests/test_init.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`
Applied to files:
netbox_librenms_plugin/tests/test_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/tests/test_utils.pynetbox_librenms_plugin/views/base/modules_view.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/views/__init__.pynetbox_librenms_plugin/urls.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Applied to files:
netbox_librenms_plugin/forms.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/**/*.py : Views should follow layered structure: extend the closest base class from `views/base/` and compose mixins like `LibreNMSAPIMixin` and `CacheMixin`
Applied to files:
netbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/views/__init__.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html : Templates should live in `templates/netbox_librenms_plugin/`. Reusable template includes should be placed under `inc/` subdirectory.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
🔇 Additional comments (27)
.devcontainer/scripts/welcome.sh (2)
2-2: Good standardization with script header.Clear file identification improves maintainability with no behavior change.
47-47: Nice UX text correction.The wording fix improves terminal guidance clarity for local setup users.
.devcontainer/scripts/start-netbox.sh (3)
2-2: Header addition looks good.Consistent script labeling helps navigation and maintenance.
29-31: Graceful shutdown fallback is a solid reliability improvement.Using SIGTERM with a timed fallback to SIGKILL reduces abrupt worker termination risk.
39-41: Consistent process cleanup strategy for NetBox server.Applying the same TERM-then-KILL pattern here is correct and keeps restart behavior predictable.
.devcontainer/scripts/diagnose.sh (2)
2-2: Script header update is good.This keeps devcontainer scripts consistently self-described.
41-41: Defensive PID validation is correct.The non-empty guard plus quoting makes the health check safer and less error-prone.
.devcontainer/scripts/setup.sh (2)
2-2: Header standardization looks good.Keeps script metadata consistent across the devcontainer tooling set.
263-263: Good security hardening in setup logs.Removing password output from the success message reduces credential leakage risk.
netbox_librenms_plugin/views/sync/interfaces.py (2)
240-246: LGTM - Admin status propagation logic is sound.The conditional correctly handles the three cases:
None(defaults to enabled), string values (case-insensitive "up" check), and boolean/truthy values. Theexclude_columnsgating follows the same pattern as other attributes.
237-238: No action needed—the code is correct.The coding guideline "Always call
LibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping" applies specifically to retrieving device/VM mappings for lookups and sync operations. This code is writing the interface'sport_idto its custom field after sync.Additionally,
_store_librenms_id()is explicitly designed for "NetBox device or VM objects" (per its docstring), not interfaces. There is no interface-level setter utility. Direct custom field assignment is the standard and correct pattern for interface-level custom fields..github/workflows/test.yaml (3)
54-56: Explicitref: mainfor NetBox checkout is appropriate.Pinning to
mainmakes the checkout behavior explicit rather than relying on default branch resolution. Be aware that testing against NetBox'smainbranch means CI could break due to upstream changes—this is acceptable if the goal is to catch compatibility issues early.
42-42: SHA pinning for GitHub Actions is correctly implemented.The actions are now pinned to full commit SHAs with version comments, which improves supply-chain security and CI reproducibility. Python 3.14 is stable and supported as of the current date (February 2026).
19-19: Python 3.14 is available onubuntu-latestrunners.Python 3.14.0 was released on October 7, 2025, and current
ubuntu-latest(Ubuntu 24.04) runners in February 2026 include Python 3.14.x in their cached tools. The test matrix is correct and will not encounter availability issues.netbox_librenms_plugin/views/base/cables_view.py (1)
55-55: Per-request interface-name resolution looks correct.This avoids stale class-level state and safely handles request-less contexts.
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (1)
252-263: Good guard against placeholdersysNamevalues.The sync action is now correctly hidden for
"-"placeholder values, preventing accidental name overwrites.netbox_librenms_plugin/utils.py (1)
531-575: Normalization pipeline hardening looks solid.Catching
re.errorper rule prevents one bad DB rule from crashing the full normalization chain.netbox_librenms_plugin/tests/test_utils.py (1)
74-89: Good addition: mapping-priority test coverage.This verifies
DeviceTypeMappingtakes precedence over part/model fallback as intended.netbox_librenms_plugin/migrations/0013_normalizationrule.py (1)
23-93:SeparateDatabaseAndStateusage is appropriate here.The migration cleanly restores ORM state while safely handling fresh installs via guarded SQL creation.
netbox_librenms_plugin/views/base/modules_view.py (1)
794-799: Good defensive handling for invalidparent_index.This now prevents
ValueErrorfrom surfacing as a 500 and keeps the flow user-friendly.netbox_librenms_plugin/urls.py (1)
109-123: Route wiring for module sync/conflicts and new mapping resources looks consistent.The added endpoints are clearly namespaced and align with the expanded view/model surface.
Also applies to: 319-323, 388-555
netbox_librenms_plugin/forms.py (3)
62-106: LGTM on the server-scoped cache key fix.The poller group cache key now correctly includes the server identifier (
api.librenms_url) to avoid returning stale data when switching between LibreNMS servers.
299-454: LGTM on new mapping forms.The new
DeviceTypeMapping,ModuleTypeMapping,ModuleBayMapping, andNormalizationRuleforms follow NetBox conventions correctly, with proper use ofDynamicModelChoiceField,CSVChoiceField, andCSVModelChoiceFieldfor their respective form types.
833-956: LGTM on DeviceImportConfigForm.The form properly handles dynamic initialization with LibreNMS device data and validation results. The deferred
Platformimport avoids circular dependencies, and the queryset filtering for suggested device types is correctly implemented.netbox_librenms_plugin/views/__init__.py (1)
1-97: LGTM on view exports.The new view re-exports follow the established pattern and correctly expose the new mapping views and action views for use throughout the plugin.
netbox_librenms_plugin/__init__.py (1)
41-45: LGTM on signal connection.The
post_migratesignal connection withdispatch_uidcorrectly ensures the handler runs once per migration process without duplicate connections.netbox_librenms_plugin/tests/test_init.py (1)
14-171: LGTM on test suite.The test suite comprehensively covers the
_ensure_librenms_id_custom_fieldhandler: creation with correct defaults, deduplication via_executedflag, content type management, exception handling, and logging behavior. The mocking strategy follows testing guidelines.
left a comment
There was a problem hiding this comment.
Actionable comments posted: 9
♻️ Duplicate comments (2)
netbox_librenms_plugin/views/imports/actions.py (1)
822-843:⚠️ Potential issue | 🔴 CriticalEnforce object-level authorization and bind mutations to the validated conflict target.
Line 830/Line 836trustsexisting_device_idfrom POST, and mutations then apply to that object. This still allows crafted requests to target unintendedDevicerecords unless you bind tovalidation["existing_device"]and authorize that specific object before mutation.🛡️ Proposed hardening
- action = request.POST.get("action") - existing_device_id = request.POST.get("existing_device_id") + action = request.POST.get("action") + existing_device_id = request.POST.get("existing_device_id") if not action or not existing_device_id: return HttpResponse("Missing action or existing_device_id", status=400) - try: - existing_device = Device.objects.get(pk=int(existing_device_id)) - except (Device.DoesNotExist, ValueError): - return HttpResponse("Existing device not found", status=404) - libre_device, validation, selections = self.get_validated_device_with_selections(device_id, request) if not libre_device: return HttpResponse("LibreNMS device not found", status=404) + + expected_existing = validation.get("existing_device") + if not expected_existing or str(expected_existing.pk) != str(existing_device_id): + return HttpResponse("existing_device_id does not match detected conflict target", status=400) + + existing_device = expected_existing + if not request.user.has_perm("dcim.change_device", existing_device): + return HttpResponse("Permission denied", status=403)🤖 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 822 - 843, Don’t trust POST existing_device_id directly: use the validated target returned by get_validated_device_with_selections (validation["existing_device"]) instead of Device.objects.get(pk=existing_device_id), and perform an object-level authorization check on that validated Device before any mutations (i.e. call the view’s object-level permission routine rather than only require_write_permission global check). Replace uses of existing_device derived from existing_device_id with validation["existing_device"] and invoke the appropriate object permission check (for example a check_object_permissions / require_write_permission_on_object-style call) on that object prior to applying link/update/sync operations.netbox_librenms_plugin/views/base/modules_view.py (1)
512-519:⚠️ Potential issue | 🟠 MajorCast extracted FPC slot before comparing with
ModuleBay.position.
match.group(1)is a string, so the Line 519 comparison can fail for valid matches whenpositionis numeric.🔧 Proposed fix
- expected_fpc = match.group(1) + try: + expected_fpc = int(match.group(1)) + except (TypeError, ValueError): + return True @@ - return parent_bay.position == expected_fpc + return parent_bay.position == expected_fpcIn current NetBox (dcim.models.ModuleBay), what is the type of the `position` field?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/modules_view.py` around lines 512 - 519, The extracted expected_fpc is a string (from match.group(1)) but ModuleBay.position is numeric, so update the logic in the block using expected_fpc/module/parent_bay to cast expected_fpc to an integer before comparing to parent_bay.position (e.g., convert match.group(1) to int, handling ValueError by returning False or True per current behavior); specifically modify where expected_fpc is set and used in the function that accesses bay, module, parent_bay and returns parent_bay.position == expected_fpc so the comparison uses a numeric expected_fpc.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.devcontainer/scripts/diagnose.sh:
- Line 35: The diagnostic echo is checking the wrong relative path for the
plugin config; update the path check in the echo that references PLUGIN_WS_DIR
so it looks for " .devcontainer/config/plugin-config.py " (the config
subdirectory used by the scripts) instead of " .devcontainer/plugin-config.py ",
and keep the same conditional pattern using test -f "$PLUGIN_WS_DIR/..." so the
message correctly reports Found vs Missing (using defaults).
In @.devcontainer/scripts/load-aliases.sh:
- Around line 93-129: The netbox-status and rq-status functions currently only
check PID files and thus can falsely report services as not running; update both
to fall back to searching the process table when the PID file is missing or the
PID doesn't match is_expected_pid. Specifically, in netbox-status (function name
netbox-status) and rq-status (function name rq-status) keep the PID-file checks
but add a fallback that uses pgrep -f or ps -ef | grep -E to look for processes
matching the same patterns used by is_expected_pid ("python.*runserver.*8000"
for NetBox and "python.*rqworker" for RQ), capture and report any found PID(s)
and their state, and only report "not running" when neither the PID file nor the
process-table search finds a matching process. Ensure the new logic preserves
existing calls to is_expected_pid and handles multiple PIDs gracefully (report
list or first PID).
In @.devcontainer/scripts/process-helpers.sh:
- Around line 6-11: The graceful_kill_pid function must re-validate that the PID
still corresponds to the original process before issuing SIGKILL to avoid
killing a recycled PID; modify graceful_kill_pid to either call the existing
is_expected_pid check internally (or accept an expected identifier like the
process start time/command) and verify it after the sleep and before kill -9, or
read /proc/<pid>/stat (or use ps) to compare start time/command for the PID and
only send SIGKILL if the identity matches; ensure you reference and use
graceful_kill_pid and is_expected_pid (or the proc-stat check) so the final kill
-9 is conditional on the re-validation.
In `@netbox_librenms_plugin/__init__.py`:
- Around line 82-85: The current early assignment
_ensure_librenms_id_custom_field._executed = True happens before the DB work so
any exception prevents the custom field from being created while blocking
retries; move the _executed assignment to after the function completes
successfully (after the DB operations/commit) inside
_ensure_librenms_id_custom_field so it only marks execution on success, and
apply the same change to the other identical pattern in the file (the block at
the other ensure function / the second _executed assignment).
In `@netbox_librenms_plugin/views/base/modules_view.py`:
- Around line 93-96: The cache check in modules_view treats falsy values (like
empty lists) as cache misses; change the conditional that inspects cached_data
(from cache.get(self.get_cache_key(obj, "inventory"))) to explicitly check for
None (e.g., if cached_data is None) so an empty inventory list is treated as a
valid cache hit and still passed into _build_context(request, obj, cached_data);
keep the early return only for None results.
- Around line 994-1015: The parent-module lookup in _find_parent_module_id
excludes module-scoped bays by filtering ModuleBay with module_id__isnull=True;
remove that restriction (or also query without module_id__isnull) so bays
installed inside other modules are considered when checking bay.name ==
parent_name/parent_descr and when resolving mapping.netbox_bay_name; also
tighten the ModuleBayMapping lookup to only exact matches by adding
is_regex=False to the filter (i.e.,
ModuleBayMapping.objects.filter(librenms_name=name, is_regex=False).first()) so
mapping resolution matches the rest of the codebase.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 887-889: The HTMX error responses interpolate untrusted values
(incoming_serial, conflict_device.name, action) directly into HTML, which can
lead to XSS; fix by escaping those values before embedding them into response
content (use django.utils.html.escape or build the message with
django.utils.html.format_html) wherever the f-strings are used (the responses
around the serial conflict and the other error messages at the locations
referencing incoming_serial, conflict_device.name, and action), e.g., replace
direct f-string interpolation with escaped_incoming = escape(incoming_serial),
escaped_name = escape(conflict_device.name) (or use format_html for safe
concatenation) and then return the HttpResponse/JsonResponse with the escaped
content and the same status codes.
In `@netbox_librenms_plugin/views/sync/cables.py`:
- Around line 128-137: The loop that syncs interfaces currently treats all
unexpected exceptions as "invalid" which conflates real validation errors with
runtime failures; modify the results dict to include a distinct category (e.g.,
"error" or "failed") and change the exception handler in the selected_interfaces
loop to append interface.get("interface","") to results["error"] instead of
results["invalid"], while keeping the logger.exception call for the stack trace;
ensure any downstream code that reads results (or user-facing messages) is
updated to handle the new "error" key so users see a clear distinction between
invalid link data and internal runtime errors.
In `@netbox_librenms_plugin/views/sync/interfaces.py`:
- Around line 242-244: The MAC-address sync must be guarded by the same
device-interface check as the ifType code to avoid AttributeError on
virtualization.VMInterface; modify the block that checks "mac_address" and calls
handle_mac_address(interface, ifPhysAddress) to only run when
is_device_interface(interface) is true (i.e., wrap the existing exclude_columns
check and handle_mac_address call with an is_device_interface(interface) guard
or combine both conditions), using the existing is_device_interface helper and
keeping the ifPhysAddress variable and handle_mac_address invocation as-is.
---
Duplicate comments:
In `@netbox_librenms_plugin/views/base/modules_view.py`:
- Around line 512-519: The extracted expected_fpc is a string (from
match.group(1)) but ModuleBay.position is numeric, so update the logic in the
block using expected_fpc/module/parent_bay to cast expected_fpc to an integer
before comparing to parent_bay.position (e.g., convert match.group(1) to int,
handling ValueError by returning False or True per current behavior);
specifically modify where expected_fpc is set and used in the function that
accesses bay, module, parent_bay and returns parent_bay.position == expected_fpc
so the comparison uses a numeric expected_fpc.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 822-843: Don’t trust POST existing_device_id directly: use the
validated target returned by get_validated_device_with_selections
(validation["existing_device"]) instead of
Device.objects.get(pk=existing_device_id), and perform an object-level
authorization check on that validated Device before any mutations (i.e. call the
view’s object-level permission routine rather than only require_write_permission
global check). Replace uses of existing_device derived from existing_device_id
with validation["existing_device"] and invoke the appropriate object permission
check (for example a check_object_permissions /
require_write_permission_on_object-style call) on that object prior to applying
link/update/sync operations.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (15)
.devcontainer/scripts/diagnose.sh.devcontainer/scripts/load-aliases.sh.devcontainer/scripts/process-helpers.sh.devcontainer/scripts/setup.sh.devcontainer/scripts/start-netbox.sh.devcontainer/scripts/welcome.shnetbox_librenms_plugin/__init__.pynetbox_librenms_plugin/forms.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/cables.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.13)
- GitHub Check: test-netbox (3.14)
🧰 Additional context used
📓 Path-based instructions (5)
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Sync pages should extend
librenms_sync_base.html.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/views/sync/cables.pynetbox_librenms_plugin/__init__.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/forms.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (22)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Use devcontainer commands (`netbox-run`, `netbox-run-bg`, `netbox-reload`, `netbox-logs`) to manage NetBox + plugin reloading during development
Applied to files:
.devcontainer/scripts/start-netbox.sh.devcontainer/scripts/diagnose.sh.devcontainer/scripts/welcome.sh.devcontainer/scripts/load-aliases.sh.devcontainer/scripts/setup.sh
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/views/sync/interfaces.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/base/cables_view.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/forms.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.htmlnetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/forms.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html : Templates should live in `templates/netbox_librenms_plugin/`. Reusable template includes should be placed under `inc/` subdirectory.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/views/base/modules_view.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.htmlnetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/forms.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/base/modules_view.py.devcontainer/scripts/setup.shnetbox_librenms_plugin/forms.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/**/*.py : Views should follow layered structure: extend the closest base class from `views/base/` and compose mixins like `LibreNMSAPIMixin` and `CacheMixin`
Applied to files:
netbox_librenms_plugin/views/base/modules_view.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/views/base/modules_view.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/__init__.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Applied to files:
netbox_librenms_plugin/__init__.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Applied to files:
netbox_librenms_plugin/forms.py
🧬 Code graph analysis (5)
netbox_librenms_plugin/views/base/cables_view.py (1)
netbox_librenms_plugin/utils.py (1)
get_interface_name_field(164-192)
netbox_librenms_plugin/views/sync/interfaces.py (2)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)netbox_librenms_plugin/views/settings_views.py (1)
get(26-43)
.devcontainer/scripts/load-aliases.sh (1)
.devcontainer/scripts/process-helpers.sh (3)
is_expected_pid(21-24)graceful_kill_pid(6-11)graceful_kill_pattern(13-18)
netbox_librenms_plugin/views/imports/actions.py (2)
netbox_librenms_plugin/utils.py (2)
find_matching_platform(297-327)match_librenms_hardware_to_device_type(195-260)netbox_librenms_plugin/import_utils.py (2)
_determine_device_name(230-283)get_import_device_cache_key(208-227)
netbox_librenms_plugin/forms.py (3)
netbox_librenms_plugin/models.py (11)
InterfaceTypeMapping(55-81)LibreNMSSettings(11-52)ModuleBayMapping(148-202)ModuleTypeMapping(116-145)NormalizationRule(205-279)Meta(41-45)Meta(74-78)Meta(107-110)Meta(139-142)Meta(194-198)Meta(273-276)netbox_librenms_plugin/views/mixins.py (1)
librenms_api(217-231)netbox_librenms_plugin/librenms_api.py (2)
LibreNMSAPI(15-1080)get_poller_groups(734-765)
🪛 Shellcheck (0.11.0)
.devcontainer/scripts/start-netbox.sh
[info] 25-25: Not following: ./process-helpers.sh was not specified as input (see shellcheck -x).
(SC1091)
.devcontainer/scripts/welcome.sh
[info] 5-5: Not following: ./load-aliases.sh was not specified as input (see shellcheck -x).
(SC1091)
.devcontainer/scripts/load-aliases.sh
[info] 26-26: Not following: ./.devcontainer/scripts/process-helpers.sh was not specified as input (see shellcheck -x).
(SC1091)
[info] 132-132: Not following: /opt/netbox/venv/bin/activate was not specified as input (see shellcheck -x).
(SC1091)
[info] 136-136: Not following: /opt/netbox/venv/bin/activate was not specified as input (see shellcheck -x).
(SC1091)
[info] 140-140: Not following: /opt/netbox/venv/bin/activate was not specified as input (see shellcheck -x).
(SC1091)
[info] 154-154: Not following: /opt/netbox/venv/bin/activate was not specified as input (see shellcheck -x).
(SC1091)
[info] 168-168: Not following: /opt/netbox/venv/bin/activate was not specified as input (see shellcheck -x).
(SC1091)
[info] 172-172: Not following: /opt/netbox/venv/bin/activate was not specified as input (see shellcheck -x).
(SC1091)
[info] 177-177: Not following: /opt/netbox/venv/bin/activate was not specified as input (see shellcheck -x).
(SC1091)
[info] 182-182: Not following: /opt/netbox/venv/bin/activate was not specified as input (see shellcheck -x).
(SC1091)
🔇 Additional comments (17)
netbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html (2)
12-16: HTMX implementation follows guidelines.The button correctly uses
hx-postwith a targetedinnerHTMLswap to#module-sync-content, avoidingouterHTMLswaps per project conventions. The CSRF token is properly included via the parent form.
26-26: Update include path to useinc/subdirectory.The included partial
_module_sync_content.htmlshould follow the same convention and be referenced from theinc/subdirectory if it's a reusable include.- {% include 'netbox_librenms_plugin/_module_sync_content.html' %} + {% include 'netbox_librenms_plugin/inc/_module_sync_content.html' %}As per coding guidelines, "Reusable template includes should be placed under
inc/subdirectory."netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (4)
245-271: LGTM! Name sync row properly guards against placeholder sysName values.The conditional logic correctly excludes the placeholder
"-"value from triggering the sync button, addressing the previous review feedback.Minor nit: Line 270 uses an em-dash (
—) for the default while the mismatch modal (lines 666-667) uses a regular hyphen (-). Consider using consistent placeholder characters throughout the template for visual consistency.
292-296: Appropriate visual distinction for device type sync.Using
btn-outline-dangerfor the device type sync button appropriately signals that this is a more impactful operation compared to other field syncs.
538-545: LGTM!The Modules tab navigation follows the established pattern used by other conditional tabs (e.g., VLANs, Cables) and is properly gated behind the
module_syncflag.
575-580: LGTM!The Modules tab content pane is properly structured and the include path correctly places the reusable partial under
inc/as per coding guidelines.netbox_librenms_plugin/views/base/cables_view.py (1)
55-66: LGTM! Safe handling of port data now in place.The refactored logic correctly addresses the prior concerns:
- Dynamic resolution of
interface_name_fieldper-request via the utility function- Proper None guard on
raw_port_idbefore string conversion prevents the bogus"None"key issue- Using
.get()forinterface_name_fieldlookup preventsKeyErrorwhen the field is missing from port datanetbox_librenms_plugin/views/imports/actions.py (1)
752-767: Good fix: unknown hardware mappings no longer produce false “all synced.”
Line 763now correctly marks unmatched hardware as unsynced, which prevents false positives inall_syncedatLine 766.netbox_librenms_plugin/views/sync/cables.py (3)
1-14: Logger initialization is clean and appropriate.Using a module-level
logger = logging.getLogger(__name__)is the right pattern for scoped exception logging.
49-53: Docstring return contract is clearer now.The updated
create_cabledocstring correctly describes the boolean success/failure contract.
92-92: Good improvement: preserve interface context on invalid match.Including
interfacein the StopIteration result improves downstream UX/reporting consistency..devcontainer/scripts/welcome.sh (1)
10-11: Dynamic workspace detection and messaging update look solid.
PLUGIN_WS_DIRfallback logic and the corrected browser hint improve portability and clarity.Also applies to: 48-48
.devcontainer/scripts/diagnose.sh (1)
42-44: Empty PID-file handling is a good robustness improvement.The explicit Line 42-44 branch avoids misleading "dead PID" messaging when the PID file is empty.
.devcontainer/scripts/start-netbox.sh (1)
24-63: Process-helper integration is a strong improvement.Using
is_expected_pidand shared graceful-kill helpers here makes shutdown behavior consistent with alias commands and reduces accidental stale-PID kills..devcontainer/scripts/setup.sh (1)
262-273: Superuser credential handling is safer now.Reading credentials from environment and logging only the username (Line 272) is the right security/UX direction for devcontainer setup.
netbox_librenms_plugin/forms.py (2)
76-83: Good fix: cache keys are now server-scoped.Using
api.librenms_urlin cache keys prevents cross-server choice leakage when switching active LibreNMS servers.Also applies to: 780-783
397-397: The concern aboutforms.NullBooleanFieldbreaking on newer Django versions is unfounded.forms.NullBooleanFieldis not deprecated or removed in Django 5.1.x (which NetBox 4.2.x requires), and is still officially documented and supported. The current code at line 397 will not break and does not require changes.Likely an incorrect or invalid review comment.
| netbox-status() { | ||
| local PID | ||
| if [ -f /tmp/netbox.pid ]; then | ||
| PID=$(cat /tmp/netbox.pid 2>/dev/null) | ||
| if [ -n "$PID" ] && is_expected_pid "$PID" "python.*runserver.*8000"; then | ||
| echo "NetBox is running (PID: $PID)" | ||
| else | ||
| echo "NetBox is not running" | ||
| fi | ||
| else | ||
| echo "NetBox is not running" | ||
| fi | ||
| if [ -f /tmp/rqworker.pid ]; then | ||
| PID=$(cat /tmp/rqworker.pid 2>/dev/null) | ||
| if [ -n "$PID" ] && is_expected_pid "$PID" "python.*rqworker"; then | ||
| echo "RQ worker is running (PID: $PID)" | ||
| else | ||
| echo "RQ worker is not running" | ||
| fi | ||
| else | ||
| echo "RQ worker is not running" | ||
| fi | ||
| } | ||
|
|
||
| rq-status() { | ||
| local PID | ||
| if [ -f /tmp/rqworker.pid ]; then | ||
| PID=$(cat /tmp/rqworker.pid 2>/dev/null) | ||
| if [ -n "$PID" ] && is_expected_pid "$PID" "python.*rqworker"; then | ||
| echo "RQ worker is running (PID: $PID)" | ||
| else | ||
| echo "RQ worker is not running" | ||
| fi | ||
| else | ||
| echo "RQ worker is not running" | ||
| fi | ||
| } |
There was a problem hiding this comment.
Status commands miss running orphan processes.
Lines 95-127 only trust PID files, so they can report false "not running" when NetBox/RQ is alive but untracked.
Suggested fix
netbox-status() {
local PID
if [ -f /tmp/netbox.pid ]; then
PID=$(cat /tmp/netbox.pid 2>/dev/null)
if [ -n "$PID" ] && is_expected_pid "$PID" "python.*runserver.*8000"; then
echo "NetBox is running (PID: $PID)"
+ elif pgrep -f "python.*runserver.*8000" >/dev/null 2>&1; then
+ echo "NetBox is running (PID file missing/stale)"
else
echo "NetBox is not running"
fi
else
- echo "NetBox is not running"
+ if pgrep -f "python.*runserver.*8000" >/dev/null 2>&1; then
+ echo "NetBox is running (PID file missing/stale)"
+ else
+ echo "NetBox is not running"
+ fi
fi
@@
if [ -f /tmp/rqworker.pid ]; then
PID=$(cat /tmp/rqworker.pid 2>/dev/null)
if [ -n "$PID" ] && is_expected_pid "$PID" "python.*rqworker"; then
echo "RQ worker is running (PID: $PID)"
+ elif pgrep -f "python.*rqworker" >/dev/null 2>&1; then
+ echo "RQ worker is running (PID file missing/stale)"
else
echo "RQ worker is not running"
fi
else
- echo "RQ worker is not running"
+ if pgrep -f "python.*rqworker" >/dev/null 2>&1; then
+ echo "RQ worker is running (PID file missing/stale)"
+ else
+ echo "RQ worker is not running"
+ fi
fi
}
@@
rq-status() {
local PID
if [ -f /tmp/rqworker.pid ]; then
PID=$(cat /tmp/rqworker.pid 2>/dev/null)
if [ -n "$PID" ] && is_expected_pid "$PID" "python.*rqworker"; then
echo "RQ worker is running (PID: $PID)"
+ elif pgrep -f "python.*rqworker" >/dev/null 2>&1; then
+ echo "RQ worker is running (PID file missing/stale)"
else
echo "RQ worker is not running"
fi
else
- echo "RQ worker is not running"
+ if pgrep -f "python.*rqworker" >/dev/null 2>&1; then
+ echo "RQ worker is running (PID file missing/stale)"
+ else
+ echo "RQ worker is not running"
+ fi
fi
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.devcontainer/scripts/load-aliases.sh around lines 93 - 129, The
netbox-status and rq-status functions currently only check PID files and thus
can falsely report services as not running; update both to fall back to
searching the process table when the PID file is missing or the PID doesn't
match is_expected_pid. Specifically, in netbox-status (function name
netbox-status) and rq-status (function name rq-status) keep the PID-file checks
but add a fallback that uses pgrep -f or ps -ef | grep -E to look for processes
matching the same patterns used by is_expected_pid ("python.*runserver.*8000"
for NetBox and "python.*rqworker" for RQ), capture and report any found PID(s)
and their state, and only report "not running" when neither the PID file nor the
process-table search finds a matching process. Ensure the new logic preserves
existing calls to is_expected_pid and handles multiple PIDs gracefully (report
list or first PID).
| cached_data = cache.get(self.get_cache_key(obj, "inventory")) | ||
| if not cached_data: | ||
| return {"table": None, "object": obj, "cache_expiry": None} | ||
| return self._build_context(request, obj, cached_data) |
There was a problem hiding this comment.
Treat cached empty inventory as a valid cache hit.
An empty cached list currently renders as “no cache”, so valid empty results are lost on initial load.
🔧 Proposed fix
- if not cached_data:
+ if cached_data is None:
return {"table": None, "object": obj, "cache_expiry": None}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/views/base/modules_view.py` around lines 93 - 96, The
cache check in modules_view treats falsy values (like empty lists) as cache
misses; change the conditional that inspects cached_data (from
cache.get(self.get_cache_key(obj, "inventory"))) to explicitly check for None
(e.g., if cached_data is None) so an empty inventory list is treated as a valid
cache hit and still passed into _build_context(request, obj, cached_data); keep
the early return only for None results.
| device_bays = ModuleBay.objects.filter(device=device, module_id__isnull=True).select_related( | ||
| "installed_module" | ||
| ) | ||
|
|
||
| for bay in device_bays: | ||
| if hasattr(bay, "installed_module") and bay.installed_module: | ||
| if bay.name == parent_name or (parent_descr and bay.name == parent_descr): | ||
| return bay.installed_module.pk | ||
|
|
||
| # Also check ModuleBayMapping for indirect matches | ||
| for name in [parent_name, parent_descr]: | ||
| if not name: | ||
| continue | ||
| mapping = ModuleBayMapping.objects.filter(librenms_name=name).first() | ||
| if mapping: | ||
| bay = ( | ||
| ModuleBay.objects.filter(device=device, name=mapping.netbox_bay_name, module_id__isnull=True) | ||
| .select_related("installed_module") | ||
| .first() | ||
| ) | ||
| if bay and hasattr(bay, "installed_module") and bay.installed_module: | ||
| return bay.installed_module.pk |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the file in question and understand its context
fd "modules_view.py" --type fRepository: marcinpsk/netbox-librenms-plugin
Length of output: 125
🏁 Script executed:
# Check the ModuleBay model definition in NetBox
find . -name "*.py" -type f | xargs grep -l "class ModuleBay" | head -5Repository: marcinpsk/netbox-librenms-plugin
Length of output: 262
🏁 Script executed:
# Search for module_id field definition in ModuleBay
rg "module_id" --type py -B 2 -A 2 | grep -A 5 -B 5 "ModuleBay"Repository: marcinpsk/netbox-librenms-plugin
Length of output: 4113
🏁 Script executed:
# Look at the actual code context around lines 994-1015
cat -n "netbox_librenms_plugin/views/base/modules_view.py" | sed -n '980,1030p'Repository: marcinpsk/netbox-librenms-plugin
Length of output: 2609
🏁 Script executed:
# Check the ModuleBayMapping model definition
cat -n netbox_librenms_plugin/models.py | grep -A 20 "class ModuleBayMapping"Repository: marcinpsk/netbox-librenms-plugin
Length of output: 1103
🏁 Script executed:
# Search for is_regex field definition
rg "is_regex" --type py -B 2 -A 2Repository: marcinpsk/netbox-librenms-plugin
Length of output: 6968
Parent-module lookup excludes module-scoped bays, breaking deeper branch installs.
Filtering only module_id__isnull=True in _find_parent_module_id prevents resolution of parents that are themselves nested within installed modules. When walking up the LibreNMS hierarchy, module-level parents will not be found if they belong to another module context.
Additionally, the ModuleBayMapping query should filter is_regex=False for exact-match lookups to align with the pattern used elsewhere in the codebase.
🔧 Proposed fix
- device_bays = ModuleBay.objects.filter(device=device, module_id__isnull=True).select_related(
- "installed_module"
- )
+ device_bays = ModuleBay.objects.filter(device=device).select_related("installed_module")
@@
- mapping = ModuleBayMapping.objects.filter(librenms_name=name).first()
+ mapping = ModuleBayMapping.objects.filter(librenms_name=name, is_regex=False).first()
@@
- ModuleBay.objects.filter(device=device, name=mapping.netbox_bay_name, module_id__isnull=True)
+ ModuleBay.objects.filter(device=device, name=mapping.netbox_bay_name)
.select_related("installed_module")
.first()📝 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.
| device_bays = ModuleBay.objects.filter(device=device, module_id__isnull=True).select_related( | |
| "installed_module" | |
| ) | |
| for bay in device_bays: | |
| if hasattr(bay, "installed_module") and bay.installed_module: | |
| if bay.name == parent_name or (parent_descr and bay.name == parent_descr): | |
| return bay.installed_module.pk | |
| # Also check ModuleBayMapping for indirect matches | |
| for name in [parent_name, parent_descr]: | |
| if not name: | |
| continue | |
| mapping = ModuleBayMapping.objects.filter(librenms_name=name).first() | |
| if mapping: | |
| bay = ( | |
| ModuleBay.objects.filter(device=device, name=mapping.netbox_bay_name, module_id__isnull=True) | |
| .select_related("installed_module") | |
| .first() | |
| ) | |
| if bay and hasattr(bay, "installed_module") and bay.installed_module: | |
| return bay.installed_module.pk | |
| device_bays = ModuleBay.objects.filter(device=device).select_related( | |
| "installed_module" | |
| ) | |
| for bay in device_bays: | |
| if hasattr(bay, "installed_module") and bay.installed_module: | |
| if bay.name == parent_name or (parent_descr and bay.name == parent_descr): | |
| return bay.installed_module.pk | |
| # Also check ModuleBayMapping for indirect matches | |
| for name in [parent_name, parent_descr]: | |
| if not name: | |
| continue | |
| mapping = ModuleBayMapping.objects.filter(librenms_name=name, is_regex=False).first() | |
| if mapping: | |
| bay = ( | |
| ModuleBay.objects.filter(device=device, name=mapping.netbox_bay_name) | |
| .select_related("installed_module") | |
| .first() | |
| ) | |
| if bay and hasattr(bay, "installed_module") and bay.installed_module: | |
| return bay.installed_module.pk |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/views/base/modules_view.py` around lines 994 - 1015,
The parent-module lookup in _find_parent_module_id excludes module-scoped bays
by filtering ModuleBay with module_id__isnull=True; remove that restriction (or
also query without module_id__isnull) so bays installed inside other modules are
considered when checking bay.name == parent_name/parent_descr and when resolving
mapping.netbox_bay_name; also tighten the ModuleBayMapping lookup to only exact
matches by adding is_regex=False to the filter (i.e.,
ModuleBayMapping.objects.filter(librenms_name=name, is_regex=False).first()) so
mapping resolution matches the rest of the codebase.
| results = {"valid": [], "invalid": [], "duplicate": [], "missing_remote": []} | ||
|
|
||
| with transaction.atomic(): | ||
| for interface in selected_interfaces: | ||
| result = self.process_single_interface(interface, cached_links) | ||
| for interface in selected_interfaces: | ||
| try: | ||
| with transaction.atomic(): | ||
| result = self.process_single_interface(interface, cached_links) | ||
| results[result["status"]].append(result.get("interface", "")) | ||
| except Exception: | ||
| logger.exception("Failed to sync cable for interface %s", interface.get("interface", "")) | ||
| results["invalid"].append(interface.get("interface", "")) |
There was a problem hiding this comment.
Separate unexpected processing errors from “invalid link data.”
Line 137 classifies unexpected exceptions as invalid, but invalid is later shown as “No LibreNMS link data found...”. This can mislead users when the real cause is an internal/runtime failure.
Proposed diff
@@
- results = {"valid": [], "invalid": [], "duplicate": [], "missing_remote": []}
+ results = {"valid": [], "invalid": [], "duplicate": [], "missing_remote": [], "failed": []}
@@
for interface in selected_interfaces:
try:
with transaction.atomic():
result = self.process_single_interface(interface, cached_links)
results[result["status"]].append(result.get("interface", ""))
except Exception:
logger.exception("Failed to sync cable for interface %s", interface.get("interface", ""))
- results["invalid"].append(interface.get("interface", ""))
+ results["failed"].append(interface.get("interface", ""))@@
def display_sync_results(self, request, results):
"""Display flash messages summarizing the cable sync results."""
+ if results["failed"]:
+ messages.error(
+ request,
+ f"Unexpected sync failure for interfaces: {', '.join(results['failed'])}. Check logs for details.",
+ )
if results["missing_remote"]:
messages.error(
request,
f"Remote device or interface not found in NetBox for: {', '.join(results['missing_remote'])}",
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/views/sync/cables.py` around lines 128 - 137, The loop
that syncs interfaces currently treats all unexpected exceptions as "invalid"
which conflates real validation errors with runtime failures; modify the results
dict to include a distinct category (e.g., "error" or "failed") and change the
exception handler in the selected_interfaces loop to append
interface.get("interface","") to results["error"] instead of results["invalid"],
while keeping the logger.exception call for the stack trace; ensure any
downstream code that reads results (or user-facing messages) is updated to
handle the new "error" key so users see a clear distinction between invalid link
data and internal runtime errors.
| if "mac_address" not in exclude_columns: | ||
| ifPhysAddress = librenms_interface.get("ifPhysAddress") | ||
| self.handle_mac_address(interface, ifPhysAddress) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the interfaces.py file to see the handle_mac_address method and context
wc -l netbox_librenms_plugin/views/sync/interfaces.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 127
🏁 Script executed:
# Read the relevant sections of the file
sed -n '180,200p' netbox_librenms_plugin/views/sync/interfaces.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 799
🏁 Script executed:
# Read lines around 242 to see context and is_device_interface
sed -n '200,250p' netbox_librenms_plugin/views/sync/interfaces.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 2232
🏁 Script executed:
# Check handle_mac_address method implementation
sed -n '183,193p' netbox_librenms_plugin/views/sync/interfaces.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 593
🏁 Script executed:
# Search for is_device_interface variable definition
rg -n "is_device_interface" netbox_librenms_plugin/views/sync/interfaces.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 221
🏁 Script executed:
# Search for VMInterface imports and usage in the file
rg -n "VMInterface" netbox_librenms_plugin/views/sync/interfaces.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 599
🏁 Script executed:
# Let's verify if VMInterface has mac_addresses attribute
# First, find where VMInterface is defined
find . -name "*.py" -type f | xargs grep -l "class VMInterface" | head -5Repository: marcinpsk/netbox-librenms-plugin
Length of output: 125
🏁 Script executed:
# Search in the codebase for any VMInterface.mac_addresses references
rg "VMInterface.*mac_address" -A 2 -B 2Repository: marcinpsk/netbox-librenms-plugin
Length of output: 58
🏁 Script executed:
# Check NetBox virtualization models documentation or source
# Let's look for interface-related models and their mac_address handling
rg "class.*Interface.*:" --type py | head -20Repository: marcinpsk/netbox-librenms-plugin
Length of output: 2495
🏁 Script executed:
# More specifically, check what attributes are used on interfaces
rg "interface\.mac_address" --type py -B 2 -A 2Repository: marcinpsk/netbox-librenms-plugin
Length of output: 1386
🏁 Script executed:
# Check if there are any conditions that guard mac_address operations
rg "mac_address.*not in exclude" --type py -B 5 -A 5Repository: marcinpsk/netbox-librenms-plugin
Length of output: 994
🏁 Script executed:
# Check the update_interface_attributes method flow to see how it's called
rg -n "update_interface_attributes" netbox_librenms_plugin/views/sync/interfaces.py -B 5 -A 10Repository: marcinpsk/netbox-librenms-plugin
Length of output: 1224
🏁 Script executed:
# Search for any try-except blocks around handle_mac_address or mac_addresses access
rg -n "try:" netbox_librenms_plugin/views/sync/interfaces.py -A 10 | grep -A 10 "mac_address"Repository: marcinpsk/netbox-librenms-plugin
Length of output: 58
🏁 Script executed:
# Check if there are any imports or references that indicate NetBox version or VMInterface capabilities
rg -n "from.*models import" netbox_librenms_plugin/views/sync/interfaces.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 261
🏁 Script executed:
# Look at the actual call sites for update_interface_attributes to confirm both Device and VMInterface are passed
rg -n "\.update_interface_attributes" netbox_librenms_plugin/views/sync/interfaces.py -B 10 -A 2Repository: marcinpsk/netbox-librenms-plugin
Length of output: 684
🏁 Script executed:
# Check if there are any references to VMInterface.mac_addresses anywhere in the codebase
rg "VMInterface" netbox_librenms_plugin/ -A 5 | grep -i "mac"Repository: marcinpsk/netbox-librenms-plugin
Length of output: 1938
🏁 Script executed:
# Let's look at the full flow where update_interface_attributes is called to confirm both paths
sed -n '140,170p' netbox_librenms_plugin/views/sync/interfaces.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 1268
🏁 Script executed:
# Check if there's any error handling or test cases for VMInterface with mac_address
rg -n "VMInterface" netbox_librenms_plugin/tests/ -A 3 -B 3Repository: marcinpsk/netbox-librenms-plugin
Length of output: 2822
🏁 Script executed:
# Let's search for any documentation or comments about VMInterface capabilities
rg -n "VMInterface.*:" netbox_librenms_plugin/ -A 5 | head -30Repository: marcinpsk/netbox-librenms-plugin
Length of output: 1797
🏁 Script executed:
# Check if there's any hasattr check or try-except around VMInterface in the codebase
rg "hasattr.*interface.*mac" netbox_librenms_plugin/ -B 2 -A 2Repository: marcinpsk/netbox-librenms-plugin
Length of output: 651
🏁 Script executed:
# Let's verify the actual NetBox model imports and see if there are any safeguards
rg "from dcim" netbox_librenms_plugin/views/sync/interfaces.py -A 2Repository: marcinpsk/netbox-librenms-plugin
Length of output: 201
🏁 Script executed:
# Check if there's a test that attempts to sync a VMInterface with mac_address
rg -n "handle_mac_address\|mac_address" netbox_librenms_plugin/tests/ -B 3 -A 3Repository: marcinpsk/netbox-librenms-plugin
Length of output: 58
🏁 Script executed:
# Let's verify once more by checking if there's any error handling around the full sync flow
sed -n '130,180p' netbox_librenms_plugin/views/sync/interfaces.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 2451
🏁 Script executed:
# Check if mac_address handling is tested anywhere or if exclude_columns typically includes mac_address
rg -n "exclude_columns" netbox_librenms_plugin/views/sync/interfaces.py -B 2 -A 2Repository: marcinpsk/netbox-librenms-plugin
Length of output: 2273
Guard MAC address sync with is_device_interface to prevent AttributeError on VMInterface objects.
handle_mac_address() accesses interface.mac_addresses and interface.primary_mac_address, which exist on dcim.Interface but not on virtualization.VMInterface. When syncing a VM interface with MAC address not excluded, this will raise an AttributeError. Apply the same is_device_interface guard used for ifType handling at line 222.
Proposed fix
- if "mac_address" not in exclude_columns:
+ if "mac_address" not in exclude_columns and is_device_interface:
ifPhysAddress = librenms_interface.get("ifPhysAddress")
self.handle_mac_address(interface, ifPhysAddress)📝 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.
| if "mac_address" not in exclude_columns: | |
| ifPhysAddress = librenms_interface.get("ifPhysAddress") | |
| self.handle_mac_address(interface, ifPhysAddress) | |
| if "mac_address" not in exclude_columns and is_device_interface: | |
| ifPhysAddress = librenms_interface.get("ifPhysAddress") | |
| self.handle_mac_address(interface, ifPhysAddress) |
🤖 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 242 - 244, The
MAC-address sync must be guarded by the same device-interface check as the
ifType code to avoid AttributeError on virtualization.VMInterface; modify the
block that checks "mac_address" and calls handle_mac_address(interface,
ifPhysAddress) to only run when is_device_interface(interface) is true (i.e.,
wrap the existing exclude_columns check and handle_mac_address call with an
is_device_interface(interface) guard or combine both conditions), using the
existing is_device_interface helper and keeping the ifPhysAddress variable and
handle_mac_address invocation as-is.
Break the monolithic import_utils.py into focused modules: - permissions.py: user permission checks - cache.py: cache key generation and management - filters.py: device filtering and retrieval from LibreNMS - virtual_chassis.py: VC detection, creation, member management - device_operations.py: device validation, import, and fetch - vm_operations.py: VM creation and bulk import - bulk_import.py: bulk device import orchestration and filter processing The __init__.py re-exports all public names, so existing callers (views, jobs, tests) continue working without import changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After splitting import_utils.py into a package, mock.patch decorators must target the actual submodule where each name is looked up, not the package __init__.py. Update all patch paths in test_import_utils.py and test_permissions.py to point to the correct submodules.
Keep Cluster as a top-level import so mock.patch targets can resolve it. Remove the redundant inline import inside validate_device_for_import.
Now that Cluster is a module-level import in device_operations, the test only needs to mock device_operations.Cluster — the extra patch on virtualization.models.Cluster was leftover from the inline-import era and caused the mock to target the wrong object.
bonzo81#227) - Add use_sysname/strip_domain params to validate_device_for_import - Store resolved name in validation result as resolved_name - Update import_single_device, bulk_import_devices_shared, bulk_import_vms to extract and pass naming prefs from sync_options - Add _resolve_naming_preferences() helper in actions.py with fallback chain: POST data -> user pref -> LibreNMSSettings -> plugin default - Update DeviceImportHelperMixin and BulkImportConfirmView to pass prefs - Replace _determine_device_name() in BulkImportConfirmView with resolved_name - Add TestDeviceNamingPreferences (5 tests) to test_import_utils.py
…ort_utils package Port 4 commits from feat/serial-matching-and-conflict-resolution: - 097ea42: Fix validation readiness (VMs require cluster.found), hostname match always sets has_actions, guard platform sync forms for Device only, use _resolve_naming_preferences in BulkImportConfirmView, add else branch for unmatched hardware in _build_sync_info, reuse resolved_name in DeviceConflictActionView actions, fix test patch targets - c0d7bfc: Block import when issues present (can_import/is_ready require no issues), modal title uses resolved_name, dedup LibreNMSSettings DB query in _resolve_naming_preferences, guard hostname form for VMs - 13bafe1: Compare existing device/VM name against resolved_name instead of raw sysName, scope mismatch force gate to link/update/update_serial/ update_type only, add librenms_id collision check before linking - dad0d22: Update device_role dict in-place during refresh to preserve schema keys (available_roles etc.)
The librenms_id conflict check added in the port requires Device.objects.filter().exclude().first() to be mocked for all tests using link/update/update_serial actions.
- utils.py set_librenms_device_id: defensively handle unexpected types (non-int, non-dict) by resetting to empty dict with a warning log - utils.py find_by_librenms_id: add OR query to also match legacy records where librenms_id is stored as a bare integer - cables_view.py: update all 6 librenms_id lookup sites to use Q()|Q() so legacy integer records are matched alongside new JSON format - actions.py: validate librenms_id (device_id) before int() cast and return 400 if missing/non-numeric; use pre-validated int throughout the link/update/update_serial action blocks - interfaces.py: guard against None port_id before set_librenms_device_id to avoid clobbering an existing server-key mapping
find_by_librenms_id now calls .filter(Q(...)|Q(...)) passing a positional Q object. The three TestSerialNumberMatching mocks only accepted **kwargs, causing TypeError which was silently caught by the except block in device_operations.py, making existing_device None. Update all three device_filter side_effects to accept *args and check str(arg) for 'librenms_id' so Q-based lookups are detected correctly.
- Add module sync (ENTITY-MIB inventory via SNMP) from inventory branch - Resolve 5 merge conflicts: devcontainer scripts, forms.py, views/__init__.py, actions.py - Accept deletion of monolithic import_utils.py (replaced by split package) - Port _try_chassis_device_type_match() + chassis fallback to device_operations.py - Port _refresh_existing_device() rewrite to bulk_import.py (uses find_by_librenms_id for JSON compat) - Add DeviceTypeMapping/ModuleTypeMapping/NormalizationRule views to __init__.py - Server-specific cache key in poller group choices (forms.py)
- __init__.py: create librenms_id custom field as type 'json' (was
'integer'); auto-migrate existing integer-typed fields to 'json' on
post_migrate so the multi-server dict format is accepted by the UI
- modules_view.py: pass device_bays (not all_bays) to _build_row for
ENTITY-MIB top-level items to prevent module-scoped bay name collisions
with device-level bays of the same name
- modules_view.py: suppress Install Branch when all candidate children
have module types that require {module_path} on an unsupported NetBox
version; now checks module_type_uses_module_path + supports_module_path
before setting has_installable_children
- Never block module installation based on {module_path} compatibility.
The old 'Requires Upgrade' status and can_install=False guard are
replaced with informational badges:
* module_path_warning → 'Upgrade NetBox' warning icon with tooltip
(module uses {module_path} but current NetBox does not support it;
installation proceeds, interface naming may not be perfect)
* module_type_upgrade_hint → info icon on end modules still using
{module} when the running NetBox already supports {module_path}
(suggests updating the module type's interface templates)
- Add module_type_uses_module_token() and module_type_is_end_module()
helpers to utils.py.
- Remove module_path_blocked guards from InstallModuleView.post() and
InstallBranchView._install_single() — install always proceeds.
- Revert Install Branch heuristic: any matched child module type enables
the button regardless of {module_path} support.
- _determine_status() no longer returns 'Requires Upgrade'.
left a comment
There was a problem hiding this comment.
Actionable comments posted: 16
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/urls.py (1)
92-557: 🧹 Nitpick | 🔵 TrivialConsider splitting
urlpatternsinto domain-specific lists.
urlpatternsis getting large and merge-conflict-prone. Grouping into smaller lists (sync/import/mappings/settings) and concatenating would reduce maintenance overhead.♻️ Example structure
+device_sync_urlpatterns = [ + # device sync routes... +] + +mapping_urlpatterns = [ + # mapping CRUD routes... +] + +settings_urlpatterns = [ + # settings/test routes... +] + urlpatterns = [ - # all routes inline... - path("api/", include("netbox_librenms_plugin.api.urls")), + *device_sync_urlpatterns, + *mapping_urlpatterns, + *settings_urlpatterns, + path("api/", include("netbox_librenms_plugin.api.urls")), ]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/urls.py` around lines 92 - 557, The urlpatterns list is too large—split it into domain-specific lists and then concatenate them into urlpatterns to reduce merge conflicts; create separate lists like sync_urlpatterns (containing paths referencing DeviceLibreNMSSyncView, DeviceInterfaceTableView, SyncInterfacesView, SyncCablesView, SyncIPAddressesView, SyncVLANsView, etc.), import_urlpatterns (LibreNMSImportView, BulkImportDevicesView, BulkImportConfirmView, DeviceValidationDetailsView, DeviceVCDetailsView, DeviceRoleUpdateView, DeviceClusterUpdateView, DeviceRackUpdateView, DeviceConflictActionView), mapping_urlpatterns (InterfaceTypeMappingListView, DeviceTypeMappingListView, ModuleTypeMappingListView, ModuleBayMappingListView, NormalizationRuleListView and their detail/create/edit/delete/changelog counterparts), settings_urlpatterns (LibreNMSSettingsView, TestLibreNMSConnectionView, SaveUserPrefView, DeviceStatusListView, VMStatusListView), and any other logical groups (vm_urlpatterns, device_update_urlpatterns); then replace the big urlpatterns with a single concatenation like urlpatterns = sync_urlpatterns + import_urlpatterns + mapping_urlpatterns + settings_urlpatterns + [...] ensuring existing path() entries and names (e.g., "device_librenms_sync", "librenms_import", "interfacetypemapping_list") are moved unchanged so imports and reverse URL lookups remain intact.
♻️ Duplicate comments (16)
netbox_librenms_plugin/views/sync/cables.py (1)
128-137:⚠️ Potential issue | 🟡 MinorSeparate unexpected runtime failures from “invalid link data.”
Line 137 still classifies unexpected exceptions as
invalid, but Lines 170-174 presentinvalidas “No LibreNMS link data found…”. This misreports runtime failures to users.🔧 Proposed fix
- results = {"valid": [], "invalid": [], "duplicate": [], "missing_remote": []} + results = {"valid": [], "invalid": [], "duplicate": [], "missing_remote": [], "failed": []} for interface in selected_interfaces: try: with transaction.atomic(): result = self.process_single_interface(interface, cached_links) results[result["status"]].append(result.get("interface", "")) except Exception: logger.exception("Failed to sync cable for interface %s", interface.get("interface", "")) - results["invalid"].append(interface.get("interface", "")) + results["failed"].append(interface.get("interface", ""))def display_sync_results(self, request, results): """Display flash messages summarizing the cable sync results.""" + if results["failed"]: + messages.error( + request, + f"Unexpected sync failure for interfaces: {', '.join(results['failed'])}. Check logs for details.", + ) if results["missing_remote"]: messages.error( request, f"Remote device or interface not found in NetBox for: {', '.join(results['missing_remote'])}", )Also applies to: 163-174
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/sync/cables.py` around lines 128 - 137, The except Exception handler is currently appending unexpected runtime failures to results["invalid"], which conflates true "invalid link data" with runtime errors; update the initial results dict to include a separate bucket (e.g., "error" or "failed") and change the except Exception blocks around the calls to process_single_interface (the for-loop that wraps process_single_interface and the similar block later) to append the interface identifier to that new bucket instead of "invalid", while keeping logger.exception as-is so unexpected exceptions are reported but not misclassified as invalid link data.netbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html (1)
26-26:⚠️ Potential issue | 🔴 CriticalFix the include path to the
inc/partial to avoid template resolution failure.Line 26 points to
netbox_librenms_plugin/_module_sync_content.html; this include should reference theinc/location.Suggested fix
- {% include 'netbox_librenms_plugin/_module_sync_content.html' %} + {% include 'netbox_librenms_plugin/inc/_module_sync_content.html' %}As per coding guidelines, "Reusable template includes should be placed under
inc/subdirectory."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html` at line 26, The template include currently references 'netbox_librenms_plugin/_module_sync_content.html' which will fail resolution; update the include in the include statement to point to the inc/ partial (i.e., 'netbox_librenms_plugin/inc/_module_sync_content.html') so the reusable partial in the inc/ subdirectory is used; change the include line that mentions _module_sync_content.html accordingly.netbox_librenms_plugin/views/base/cables_view.py (1)
56-67:⚠️ Potential issue | 🟡 MinorAdd an
ifNamefallback when the resolved interface field is missing.On Line 64–66, rows are dropped if the selected field is absent; this can silently remove valid local-port mappings.
Suggested patch
- port_name = port.get(interface_name_field) - if port_name is None: + port_name = port.get(interface_name_field) or port.get("ifName") + if port_name is None: continue🤖 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 56 - 67, The loop building local_ports_map currently drops rows when port_name is None; modify the logic in the block that uses interface_name_field (the code around get_interface_name_field, get_ports_data and local_ports_map) to attempt a fallback lookup using port.get("ifName") before skipping the row, and only continue if both the resolved field and the "ifName" fallback are missing; ensure the final mapping uses the fallback value when used.tests/e2e/test_module_install.py (3)
115-120:⚠️ Potential issue | 🟡 MinorAssert login success in the
pagefixture before yielding.Without a post-login assertion (Line 115–120), auth failures show up later as unrelated test errors.
Suggested patch
`@pytest.fixture` def page(browser): """Create a new page and log in to NetBox.""" + from playwright.sync_api import expect + ctx = browser.new_context(ignore_https_errors=True) pg = ctx.new_page() @@ pg.click("button[type=submit]") pg.wait_for_load_state("networkidle") + expect(pg).not_to_have_url(f"{NETBOX_URL}/login/") + expect(pg.locator("text=Log out")).to_be_visible() yield pg ctx.close()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/test_module_install.py` around lines 115 - 120, The fixture logs in using pg.goto/fill/click but doesn't assert login succeeded, causing later tests to fail cryptically; update the page fixture (the code using pg, NETBOX_URL, NETBOX_USER, NETBOX_PASS) to verify post-login success before yielding by waiting for and asserting a reliable post-login indicator (e.g., wait_for_selector or check pg.url contains the dashboard or a logout element) and surface a clear error if authentication failed so tests stop early with a meaningful message.
36-69:⚠️ Potential issue | 🟠 MajorHarden Docker subprocess helpers with timeout and exit-code handling.
On Line 36 and Line 54, helper commands can hang or fail silently; this makes failures non-diagnostic and flaky.
Suggested patch
def _get_container(): @@ - result = subprocess.run( - ["docker", "ps", "--format", "{{.Names}}"], - capture_output=True, - text=True, - ) + try: + result = subprocess.run( + ["docker", "ps", "--format", "{{.Names}}"], + capture_output=True, + text=True, + timeout=10, + ) + except subprocess.TimeoutExpired: + pytest.skip("docker ps timed out") + if result.returncode != 0: + pytest.skip(f"docker ps failed: {result.stderr.strip() or 'unknown error'}") @@ def _netbox_shell(code): @@ - result = subprocess.run( + try: + result = subprocess.run( [ "docker", "exec", container, @@ capture_output=True, text=True, env={"PATH": "/usr/bin:/bin", "HOME": "/root"}, - ) + timeout=60, + ) + except subprocess.TimeoutExpired: + pytest.fail(f"docker exec timed out for container '{container}'") + if result.returncode != 0: + pytest.fail( + f"docker exec failed for container '{container}': " + f"{result.stderr.strip() or result.stdout.strip() or 'unknown error'}" + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/test_module_install.py` around lines 36 - 69, The Docker helper functions _get_container and _netbox_shell can hang or fail silently; update their subprocess.run calls to include a timeout (e.g., timeout=30) and handle non-zero exit codes by either using check=True or inspecting result.returncode and raising a clear exception that includes result.stderr and result.stdout; in _get_container also guard against empty stdout when splitting container names before searching for "devcontainer-devcontainer" and call pytest.skip only after verifying no container found. Ensure the env, capture_output, and text arguments remain and propagate stderr in the raised error message for easier debugging.
133-139: 🧹 Nitpick | 🔵 TrivialReplace fixed sleeps with deterministic Playwright waits.
On Line 133, Line 139, and the other listed segments,
time.sleep(...)introduces avoidable flakiness and slowdowns.Suggested patch pattern (apply similarly to all sleep sites)
def _goto_modules_tab(self, page): """Navigate to the modules sync tab and refresh data.""" page.goto(f"{NETBOX_URL}/dcim/devices/{self.DEVICE_ID}/librenms-sync/?tab=modules") page.wait_for_load_state("networkidle") - time.sleep(2) + page.wait_for_selector('button:has-text("Refresh Modules")') # Click Refresh Modules btn = page.query_selector('button:has-text("Refresh Modules")') assert btn is not None, "Refresh Modules button not found" btn.click() - time.sleep(8) + page.wait_for_load_state("networkidle") + page.wait_for_selector("#modules table tr")Also applies to: 188-188, 215-217, 247-249, 286-287, 297-301, 312-317
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/test_module_install.py` around lines 133 - 139, Replace hard sleeps with deterministic Playwright waits: instead of time.sleep(...) around interactions that use page.query_selector and btn.click, wait for the UI state change you expect (e.g., use page.wait_for_selector('button:has-text("Refresh Modules")') before clicking, and after clicking use page.wait_for_selector or page.wait_for_response or page.wait_for_load_state to wait for the modules list/update indicator to appear or finish). Update each occurrence (the calls around page.query_selector, btn.click and the other listed sleep sites) to use the appropriate Playwright wait method (page.wait_for_selector, page.wait_for_response, page.wait_for_load_state, or expect(...).to_be_visible) targeting the specific selector or network response that indicates completion rather than time.sleep.netbox_librenms_plugin/templates/netbox_librenms_plugin/_module_sync_content.html (1)
1-31:⚠️ Potential issue | 🟠 MajorMove this fragment into the HTMX templates directory.
This template is structured as a swapped content fragment; keeping it under
templates/netbox_librenms_plugin/htmx/is required for consistent HTMX response organization and target wiring.Based on learnings, "Follow sync pipeline flow: ... render HTMX fragments in
templates/netbox_librenms_plugin/htmx/."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/templates/netbox_librenms_plugin/_module_sync_content.html` around lines 1 - 31, This fragment that renders the HTMX-swapped module sync content (checks module_sync.table, uses module_sync.cache_expiry and elements with ids module-cache-countdown and module-countdown-timer) should be moved from templates/netbox_librenms_plugin/ to the HTMX templates directory; relocate the file into templates/netbox_librenms_plugin/htmx/ and update any render/include references that point to netbox_librenms_plugin/_module_sync_content.html to the new path (e.g., netbox_librenms_plugin/htmx/_module_sync_content.html) so HTMX responses follow the plugin's HTMX template organization and the target wiring remains consistent.netbox_librenms_plugin/views/sync/interfaces.py (1)
244-246:⚠️ Potential issue | 🟠 MajorGuard MAC address sync with
is_device_interfaceto prevent AttributeError on VMInterface objects.
handle_mac_address()accessesinterface.mac_addressesandinterface.primary_mac_address, which exist ondcim.Interfacebut not onvirtualization.VMInterface. When syncing a VM interface with MAC address not excluded, this will raise anAttributeError. Apply the sameis_device_interfaceguard used forifTypehandling at line 222.Proposed fix
- if "mac_address" not in exclude_columns: + if "mac_address" not in exclude_columns and is_device_interface: ifPhysAddress = librenms_interface.get("ifPhysAddress") self.handle_mac_address(interface, ifPhysAddress)🤖 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 244 - 246, The MAC address sync can raise AttributeError for virtualization.VMInterface because handle_mac_address() expects dcim.Interface attributes; wrap the existing block that reads ifPhysAddress and calls self.handle_mac_address(interface, ifPhysAddress) with the same is_device_interface guard used for ifType (i.e., only call handle_mac_address when is_device_interface(interface) is True and "mac_address" not in exclude_columns), ensuring you reference librenms_interface.get("ifPhysAddress") and the handle_mac_address method.netbox_librenms_plugin/filters.py (1)
46-53: 🧹 Nitpick | 🔵 Trivial
manufacturerfilter relies on implicit generation — consider explicit declaration for consistency.The
manufacturerfield is aForeignKeyonNormalizationRule, so django-filters auto-generates a PK-based filter. While functional, adding an explicitModelChoiceFilterwould make the expected behavior clear and align with form patterns used elsewhere.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/filters.py` around lines 46 - 53, The manufacturer FK filter is currently implicit; explicitly declare it in NormalizationRuleFilterSet by adding a django_filters.ModelChoiceFilter named manufacturer (e.g., manufacturer = django_filters.ModelChoiceFilter(field_name="manufacturer", queryset=Manufacturer.objects.all())) and import the Manufacturer model (or derive it via NormalizationRule._meta.get_field('manufacturer').related_model) so the filter behavior is explicit and consistent with other forms.netbox_librenms_plugin/__init__.py (1)
82-85:⚠️ Potential issue | 🟠 MajorSet
_executedonly after successful custom-field setup.
_executedis set on Line 84 before imports/DB operations. If anything in thetryblock fails, laterpost_migratecallbacks in the same process become no-ops, so the field may never be created.🛠️ Proposed fix
def _ensure_librenms_id_custom_field(sender, **kwargs): @@ if getattr(_ensure_librenms_id_custom_field, "_executed", False): return - _ensure_librenms_id_custom_field._executed = True # not reset; see comment above @@ except Exception as e: # Don't break startup if custom field creation fails (e.g., during initial migration), # but log the error so it's not silently swallowed. logging.getLogger("netbox_librenms_plugin").exception("Failed to auto-create 'librenms_id' custom field: %s", e) + return + + _ensure_librenms_id_custom_field._executed = TrueAlso applies to: 131-135
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/__init__.py` around lines 82 - 85, The guard flag _executed on _ensure_librenms_id_custom_field is being set before the try/import/DB work, causing failures to suppress future post_migrate runs; move setting _ensure_librenms_id_custom_field._executed = True to the end of the function after the custom-field creation has completed successfully (i.e., inside the try block after all imports and DB operations succeed), and mirror the same change for the similar block referenced at lines 131-135 so the flag is only set on successful completion.netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)
23-23:⚠️ Potential issue | 🟠 MajorReplace Bootstrap dismiss attributes with the project’s HTMX modal-close pattern.
Both close buttons still use
data-bs-dismiss="modal", which bypasses the established HTMX modal wrapper flow in this plugin.As per coding guidelines, "Modal buttons should target the
htmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs."Also applies to: 562-563
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html` at line 23, Remove the Bootstrap-specific data-bs-dismiss="modal" from the <button class="btn-close"> elements and replace it with the plugin's HTMX modal-close pattern so the wrapper JS (in librenms_import.html) is used to close the modal; specifically, ensure the close buttons target the htmx-modal-content element (by adding the project's modal-close attribute/class used elsewhere in the plugin) and apply the same change to the other close button occurrences mentioned.netbox_librenms_plugin/views/base/modules_view.py (3)
93-96:⚠️ Potential issue | 🟡 MinorTreat cached empty inventory as a valid cache hit.
Line 94 uses a falsy check, so an intentionally cached empty list is treated as a miss.
🔧 Suggested fix
- if not cached_data: + if cached_data is None: return {"table": None, "object": obj, "cache_expiry": None}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/modules_view.py` around lines 93 - 96, The cache lookup in modules_view.py wrongly treats falsy cached values (like an intentionally cached empty list) as a miss; update the check in the code that calls cache.get(self.get_cache_key(obj, "inventory")) so it only treats a None return as a cache miss (e.g., change the condition from "if not cached_data" to "if cached_data is None"), then pass the cached value into self._build_context(request, obj, cached_data) so empty inventories are handled as valid cache hits; references: get_cache_key and _build_context in the modules view.
516-527:⚠️ Potential issue | 🟠 MajorNormalize FPC slot type before comparing to
ModuleBay.position.Line 519 keeps
expected_fpcas string from regex, butparent_bay.positionis numeric in NetBox models. This makes valid matches fail.🐛 Suggested fix
- expected_fpc = match.group(1) + try: + expected_fpc = int(match.group(1)) + except ValueError: + return True ... return parent_bay.position == expected_fpc#!/bin/bash rg -n "expected_fpc = match.group\\(1\\)|parent_bay.position == expected_fpc" netbox_librenms_plugin/views/base/modules_view.py -C2🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/modules_view.py` around lines 516 - 527, The comparison uses expected_fpc (string from match.group(1)) against parent_bay.position (numeric); convert expected_fpc to an integer before comparing and handle parse errors gracefully. Locate the block that defines expected_fpc = match.group(1) and replace the direct comparison with code that attempts to cast expected_fpc = int(expected_fpc) in a try/except (or equivalent), returning True on ValueError/TypeError to preserve current behavior, then return parent_bay.position == expected_fpc; keep the surrounding checks for bay/module/module_bay unchanged.
994-1013:⚠️ Potential issue | 🟠 MajorParent-module resolution is over-restricted for nested branch installs.
Line 994 filters only
module_id__isnull=True, so parent discovery misses installed modules under module-scoped bays. Also, Line 1007 should enforce exact-only mapping lookup withis_regex=False.🧩 Suggested fix
- device_bays = ModuleBay.objects.filter(device=device, module_id__isnull=True).select_related( + device_bays = ModuleBay.objects.filter(device=device).select_related( "installed_module" ) ... - mapping = ModuleBayMapping.objects.filter(librenms_name=name).first() + mapping = ModuleBayMapping.objects.filter(librenms_name=name, is_regex=False).first() if mapping: bay = ( - ModuleBay.objects.filter(device=device, name=mapping.netbox_bay_name, module_id__isnull=True) + ModuleBay.objects.filter(device=device, name=mapping.netbox_bay_name) .select_related("installed_module") .first() )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/base/modules_view.py` around lines 994 - 1013, The parent-module resolution is too restrictive: remove the module_id__isnull=True constraint so ModuleBay queries consider module-scoped bays when discovering a parent (adjust both the initial device_bays query that builds device_bays and the later ModuleBay lookup after finding a ModuleBayMapping), and make the mapping lookup require exact matches by adding is_regex=False to the ModuleBayMapping filter (i.e. filter(librenms_name=name, is_regex=False)). Ensure you still use select_related("installed_module") when loading bays so installed_module.pk can be returned.netbox_librenms_plugin/views/imports/actions.py (2)
910-913:⚠️ Potential issue | 🟠 MajorEscape untrusted values in HTMX error HTML responses.
Values like
incoming_serial,conflict_device.name,librenms_os,hardware, andactionare interpolated directly into HTML strings. These responses are HTMX-injected and can become XSS vectors.🛡️ Suggested fix
+from django.utils.html import escape ... - return HttpResponse( - f"Serial conflict: '{incoming_serial}' is already assigned to device " - f"'{conflict_device.name}' (ID: {conflict_device.pk})", - status=409, - ) + return HttpResponse( + f"Serial conflict: '{escape(incoming_serial)}' is already assigned to device " + f"'{escape(conflict_device.name)}' (ID: {conflict_device.pk})", + status=409, + ) ... - return HttpResponse(f"Platform '{librenms_os}' not found in NetBox", status=400) + return HttpResponse(f"Platform '{escape(librenms_os)}' not found in NetBox", status=400) ... - return HttpResponse(f"No matching device type for '{hardware}'", status=400) + return HttpResponse(f"No matching device type for '{escape(hardware)}'", status=400) ... - return HttpResponse(f"Unknown action: {action}", status=400) + return HttpResponse(f"Unknown action: {escape(action)}", status=400)Also applies to: 952-954, 974-976, 1025-1027, 1047-1047, 1062-1062, 1065-1065
🤖 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 910 - 913, Several HttpResponse() calls in netbox_librenms_plugin/views/imports/actions.py return HTMX-injected HTML built with f-strings containing untrusted values (e.g., incoming_serial, id_conflict.name, librenms_os, hardware, action) which can lead to XSS; replace those raw f-strings with Django-safe interpolations using django.utils.html.format_html() or escape() (e.g., format_html("... ID {0} ...", escape(incoming_serial))) for each HttpResponse that currently injects these variables (the LibreNMS ID conflict message and the other occurrences around the reported ranges), ensuring the values are escaped before insertion and preserving content_type='text/html' if needed.
857-880:⚠️ Potential issue | 🔴 CriticalEnforce object-level authorization and bind conflict actions to the validated target.
Line 874 accepts
existing_device_idfrom POST and mutates that object after only plugin-level permission checks. This allows crafted requests to target unrelatedDevicerecords.🔒 Suggested fix
-from netbox_librenms_plugin.views.mixins import LibreNMSAPIMixin, LibreNMSPermissionMixin +from netbox_librenms_plugin.views.mixins import ( + LibreNMSAPIMixin, + LibreNMSPermissionMixin, + NetBoxObjectPermissionMixin, +) ... -class DeviceConflictActionView(LibreNMSPermissionMixin, LibreNMSAPIMixin, DeviceImportHelperMixin, View): +class DeviceConflictActionView( + LibreNMSPermissionMixin, LibreNMSAPIMixin, NetBoxObjectPermissionMixin, DeviceImportHelperMixin, View +): ... def post(self, request, device_id): """Resolve a device conflict by linking, updating, or syncing serial.""" if error := self.require_write_permission(): return error from dcim.models import Device + self.required_object_permissions = {"POST": [("change", Device)]} + if error := self.require_all_permissions_json("POST"): + return error ... libre_device, validation, selections = self.get_validated_device_with_selections(device_id, request) if not libre_device: return HttpResponse("LibreNMS device not found", status=404) + expected_existing = validation.get("existing_device") + if not expected_existing or expected_existing.pk != existing_device.pk: + return HttpResponse("existing_device_id does not match detected conflict target", status=400)Also applies to: 873-877
🤖 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 857 - 880, After calling get_validated_device_with_selections(...) and before mutating existing_device, ensure you enforce object-level authorization and bind the action to the validated target: verify that int(existing_device_id) equals libre_device.pk or is one of the devices in selections (reject with 403 if not), and then perform the mixin's object-level write permission check against that existing_device (use the project’s existing object-permission helper from LibreNMSPermissionMixin or equivalent) before making any changes in DeviceConflictActionView.post.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/usage_tips/permissions.md`:
- Line 29: Fix the numbering inconsistency in the permissions list: change the
second list item that currently reads "1. **Tier 2: Object permission**: User
needs `dcim.add_device`..." to use "2." so the ordered list correctly continues
after "1. **Tier 1: Plugin permission**". Ensure the displayed text "Tier 2:
Object permission" and permission `dcim.add_device` remain unchanged aside from
the numeric prefix.
In `@netbox_librenms_plugin/forms.py`:
- Line 397: The form field is using the deprecated NullBooleanField for
is_regex; replace it with a forward-compatible alternative such as
forms.TypedChoiceField(...) or forms.BooleanField(required=False) to avoid
breakage in Django >=4.0—update the is_regex declaration in the form (look for
the is_regex assignment in the form class) to use TypedChoiceField with explicit
coerce and choices to represent None/True/False or use
BooleanField(required=False) if tri-state is not needed, and adjust any form
cleaning/consumption code that expects None/True/False accordingly (e.g.,
clean_is_regex or places referencing form.cleaned_data['is_regex']).
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 426-430: The early-exit branches in process_device_filters()
currently return a bare empty list on connection/error or when request is
truthy, breaking the declared tuple return type when return_cache_status=True;
update those return statements inside the except blocks and any other early
returns (including the similar branches around the blocks referenced) to return
a two-tuple of ([], False) when the function was called with
return_cache_status=True, otherwise keep returning [] to preserve previous
behavior—i.e., check the return_cache_status parameter in the except handlers in
process_device_filters() and return ([], False) instead of [] for the tuple
contract, and apply the same change to the other noted early-exit branches in
the same function.
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 74-78: The code assumes metadata.get("cached_at") is always
present and ISO-formatted; wrap parsing of cached_at in a guard and fallback:
check if metadata.get("cached_at") is truthy, attempt
datetime.fromisoformat(...) inside a try/except (catch ValueError/TypeError),
and on failure set cached_at to a safe fallback (e.g.,
datetime.now(timezone.utc) minus cache_timeout seconds or
datetime.fromtimestamp(0, timezone.utc) to force expired) so the rest of the
logic (age_seconds, remaining_seconds) does not raise; update references to
cached_at, cache_timeout, and remaining_seconds accordingly so invalid or
missing metadata yields an expired cache instead of crashing.
- Around line 133-136: The code currently uses Python's randomized hash() on
sorted(filters.items()), causing non-deterministic keys across processes;
replace that with a deterministic digest (e.g. use hashlib.sha256 on a stable
serialization of filters) so filter_hash is computed deterministically across
processes. Specifically, serialize the filters in a stable way (JSON with sorted
keys or joining sorted items), compute a hex digest (sha256.hexdigest() or
truncated portion) and assign that to filter_hash, leaving vc_part, server_key,
and device_id usage and the final
f"validated_device_{server_key}_{filter_hash}_{device_id}_{vc_part}" return
intact.
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 266-271: The code forces VM mode by setting result["import_as_vm"]
when an existing VM is found but later branches still read the original
import_as_vm parameter, causing contradictory validation; fix by making
downstream logic read a single source-of-truth (result["import_as_vm"]) after
any modifications: after you set result["import_as_vm"] = True in the
existing-VM branch, assign a local variable (e.g., effective_import_as_vm =
result["import_as_vm"] or simply reference result["import_as_vm"] everywhere)
and replace all subsequent checks that use the original import_as_vm parameter
with checks against result["import_as_vm"] (also update the same pattern at the
other occurrences referenced around the import/validation logic to ensure
consistency).
In `@netbox_librenms_plugin/import_utils/filters.py`:
- Line 174: The cache key generation in get_librenms_devices_for_import
currently uses Python's built-in hash() and the literal server_key which can be
"None"; change it to build a deterministic, server-aware key by using the actual
server identifier (use api.server_key when server_key is None) and replace
hash(str(...)) with a stable digest (e.g., compute a hex digest from a
canonicalized representation of api_filters and client_filters using hashlib
like sha256 over json.dumps(..., sort_keys=True) or repr(... )). Also update the
identical pattern in get_validated_device_cache_key in import_utils/cache.py so
both functions produce stable, cross-process cache keys and avoid collisions
across servers.
In `@netbox_librenms_plugin/import_utils/permissions.py`:
- Around line 1-9: The module defines an unused logger variable (logger =
logging.getLogger(__name__)) which should be removed to avoid dead code; either
delete the logger import and the logger assignment or, if you intend to use it
later, add a brief comment above logger explaining its planned use (e.g., "#
reserved for future logging in permission checks") so linters won't flag
it—update imports accordingly by removing the logging import if deleting the
logger.
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 375-379: The loop over members_info currently skips entries when
member.get("serial") equals master_device.serial and also treats blank serials
as matches, and later code rewrites slot positions sequentially; update the
logic in the loop that iterates members_info so it only skips a member if
member.get("serial") is truthy and exactly equals master_device.serial (i.e.,
check for a non-empty serial before comparing), and when creating/updating
member records (the code paths around where Member objects/positions are
created/updated following members_info processing) preserve the original
member["position"] from members_info instead of assigning incremental slot
numbers; apply the same fix to the analogous blocks referenced (the later
sections handling member creation/positioning around the 392-415 and 417-418
areas) so blank serials are not treated as the master and detected positions are
retained.
In `@netbox_librenms_plugin/tables/device_status.py`:
- Around line 489-496: The details button markup currently relies on title and
icon-only visuals when btn_label is empty, which is inaccessible; update the
button rendering (the string built using details_url, device_id, btn_class,
btn_icon, btn_label, btn_title) to add an aria-label attribute when btn_label is
falsy—use btn_title (or another descriptive string) for aria-label so screen
readers announce the button consistently while preserving the existing title
attribute and behavior.
In `@netbox_librenms_plugin/tables/interfaces.py`:
- Line 16: Replace direct use of the helper get_librenms_device_id(...) with the
official accessor LibreNMSAPI.get_librenms_id(...) in the interfaces resolution
flow (where get_librenms_device_id is currently invoked); update imports to
remove the direct helper and ensure LibreNMSAPI is imported/available, call the
API accessor with the same arguments and handle its return value identically
(including None/exception cases) so the librenms_id mapping is retrieved via
LibreNMSAPI.get_librenms_id rather than touching the custom-field helper
directly.
In `@netbox_librenms_plugin/tables/modules.py`:
- Around line 121-128: The tooltip text currently contains a literal
"{module_path}" instead of the actual module_path value; update the format_html
call so the final string uses a placeholder consumed by format_html and pass
module_path as the last argument (i.e., change the title argument from the
literal "Upgrade NetBox to fully support {module_path}" to a format placeholder
like "Upgrade NetBox to fully support {}" and pass module_path alongside
badge_class, warning, value) so the module_path variable is rendered into the
icon title; refer to the variables badge_class, warning, value and module_path
in the format_html call.
In `@netbox_librenms_plugin/tests/test_init.py`:
- Around line 129-144: In the test_exception_does_not_propagate test, after
invoking _ensure_librenms_id_custom_field(sender=None) and verifying
logger.exception was called, add an assertion checking the function-level flag
_ensure_librenms_id_custom_field._executed to ensure it reflects the one-shot
execution state (e.g., assert _ensure_librenms_id_custom_field._executed is
True) so failed attempts don't silently allow retries.
- Around line 46-57: Update the test assertion to match the new default
custom-field type: change the expected "type" value from "integer" to "json" in
the MockCustomField.objects.get_or_create assertion inside the test for
_ensure_librenms_id_custom_field so the defaults dict reflects "type": "json"
while keeping all other keys unchanged.
In `@netbox_librenms_plugin/views/base/librenms_sync_view.py`:
- Around line 235-237: Remove the redundant assignment to found_in_librenms (the
second "found_in_librenms = True") in the block where you set mismatched_device
= True; leave the mismatched_device assignment and the explanatory comment but
delete the duplicate found_in_librenms = True so the code only sets
found_in_librenms once (the earlier assignment after the successful API call).
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 903-908: The current id_conflict query in the import action
(variable id_conflict in netbox_librenms_plugin.views.imports.actions) only
checks the legacy custom_field_data__librenms_id=int(librenms_id) and misses the
new per-server dict shape; update the Device.objects.filter(...) used to set
id_conflict to check both shapes by OR-ing a Q for the legacy integer lookup
with a Q that checks the per-server dict contains the server key mapping to the
same int(librenms_id) (e.g. custom_field_data__librenms_id__contains={server:
int(librenms_id)}), keep the .exclude(pk=existing_device.pk).first() behavior
and add the necessary Q import. Ensure you reference the same librenms_id and
existing_device variables used currently.
---
Outside diff comments:
In `@netbox_librenms_plugin/urls.py`:
- Around line 92-557: The urlpatterns list is too large—split it into
domain-specific lists and then concatenate them into urlpatterns to reduce merge
conflicts; create separate lists like sync_urlpatterns (containing paths
referencing DeviceLibreNMSSyncView, DeviceInterfaceTableView,
SyncInterfacesView, SyncCablesView, SyncIPAddressesView, SyncVLANsView, etc.),
import_urlpatterns (LibreNMSImportView, BulkImportDevicesView,
BulkImportConfirmView, DeviceValidationDetailsView, DeviceVCDetailsView,
DeviceRoleUpdateView, DeviceClusterUpdateView, DeviceRackUpdateView,
DeviceConflictActionView), mapping_urlpatterns (InterfaceTypeMappingListView,
DeviceTypeMappingListView, ModuleTypeMappingListView, ModuleBayMappingListView,
NormalizationRuleListView and their detail/create/edit/delete/changelog
counterparts), settings_urlpatterns (LibreNMSSettingsView,
TestLibreNMSConnectionView, SaveUserPrefView, DeviceStatusListView,
VMStatusListView), and any other logical groups (vm_urlpatterns,
device_update_urlpatterns); then replace the big urlpatterns with a single
concatenation like urlpatterns = sync_urlpatterns + import_urlpatterns +
mapping_urlpatterns + settings_urlpatterns + [...] ensuring existing path()
entries and names (e.g., "device_librenms_sync", "librenms_import",
"interfacetypemapping_list") are moved unchanged so imports and reverse URL
lookups remain intact.
---
Duplicate comments:
In `@netbox_librenms_plugin/__init__.py`:
- Around line 82-85: The guard flag _executed on
_ensure_librenms_id_custom_field is being set before the try/import/DB work,
causing failures to suppress future post_migrate runs; move setting
_ensure_librenms_id_custom_field._executed = True to the end of the function
after the custom-field creation has completed successfully (i.e., inside the try
block after all imports and DB operations succeed), and mirror the same change
for the similar block referenced at lines 131-135 so the flag is only set on
successful completion.
In `@netbox_librenms_plugin/filters.py`:
- Around line 46-53: The manufacturer FK filter is currently implicit;
explicitly declare it in NormalizationRuleFilterSet by adding a
django_filters.ModelChoiceFilter named manufacturer (e.g., manufacturer =
django_filters.ModelChoiceFilter(field_name="manufacturer",
queryset=Manufacturer.objects.all())) and import the Manufacturer model (or
derive it via NormalizationRule._meta.get_field('manufacturer').related_model)
so the filter behavior is explicit and consistent with other forms.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/_module_sync_content.html`:
- Around line 1-31: This fragment that renders the HTMX-swapped module sync
content (checks module_sync.table, uses module_sync.cache_expiry and elements
with ids module-cache-countdown and module-countdown-timer) should be moved from
templates/netbox_librenms_plugin/ to the HTMX templates directory; relocate the
file into templates/netbox_librenms_plugin/htmx/ and update any render/include
references that point to netbox_librenms_plugin/_module_sync_content.html to the
new path (e.g., netbox_librenms_plugin/htmx/_module_sync_content.html) so HTMX
responses follow the plugin's HTMX template organization and the target wiring
remains consistent.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Line 23: Remove the Bootstrap-specific data-bs-dismiss="modal" from the
<button class="btn-close"> elements and replace it with the plugin's HTMX
modal-close pattern so the wrapper JS (in librenms_import.html) is used to close
the modal; specifically, ensure the close buttons target the htmx-modal-content
element (by adding the project's modal-close attribute/class used elsewhere in
the plugin) and apply the same change to the other close button occurrences
mentioned.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.html`:
- Line 26: The template include currently references
'netbox_librenms_plugin/_module_sync_content.html' which will fail resolution;
update the include in the include statement to point to the inc/ partial (i.e.,
'netbox_librenms_plugin/inc/_module_sync_content.html') so the reusable partial
in the inc/ subdirectory is used; change the include line that mentions
_module_sync_content.html accordingly.
In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 56-67: The loop building local_ports_map currently drops rows when
port_name is None; modify the logic in the block that uses interface_name_field
(the code around get_interface_name_field, get_ports_data and local_ports_map)
to attempt a fallback lookup using port.get("ifName") before skipping the row,
and only continue if both the resolved field and the "ifName" fallback are
missing; ensure the final mapping uses the fallback value when used.
In `@netbox_librenms_plugin/views/base/modules_view.py`:
- Around line 93-96: The cache lookup in modules_view.py wrongly treats falsy
cached values (like an intentionally cached empty list) as a miss; update the
check in the code that calls cache.get(self.get_cache_key(obj, "inventory")) so
it only treats a None return as a cache miss (e.g., change the condition from
"if not cached_data" to "if cached_data is None"), then pass the cached value
into self._build_context(request, obj, cached_data) so empty inventories are
handled as valid cache hits; references: get_cache_key and _build_context in the
modules view.
- Around line 516-527: The comparison uses expected_fpc (string from
match.group(1)) against parent_bay.position (numeric); convert expected_fpc to
an integer before comparing and handle parse errors gracefully. Locate the block
that defines expected_fpc = match.group(1) and replace the direct comparison
with code that attempts to cast expected_fpc = int(expected_fpc) in a try/except
(or equivalent), returning True on ValueError/TypeError to preserve current
behavior, then return parent_bay.position == expected_fpc; keep the surrounding
checks for bay/module/module_bay unchanged.
- Around line 994-1013: The parent-module resolution is too restrictive: remove
the module_id__isnull=True constraint so ModuleBay queries consider
module-scoped bays when discovering a parent (adjust both the initial
device_bays query that builds device_bays and the later ModuleBay lookup after
finding a ModuleBayMapping), and make the mapping lookup require exact matches
by adding is_regex=False to the ModuleBayMapping filter (i.e.
filter(librenms_name=name, is_regex=False)). Ensure you still use
select_related("installed_module") when loading bays so installed_module.pk can
be returned.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 910-913: Several HttpResponse() calls in
netbox_librenms_plugin/views/imports/actions.py return HTMX-injected HTML built
with f-strings containing untrusted values (e.g., incoming_serial,
id_conflict.name, librenms_os, hardware, action) which can lead to XSS; replace
those raw f-strings with Django-safe interpolations using
django.utils.html.format_html() or escape() (e.g., format_html("... ID {0} ...",
escape(incoming_serial))) for each HttpResponse that currently injects these
variables (the LibreNMS ID conflict message and the other occurrences around the
reported ranges), ensuring the values are escaped before insertion and
preserving content_type='text/html' if needed.
- Around line 857-880: After calling get_validated_device_with_selections(...)
and before mutating existing_device, ensure you enforce object-level
authorization and bind the action to the validated target: verify that
int(existing_device_id) equals libre_device.pk or is one of the devices in
selections (reject with 403 if not), and then perform the mixin's object-level
write permission check against that existing_device (use the project’s existing
object-permission helper from LibreNMSPermissionMixin or equivalent) before
making any changes in DeviceConflictActionView.post.
In `@netbox_librenms_plugin/views/sync/cables.py`:
- Around line 128-137: The except Exception handler is currently appending
unexpected runtime failures to results["invalid"], which conflates true "invalid
link data" with runtime errors; update the initial results dict to include a
separate bucket (e.g., "error" or "failed") and change the except Exception
blocks around the calls to process_single_interface (the for-loop that wraps
process_single_interface and the similar block later) to append the interface
identifier to that new bucket instead of "invalid", while keeping
logger.exception as-is so unexpected exceptions are reported but not
misclassified as invalid link data.
In `@netbox_librenms_plugin/views/sync/interfaces.py`:
- Around line 244-246: The MAC address sync can raise AttributeError for
virtualization.VMInterface because handle_mac_address() expects dcim.Interface
attributes; wrap the existing block that reads ifPhysAddress and calls
self.handle_mac_address(interface, ifPhysAddress) with the same
is_device_interface guard used for ifType (i.e., only call handle_mac_address
when is_device_interface(interface) is True and "mac_address" not in
exclude_columns), ensuring you reference librenms_interface.get("ifPhysAddress")
and the handle_mac_address method.
In `@tests/e2e/test_module_install.py`:
- Around line 115-120: The fixture logs in using pg.goto/fill/click but doesn't
assert login succeeded, causing later tests to fail cryptically; update the page
fixture (the code using pg, NETBOX_URL, NETBOX_USER, NETBOX_PASS) to verify
post-login success before yielding by waiting for and asserting a reliable
post-login indicator (e.g., wait_for_selector or check pg.url contains the
dashboard or a logout element) and surface a clear error if authentication
failed so tests stop early with a meaningful message.
- Around line 36-69: The Docker helper functions _get_container and
_netbox_shell can hang or fail silently; update their subprocess.run calls to
include a timeout (e.g., timeout=30) and handle non-zero exit codes by either
using check=True or inspecting result.returncode and raising a clear exception
that includes result.stderr and result.stdout; in _get_container also guard
against empty stdout when splitting container names before searching for
"devcontainer-devcontainer" and call pytest.skip only after verifying no
container found. Ensure the env, capture_output, and text arguments remain and
propagate stderr in the raised error message for easier debugging.
- Around line 133-139: Replace hard sleeps with deterministic Playwright waits:
instead of time.sleep(...) around interactions that use page.query_selector and
btn.click, wait for the UI state change you expect (e.g., use
page.wait_for_selector('button:has-text("Refresh Modules")') before clicking,
and after clicking use page.wait_for_selector or page.wait_for_response or
page.wait_for_load_state to wait for the modules list/update indicator to appear
or finish). Update each occurrence (the calls around page.query_selector,
btn.click and the other listed sleep sites) to use the appropriate Playwright
wait method (page.wait_for_selector, page.wait_for_response,
page.wait_for_load_state, or expect(...).to_be_visible) targeting the specific
selector or network response that indicates completion rather than time.sleep.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (79)
.devcontainer/README.md.devcontainer/scripts/diagnose.sh.devcontainer/scripts/load-aliases.sh.devcontainer/scripts/setup.sh.devcontainer/scripts/start-netbox.sh.devcontainer/scripts/welcome.sh.github/workflows/lint-format.yamlcontrib/README.mdcontrib/device_type_mappings.yamlcontrib/interface_name_rules.yamlcontrib/interface_type_mappings.yamlcontrib/module_bay_mappings.yamlcontrib/module_type_mappings.yamlcontrib/normalization_rules.yamldocs/usage_tips/custom_field.mddocs/usage_tips/permissions.mdnetbox_librenms_plugin/__init__.pynetbox_librenms_plugin/api/serializers.pynetbox_librenms_plugin/api/urls.pynetbox_librenms_plugin/api/views.pynetbox_librenms_plugin/filters.pynetbox_librenms_plugin/forms.pynetbox_librenms_plugin/import_utils.pynetbox_librenms_plugin/import_utils/__init__.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/permissions.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/import_utils/vm_operations.pynetbox_librenms_plugin/librenms_api.pynetbox_librenms_plugin/migrations/0009_add_devicetypemapping.pynetbox_librenms_plugin/migrations/0010_add_moduletypemapping.pynetbox_librenms_plugin/migrations/0011_modulebaymapping.pynetbox_librenms_plugin/migrations/0012_add_is_regex_to_modulebaymapping.pynetbox_librenms_plugin/migrations/0013_normalizationrule.pynetbox_librenms_plugin/models.pynetbox_librenms_plugin/navigation.pynetbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_sync.jsnetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tables/interfaces.pynetbox_librenms_plugin/tables/mappings.pynetbox_librenms_plugin/tables/modules.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/_module_sync_content.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/devicetypemapping.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/devicetypemapping_list.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/inc/_module_sync.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/modulebaymapping.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/modulebaymapping_list.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/moduletypemapping.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/moduletypemapping_list.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/normalizationrule.htmlnetbox_librenms_plugin/templates/netbox_librenms_plugin/normalizationrule_list.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_init.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/tests/test_utils.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/base/modules_view.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/mapping_views.pynetbox_librenms_plugin/views/object_sync/__init__.pynetbox_librenms_plugin/views/object_sync/devices.pynetbox_librenms_plugin/views/object_sync/vms.pynetbox_librenms_plugin/views/sync/cables.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/sync/devices.pynetbox_librenms_plugin/views/sync/interfaces.pytests/e2e/__init__.pytests/e2e/conftest.pytests/e2e/test_module_install.py
💤 Files with no reviewable changes (1)
- netbox_librenms_plugin/import_utils.py
| The Plugin also enforces Netbox object permissions so the following permission would also be required: | ||
|
|
||
| 2. **Tier 2: Object permission**: User needs `dcim.add_device` (to create the device in NetBox) | ||
| 1. **Tier 2: Object permission**: User needs `dcim.add_device` (to create the device in NetBox) |
There was a problem hiding this comment.
Numbering inconsistency in the permission tiers list.
Line 22 shows "1. Tier 1: Plugin permission" and this line shows "1. Tier 2: Object permission". Both items being numbered "1." breaks the list structure. This should likely remain "2." to properly continue the numbered list describing the two-tier permission model.
📝 Suggested fix
-1. **Tier 2: Object permission**: User needs `dcim.add_device` (to create the device in NetBox)
+2. **Tier 2: Object permission**: User needs `dcim.add_device` (to create the device in NetBox)📝 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.
| 1. **Tier 2: Object permission**: User needs `dcim.add_device` (to create the device in NetBox) | |
| 2. **Tier 2: Object permission**: User needs `dcim.add_device` (to create the device in NetBox) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/usage_tips/permissions.md` at line 29, Fix the numbering inconsistency
in the permissions list: change the second list item that currently reads "1.
**Tier 2: Object permission**: User needs `dcim.add_device`..." to use "2." so
the ordered list correctly continues after "1. **Tier 1: Plugin permission**".
Ensure the displayed text "Tier 2: Object permission" and permission
`dcim.add_device` remain unchanged aside from the numeric prefix.
| librenms_name = forms.CharField(required=False, label="LibreNMS Name") | ||
| librenms_class = forms.CharField(required=False, label="LibreNMS Class") | ||
| netbox_bay_name = forms.CharField(required=False, label="NetBox Bay Name") | ||
| is_regex = forms.NullBooleanField(required=False, label="Regex") |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Version constraints (pyproject.toml, requirements, etc.) =="
find . -maxdepth 2 -type f \( -name "pyproject.toml" -o -name "requirements*.txt" -o -name "setup.py" -o -name "setup.cfg" -o -name "poetry.lock" \) | head -20
echo
echo "== Checking pyproject.toml for Django/NetBox versions =="
if [ -f "pyproject.toml" ]; then
cat pyproject.toml
fi
echo
echo "== Checking setup.py for Django/NetBox versions =="
if [ -f "setup.py" ]; then
cat setup.py
fiRepository: marcinpsk/netbox-librenms-plugin
Length of output: 1939
🏁 Script executed:
echo "== NullBooleanField usages in repository =="
rg -n -C2 'NullBooleanField' --type pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 554
🏁 Script executed:
echo "== Check Django version imports/comments =="
rg -n 'django.*version|Django.*version|DJANGO_VERSION' --type pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 119
🏁 Script executed:
echo "== Check README for NetBox/Django version requirements =="
if [ -f "README.md" ]; then
head -100 README.md
fi
echo
echo "== Check for NetBox version compatibility info =="
rg -n 'netbox|NetBox' --type md | head -20Repository: marcinpsk/netbox-librenms-plugin
Length of output: 6832
🏁 Script executed:
echo "== Check if there's any Django/NetBox import or version check =="
rg -n 'from django' netbox_librenms_plugin/forms.py | head -10
echo
echo "== Check full forms.py to understand context =="
head -50 netbox_librenms_plugin/forms.pyRepository: marcinpsk/netbox-librenms-plugin
Length of output: 1910
Consider replacing deprecated NullBooleanField with an alternative field for forward compatibility.
NullBooleanField is deprecated in Django 3.14 and removed in Django 4.0+. Since this plugin has no explicit Django version constraint in pyproject.toml, it may eventually run against a Django version that no longer provides this field. Replace with TypedChoiceField or BooleanField with required=False to future-proof the code.
Suggested replacement
- is_regex = forms.NullBooleanField(required=False, label="Regex")
+ is_regex = forms.TypedChoiceField(
+ required=False,
+ label="Regex",
+ choices=(("", "---------"), ("true", "Yes"), ("false", "No")),
+ coerce=lambda v: {"true": True, "false": False}.get(v, None),
+ empty_value=None,
+ )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/forms.py` at line 397, The form field is using the
deprecated NullBooleanField for is_regex; replace it with a forward-compatible
alternative such as forms.TypedChoiceField(...) or
forms.BooleanField(required=False) to avoid breakage in Django >=4.0—update the
is_regex declaration in the form (look for the is_regex assignment in the form
class) to use TypedChoiceField with explicit coerce and choices to represent
None/True/False or use BooleanField(required=False) if tri-state is not needed,
and adjust any form cleaning/consumption code that expects None/True/False
accordingly (e.g., clean_is_regex or places referencing
form.cleaned_data['is_regex']).
| convert_speed_to_kbps, | ||
| format_mac_address, | ||
| get_interface_name_field, | ||
| get_librenms_device_id, |
There was a problem hiding this comment.
Use LibreNMSAPI.get_librenms_id here instead of direct custom-field helper access.
Line 365 now resolves IDs via get_librenms_device_id(...), but this flow bypasses the required API accessor path defined for librenms_id mapping retrieval. Please switch this call to LibreNMSAPI.get_librenms_id(...).
As per coding guidelines: "Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly".
Also applies to: 365-365
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/tables/interfaces.py` at line 16, Replace direct use
of the helper get_librenms_device_id(...) with the official accessor
LibreNMSAPI.get_librenms_id(...) in the interfaces resolution flow (where
get_librenms_device_id is currently invoked); update imports to remove the
direct helper and ensure LibreNMSAPI is imported/available, call the API
accessor with the same arguments and handle its return value identically
(including None/exception cases) so the librenms_id mapping is retrieved via
LibreNMSAPI.get_librenms_id rather than touching the custom-field helper
directly.
| return format_html( | ||
| '<span class="badge {}" title="{}">{}</span>' | ||
| ' <i class="mdi mdi-alert-outline text-warning" title="{}"></i>', | ||
| badge_class, | ||
| warning, | ||
| value, | ||
| "Upgrade NetBox to fully support {module_path}", | ||
| ) |
There was a problem hiding this comment.
Fix the unrendered {module_path} placeholder in the warning tooltip.
At Line 127, the icon title is a literal string ("Upgrade NetBox to fully support {module_path}"), so users won’t see contextual detail.
💡 Suggested fix
if warning := record.get("module_path_warning"):
return format_html(
'<span class="badge {}" title="{}">{}</span>'
' <i class="mdi mdi-alert-outline text-warning" title="{}"></i>',
badge_class,
warning,
value,
- "Upgrade NetBox to fully support {module_path}",
+ warning,
)📝 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.
| return format_html( | |
| '<span class="badge {}" title="{}">{}</span>' | |
| ' <i class="mdi mdi-alert-outline text-warning" title="{}"></i>', | |
| badge_class, | |
| warning, | |
| value, | |
| "Upgrade NetBox to fully support {module_path}", | |
| ) | |
| return format_html( | |
| '<span class="badge {}" title="{}">{}</span>' | |
| ' <i class="mdi mdi-alert-outline text-warning" title="{}"></i>', | |
| badge_class, | |
| warning, | |
| value, | |
| warning, | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/tables/modules.py` around lines 121 - 128, The tooltip
text currently contains a literal "{module_path}" instead of the actual
module_path value; update the format_html call so the final string uses a
placeholder consumed by format_html and pass module_path as the last argument
(i.e., change the title argument from the literal "Upgrade NetBox to fully
support {module_path}" to a format placeholder like "Upgrade NetBox to fully
support {}" and pass module_path alongside badge_class, warning, value) so the
module_path variable is rendered into the icon title; refer to the variables
badge_class, warning, value and module_path in the format_html call.
| def test_exception_does_not_propagate(self, MockCustomField): | ||
| """Exceptions during custom field creation are caught and logged.""" | ||
| from netbox_librenms_plugin import _ensure_librenms_id_custom_field | ||
|
|
||
| MockCustomField.objects.get_or_create.side_effect = Exception("DB not ready") | ||
|
|
||
| with patch("logging.getLogger") as mock_get_logger: | ||
| # Should not raise | ||
| _ensure_librenms_id_custom_field(sender=None) | ||
|
|
||
| # Verify the exception was logged | ||
| logger_instance = mock_get_logger.return_value | ||
| logger_instance.exception.assert_called_once() | ||
| call_args = logger_instance.exception.call_args | ||
| assert "librenms_id" in call_args[0][0] | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add a retry-semantics assertion after exception handling.
test_exception_does_not_propagate should also assert _ensure_librenms_id_custom_field._executed state after failure, so one-shot failure behavior can’t regress silently.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/tests/test_init.py` around lines 129 - 144, In the
test_exception_does_not_propagate test, after invoking
_ensure_librenms_id_custom_field(sender=None) and verifying logger.exception was
called, add an assertion checking the function-level flag
_ensure_librenms_id_custom_field._executed to ensure it reflects the one-shot
execution state (e.g., assert _ensure_librenms_id_custom_field._executed is
True) so failed attempts don't silently allow retries.
- Add migrate_legacy_librenms_id(obj, server_key) helper in utils.py
Converts a bare-integer librenms_id to {server_key: int_value}.
Returns True if migration happened, False if already JSON or absent.
Does not call save() — caller is responsible.
- Detect legacy int format in validate_device_for_import()
When find_by_librenms_id matches a Device or VM whose librenms_id CF
is still a bare integer, set result['librenms_id_needs_migration']=True
so the import page can surface a migration action.
- Add 'migrate_librenms_id' action in DeviceConflictActionView
Verifies CF is still an int, requires serial_confirmed or force checkbox,
calls migrate_legacy_librenms_id() + save().
- Fix collision check to use find_by_librenms_id instead of raw queryset
The old Device.objects.filter(custom_field_data__librenms_id=int(...))
only matched the legacy integer format; the new helper matches both
the JSON dict format and the legacy format.
- Show 'Legacy ID format' badge + 'Migrate ID format' button in template
Appears in the existing_match_type=='librenms_id' section when
librenms_id_needs_migration is set. Button is disabled (requires force
checkbox) when serial not confirmed.
- Add TestLegacyLibreNMSIdMigration test class (6 tests)
- Update TestDeviceConflictActionView mocks for new filter().first() chain
left a comment
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
netbox_librenms_plugin/views/imports/actions.py (2)
867-880:⚠️ Potential issue | 🔴 CriticalBind conflict mutations to the validated target and enforce object-level authorization.
Line 868 accepts
existing_device_idfrom POST, and Line 874 loads/mutates that record without verifying it matchesvalidation["existing_device"]. A crafted request can target an unrelated device.🛡️ Proposed fix
libre_device, validation, selections = self.get_validated_device_with_selections(device_id, request) if not libre_device: return HttpResponse("LibreNMS device not found", status=404) + + expected_existing = validation.get("existing_device") + if not expected_existing or expected_existing.pk != existing_device.pk: + return HttpResponse("existing_device_id does not match detected conflict target", status=400) + if not request.user.has_perm("dcim.change_device", existing_device): + return HttpResponse("Permission denied", status=403)🤖 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 867 - 880, The code currently loads an arbitrary Device by POSTed existing_device_id and mutates it without ensuring it matches the validated target returned by get_validated_device_with_selections; instead, bind mutations to the validated object and enforce object-level authorization by either (a) use the validated device from validation["existing_device"] (or replace existing_device with that object) rather than reloading from existing_device_id, or (b) if you must reload, verify int(existing_device_id) == validation["existing_device"].pk and return 403 on mismatch; then run the view's object-level permission check (e.g., call your permission check helper or use request.user.has_perm / self.check_object_permissions against the resolved Device) before performing any mutations so only authorized users can modify it.
909-911:⚠️ Potential issue | 🟠 MajorEscape interpolated values in HTMX HTML responses to prevent XSS.
Lines 909, 950, 972, 1023, 1045, 1060, and 1087 interpolate untrusted values (
incoming_serial, device names,librenms_os,hardware,action) directly into HTML response bodies.🔒 Proposed fix
from django.http import HttpResponse, JsonResponse from django.shortcuts import redirect, render +from django.utils.html import escape @@ - f"'{id_conflict.name}' (ID: {id_conflict.pk})", + f"'{escape(id_conflict.name)}' (ID: {id_conflict.pk})", @@ - f"Serial conflict: '{incoming_serial}' is already assigned to device " - f"'{conflict_device.name}' (ID: {conflict_device.pk})", + f"Serial conflict: '{escape(incoming_serial)}' is already assigned to device " + f"'{escape(conflict_device.name)}' (ID: {conflict_device.pk})", @@ - return HttpResponse(f"Platform '{librenms_os}' not found in NetBox", status=400) + return HttpResponse(f"Platform '{escape(librenms_os)}' not found in NetBox", status=400) @@ - return HttpResponse(f"No matching device type for '{hardware}'", status=400) + return HttpResponse(f"No matching device type for '{escape(hardware)}'", status=400) @@ - return HttpResponse(f"Unknown action: {action}", status=400) + return HttpResponse(f"Unknown action: {escape(action)}", status=400)Also applies to: 950-952, 972-974, 1023-1025, 1045-1046, 1060-1061, 1087-1087
🤖 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 909 - 911, The HTMX HTML responses interpolate untrusted values (incoming_serial, id_conflict.name, librenms_os, hardware, action and device names) directly into f-strings which can lead to XSS; import and use django.utils.html.escape (or mark_safe only for trusted content) to escape each of those variables before embedding them in the response strings in the actions that build HttpResponse/JsonResponse HTML (the blocks that currently build messages like "LibreNMS ID conflict: ID {librenms_id} ... '{id_conflict.name}'" and the other similar f-strings); replace the raw interpolations with escaped versions (e.g., escape(incoming_serial), escape(id_conflict.name), escape(librenms_os), escape(hardware), escape(action)) so all dynamic values are HTML-escaped in the HTMX responses.netbox_librenms_plugin/import_utils/device_operations.py (1)
271-272:⚠️ Potential issue | 🟠 MajorUse one VM-mode source of truth after existing VM detection.
Line 271/358 force
result["import_as_vm"] = True, but Lines 441, 565, 612, and 625 still branch/log using the originalimport_as_vmparameter. That can run device-only validation and VC logic for a VM match.🔧 Proposed fix
- # Validate based on import type (Device or VM) - if import_as_vm: + # Validate based on effective import type (may be forced to VM when existing VM is found) + effective_import_as_vm = result["import_as_vm"] + if effective_import_as_vm: @@ - if serial and serial != "-" and not import_as_vm: + if serial and serial != "-" and not effective_import_as_vm: @@ - if primary_ip and not import_as_vm: + if primary_ip and not effective_import_as_vm: @@ - if include_vc_detection and not import_as_vm and api is not None: + if include_vc_detection and not effective_import_as_vm and api is not None: @@ - if import_as_vm: + if effective_import_as_vm: @@ - f"Validation for {libre_device.get('hostname')} ({'VM' if import_as_vm else 'Device'}): " + f"Validation for {libre_device.get('hostname')} ({'VM' if effective_import_as_vm else 'Device'}): "Also applies to: 358-359, 441-442, 565-566, 612-625
🤖 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 271 - 272, After forcing result["import_as_vm"] = True when an existing VM is detected, introduce a single source-of-truth boolean (e.g., effective_import_as_vm = result.get("import_as_vm", import_as_vm)) and replace all subsequent uses of the original import_as_vm parameter (branches, validations, logging, VC logic) to reference effective_import_as_vm instead; update any log messages to report effective_import_as_vm so device-only validation and VC logic follow the corrected VM-mode decision consistently across the function.
🤖 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/tests/test_import_utils.py`:
- Around line 2216-2223: The test is patching dcim.models.Platform but the code
under test imports and calls netbox_librenms_plugin.utils.find_matching_platform
at runtime; update the test to patch find_matching_platform (e.g.,
patch("netbox_librenms_plugin.utils.find_matching_platform")) instead of
patching Platform so the sync-platform branch uses the mocked result; keep the
other mocks (DeviceConflictActionView.get_validated_device_with_selections,
DeviceConflictActionView.render_device_row, dcim.models.Device) and have the new
find_matching_platform mock return the desired mock_platform value used in the
assertion.
In `@netbox_librenms_plugin/utils.py`:
- Around line 636-649: The function has_nested_name_conflict currently only
checks for MODULE_TOKEN usage and will flag a conflict even when templates use
MODULE_PATH_TOKEN (which yields unique names); update the token checks in
has_nested_name_conflict to detect both tokens: compute uses_name_token =
any(MODULE_TOKEN in t.name for t in templates) and uses_path_token =
any(MODULE_PATH_TOKEN in t.name for t in templates); if neither token is present
return False; if only MODULE_PATH_TOKEN is present return False (no conflict);
only proceed to count ModuleBayModel siblings (using ModuleBayModel, module_bay,
module_id) and return sibling_count > 1 when MODULE_TOKEN is used (i.e.,
uses_name_token is True).
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 906-907: The current collision check uses find_by_librenms_id(...)
and then ignores the match if it equals existing_device, which misses other
duplicates; change the logic to detect any other Device with the same
librenms_id and server key (e.g., query Device for librenms_id and the server
key and exclude existing_device.pk) and treat existence as a collision. Either
adjust find_by_librenms_id to return a queryset or add an exclusion-aware check
(use .exclude(pk=existing_device.pk).exists()) so multiple conflicting devices
are caught.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 271-272: After forcing result["import_as_vm"] = True when an
existing VM is detected, introduce a single source-of-truth boolean (e.g.,
effective_import_as_vm = result.get("import_as_vm", import_as_vm)) and replace
all subsequent uses of the original import_as_vm parameter (branches,
validations, logging, VC logic) to reference effective_import_as_vm instead;
update any log messages to report effective_import_as_vm so device-only
validation and VC logic follow the corrected VM-mode decision consistently
across the function.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 867-880: The code currently loads an arbitrary Device by POSTed
existing_device_id and mutates it without ensuring it matches the validated
target returned by get_validated_device_with_selections; instead, bind mutations
to the validated object and enforce object-level authorization by either (a) use
the validated device from validation["existing_device"] (or replace
existing_device with that object) rather than reloading from existing_device_id,
or (b) if you must reload, verify int(existing_device_id) ==
validation["existing_device"].pk and return 403 on mismatch; then run the view's
object-level permission check (e.g., call your permission check helper or use
request.user.has_perm / self.check_object_permissions against the resolved
Device) before performing any mutations so only authorized users can modify it.
- Around line 909-911: The HTMX HTML responses interpolate untrusted values
(incoming_serial, id_conflict.name, librenms_os, hardware, action and device
names) directly into f-strings which can lead to XSS; import and use
django.utils.html.escape (or mark_safe only for trusted content) to escape each
of those variables before embedding them in the response strings in the actions
that build HttpResponse/JsonResponse HTML (the blocks that currently build
messages like "LibreNMS ID conflict: ID {librenms_id} ... '{id_conflict.name}'"
and the other similar f-strings); replace the raw interpolations with escaped
versions (e.g., escape(incoming_serial), escape(id_conflict.name),
escape(librenms_os), escape(hardware), escape(action)) so all dynamic values are
HTML-escaped in the HTMX responses.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
netbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_import_utils.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (18)
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
🧬 Code graph analysis (3)
netbox_librenms_plugin/utils.py (1)
netbox_librenms_plugin/models.py (2)
DeviceTypeMapping(84-113)NormalizationRule(205-279)
netbox_librenms_plugin/import_utils/device_operations.py (6)
netbox_librenms_plugin/librenms_api.py (3)
LibreNMSAPI(15-1084)get_inventory_filtered(771-848)get_device_info(314-337)netbox_librenms_plugin/utils.py (2)
find_matching_platform(404-434)find_by_librenms_id(67-86)netbox_librenms_plugin/import_utils/cache.py (1)
get_import_device_cache_key(139-158)netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
empty_virtual_chassis_data(15-23)get_virtual_chassis_data(60-80)netbox_librenms_plugin/views/imports/actions.py (2)
get(707-727)get(733-757)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(25-46)
netbox_librenms_plugin/tests/test_import_utils.py (2)
netbox_librenms_plugin/import_utils/device_operations.py (1)
validate_device_for_import(117-635)netbox_librenms_plugin/utils.py (1)
migrate_legacy_librenms_id(89-119)
🔇 Additional comments (5)
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (5)
23-23: Modal close button uses Bootstrap dismissal instead of HTMX-targeted close.This close button uses
data-bs-dismiss="modal"which conflicts with the HTMX/Tabler modal pattern. It should target#htmx-modal-contentper the project guidelines.
40-79: LGTM!The LibreNMS Status card is clean and well-structured with proper status badge coloring and consistent table formatting.
86-121: LGTM!The Device Information table with per-field actions is well-implemented. The HTMX forms correctly use
hx-swap="none"and include CSRF tokens. The conditional inclusion ofhx-includefor name-related toggles aligns with action-specific requirements.
359-406: LGTM!The validation state banners and migration flow are well-implemented with proper conditional rendering and inline form controls. The force-checkbox pattern with JavaScript-enabled submit buttons provides appropriate guardrails for risky operations.
588-590: Footer close button also uses Bootstrap dismissal.Same issue as the header close button—this should target
#htmx-modal-contentrather than relying ondata-bs-dismiss="modal".
| uses_module_token = any(MODULE_TOKEN in t.name for t in templates) | ||
| if not uses_module_token: | ||
| return False # Template doesn't use {module} | ||
|
|
||
| # Count how many unique interface names this template would produce across siblings | ||
| # If all siblings resolve to the same name, there's a conflict | ||
| from dcim.models import ModuleBay as ModuleBayModel | ||
|
|
||
| sibling_count = ModuleBayModel.objects.filter( | ||
| device=module_bay.device, | ||
| module_id=module_bay.module_id, | ||
| ).count() | ||
|
|
||
| return sibling_count > 1 |
There was a problem hiding this comment.
has_nested_name_conflict() can falsely block valid nested installs when {module_path} is present.
Line 636 only checks for {module} token usage. If templates include {module_path}, names can remain unique, but this branch still flags a conflict based on sibling count.
🔧 Proposed fix
def has_nested_name_conflict(module_type, module_bay):
@@
uses_module_token = any(MODULE_TOKEN in t.name for t in templates)
if not uses_module_token:
return False # Template doesn't use {module}
+ # If {module_path} is used, nested sibling names can still resolve uniquely.
+ if module_type_uses_module_path(module_type):
+ return False
# Count how many unique interface names this template would produce across siblings🤖 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 636 - 649, The function
has_nested_name_conflict currently only checks for MODULE_TOKEN usage and will
flag a conflict even when templates use MODULE_PATH_TOKEN (which yields unique
names); update the token checks in has_nested_name_conflict to detect both
tokens: compute uses_name_token = any(MODULE_TOKEN in t.name for t in templates)
and uses_path_token = any(MODULE_PATH_TOKEN in t.name for t in templates); if
neither token is present return False; if only MODULE_PATH_TOKEN is present
return False (no conflict); only proceed to count ModuleBayModel siblings (using
ModuleBayModel, module_bay, module_id) and return sibling_count > 1 when
MODULE_TOKEN is used (i.e., uses_name_token is True).
… accessibility - device_operations.py: preserve device_type schema dict instead of replacing with raw dt_match; 'found' key is now always set regardless of match result - device_operations.py: use api.server_key instead of server_key or 'default' to consistently use the resolved server key after LibreNMSAPI instantiation - device_operations.py: move Q import to module level; remove inline 'from dcim.models import Rack' so test mock patching works correctly - bulk_import.py: fix 7 early-return sites to return ([], False) when return_cache_status=True, keeping the return shape consistent for callers - device_status.py: add 'Legacy ID' warning badge when librenms_id_needs_migration is set on an existing device matched by librenms_id - device_status.py: add aria-label to icon-only buttons for accessibility - device_validation_details.html: add rel='noopener noreferrer' to all target='_blank' anchors (8 links) - test_import_utils.py: add DeviceType to SERIAL_PATCHES so suggestions path is mocked; fix _create_view to use real instantiation; fix _setup_no_existing to use named variables instead of fragile negative indexing; remove dead mock_rack.objects.filter.return_value = [] lines (inline import bypass meant these were never effective); fix device_type assertions to use 'found' key
- import_utils/cache.py: guard datetime.fromisoformat with try/except fallback; replace hash() with deterministic sha256 for cross-process stable cache keys - import_utils/filters.py: replace hash() with deterministic sha256 for librenms_devices_import cache key; add hashlib/json imports - import_utils/permissions.py: remove unused logger and logging import - import_utils/virtual_chassis.py: fix blank serial skip logic - only skip member when serial is non-empty AND matches master serial; same fix in expected_members count - device_validation_details.html: add aria-label to 6 icon-only sync buttons for screen-reader accessibility - test_import_utils.py: replace fragile mocks[-2] indexing with named unpacking - views/imports/actions.py: import escape(); HTML-escape untrusted values (id_conflict.name, librenms_os, hardware, action) in HttpResponse f-strings - views/sync/interfaces.py: guard handle_mac_address with is_device_interface to avoid setting MAC on VMInterface objects - import_utils/device_operations.py: reassign import_as_vm from result dict after existing-object detection so downstream branches use effective VM mode
- __init__.py: move _executed = True to inside try block after successful custom field creation, so failures allow retry on next post_migrate - tests/test_init.py: assert _executed is False after exception in test_exception_does_not_propagate to verify retry behaviour - views/base/librenms_sync_view.py: remove redundant found_in_librenms = True inside mismatched_device block (already set on line 184 after API call)
commented
Mar 2, 2026
|
@coderabbitai review |
commented
Mar 2, 2026
✅ Actions performedReview triggered.
|
left a comment
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
netbox_librenms_plugin/import_utils/device_operations.py (1)
268-273:⚠️ Potential issue | 🟠 MajorUse a single VM-mode source of truth after existing-VM detection.
After VM mode is forced on Line 272 / Line 359, later branches still read the original
import_as_vmparameter (Line 399, Line 423, Line 442, Line 561, Line 608, Line 621). That can run device-only validation/VC paths for VM-linked records.🔧 Proposed fix
@@ - try: + try: + effective_import_as_vm = import_as_vm @@ - result["import_as_vm"] = True # Force VM mode since VM exists + result["import_as_vm"] = True # Force VM mode since VM exists + effective_import_as_vm = True @@ - result["import_as_vm"] = True # Force VM mode since VM exists + result["import_as_vm"] = True # Force VM mode since VM exists + effective_import_as_vm = True @@ - if serial and serial != "-" and not import_as_vm: + if serial and serial != "-" and not effective_import_as_vm: @@ - if primary_ip and not import_as_vm: + if primary_ip and not effective_import_as_vm: @@ - if import_as_vm: + if effective_import_as_vm: @@ - if include_vc_detection and not import_as_vm and api is not None: + if include_vc_detection and not effective_import_as_vm and api is not None: @@ - if import_as_vm: + if effective_import_as_vm: @@ - f"Validation for {libre_device.get('hostname')} ({'VM' if import_as_vm else 'Device'}): " + f"Validation for {libre_device.get('hostname')} ({'VM' if effective_import_as_vm else 'Device'}): "Also applies to: 359-360, 399-399, 423-423, 442-443, 561-562, 608-609, 621-621
🤖 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 268 - 273, After detecting an existing VM (existing_vm) you force VM mode by setting result["import_as_vm"]=True but later branches still read the original import_as_vm parameter; update the code so all subsequent checks use a single VM-mode source of truth (e.g., replace references to the original import_as_vm with result["import_as_vm"] or create a local vm_mode = result.get("import_as_vm", import_as_vm) and use vm_mode everywhere) including in the validation/VC paths that currently read import_as_vm (the branches around the usages indicated by existing_vm, and the later checks at the spots referenced in the review).netbox_librenms_plugin/tests/test_import_utils.py (1)
2198-2206:⚠️ Potential issue | 🟠 MajorPatch the runtime helper in
test_sync_platform_action.Line 2202 patches
dcim.models.Platform, but the sync-platform flow resolves platform through the matching helper. The test currently doesn’t control the dependency that actually executes.🔧 Proposed fix
with ( patch.object(DeviceConflictActionView, "get_validated_device_with_selections") as mock_validate, patch.object(DeviceConflictActionView, "render_device_row") as mock_render, patch("dcim.models.Device") as mock_device_cls, - patch("dcim.models.Platform") as mock_platform_cls, + patch("netbox_librenms_plugin.utils.find_matching_platform") as mock_find_platform, ): mock_device_cls.objects.get.return_value = existing_device - mock_platform_cls.objects.get.return_value = mock_platform + mock_find_platform.return_value = {"found": True, "platform": mock_platform, "match_type": "exact"} mock_validate.return_value = (libre_device, validation, selections) mock_render.return_value = MagicMock()Based on learnings: Patch deferred/inline imports at their source module (e.g.,
netbox_librenms_plugin.import_utils.process_device_filters), not the consuming module.
🤖 Prompt for 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 321-329: libre_device may lack _server_key so server_key can be
None and cause find_by_librenms_id to miss JSON-mapped records; ensure
server_key defaults to a non-None value before the librenms_id check by
replacing server_key = libre_device.get("_server_key") with something like
server_key = libre_device.get("_server_key") or "" (or another appropriate
fallback from your import context) so the call to find_by_librenms_id(Device,
int(librenms_id), server_key) always receives a valid string key.
- Around line 296-306: The code is forcing validation["can_import"]=True and
computing is_ready using device fields even for VMs; update the logic to mirror
validate_device_for_import: do not unconditionally set can_import True, instead
compute can_import/is_ready based on validation.get("import_as_vm")—if True
require site and cluster (and any other VM-specific checks from
validate_device_for_import), otherwise require site, device_type and
device_role; you can either call validate_device_for_import to derive these
flags or duplicate its exact checks using the validation keys ("import_as_vm",
"site", "cluster", "device_type", "device_role") so VM rows are only marked
ready when a cluster is found.
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 276-277: Replace direct reads of
existing_vm.custom_field_data["librenms_id"] with the centralized accessor
LibreNMSAPI.get_librenms_id(...) so validation follows the project's ID-access
contract; specifically, in device_operations.py where you set
result["librenms_id_needs_migration"] (and the similar checks around the related
block at the other occurrence noted), call
LibreNMSAPI.get_librenms_id(existing_vm) (or the appropriate instance/context)
and base the int-type check on its return value instead of reading
custom_field_data directly.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 111-113: Several icon-only <button> elements (e.g., the button
with class "btn btn-sm btn-outline-primary py-0 px-1" and title "Sync name to {{
validation.suggested_name }}") rely solely on title attributes which are not
reliably announced by screen readers; add aria-label attributes that match the
title text for each icon-only button in this template (including the other
instances with similar classes/icons), e.g., aria-label="Sync name to {{
validation.suggested_name }}", ensuring every icon-only button (sync/action
buttons with only an <i class="mdi ..."> inside) has a corresponding aria-label
that mirrors its title.
- Around line 45-76: The template device_validation_details.html repeats inline
style "padding-left: 0.75rem;" across several <th> (and some <td>) elements;
replace those inline styles with a reusable CSS class (e.g., add
class="tbl-cell-pl" or use an existing utility like "pl-3") on the <th> and <td>
elements in the table markup and add the CSS rule (e.g., .tbl-cell-pl {
padding-left: 0.75rem; }) to the plugin's stylesheet so all occurrences
(including the Status/Hostname/ID/IP/Location rows) use the class instead of
inline style.
---
Duplicate comments:
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 268-273: After detecting an existing VM (existing_vm) you force VM
mode by setting result["import_as_vm"]=True but later branches still read the
original import_as_vm parameter; update the code so all subsequent checks use a
single VM-mode source of truth (e.g., replace references to the original
import_as_vm with result["import_as_vm"] or create a local vm_mode =
result.get("import_as_vm", import_as_vm) and use vm_mode everywhere) including
in the validation/VC paths that currently read import_as_vm (the branches around
the usages indicated by existing_vm, and the later checks at the spots
referenced in the review).
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (6)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_init.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tests/test_init.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
🧠 Learnings (17)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/tables/device_status.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.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_init.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/device_operations.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_init.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances
Applied to files:
netbox_librenms_plugin/tests/test_init.pynetbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
🧬 Code graph analysis (4)
netbox_librenms_plugin/tables/device_status.py (1)
netbox_librenms_plugin/views/imports/actions.py (2)
get(707-727)get(733-757)
netbox_librenms_plugin/tests/test_init.py (1)
netbox_librenms_plugin/__init__.py (1)
_ensure_librenms_id_custom_field(71-134)
netbox_librenms_plugin/tests/test_import_utils.py (1)
netbox_librenms_plugin/utils.py (1)
migrate_legacy_librenms_id(89-119)
netbox_librenms_plugin/import_utils/bulk_import.py (7)
netbox_librenms_plugin/librenms_api.py (2)
LibreNMSAPI(15-1084)get_device_info(314-337)netbox_librenms_plugin/utils.py (1)
find_by_librenms_id(67-86)netbox_librenms_plugin/import_utils/cache.py (3)
get_cache_metadata_key(10-24)get_import_device_cache_key(139-158)get_validated_device_cache_key(112-136)netbox_librenms_plugin/import_utils/device_operations.py (2)
import_single_device(634-853)validate_device_for_import(118-631)netbox_librenms_plugin/import_utils/filters.py (1)
get_librenms_devices_for_import(43-213)netbox_librenms_plugin/import_utils/permissions.py (1)
require_permissions(31-48)netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
create_virtual_chassis_with_members(310-440)empty_virtual_chassis_data(15-23)prefetch_vc_data_for_devices(83-117)
🔇 Additional comments (6)
netbox_librenms_plugin/tests/test_init.py (1)
63-64: Good log verification depth.This now verifies the logger instance actually emitted
.info(), not just thatgetLogger()was called.netbox_librenms_plugin/tables/device_status.py (1)
491-503: Accessible icon-only button handling looks good.The conditional
aria-labelinjection for empty-label buttons improves screen-reader coverage while preserving existing title/HTMX behavior.netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (4)
23-23: Modal close buttons still usedata-bs-dismissinstead of targeting#htmx-modal-content.This was flagged in a previous review. The close buttons at lines 23 and 588 use Bootstrap-style dismissal which conflicts with the HTMX/Tabler modal pattern. As per coding guidelines, modal buttons should target the
htmx-modal-contentelement.Also applies to: 588-588
100-102: Good use ofrel="noopener noreferrer"on external links.All external links (
target="_blank") correctly include security attributes to prevent tabnabbing attacks.Also applies to: 422-423, 482-484, 519-521, 529-530, 571-578, 581-583
104-114: HTMX form patterns are appropriate.The
hx-swap="none"usage is correct for these action forms since they trigger server-side operations without needing to swap content. Thehx-includedirective properly references external toggle inputs.Also applies to: 151-162, 166-175, 206-215, 260-269, 284-293
6-21: Header icon state logic is clear and comprehensive.The conditional icon rendering covers all relevant validation states (device type mismatch, librenms_id match, existing device warnings, ready, can_import, blocked) with appropriate visual indicators.
| if isinstance(existing_vm.custom_field_data.get("librenms_id"), int): | ||
| result["librenms_id_needs_migration"] = True |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Stop reading custom_field_data["librenms_id"] directly in validation.
These checks bypass the project’s central ID-access contract and can drift from server-key handling rules. Route reads through the standard helper/API method for librenms_id access.
As per coding guidelines: Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly.
Also applies to: 303-304
🤖 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 276 -
277, Replace direct reads of existing_vm.custom_field_data["librenms_id"] with
the centralized accessor LibreNMSAPI.get_librenms_id(...) so validation follows
the project's ID-access contract; specifically, in device_operations.py where
you set result["librenms_id_needs_migration"] (and the similar checks around the
related block at the other occurrence noted), call
LibreNMSAPI.get_librenms_id(existing_vm) (or the appropriate instance/context)
and base the int-type check on its return value instead of reading
custom_field_data directly.
There was a problem hiding this comment.
direct read is required in this one place - we are detecting if we have legacy key saved and need to flag it for migration - or not. alternative would be to extend helper to flag records in this way (return as well librenms_id_needs_migration) - which seems like not really needed.
There was a problem hiding this comment.
@marcinpsk, that's a fair point! You're right that detecting the legacy format requires inspecting the raw field type. The helper function is designed to normalize access and hide that distinction, which is exactly what you don't want here.
This is a legitimate exception to the guideline—you need to inspect the storage format itself to determine if migration is needed, not just retrieve the value. The existing guideline still applies for normal data access throughout the codebase.
To make this intent clear to future maintainers, I'd suggest adding a brief comment above the direct access explaining why it's necessary:
# Direct access needed to detect legacy integer format for migration prompt
if isinstance(existing_vm.custom_field_data.get("librenms_id"), int):
result["librenms_id_needs_migration"] = TrueThis documents the exception while keeping the code straightforward.
🧠 Learnings used
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
| <table class="table table-sm mb-0"> | ||
| <tbody> | ||
| <tr> | ||
| <th style="padding-left: 0.75rem;">Status</th> | ||
| <td> | ||
| {% if libre_device.status == 1 %} | ||
| <span class="badge bg-success text-white"><i class="mdi mdi-check"></i> Up</span> | ||
| {% elif libre_device.status == 0 %} | ||
| <span class="badge bg-danger text-white"><i class="mdi mdi-close"></i> Down</span> | ||
| {% else %} | ||
| <span class="badge bg-secondary text-white"><i class="mdi mdi-help"></i> Unknown</span> | ||
| {% endif %} | ||
| </td> | ||
| </tr> | ||
| <tr> | ||
| <th style="padding-left: 0.75rem;">Hostname</th> | ||
| <td>{{ libre_device.hostname }}</td> | ||
| </tr> | ||
| <tr> | ||
| <th style="padding-left: 0.75rem;">ID</th> | ||
| <td>{{ libre_device.device_id }}</td> | ||
| </tr> | ||
| <tr> | ||
| <th style="padding-left: 0.75rem;">IP</th> | ||
| <td>{{ libre_device.ip|default:"—" }}</td> | ||
| </tr> | ||
| <tr> | ||
| <th style="padding-left: 0.75rem;">Location</th> | ||
| <td>{{ libre_device.location|default:"—" }}</td> | ||
| </tr> | ||
| </tbody> | ||
| </table> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider extracting repeated inline styles to CSS classes.
The style="padding-left: 0.75rem;" pattern appears on multiple <th> and <td> elements. Consider using a utility class or defining a reusable style to improve maintainability.
Also applies to: 86-93
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`
around lines 45 - 76, The template device_validation_details.html repeats inline
style "padding-left: 0.75rem;" across several <th> (and some <td>) elements;
replace those inline styles with a reusable CSS class (e.g., add
class="tbl-cell-pl" or use an existing utility like "pl-3") on the <th> and <td>
elements in the table markup and add the CSS rule (e.g., .tbl-cell-pl {
padding-left: 0.75rem; }) to the plugin's stylesheet so all occurrences
(including the Status/Hostname/ID/IP/Location rows) use the class instead of
inline style.
left a comment
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/jobs.py (1)
110-117:⚠️ Potential issue | 🟡 MinorPersist resolved server key in
job.data.Storing raw
server_keycan leaveNonein metadata. Persistapi.server_keyso downstream cache-key reconstruction is always consistent.🔧 Proposed fix
- "server_key": server_key, + "server_key": api.server_key, ... - "server_key": server_key, + "server_key": api.server_key,Also applies to: 244-250
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/jobs.py` around lines 110 - 117, The job currently stores the local variable server_key into self.job.data which can be None; change the assignments where job metadata is built (the self.job.data dict at the first block and the similar block around lines 244–250) to persist the resolved API key from the API client (use api.server_key) instead of server_key so downstream cache-key reconstruction uses the actual persisted key; update the dict key "server_key" to be assigned api.server_key in both places (and ensure api is in scope where you make the change).
♻️ Duplicate comments (2)
netbox_librenms_plugin/views/imports/actions.py (1)
1088-1107:⚠️ Potential issue | 🟠 Major
sync_serialstill performs conflict check and write outside a transaction.This leaves a race window where concurrent requests can both pass the conflict check and then write conflicting serial state.
🛡️ Proposed fix
elif action == "sync_serial": # Sync serial number from LibreNMS incoming_serial = libre_device.get("serial") or "" if incoming_serial and incoming_serial != "-": - # Check for serial ownership conflict - conflict_device = Device.objects.filter(serial=incoming_serial).exclude(pk=existing_device.pk).first() - if conflict_device: - logger.warning( - f"Serial sync blocked: '{incoming_serial}' already assigned to " - f"'{conflict_device.name}' (pk={conflict_device.pk})" - ) - return HttpResponse( - f"Serial conflict: '{escape(incoming_serial)}' is already assigned to device " - f"'{escape(conflict_device.name)}' (ID: {conflict_device.pk})", - status=409, - ) - existing_device.serial = incoming_serial - if err := _save_device(existing_device): - return err - logger.info(f"Synced serial on '{existing_device.name}' to {incoming_serial}") + with transaction.atomic(): + locked_device = Device.objects.select_for_update().get(pk=existing_device.pk) + conflict_device = ( + Device.objects.select_for_update() + .filter(serial=incoming_serial) + .exclude(pk=locked_device.pk) + .first() + ) + if conflict_device: + logger.warning( + f"Serial sync blocked: '{incoming_serial}' already assigned to " + f"'{conflict_device.name}' (pk={conflict_device.pk})" + ) + return HttpResponse( + f"Serial conflict: '{escape(incoming_serial)}' is already assigned to device " + f"'{escape(conflict_device.name)}' (ID: {conflict_device.pk})", + status=409, + ) + locked_device.serial = incoming_serial + if err := _save_device(locked_device): + return err + logger.info(f"Synced serial on '{locked_device.name}' to {incoming_serial}")🤖 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 1088 - 1107, The sync_serial branch currently checks for serial conflicts and writes outside a transaction, allowing a race; wrap the conflict-check-and-update in a DB transaction (e.g., transaction.atomic) and acquire appropriate row locks (use select_for_update on the target existing_device and/or query conflicting Device rows) before re-checking for an existing serial, then perform the assignment and call _save_device inside that transaction so the conflict is re-validated under lock and concurrent requests cannot both succeed.netbox_librenms_plugin/views/imports/list.py (1)
266-277:⚠️ Potential issue | 🟠 MajorSubmitted naming toggles are still ignored when user prefs are unset.
Both blocks only consider submitted toggle values when a user pref exists. When prefs are
None, request-submitted toggles are dropped and defaults are used, causing inconsistent naming/cache behavior.♻️ Proposed fix
- _use_sysname = ( - _use_sysname_pref - if _use_sysname_pref is not None - else (getattr(settings, "use_sysname_default", True) if settings else True) - ) - _strip_domain = ( - _strip_domain_pref - if _strip_domain_pref is not None - else (getattr(settings, "strip_domain_default", False) if settings else False) - ) + submitted_use_sysname = self._filter_form_data.get("use_sysname_toggle") + submitted_strip_domain = self._filter_form_data.get("strip_domain_toggle") + if submitted_use_sysname is not None: + _use_sysname = submitted_use_sysname + elif _use_sysname_pref is not None: + _use_sysname = _use_sysname_pref + else: + _use_sysname = getattr(settings, "use_sysname_default", True) if settings else True + + if submitted_strip_domain is not None: + _strip_domain = submitted_strip_domain + elif _strip_domain_pref is not None: + _strip_domain = _strip_domain_pref + else: + _strip_domain = getattr(settings, "strip_domain_default", False) if settings else False ... - use_sysname = ( - data_source.get("use_sysname_toggle", use_sysname_pref) - if use_sysname_pref is not None - else (getattr(_settings, "use_sysname_default", True) if _settings else True) - ) - strip_domain = ( - data_source.get("strip_domain_toggle", strip_domain_pref) - if strip_domain_pref is not None - else (getattr(_settings, "strip_domain_default", False) if _settings else False) - ) + use_sysname = data_source.get("use_sysname_toggle") + if use_sysname is None: + 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 = data_source.get("strip_domain_toggle") + if strip_domain is None: + strip_domain = ( + strip_domain_pref + if strip_domain_pref is not None + else (getattr(_settings, "strip_domain_default", False) if _settings else False) + )Also applies to: 446-455
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/views/imports/list.py` around lines 266 - 277, The current logic only applies submitted toggle values when a user preference exists, so when _use_sysname_pref or _strip_domain_pref is None the code ignores request-submitted toggles and falls straight to settings/defaults; change the assignment of _use_sysname and _strip_domain to first check the request-submitted toggle (e.g., request.GET/POST param or form field for "use_sysname" and "strip_domain") when the corresponding pref is None, then fall back to getattr(settings, "..._default", ...) and finally to the hardcoded default; update the same pattern in the other occurrence (lines referencing the same variables around 446-455) and use the unique symbols _use_sysname_pref, _strip_domain_pref, _use_sysname, _strip_domain to locate the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 192-197: The current vc_domain key uses device_id (vc_domain =
f"librenms-{device_id}") which causes duplicate VC creation for stack members;
change vc_domain to be stack-scoped by deriving a stack identifier from the
device record (e.g., stack_id, stack_master_id or another stack-unique field on
the device) and build the key like f"librenms-{stack_id}" (falling back to
device_id only if no stack identifier exists), then use that new vc_domain when
checking/adding to processed_vc_domains before creation; update references in
this block (vc_domain, processed_vc_domains, device_id) accordingly so all stack
members dedupe correctly.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 50-66: The parsing only checks hyphenated keys (e.g.,
"use-sysname-toggle" and "strip-domain-toggle") so underscore or hidden-field
variants are ignored; update the logic around use_sysname and strip_domain to
look for both hyphenated and underscored keys in request.POST then request.GET
(e.g., check "use-sysname-toggle" OR "use_sysname-toggle") before falling back
to get_user_pref and LibreNMSSettings, ensuring you use the same normalized
boolean evaluation (== "on") for any found key.
---
Outside diff comments:
In `@netbox_librenms_plugin/jobs.py`:
- Around line 110-117: The job currently stores the local variable server_key
into self.job.data which can be None; change the assignments where job metadata
is built (the self.job.data dict at the first block and the similar block around
lines 244–250) to persist the resolved API key from the API client (use
api.server_key) instead of server_key so downstream cache-key reconstruction
uses the actual persisted key; update the dict key "server_key" to be assigned
api.server_key in both places (and ensure api is in scope where you make the
change).
---
Duplicate comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1088-1107: The sync_serial branch currently checks for serial
conflicts and writes outside a transaction, allowing a race; wrap the
conflict-check-and-update in a DB transaction (e.g., transaction.atomic) and
acquire appropriate row locks (use select_for_update on the target
existing_device and/or query conflicting Device rows) before re-checking for an
existing serial, then perform the assignment and call _save_device inside that
transaction so the conflict is re-validated under lock and concurrent requests
cannot both succeed.
In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 266-277: The current logic only applies submitted toggle values
when a user preference exists, so when _use_sysname_pref or _strip_domain_pref
is None the code ignores request-submitted toggles and falls straight to
settings/defaults; change the assignment of _use_sysname and _strip_domain to
first check the request-submitted toggle (e.g., request.GET/POST param or form
field for "use_sysname" and "strip_domain") when the corresponding pref is None,
then fall back to getattr(settings, "..._default", ...) and finally to the
hardcoded default; update the same pattern in the other occurrence (lines
referencing the same variables around 446-455) and use the unique symbols
_use_sysname_pref, _strip_domain_pref, _use_sysname, _strip_domain to locate the
code.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (9)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/jobs.pynetbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/jobs.pynetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/imports/actions.py
netbox_librenms_plugin/templates/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return<tr hx-swap-oob="true">.
AvoidouterHTMLswaps in HTMX; use OOB or targetedinnerHTMLswaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but withoutbootstrap.Modalhelpers.
Modal buttons should target thehtmx-modal-contentelement. JavaScript inlibrenms_import.htmltoggles the wrapper; do not reintroducedata-bs-toggleor duplicate modal IDs.
Keep<select class="device-role-select">markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not addtable-responsivewrappers as they were deliberately removed to prevent dropdown clipping.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
HTMX fragments should live in
templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html
📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)
Templates should live in
templates/netbox_librenms_plugin/. Reusable template includes should be placed underinc/subdirectory.
Files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/jobs.py
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/jobs.py: Background jobs must use NetBox'sJobRunnerbase class (netbox.jobs.JobRunner) for long-running operations like device filtering with VC detection
Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
RQ status values must be:queued,started,finished,stopped,failed(NOTcompleted)
Database Job status values must be:pending,scheduled,running,completed,failed,errored(NOcancelledstatus exists)
Files:
netbox_librenms_plugin/jobs.py
🧠 Learnings (30)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`
Applied to files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Applied to files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/tests/test_background_jobs.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {jobs.py,views/imports/**/*.py} : Background job files and import views follow conventions documented in `.github/instructions/background-jobs.instructions.md`
Applied to files:
netbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.htmlnetbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.
Applied to files:
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection
Applied to files:
netbox_librenms_plugin/jobs.pynetbox_librenms_plugin/tests/test_background_jobs.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Instantiate `JobRunner` subclasses using `object.__new__(JobClass)` to bypass `__init__`, then set `job.job = MagicMock()` and `job.logger = MagicMock()`
Applied to files:
netbox_librenms_plugin/tests/test_background_jobs.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Check `rq_job.is_stopped` or `rq_job.is_failed` flags in Redis for cancellation detection, not database status
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call `/api/core/background-tasks/{uuid}/stop/` to stop RQ job in the job cancellation flow
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database after stopping RQ job
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Poll `/api/core/background-tasks/{uuid}/` for real-time RQ status instead of polling the database
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Use Job UUID (`job.job_id`) for RQ API endpoints: `/api/core/background-tasks/{uuid}/`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Database Job status values must be: `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : RQ status values must be: `queued`, `started`, `finished`, `stopped`, `failed` (NOT `completed`)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
🧬 Code graph analysis (1)
netbox_librenms_plugin/import_utils/cache.py (1)
netbox_librenms_plugin/views/imports/list.py (1)
get(128-370)
🔇 Additional comments (11)
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (7)
23-24: Modal close button still usesdata-bs-dismiss="modal".This was previously flagged. The close buttons on lines 23 and 612 use Bootstrap-style dismissal which conflicts with the HTMX/Tabler modal pattern. As per coding guidelines, "Modal buttons should target the
htmx-modal-contentelement."
50-56: Good defensive status comparison.The dual type check (
== 1 or == "1") properly handles the API returning status as either integer or string, aligning with the PR's status normalization goal.
116-126: LGTM on form patterns and accessibility.Forms consistently include CSRF tokens, use appropriate
hx-swap="none"for action endpoints, and icon-only buttons havearia-labelattributes. External links properly includerel="noopener noreferrer".
469-490: Force-action checkbox pattern is correctly implemented.The JavaScript enable/disable logic via
querySelector('.force-action-btn')is properly scoped—the checkbox and the corresponding button class only exist whenvalidation.device_type_mismatchis true, so the selector will always find the correct button within the form context.
45-76: Repeated inline styles could be extracted to a CSS class.The
style="padding-left: 0.75rem;"pattern appears on numerous<th>and<td>elements throughout the template. This was previously noted as a maintainability improvement.
28-35: Good DRY pattern for URL computation.Computing
existing_device_urlonce at the top and reusing it throughout the template avoids repetition and ensures consistency in the VM vs. device routing logic.
604-609: Good conditional display for Full Sync Page link.The "Full Sync Page" button is appropriately shown only when
existing_match_type == 'librenms_id', directing users to the detailed sync view only when the device is properly linked.netbox_librenms_plugin/views/sync/device_fields.py (2)
281-290: Good transactional boundary for create-and-assign.Creating
Platformand assigning it toDevicein one atomic block prevents partial state when either step fails.
426-435: Race-safe mapping removal path looks solid.The lock + in-transaction re-check pattern is correct for avoiding stale-read deletes in concurrent requests.
netbox_librenms_plugin/import_utils/cache.py (1)
83-93: Nice hardening on cache stability.Timezone normalization for
cached_atand deterministic hash-based validated keys are both good reliability improvements.Also applies to: 158-163
netbox_librenms_plugin/tests/test_background_jobs.py (1)
689-723: Good coverage for naming-aware cache key reconstruction.The updated assertions around
use_sysnameandstrip_domainclose an important regression gap for background-job result loading.Also applies to: 743-761
… handling - actions.py: check both hyphenated and underscore toggle key variants in _resolve_naming_preferences (e.g. 'use-sysname-toggle' and 'use_sysname-toggle') - actions.py: add select_for_update() row lock inside link/update/update_serial transaction block; document DB-unique-constraint limitation - actions.py sync_serial: wrap conflict-check-and-write in transaction.atomic() with select_for_update() to prevent concurrent serial assignment race - device_fields.py RemoveServerMappingView: wrap full_clean()/save() in try/except for ValidationError and Exception; call transaction.set_rollback(), messages.error(), and redirect on failure instead of bubbling as 500 - bulk_import.py: fix vc_domain dedup key to use sorted member serials instead of per-device device_id, preventing duplicate VC creation for stack members - list.py: add comment explaining why POST toggles are not re-read (they are saved via savePref AJAX before filter form submission) - cache.py: add TODO comment noting location cache key should be server-scoped in a future PR - tests: add TestResolveNamingPreferencesKeys, TestVCDomainStackDedup, and TestRemoveServerMappingViewErrorHandling; update DeviceConflictActionView tests to mock select_for_update() and transaction.atomic()
commented
Mar 3, 2026
|
@coderabbitai review |
commented
Mar 3, 2026
✅ Actions performedReview triggered.
|
left a comment
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (2)
netbox_librenms_plugin/views/imports/list.py (1)
260-282:⚠️ Potential issue | 🟠 MajorHonor explicit toggle values before stored/default naming preferences.
Line 273 and Line 452 still allow explicit toggle input to be skipped when the saved preference is
None, which can produce inconsistent naming and cache-key mismatches between sync/background paths.🔧 Proposed fix
@@ - _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) - ) + def _coerce_toggle(value): + if value is None or isinstance(value, bool): + return value + return str(value).strip().lower() in {"1", "true", "on", "yes"} + + _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") + submitted_use_sysname = request.GET.get("use_sysname_toggle") + if submitted_use_sysname is None: + submitted_use_sysname = request.GET.get("use-sysname-toggle") + submitted_strip_domain = request.GET.get("strip_domain_toggle") + if submitted_strip_domain is None: + submitted_strip_domain = request.GET.get("strip-domain-toggle") + + submitted_use_sysname = _coerce_toggle(submitted_use_sysname) + submitted_strip_domain = _coerce_toggle(submitted_strip_domain) + + if submitted_use_sysname is not None: + _use_sysname = submitted_use_sysname + elif _use_sysname_pref is not None: + _use_sysname = _use_sysname_pref + else: + _use_sysname = getattr(settings, "use_sysname_default", True) if settings else True + + if submitted_strip_domain is not None: + _strip_domain = submitted_strip_domain + elif _strip_domain_pref is not None: + _strip_domain = _strip_domain_pref + else: + _strip_domain = getattr(settings, "strip_domain_default", False) if settings else False @@ - use_sysname = ( - data_source.get("use_sysname_toggle", use_sysname_pref) - if use_sysname_pref is not None - else (getattr(_settings, "use_sysname_default", True) if _settings else True) - ) - strip_domain = ( - data_source.get("strip_domain_toggle", strip_domain_pref) - if strip_domain_pref is not None - else (getattr(_settings, "strip_domain_default", False) if _settings else False) - ) + submitted_use_sysname = data_source.get("use_sysname_toggle") + submitted_strip_domain = data_source.get("strip_domain_toggle") + + if submitted_use_sysname is None: + submitted_use_sysname = self._request.GET.get("use_sysname_toggle") + if submitted_use_sysname is None: + submitted_use_sysname = self._request.GET.get("use-sysname-toggle") + if submitted_strip_domain is None: + submitted_strip_domain = self._request.GET.get("strip_domain_toggle") + if submitted_strip_domain is None: + submitted_strip_domain = self._request.GET.get("strip-domain-toggle") + + submitted_use_sysname = _coerce_toggle(submitted_use_sysname) + submitted_strip_domain = _coerce_toggle(submitted_strip_domain) + + if submitted_use_sysname is not None: + use_sysname = submitted_use_sysname + elif use_sysname_pref is not None: + use_sysname = use_sysname_pref + else: + use_sysname = getattr(_settings, "use_sysname_default", True) if _settings else True + + if submitted_strip_domain is not None: + strip_domain = submitted_strip_domain + elif strip_domain_pref is not None: + strip_domain = strip_domain_pref + else: + strip_domain = getattr(_settings, "strip_domain_default", False) if _settings else FalseAlso applies to: 443-460
🤖 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 260 - 282, The toggle resolution currently falls back to stored/default prefs before honoring explicit form toggles, causing mismatches; update the logic in list.py around get_user_pref, _use_sysname_pref, _strip_domain_pref, _use_sysname and _strip_domain to first check for explicit toggle values from the incoming request (the "use-sysname-toggle" and "strip-domain-toggle" parameters from request.GET/request.POST as applicable), then fall back to get_user_pref(request, ...) if no explicit toggle was provided, and finally use LibreNMSSettings defaults (getattr(settings, "..._default", ...)) when both explicit and saved prefs are absent; ensure the code reads request parameters before consulting get_user_pref so explicit toggles always win.netbox_librenms_plugin/views/imports/actions.py (1)
43-86:⚠️ Potential issue | 🟠 MajorNaming preference parsing is still inconsistent with canonical import keys.
This helper ignores
use_sysname/strip_domain, while Line 495-Line 496 use those keys for actual import execution. If only canonical keys are posted, preview/validation can diverge from import behavior.Suggested patch
- _USE_SYSNAME_KEYS = ("use-sysname-toggle", "use_sysname-toggle") - _STRIP_DOMAIN_KEYS = ("strip-domain-toggle", "strip_domain-toggle") + _USE_SYSNAME_KEYS = ("use-sysname-toggle", "use_sysname-toggle", "use_sysname") + _STRIP_DOMAIN_KEYS = ("strip-domain-toggle", "strip_domain-toggle", "strip_domain") + _TRUTHY = {"on", "true", "1", "yes"} @@ - use_sysname = _use_sysname_post == "on" + use_sysname = str(_use_sysname_post).strip().lower() in _TRUTHY @@ - use_sysname = _use_sysname_get == "on" + use_sysname = str(_use_sysname_get).strip().lower() in _TRUTHY @@ - strip_domain = _strip_domain_post == "on" + strip_domain = str(_strip_domain_post).strip().lower() in _TRUTHY @@ - strip_domain = _strip_domain_get == "on" + strip_domain = str(_strip_domain_get).strip().lower() in _TRUTHY🤖 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 43 - 86, The _resolve_naming_preferences helper ignores the canonical request keys "use_sysname" and "strip_domain", causing preview/validation to differ from import execution; update the _USE_SYSNAME_KEYS and _STRIP_DOMAIN_KEYS tuples in function _resolve_naming_preferences to include the canonical key names ("use_sysname" and "strip_domain") alongside the existing hyphenated/underscored variants so the next(... request.POST/GET ...) lookups will detect those posted keys and produce consistent use_sysname/strip_domain values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 72-76: Change the global location cache key to be scoped by
server_key so labels don't bleed between servers: replace the constant
location_cache_key = "librenms_locations_choices" and subsequent cache.get call
with a server-scoped key (e.g. build key using server_key like
f"librenms_locations_choices:{server_key}") and use that key for cache.get;
update any places that set/evict this cache to use the same server-scoped key
(refer to the location_cache_key variable and cached_locations usage to locate
the changes).
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1016-1017: The locked re-fetches using
Device.objects.select_for_update().get (the one assigning existing_device and
the second similar call later) must be wrapped in try/except blocks to catch
Device.DoesNotExist; if the exception is raised, return the appropriate
controlled response (e.g., Http404 or a 409 conflict response consistent with
surrounding logic) instead of letting the exception propagate and cause a 500.
Locate the two select_for_update().get calls (the existing_device re-fetch and
the later locked get) and add try/except Device.DoesNotExist around each,
mapping the exception to the same error handling path used elsewhere in this
view.
In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 245-246: The cache pre-check currently logs a generic message and
swallows the exception (logger.debug("Cache check failed; proceeding without
cached result") followed by pass); update this to preserve the exception context
by either passing exc_info=True to logger.debug or logging the caught exception
variable (e.g., logger.debug("Cache check failed; proceeding without cached
result: %s", e, exc_info=True)). Locate the try/except block around the cache
pre-check (the lines containing logger.debug and the subsequent pass) and modify
the except clause to include the exception context in the log call rather than
dropping it.
- Around line 336-339: The logger.exception call in the LibreNMS import error
path is embedding the full request.user object (logger.exception(...,
getattr(request, "user", None))), which can serialize sensitive or large data;
change it to log a minimal identifier instead (e.g., getattr(request, "user",
None) and then extract a safe attribute such as user.id or user.username) and
pass that scalar as the format argument in the same logger.exception call
(reference the logger.exception invocation in this file to locate and update
it).
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 416-424: The guard in device_fields.py only checks
plugins_cfg.get("servers") and misses legacy single-server mode where
plugins_cfg contains "librenms_url" and the implicit "default" server is
configured, allowing removal of that mapping; update the check around
plugins_cfg, configured_servers and server_key so it also treats the legacy case
as configured: if plugins_cfg contains a non-empty "librenms_url" and
(configured_servers is empty or does not contain server_key) then treat
server_key == "default" as protected (i.e. call messages.error and redirect as
currently done). Use the existing symbols plugins_cfg,
django_settings.PLUGINS_CONFIG, configured_servers and server_key to implement
this additional condition.
- Around line 426-427: The select_for_update().get(pk=pk) inside the
transaction.atomic() can raise Device.DoesNotExist if the row was deleted after
initial validation; update the view to wrap the lock acquisition call
(Device.objects.select_for_update().get(pk=pk)) in a try/except catching
Device.DoesNotExist and handle it by redirecting/returning the same controlled
error response used earlier (with a helpful message) instead of allowing a 500;
ensure this occurs inside the same transaction block so the transaction is
rolled back cleanly and reuse the existing error/redirect logic used after the
initial validation to keep behavior consistent.
---
Duplicate comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 43-86: The _resolve_naming_preferences helper ignores the
canonical request keys "use_sysname" and "strip_domain", causing
preview/validation to differ from import execution; update the _USE_SYSNAME_KEYS
and _STRIP_DOMAIN_KEYS tuples in function _resolve_naming_preferences to include
the canonical key names ("use_sysname" and "strip_domain") alongside the
existing hyphenated/underscored variants so the next(... request.POST/GET ...)
lookups will detect those posted keys and produce consistent
use_sysname/strip_domain values.
In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 260-282: The toggle resolution currently falls back to
stored/default prefs before honoring explicit form toggles, causing mismatches;
update the logic in list.py around get_user_pref, _use_sysname_pref,
_strip_domain_pref, _use_sysname and _strip_domain to first check for explicit
toggle values from the incoming request (the "use-sysname-toggle" and
"strip-domain-toggle" parameters from request.GET/request.POST as applicable),
then fall back to get_user_pref(request, ...) if no explicit toggle was
provided, and finally use LibreNMSSettings defaults (getattr(settings,
"..._default", ...)) when both explicit and saved prefs are absent; ensure the
code reads request parameters before consulting get_user_pref so explicit
toggles always win.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (7)
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (27)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Applied to files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {jobs.py,views/imports/**/*.py} : Background job files and import views follow conventions documented in `.github/instructions/background-jobs.instructions.md`
Applied to files:
netbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection
Applied to files:
netbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`
Applied to files:
netbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_permissions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Check `rq_job.is_stopped` or `rq_job.is_failed` flags in Redis for cancellation detection, not database status
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call `/api/core/background-tasks/{uuid}/stop/` to stop RQ job in the job cancellation flow
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database after stopping RQ job
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Poll `/api/core/background-tasks/{uuid}/` for real-time RQ status instead of polling the database
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Use Job UUID (`job.job_id`) for RQ API endpoints: `/api/core/background-tasks/{uuid}/`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Database Job status values must be: `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : RQ status values must be: `queued`, `started`, `finished`, `stopped`, `failed` (NOT `completed`)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
🧬 Code graph analysis (4)
netbox_librenms_plugin/views/sync/device_fields.py (4)
netbox_librenms_plugin/utils.py (1)
match_librenms_hardware_to_device_type(302-367)netbox_librenms_plugin/views/mixins.py (2)
LibreNMSAPIMixin(196-282)librenms_api(217-231)netbox_librenms_plugin/librenms_api.py (2)
get_librenms_id(173-231)get_device_info(314-337)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(26-47)
netbox_librenms_plugin/views/imports/list.py (5)
netbox_librenms_plugin/views/imports/actions.py (2)
get(761-781)get(787-816)netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
get(26-47)netbox_librenms_plugin/views/settings_views.py (1)
get(26-43)netbox_librenms_plugin/models.py (1)
LibreNMSSettings(11-52)netbox_librenms_plugin/utils.py (1)
get_user_pref(255-259)
netbox_librenms_plugin/import_utils/cache.py (1)
netbox_librenms_plugin/views/imports/list.py (1)
get(128-375)
netbox_librenms_plugin/tests/test_permissions.py (1)
netbox_librenms_plugin/views/sync/device_fields.py (8)
RemoveServerMappingView(389-448)post(19-56)post(66-110)post(120-171)post(181-238)post(251-310)post(320-386)post(396-448)
🔇 Additional comments (3)
netbox_librenms_plugin/views/imports/list.py (1)
95-96: Good propagation of naming flags through job payload and cache keys.The updates on Line 95-96, Line 114-115, Line 299-300, Line 471-472, and Line 486-487 consistently thread
use_sysname/strip_domainthrough enqueue, cache reads, and cache metadata lookups. This prevents cross-preference cache collisions.Also applies to: 114-115, 299-300, 471-472, 486-487
netbox_librenms_plugin/import_utils/bulk_import.py (1)
97-125: RQ-first cancellation handling is a solid improvement.The stop-state check order and DB fallback are well-structured and reduce delayed cancellation behavior in bulk imports.
netbox_librenms_plugin/tests/test_permissions.py (1)
961-1038: Good addition: error-path tests forRemoveServerMappingView.These cases materially improve confidence in graceful handling of validation failures and configured-server protection.
- cache.py + forms.py: scope location choices cache key by server_key
(f"librenms_locations_choices:{server_key}") so labels from different
LibreNMS servers don't bleed into each other's filter summaries
- list.py ~245: include exc_info=True in swallowed cache-check exception log
- list.py ~337: log user.username scalar instead of full user object to avoid
serialising sensitive/large data in exception handler
- list.py ~451: fix reversed ternary for use_sysname/strip_domain — submitted
form toggle now correctly takes priority over user pref which takes priority
over settings default (was: pref check gated the toggle, ignoring toggle when
pref was None)
- actions.py ~52: add plain "use_sysname" / "strip_domain" canonical keys to
_USE_SYSNAME_KEYS / _STRIP_DOMAIN_KEYS so preview and import use the same
resolution logic regardless of which key name the form sends
- actions.py ~1016 / ~1136: wrap select_for_update().get() in try/except
Device.DoesNotExist and return 409 instead of propagating a 500
- actions.py ~1054 / ~1084: add select_for_update() to serial-conflict filter
query inside transaction to reduce concurrent-assignment race window
- device_fields.py ~416: also protect "default" server_key when legacy
single-server librenms_url is configured (was only checking servers dict)
- device_fields.py ~430: wrap select_for_update().get(pk=pk) in try/except
Device.DoesNotExist and redirect with error message instead of 500
- bulk_import.py ~127: move progress log inside success path after the import
completes; use idx+1 so the count reflects completed items
- tests: add positive-path removal test; update DeviceConflictActionView mocks
to cover select_for_update().filter() chain
left a comment
There was a problem hiding this comment.
Actionable comments posted: 9
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/forms.py (1)
728-747: 🧹 Nitpick | 🔵 TrivialComplex default checkbox logic could be simplified.
The logic to apply
use_background_jobdefault only on initial load involves checking multiple conditions. Consider extracting this into a helper method for readability, or adding a brief inline comment explaining the intent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/forms.py` around lines 728 - 747, Extract the complex default-checkbox decision into a small helper on the form class (e.g., _should_set_use_background_job_default) that accepts the incoming data dict and returns True when "use_background_job" should be set; move the current checks (presence of "use_background_job", absence of "job_id", and no filter field values for librenms_location/type/os/hostname/sysname/hardware) into that helper, call it where the code currently sets data["use_background_job"] = "on", and add a one-line docstring to the helper explaining "apply default only on initial load (no filters, no job_id)".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@netbox_librenms_plugin/forms.py`:
- Around line 74-85: The code creates LibreNMSAPI() twice—once to build
cache_key from api.librenms_url and again for the real API call; reuse the first
instance instead: instantiate LibreNMSAPI() once into api, compute server_id and
cache_key from api.librenms_url, check cache, and then use the same api for
subsequent API calls (referencing LibreNMSAPI, api, cache_key, and librenms_url)
to avoid redundant instantiation and overhead.
- Around line 315-322: DeviceTypeMappingImportForm and
ModuleTypeMappingImportForm must define their ForeignKey import fields using
CSVModelChoiceField so CSVs can reference human-readable identifiers instead of
DB IDs; add explicit fields netbox_device_type =
CSVModelChoiceField(queryset=DeviceType.objects.all(), to_field_name="model" or
"slug", help_text="NetBox device type model name") in
DeviceTypeMappingImportForm and netbox_module_type =
CSVModelChoiceField(queryset=ModuleType.objects.all(), to_field_name="model" or
"slug", help_text="NetBox module type model name") in
ModuleTypeMappingImportForm, leaving Meta.model/fields intact.
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 186-188: The progress log in bulk_import.py uses idx which is
already 1-based (enumerate(..., start=1)), so the message
job.logger.info(f"Imported device {idx + 1} of {total}") is off-by-one; update
the logging call in the relevant function to use idx (not idx + 1) so the
message becomes "Imported device {idx} of {total}" while keeping job and total
as before.
In `@netbox_librenms_plugin/tests/test_permissions.py`:
- Around line 1073-1077: The test currently only checks the nested dict under
mock_locked.custom_field_data["librenms_id"], which can miss misspelled
top-level keys; update the assertions to explicitly verify the top-level keys of
mock_locked.custom_field_data (e.g., assert that "librenms_id" exists and that
no misspelled variant like "librenrenms_id" is present) or assert equality
against the exact expected mapping for mock_locked.custom_field_data (so the
entire custom_field_data dict matches the expected keys/values), while keeping
the existing assert that mock_locked.save.assert_called_once() to ensure save
was invoked.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 59-62: The current logic sets use_sysname (and similarly
strip_domain) to True only when the posted/get value equals "on", which
misparses "true" or "1"; update the parsing around
_use_sysname_post/_use_sysname_get and _strip_domain_post/_strip_domain_get to
treat common truthy strings ("on", "true", "1") case-insensitively as True and
everything else as False, e.g., normalize the value to lowercase and check
membership in a truthy set before assigning to use_sysname/strip_domain so both
POST and GET branches behave consistently.
- Around line 1135-1145: The serial-conflict check in sync_serial is not
lock-consistent: after locking existing_device into locked_device you call
Device.objects.filter(serial=incoming_serial).exclude(pk=locked_device.pk).first()
without acquiring a row lock, allowing a race where two concurrent syncs both
pass and write the same serial. Change that query to acquire the same FOR UPDATE
lock (e.g., use .select_for_update() on the queryset) so the conflict_device
lookup is performed under the transaction lock, referencing the same variables
(locked_device, incoming_serial) and keeping the logic consistent with the
earlier locked read; optionally note adding a DB unique constraint on
Device.serial for full enforcement.
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 418-421: The code assumes plugins_cfg.get("servers") is iterable
which can raise if it's None or malformed; update the check around
plugins_cfg/configured_servers in the device_fields logic (where plugins_cfg,
configured_servers, legacy_url_configured and server_key are used) to first
coerce or validate configured_servers (e.g. set configured_servers =
plugins_cfg.get("servers") or {} or verify isinstance(configured_servers, (dict,
list, set, tuple))) and then perform membership testing only on validated
iterables (e.g. check server_key in configured_servers when it's a
dict/list/etc.), preserving the existing legacy_url_configured && server_key ==
"default" branch.
- Around line 291-299: The code currently builds error_str from the exception
and shows it to users via messages.error (see error_str, messages.error,
platform_name, and the redirect to device_librenms_sync/ pk); change this to log
the full exception server-side (use logging.getLogger(__name__) and
logger.exception or logger.error(..., exc_info=True)) and replace the
user-facing messages.error calls with a generic message like "Failed to assign
platform '{platform_name}'. Please contact an administrator." Apply the same
change to both occurrences that currently expose exception text so the
redirect/flow (redirect("plugins:netbox_librenms_plugin:device_librenms_sync",
pk=pk)) remains unchanged.
- Around line 389-390: The code in RemoveServerMappingView directly accesses
custom_field_data["librenms_id"]; replace those direct reads with the
centralized helper LibreNMSAPI.get_librenms_id(...) so retrieval honors the
project’s mapping access contract — update both occurrences referenced (the
retrieval around the validation/lookup prior to removal and the subsequent check
before modifying the mapping) to call LibreNMSAPI.get_librenms_id with the
Device/VM instance, handle a None/absent return the same way the current logic
expects, and then proceed to update/delete the mapping using the existing update
flow.
---
Outside diff comments:
In `@netbox_librenms_plugin/forms.py`:
- Around line 728-747: Extract the complex default-checkbox decision into a
small helper on the form class (e.g., _should_set_use_background_job_default)
that accepts the incoming data dict and returns True when "use_background_job"
should be set; move the current checks (presence of "use_background_job",
absence of "job_id", and no filter field values for
librenms_location/type/os/hostname/sysname/hardware) into that helper, call it
where the code currently sets data["use_background_job"] = "on", and add a
one-line docstring to the helper explaining "apply default only on initial load
(no filters, no job_id)".
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (8)
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Reuse thelibrenms_api.pyLibreNMS client instead of making newrequestscalls; it handles multi-server configs viaLibreNMSSettingsmodel and theserversplugin config, plus caching
Always callLibreNMSAPI.get_librenms_idto retrieve the device/VM LibreNMS mapping via thelibrenms_idcustom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functionsfind_matching_site,match_librenms_hardware_to_device_type, andfind_matching_platformfromutils.py
Files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/forms.py
**/views/imports/**
📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)
**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints:/api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Checkrq_job.is_stoppedorrq_job.is_failedflags in Redis for cancellation detection, not database status
Call/api/core/background-tasks/{uuid}/stop/to stop RQ job in the job cancellation flow
Call plugin's sync endpoint/api/plugins/librenms_plugin/jobs/{pk}/sync-status/to update database after stopping RQ job
Poll/api/core/background-tasks/{uuid}/for real-time RQ status instead of polling the database
api/views.py::sync_job_status()must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (27)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/forms.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`
Applied to files:
netbox_librenms_plugin/views/sync/device_fields.pynetbox_librenms_plugin/forms.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {jobs.py,views/imports/**/*.py} : Background job files and import views follow conventions documented in `.github/instructions/background-jobs.instructions.md`
Applied to files:
netbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`
Applied to files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/import_utils/cache.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Applied to files:
netbox_librenms_plugin/views/imports/list.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection
Applied to files:
netbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Applied to files:
netbox_librenms_plugin/tests/test_permissions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`
Applied to files:
netbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Check `rq_job.is_stopped` or `rq_job.is_failed` flags in Redis for cancellation detection, not database status
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call `/api/core/background-tasks/{uuid}/stop/` to stop RQ job in the job cancellation flow
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database after stopping RQ job
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Poll `/api/core/background-tasks/{uuid}/` for real-time RQ status instead of polling the database
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Use Job UUID (`job.job_id`) for RQ API endpoints: `/api/core/background-tasks/{uuid}/`
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Database Job status values must be: `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : RQ status values must be: `queued`, `started`, `finished`, `stopped`, `failed` (NOT `completed`)
Applied to files:
netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.
Applied to files:
netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Applied to files:
netbox_librenms_plugin/forms.py
🧬 Code graph analysis (3)
netbox_librenms_plugin/views/sync/device_fields.py (2)
netbox_librenms_plugin/utils.py (1)
match_librenms_hardware_to_device_type(302-367)netbox_librenms_plugin/librenms_api.py (2)
get_librenms_id(173-231)get_device_info(314-337)
netbox_librenms_plugin/tests/test_permissions.py (1)
netbox_librenms_plugin/views/sync/device_fields.py (8)
RemoveServerMappingView(389-456)post(19-56)post(66-110)post(120-171)post(181-238)post(251-310)post(320-386)post(396-456)
netbox_librenms_plugin/import_utils/cache.py (1)
netbox_librenms_plugin/views/imports/list.py (1)
get(128-375)
🔇 Additional comments (9)
netbox_librenms_plugin/views/sync/device_fields.py (1)
281-290: Atomic create-and-assign flow is a solid reliability improvement.Wrapping platform creation and device assignment in one transaction prevents partial writes in this path.
netbox_librenms_plugin/forms.py (5)
1-32: LGTM!Import additions align with the new mapping form classes and their field requirements.
108-238: LGTM!
ServerConfigFormandImportSettingsFormare well-structured with comprehensive validation for the VC member naming pattern.
397-397: Consider replacing deprecatedNullBooleanFieldwith an alternative field for forward compatibility.
NullBooleanFieldis deprecated in Django 3.1 and removed in Django 4.0+. Replace withTypedChoiceFieldorChoiceFieldto future-proof the code.
456-611: LGTM!SNMP forms properly delegate poller group population to the shared helper function, maintaining DRY principles.
780-781: LGTM!Location cache is properly scoped by
api.server_key, preventing cross-server label bleed.netbox_librenms_plugin/import_utils/cache.py (1)
71-95: Cache keying and metadata hardening look solid.The server-scoped location cache key and guarded
cached_athandling materially improve multi-server correctness and resilience.Also applies to: 129-164
netbox_librenms_plugin/views/imports/list.py (1)
95-116: Good propagation and observability improvements.The naming-preference propagation into cache-key paths plus
exc_info=Trueon cache pre-check failures improves consistency and debuggability.Also applies to: 243-245, 443-467, 478-495
netbox_librenms_plugin/tests/test_permissions.py (1)
1008-1009: The lambda approach for__enter__/__exit__mocking works correctly and does not raiseTypeError. Both the original code and the proposed alternative approach function properly with Python's context manager protocol.Likely an incorrect or invalid review comment.
| try: | ||
| api = LibreNMSAPI() | ||
| server_id = api.librenms_url.rstrip("/") | ||
| cache_key = f"librenms_poller_group_choices_{server_id}" | ||
| except Exception: | ||
| cache_key = "librenms_poller_group_choices" | ||
| cached_choices = cache.get(cache_key) | ||
| if cached_choices: | ||
| return cached_choices | ||
|
|
||
| try: | ||
| api = LibreNMSAPI() |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Redundant LibreNMSAPI instantiation.
The first api instance created at line 75 is only used to derive the cache key, then discarded. A second instance is created at line 85 for the actual API call. Reuse the first instance to avoid the overhead.
♻️ Proposed fix
choices = [("0", "Default (0)")]
try:
api = LibreNMSAPI()
server_id = api.librenms_url.rstrip("/")
cache_key = f"librenms_poller_group_choices_{server_id}"
except Exception:
+ api = None
cache_key = "librenms_poller_group_choices"
cached_choices = cache.get(cache_key)
if cached_choices:
return cached_choices
try:
- api = LibreNMSAPI()
+ if api is None:
+ api = LibreNMSAPI()
success, poller_groups = api.get_poller_groups()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@netbox_librenms_plugin/forms.py` around lines 74 - 85, The code creates
LibreNMSAPI() twice—once to build cache_key from api.librenms_url and again for
the real API call; reuse the first instance instead: instantiate LibreNMSAPI()
once into api, compute server_id and cache_key from api.librenms_url, check
cache, and then use the same api for subsequent API calls (referencing
LibreNMSAPI, api, cache_key, and librenms_url) to avoid redundant instantiation
and overhead.
| 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, | ||
| ) | ||
| # Re-check for serial ownership conflict under lock | ||
| conflict_device = Device.objects.filter(serial=incoming_serial).exclude(pk=locked_device.pk).first() | ||
| if conflict_device: |
There was a problem hiding this comment.
sync_serial conflict check is not lock-consistent with the transactional intent.
Line 1144 checks for existing serial ownership without select_for_update(). Two concurrent syncs on different devices can still both pass the check and write the same serial. This branch should mirror the locked conflict read used in update/update_serial.
🔒 Proposed fix
- conflict_device = Device.objects.filter(serial=incoming_serial).exclude(pk=locked_device.pk).first()
+ conflict_device = (
+ Device.objects.select_for_update()
+ .filter(serial=incoming_serial)
+ .exclude(pk=locked_device.pk)
+ .first()
+ )For full enforcement, consider a DB-level unique constraint on Device.serial if business rules allow it.
🤖 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 1135 - 1145,
The serial-conflict check in sync_serial is not lock-consistent: after locking
existing_device into locked_device you call
Device.objects.filter(serial=incoming_serial).exclude(pk=locked_device.pk).first()
without acquiring a row lock, allowing a race where two concurrent syncs both
pass and write the same serial. Change that query to acquire the same FOR UPDATE
lock (e.g., use .select_for_update() on the queryset) so the conflict_device
lookup is performed under the transaction lock, referencing the same variables
(locked_device, incoming_serial) and keeping the logic consistent with the
earlier locked read; optionally note adding a DB unique constraint on
Device.serial for full enforcement.
| class RemoveServerMappingView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, View): | ||
| """Remove a single server entry from the device's librenms_id custom field dict.""" |
There was a problem hiding this comment.
Use LibreNMS API helper for mapping retrieval instead of direct custom field reads.
Line 407 and Line 435 directly read custom_field_data["librenms_id"] to retrieve mapping state. This bypasses the project’s mapping access contract and can drift from centralized retrieval behavior.
As per coding guidelines: "Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly".
Also applies to: 407-409, 435-435
🤖 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 389 - 390,
The code in RemoveServerMappingView directly accesses
custom_field_data["librenms_id"]; replace those direct reads with the
centralized helper LibreNMSAPI.get_librenms_id(...) so retrieval honors the
project’s mapping access contract — update both occurrences referenced (the
retrieval around the validation/lookup prior to removal and the subsequent check
before modifying the mapping) to call LibreNMSAPI.get_librenms_id with the
Device/VM instance, handle a None/absent return the same way the current logic
expects, and then proceed to update/delete the mapping using the existing update
flow.
| plugins_cfg = django_settings.PLUGINS_CONFIG.get("netbox_librenms_plugin", {}) | ||
| configured_servers = plugins_cfg.get("servers", {}) | ||
| legacy_url_configured = bool(plugins_cfg.get("librenms_url")) | ||
| if server_key in configured_servers or (legacy_url_configured and server_key == "default"): |
There was a problem hiding this comment.
Harden configured-server checks against malformed plugin config.
At Line 421 and Line 438, server_key in configured_servers assumes servers is a dict/iterable. If servers is None or another invalid type, this can raise and return a 500 from this endpoint.
Suggested fix
- configured_servers = plugins_cfg.get("servers", {})
+ configured_servers_raw = plugins_cfg.get("servers", {})
+ configured_servers = configured_servers_raw if isinstance(configured_servers_raw, dict) else {}
legacy_url_configured = bool(plugins_cfg.get("librenms_url"))
if server_key in configured_servers or (legacy_url_configured and server_key == "default"):
messages.error(
request,
f"Cannot remove mapping for configured server '{server_key}'. "
"Remove the server from plugin configuration first, then retry.",
)
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
@@
- _is_protected = server_key in configured_servers or (legacy_url_configured and server_key == "default")
+ _is_protected = server_key in configured_servers or (legacy_url_configured and server_key == "default")Also applies to: 437-438
🤖 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 418 - 421,
The code assumes plugins_cfg.get("servers") is iterable which can raise if it's
None or malformed; update the check around plugins_cfg/configured_servers in the
device_fields logic (where plugins_cfg, configured_servers,
legacy_url_configured and server_key are used) to first coerce or validate
configured_servers (e.g. set configured_servers = plugins_cfg.get("servers") or
{} or verify isinstance(configured_servers, (dict, list, set, tuple))) and then
perform membership testing only on validated iterables (e.g. check server_key in
configured_servers when it's a dict/list/etc.), preserving the existing
legacy_url_configured && server_key == "default" branch.
…latform errors, CSV forms
left a comment
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (5)
netbox_librenms_plugin/views/sync/device_fields.py (2)
51-55:⚠️ Potential issue | 🟠 MajorDo not return raw exception details in user-facing messages.
These branches expose validation/runtime internals directly to end users. Keep UI messages generic and log full exceptions server-side.
🔒 Proposed fix pattern
- except (ValidationError, IntegrityError) as e: + except (ValidationError, IntegrityError): device.name = old_name - error_msg = e.message_dict if hasattr(e, "message_dict") else str(e) - messages.error(request, f"Failed to update device name to '{sys_name}': {error_msg}") + logger.exception("Failed to update device name to '%s' for device pk=%s", sys_name, pk) + messages.error(request, f"Failed to update device name to '{sys_name}'.") return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk) @@ - except ValidationError as exc: + except ValidationError: transaction.set_rollback(True) - messages.error(request, f"Validation error removing mapping: {exc}") + logger.exception("Validation error removing mapping for server '%s' on device pk=%s", server_key, pk) + messages.error(request, "Validation error removing mapping.") return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk) - except Exception as exc: + except Exception: transaction.set_rollback(True) - messages.error(request, f"Error removing mapping for server '{server_key}': {exc}") + logger.exception("Error removing mapping for server '%s' on device pk=%s", server_key, pk) + messages.error(request, f"Error removing mapping for server '{server_key}'.") return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)Also applies to: 460-467
🤖 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 51 - 55, Replace the current user-facing error that includes raw exception details in the except block handling ValidationError/IntegrityError (the clause starting with "except (ValidationError, IntegrityError) as e" and the subsequent messages.error(...) call) with a generic friendly message; instead, log the full exception server-side (using the module logger or request logger) including the exception object and stacktrace, keep device.name = old_name and the redirect to "plugins:netbox_librenms_plugin:device_librenms_sync", and update the messages.error text to a generic string like "Failed to update device name." Apply the same change pattern to the other similar branch mentioned in the review (duplicate block).
435-437:⚠️ Potential issue | 🟠 MajorValidate
serversconfig type before membership checks.
server_key in configured_serverswill raise ifserversisNoneor malformed, causing a 500 in this POST path.🔧 Proposed fix
- configured_servers = plugins_cfg.get("servers", {}) + configured_servers_raw = plugins_cfg.get("servers", {}) + configured_servers = configured_servers_raw if isinstance(configured_servers_raw, dict) else {} legacy_url_configured = bool(plugins_cfg.get("librenms_url")) if server_key in configured_servers or (legacy_url_configured and server_key == "default"): @@ - _is_protected = server_key in configured_servers or (legacy_url_configured and server_key == "default") + _is_protected = server_key in configured_servers or (legacy_url_configured and server_key == "default")Also applies to: 453-454
🤖 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 435 - 437, The membership check uses configured_servers = plugins_cfg.get("servers", {}) but if plugins_cfg["servers"] is None or not an iterable/mapping the expression server_key in configured_servers will raise; update the code around configured_servers, plugins_cfg and server_key to validate/normalize the servers value before using "in" (e.g. ensure configured_servers is a dict or list by checking isinstance(configured_servers, (dict, list, set)) and falling back to {} or []), then perform the server_key membership check; apply the same validation/normalization to the other occurrence around lines 453-454 where configured_servers is used.netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
309-317:⚠️ Potential issue | 🟠 MajorFix 1-based position handling in
update_vc_member_suggested_names().This block adds
+1to positions that are already 1-based and can persist0back tomember["position"]on fallback, producing incorrect suggested names and invalid VC positions.🔧 Proposed fix
- for idx, member in enumerate(vc_data.get("members", [])): - raw_position = member.get("position", idx) + for idx, member in enumerate(vc_data.get("members", [])): + raw_position = member.get("position", idx + 1) try: base_position = int(raw_position) except (TypeError, ValueError): - base_position = idx - position = base_position + 1 # Convert to 1-based position - member["position"] = base_position + base_position = idx + 1 + if base_position < 1: + base_position = idx + 1 + position = base_position + member["position"] = position member["suggested_name"] = _generate_vc_member_name( master_name, position, serial=member.get("serial"), pattern=vc_pattern )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 309 - 317, The code currently mis-handles 1-based vs 0-based positions: parse raw_position into an int, then if parsed > 0 treat that as a 1-based value and set base_index = parsed - 1, otherwise fall back to base_index = idx; compute position = base_index + 1 (1-based) and set member["position"] = position (not base_index), then pass position into _generate_vc_member_name; update the block using vc_data, member, raw_position, base_position (rename to base_index if you like), position and _generate_vc_member_name accordingly so we never persist a 0 and correctly generate suggested names.netbox_librenms_plugin/forms.py (1)
74-85: 🧹 Nitpick | 🔵 TrivialReuse the first
LibreNMSAPIinstance in_get_librenms_poller_group_choices().The helper currently creates one client for keying and another for retrieval. Reusing one instance is cleaner and avoids unnecessary init work.
♻️ Proposed refactor
- try: - api = LibreNMSAPI() - server_id = api.librenms_url.rstrip("/") - cache_key = f"librenms_poller_group_choices_{server_id}" - except Exception: - cache_key = "librenms_poller_group_choices" + api = None + try: + api = LibreNMSAPI() + server_id = api.librenms_url.rstrip("/") + cache_key = f"librenms_poller_group_choices_{server_id}" + except Exception: + cache_key = "librenms_poller_group_choices" @@ - try: - api = LibreNMSAPI() + try: + if api is None: + api = LibreNMSAPI() success, poller_groups = api.get_poller_groups()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@netbox_librenms_plugin/forms.py` around lines 74 - 85, In _get_librenms_poller_group_choices(), remove the duplicate LibreNMSAPI() instantiation and reuse the first 'api' instance: initialize api = LibreNMSAPI() in the initial try, derive server_id and cache_key from api.librenms_url, check the cache with that cache_key, and then call the API retrieval on the same 'api' object; if the initial creation failed, set api = None in the except and only instantiate LibreNMSAPI() right before retrieval when api is None. Reference symbols: _get_librenms_poller_group_choices, LibreNMSAPI, api, cache_key.netbox_librenms_plugin/views/imports/actions.py (1)
1148-1149:⚠️ Potential issue | 🟠 MajorLock the
sync_serialconflict lookup to match transactional intent.Line 1148 still performs an unlocked read (
Device.objects.filter(...).first()) inside the serial sync transaction. Two concurrent requests can still pass this check and race into duplicate serial assignment.🔧 Proposed fix
- conflict_device = Device.objects.filter(serial=incoming_serial).exclude(pk=locked_device.pk).first() + conflict_device = ( + Device.objects.select_for_update() + .filter(serial=incoming_serial) + .exclude(pk=locked_device.pk) + .first() + )Run this to verify the current query is unlocked and confirm the fix:
#!/bin/bash set -euo pipefail FILE="netbox_librenms_plugin/views/imports/actions.py" echo "Locate sync_serial branch:" rg -n 'elif action == "sync_serial"' "$FILE" -A30 -B5 echo echo "Locate conflict query inside sync_serial:" rg -n 'conflict_device\s*=\s*Device\.objects\..*serial=incoming_serial' "$FILE" -A4 -B2 echo echo "Expected after fix: query chain contains select_for_update() before filter(serial=incoming_serial)."🤖 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 1148 - 1149, The conflict lookup in the sync_serial branch uses an unlocked read (conflict_device = Device.objects.filter(serial=incoming_serial).exclude(pk=locked_device.pk).first()) which allows a race; modify the lookup to acquire the same row lock as the surrounding transaction by using select_for_update() on the queryset before filtering by serial (i.e. call Device.objects.select_for_update().filter(serial=incoming_serial).exclude(pk=locked_device.pk).first()), so the conflict_device query participates in the transaction lock and prevents concurrent duplicate serial assignments.
🤖 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 639-645: The code currently hardcodes the cache index key format
("librenms_cache_index_{api.server_key}"); replace that with the shared cache
helper so keys stay consistent across flows: call
get_active_cached_searches(...) (passing the api.server_key or api object as
required by the helper) to obtain the cache index key instead of the f-string,
use get_cache_metadata_key(...) to produce/validate cache_metadata_key where
applicable, and then append and cache.set the index with api.cache_timeout as
before; ensure all references in process_device_filters() use
get_active_cached_searches, get_cache_metadata_key,
get_validated_device_cache_key or get_import_device_cache_key as appropriate
rather than hardcoded key strings.
---
Duplicate comments:
In `@netbox_librenms_plugin/forms.py`:
- Around line 74-85: In _get_librenms_poller_group_choices(), remove the
duplicate LibreNMSAPI() instantiation and reuse the first 'api' instance:
initialize api = LibreNMSAPI() in the initial try, derive server_id and
cache_key from api.librenms_url, check the cache with that cache_key, and then
call the API retrieval on the same 'api' object; if the initial creation failed,
set api = None in the except and only instantiate LibreNMSAPI() right before
retrieval when api is None. Reference symbols:
_get_librenms_poller_group_choices, LibreNMSAPI, api, cache_key.
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 309-317: The code currently mis-handles 1-based vs 0-based
positions: parse raw_position into an int, then if parsed > 0 treat that as a
1-based value and set base_index = parsed - 1, otherwise fall back to base_index
= idx; compute position = base_index + 1 (1-based) and set member["position"] =
position (not base_index), then pass position into _generate_vc_member_name;
update the block using vc_data, member, raw_position, base_position (rename to
base_index if you like), position and _generate_vc_member_name accordingly so we
never persist a 0 and correctly generate suggested names.
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1148-1149: The conflict lookup in the sync_serial branch uses an
unlocked read (conflict_device =
Device.objects.filter(serial=incoming_serial).exclude(pk=locked_device.pk).first())
which allows a race; modify the lookup to acquire the same row lock as the
surrounding transaction by using select_for_update() on the queryset before
filtering by serial (i.e. call
Device.objects.select_for_update().filter(serial=incoming_serial).exclude(pk=locked_device.pk).first()),
so the conflict_device query participates in the transaction lock and prevents
concurrent duplicate serial assignments.
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 51-55: Replace the current user-facing error that includes raw
exception details in the except block handling ValidationError/IntegrityError
(the clause starting with "except (ValidationError, IntegrityError) as e" and
the subsequent messages.error(...) call) with a generic friendly message;
instead, log the full exception server-side (using the module logger or request
logger) including the exception object and stacktrace, keep device.name =
old_name and the redirect to
"plugins:netbox_librenms_plugin:device_librenms_sync", and update the
messages.error text to a generic string like "Failed to update device name."
Apply the same change pattern to the other similar branch mentioned in the
review (duplicate block).
- Around line 435-437: The membership check uses configured_servers =
plugins_cfg.get("servers", {}) but if plugins_cfg["servers"] is None or not an
iterable/mapping the expression server_key in configured_servers will raise;
update the code around configured_servers, plugins_cfg and server_key to
validate/normalize the servers value before using "in" (e.g. ensure
configured_servers is a dict or list by checking isinstance(configured_servers,
(dict, list, set)) and falling back to {} or []), then perform the server_key
membership check; apply the same validation/normalization to the other
occurrence around lines 453-454 where configured_servers is used.
ℹ️ Review info
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (8)
netbox_librenms_plugin/forms.pynetbox_librenms_plugin/import_utils/bulk_import.pynetbox_librenms_plugin/import_utils/filters.pynetbox_librenms_plugin/import_utils/virtual_chassis.pynetbox_librenms_plugin/tests/test_import_utils.pynetbox_librenms_plugin/tests/test_permissions.pynetbox_librenms_plugin/views/imports/actions.pynetbox_librenms_plugin/views/sync/device_fields.py
| cache_index_key = f"librenms_cache_index_{api.server_key}" | ||
| cache_index = cache.get(cache_index_key, []) | ||
| # Add this cache key if not already in index | ||
| if cache_metadata_key not in cache_index: | ||
| cache_index.append(cache_metadata_key) | ||
| # Store index with same timeout as the metadata | ||
| cache.set(cache_index_key, cache_index, timeout=api.cache_timeout) |
There was a problem hiding this comment.
Avoid hardcoded cache-index key formats in process_device_filters().
This section still constructs librenms_cache_index_* inline. It should use the shared cache helper contract to avoid key drift between sync/background flows and related list-view lookups.
Based on learnings: “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”.
🤖 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 639 - 645,
The code currently hardcodes the cache index key format
("librenms_cache_index_{api.server_key}"); replace that with the shared cache
helper so keys stay consistent across flows: call
get_active_cached_searches(...) (passing the api.server_key or api object as
required by the helper) to obtain the cache index key instead of the f-string,
use get_cache_metadata_key(...) to produce/validate cache_metadata_key where
applicable, and then append and cache.set the index with api.cache_timeout as
before; ensure all references in process_device_filters() use
get_active_cached_searches, get_cache_metadata_key,
get_validated_device_cache_key or get_import_device_cache_key as appropriate
rather than hardcoded key strings.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests