first commit
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>Cable Sync</h2>
|
||||
<div class="btn-list">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% if has_librenms_id %}
|
||||
{% with model_name=object|meta:"model_name" %}
|
||||
{% if model_name == "device" %}
|
||||
<button hx-post="{% url 'plugins:netbox_librenms_plugin:device_cable_sync' pk=object.pk %}"
|
||||
hx-target="#cable-sync-content"
|
||||
class="btn btn-outline-primary">
|
||||
Refresh Cables
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Container for the cable sync content -->
|
||||
<div id="cable-sync-content">
|
||||
{% include 'netbox_librenms_plugin/_cable_sync_content.html' %}
|
||||
</div>
|
||||
@@ -0,0 +1,118 @@
|
||||
{% load helpers %}
|
||||
{% include 'inc/messages.html' %}
|
||||
|
||||
<!-- Cable Sync Table -->
|
||||
{% if cable_sync.table %}
|
||||
<form method="post" action="{% url 'plugins:netbox_librenms_plugin:sync_device_cables' cable_sync.object.pk %}">
|
||||
{% csrf_token %}
|
||||
{% if cable_sync.server_key %}<input type="hidden" name="server_key" value="{{ cable_sync.server_key }}">{% endif %}
|
||||
<input type="hidden" id="selected_port" name="select" value="">
|
||||
<div class="noprint d-flex justify-content-between align-items-center mt-3 mb-3">
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="spinner spinner-border d-none" id="sync-spinner"></span>
|
||||
<span>Sync Selected Cables</span>
|
||||
</button>
|
||||
<a href="#" class="m-2" data-bs-toggle="modal" data-bs-target="#cableSyncHelpModal">
|
||||
<i class="mdi mdi-help-circle"></i> info
|
||||
</a>
|
||||
</div>
|
||||
{% if cable_sync.cache_expiry %}
|
||||
<div id="cable-cache-countdown" class="me-3">
|
||||
Cache expires in: <span id="cable-countdown-timer" data-expiry="{{ cable_sync.cache_expiry|date:'c' }}"></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="d-flex justify-content-end align-items-center mb-3">
|
||||
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#cableFilterSection" aria-expanded="false" aria-controls="cableFilterSection">
|
||||
<i class="mdi mdi-filter"></i> Toggle Filters
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse mb-3" id="cableFilterSection">
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">
|
||||
<i class="mdi mdi-information"></i> Filters apply to currently displayed cables.
|
||||
</small>
|
||||
</div>
|
||||
<div class="filter-container d-flex gap-2">
|
||||
{% if cable_sync.table.attrs.id == "librenms-cable-table-vc" %}
|
||||
<input type="text" id="filter-vc-member" placeholder="Filter by VC Member" class="form-control">
|
||||
{% endif %}
|
||||
<input type="text" id="filter-local-port" placeholder="Filter by Local Port" class="form-control">
|
||||
<input type="text" id="filter-remote-port" placeholder="Filter by Remote Port" class="form-control">
|
||||
<input type="text" id="filter-remote-device" placeholder="Filter by Remote Device" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
/* Your existing CSS rules */
|
||||
.ts-wrapper.multi .ts-control {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
/* Updated rules using min-width */
|
||||
td[data-col="device_selection"] {
|
||||
width: 300px;
|
||||
min-width: 200px;
|
||||
}
|
||||
td[data-col="device_selection"] .ts-wrapper {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<div class="card">
|
||||
{% include 'netbox_librenms_plugin/inc/paginator.html' with table=cable_sync.table %}
|
||||
{% include 'inc/table.html' with table=cable_sync.table %}
|
||||
{% include 'netbox_librenms_plugin/inc/paginator.html' with table=cable_sync.table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<div class="card-body text-center text-muted py-4">
|
||||
<i class="mdi mdi-sync-off mdi-48px"></i>
|
||||
<p class="mt-2 mb-0">No cable data loaded. Click <strong>Refresh Cables</strong> to fetch data from LibreNMS.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Interface Type Help Modal -->
|
||||
<div class="modal fade" id="cableSyncHelpModal" tabindex="-1" aria-labelledby="cableSyncHelpModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="cableSyncHelpModalLabel">NetBox Interface Sync Info</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5>Device Association</h5>
|
||||
<p>The plugin uses two methods to associate LibreNMS devices with NetBox devices:</p>
|
||||
<ol>
|
||||
<li><strong>LibreNMS ID (Recommended)</strong>
|
||||
<ul>
|
||||
<li>Uses the custom field 'librenms_id' on NetBox devices</li>
|
||||
<li>Automatically populated when viewing the LibreNMS Sync page if device is found.</li>
|
||||
<li>Can be manually entered in device custom fields</li>
|
||||
<li>Provides the most reliable device matching</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Device Name Fallback</strong>
|
||||
<ul>
|
||||
<li>Used when librenms_id is not available</li>
|
||||
<li>Matches are case-sensitive</li>
|
||||
<li>Less reliable due to potential naming differences</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p>For best results, ensure the librenms_id custom field is populated on your devices.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>Interface Sync</h2>
|
||||
<div class="btn-list">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% if has_librenms_id %}
|
||||
{% with model_name=object|meta:"model_name" %}
|
||||
{% if model_name == "device" %}
|
||||
<button hx-post="{% url 'plugins:netbox_librenms_plugin:device_interface_sync' pk=object.pk %}"
|
||||
hx-target="#interface-sync-content"
|
||||
hx-include="[name='interface_name_field']"
|
||||
class="btn btn-outline-primary">
|
||||
Refresh Interfaces
|
||||
</button>
|
||||
{% elif model_name == "virtualmachine" %}
|
||||
<button hx-post="{% url 'plugins:netbox_librenms_plugin:vm_interface_sync' pk=object.pk %}"
|
||||
hx-target="#interface-sync-content"
|
||||
hx-include="[name='interface_name_field']"
|
||||
class="btn btn-outline-primary">
|
||||
Refresh Interfaces
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Action Buttons -->
|
||||
|
||||
<!-- Container for the interface sync content -->
|
||||
<div id="interface-sync-content">
|
||||
{% include 'netbox_librenms_plugin/_interface_sync_content.html' %}
|
||||
</div>
|
||||
@@ -0,0 +1,358 @@
|
||||
{% load helpers %}
|
||||
{% include 'inc/messages.html' %}
|
||||
|
||||
<!-- Interface Sync Table -->
|
||||
{% if interface_sync.table %}
|
||||
|
||||
{% with model_name=interface_sync.object|meta:"model_name" %}
|
||||
<form method="post"
|
||||
action="{% url 'plugins:netbox_librenms_plugin:sync_selected_interfaces' object_type=model_name object_id=interface_sync.object.pk %}?interface_name_field={{ interface_name_field }}">
|
||||
{% endwith %}
|
||||
{% csrf_token %}
|
||||
{% if interface_sync.server_key %}<input type="hidden" name="server_key" value="{{ interface_sync.server_key }}">{% endif %}
|
||||
{% block table_actions %}
|
||||
<div class="noprint d-flex justify-content-between align-items-center mt-3 mb-3">
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="spinner spinner-border d-none" id="sync-spinner"></span>
|
||||
<span>Sync Selected Interfaces</span>
|
||||
</button>
|
||||
<a href="#" class="m-2" data-bs-toggle="modal" data-bs-target="#interfaceTypeHelpModal">
|
||||
<i class="mdi mdi-help-circle"></i> info
|
||||
</a>
|
||||
</div>
|
||||
<div class="ms-auto d-flex align-items-center">
|
||||
<div class="exclude-columns-section d-flex align-items-center gap-2 me-2">
|
||||
<h6 class="mb-0">Exclude from Sync:</h6>
|
||||
<div class="d-flex align-items-center m-0">
|
||||
<span class="small me-1">Type</span>
|
||||
<input class="form-check-input form-check-input-sm" type="checkbox" name="exclude_columns"
|
||||
value="type" id="excludeType">
|
||||
</div>
|
||||
<div class="d-flex align-items-center m-1">
|
||||
<span class="small me-1">Speed</span>
|
||||
<input class="form-check-input form-check-input-sm" type="checkbox" name="exclude_columns"
|
||||
value="speed" id="excludeSpeed">
|
||||
</div>
|
||||
<div class="d-flex align-items-center m-1">
|
||||
<span class="small me-1">VLANs</span>
|
||||
<input class="form-check-input form-check-input-sm" type="checkbox" name="exclude_columns"
|
||||
value="vlans" id="excludeVlans">
|
||||
</div>
|
||||
<div class="d-flex align-items-center m-1">
|
||||
<span class="small me-1">MAC</span>
|
||||
<input class="form-check-input form-check-input-sm" type="checkbox" name="exclude_columns"
|
||||
value="mac_address" id="excludeMACAddress">
|
||||
</div>
|
||||
<div class="d-flex align-items-center m-1">
|
||||
<span class="small me-1">MTU</span>
|
||||
<input class="form-check-input form-check-input-sm" type="checkbox" name="exclude_columns"
|
||||
value="mtu" id="excludeMTU">
|
||||
</div>
|
||||
<div class="d-flex align-items-center m-1">
|
||||
<span class="small me-1">Enabled</span>
|
||||
<input class="form-check-input form-check-input-sm" type="checkbox" name="exclude_columns"
|
||||
value="enabled" id="excludeEnabled">
|
||||
</div>
|
||||
<div class="d-flex align-items-center m-1">
|
||||
<span class="small me-1">Description</span>
|
||||
<input class="form-check-input form-check-input-sm" type="checkbox" name="exclude_columns"
|
||||
value="description" id="excludeDescription">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %} <!-- End block table_actions -->
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
{% if interface_sync.object.virtual_chassis %}
|
||||
<button type="button" class="btn btn-secondary" id="bulk-vc-member-button" data-bs-toggle="modal"
|
||||
data-bs-target="#bulkVCMemberModal" disabled>
|
||||
Bulk Edit VC Member
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interface_sync.netbox_only_interfaces %}
|
||||
<a href="#" class="ms-2 text-warning text-decoration-none netbox-only-link" data-bs-toggle="modal"
|
||||
data-bs-target="#netboxOnlyInterfacesModal"
|
||||
title="Click to view and delete NetBox-only interfaces">
|
||||
<i class="mdi mdi-alert-circle-outline me-1"></i>
|
||||
{{interface_sync.netbox_only_interfaces|length}} NetBox only interfaces
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ms-auto d-flex align-items-center">
|
||||
{% if interface_sync.cache_expiry %}
|
||||
<div id="cache-countdown" class="me-3">
|
||||
Cache expires in: <span id="countdown-timer"
|
||||
data-expiry="{{ interface_sync.cache_expiry|date:'c' }}"></span>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<div class="color-key me-3">
|
||||
<span class="badge text-success text-white">Matching values</span>
|
||||
<span class="badge text-warning text-white">Mismatched values</span>
|
||||
<span class="badge text-danger text-white">Not present in NetBox</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#interfaceFilterSection" aria-expanded="false"
|
||||
aria-controls="interfaceFilterSection">
|
||||
<i class="mdi mdi-filter"></i> Toggle Filters
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse mb-3" id="interfaceFilterSection">
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">
|
||||
<i class="mdi mdi-information"></i> Filters apply to currently displayed interfaces. Adjust the
|
||||
"per page" setting to apply the filters to more interfaces.
|
||||
</small>
|
||||
</div>
|
||||
<div class="filter-container d-flex gap-2">
|
||||
<input type="text" id="filter-name" placeholder="Filter by Name" class="form-control">
|
||||
{% if interface_sync.table.attrs.id == 'librenms-interface-table' %}
|
||||
<input type="text" id="filter-type" placeholder="Filter by Type" class="form-control">
|
||||
<input type="text" id="filter-speed" placeholder="Filter by Speed" class="form-control">
|
||||
{% endif %}
|
||||
<input type="text" id="filter-mac" placeholder="Filter by MAC" class="form-control">
|
||||
<input type="text" id="filter-mtu" placeholder="Filter by MTU" class="form-control">
|
||||
<input type="text" id="filter-enabled" placeholder="Filter by Status" class="form-control">
|
||||
<input type="text" id="filter-description" placeholder="Filter by Description" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
/* Your existing CSS rules */
|
||||
.ts-wrapper.multi .ts-control {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Updated rules using min-width */
|
||||
td[data-col="device_selection"] {
|
||||
width: 300px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
td[data-col="device_selection"] .ts-wrapper {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* NetBox-only interfaces link styling */
|
||||
.netbox-only-link {
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.netbox-only-link:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
{% include 'netbox_librenms_plugin/inc/paginator.html' with table=interface_sync.table %}
|
||||
{% include 'inc/table.html' with table=interface_sync.table %}
|
||||
{% include 'netbox_librenms_plugin/inc/paginator.html' with table=interface_sync.table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VLAN Detail Modal (inside form so hidden inputs are submitted) -->
|
||||
<div class="modal fade" id="vlanDetailModal" tabindex="-1" aria-labelledby="vlanDetailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="vlanDetailModalLabel">VLAN Assignments</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-muted mb-2">Interface: <strong id="vlanModalInterfaceName"></strong></p>
|
||||
<table class="table table-sm table-hover mb-3" id="vlanDetailTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">VID</th>
|
||||
<th style="width: 60px;">Type</th>
|
||||
<th>VLAN Group</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="vlanDetailTableBody">
|
||||
<!-- Populated by JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="applyVlanGroupToAll">
|
||||
<label class="form-check-label" for="applyVlanGroupToAll">
|
||||
Apply group assignments to all interfaces with matching VLANs
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" id="saveVlanGroups">Save</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<div class="card-body text-center text-muted py-4">
|
||||
<i class="mdi mdi-sync-off mdi-48px"></i>
|
||||
<p class="mt-2 mb-0">No interface data loaded. Click <strong>Refresh Interfaces</strong> to fetch data from LibreNMS.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} <!-- End if interface_sync.table -->
|
||||
|
||||
<!-- Interface Type Help Modal -->
|
||||
<div class="modal fade" id="interfaceTypeHelpModal" tabindex="-1" aria-labelledby="interfaceTypeHelpModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="interfaceTypeHelpModalLabel">NetBox Interface Sync Info</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5>Interface Type mapping</h5>
|
||||
<p>Interface type mappings control how LibreNMS interface types are translated to NetBox interface types
|
||||
during synchronization. These mappings can be customized in the plugin settings menu. The icons in
|
||||
the interface list indicate the mapping status for each interface type:</p>
|
||||
<ul>
|
||||
<li><i class="mdi mdi-link-variant"></i> - A mapping is configured for this interface type</li>
|
||||
<li><i class="mdi mdi-link-variant-off"></i> - No mapping is currently set for this interface type
|
||||
</li>
|
||||
</ul>
|
||||
<h5>Virtual Chassis Member Selection</h5>
|
||||
<p>For devices that are part of a virtual chassis, the plugin will attempt to select the correct virutal
|
||||
chassis member by matching the first number in the interface name to the device position in the
|
||||
virutal chassis. The selected device can be changed in the table before sync the interface.</p>
|
||||
<p>When changing the selected device for an interface row, the data will be checked again against the
|
||||
newly selected device.</p>
|
||||
<h5>VLAN Group Selection</h5>
|
||||
<p>Each VLAN in the VLANs column is automatically assigned to a VLAN group using a priority
|
||||
scope order: <strong>Rack → Location → Site → Site Group → Region → Global</strong>.
|
||||
The most specific scope that contains the VLAN wins. If a VLAN exists in only one group,
|
||||
that group is selected regardless of scope. Click the <i class="mdi mdi-pencil"></i> icon
|
||||
on any row to change the group assignment for individual VLANs.</p>
|
||||
<p>Check <strong>Apply group assignments to all interfaces with matching VLANs</strong> in the
|
||||
modal to apply your selection to every interface that shares the same VLAN IDs. This choice is
|
||||
saved for the duration of the cache, so subsequent table pages will also use it.</p>
|
||||
<p>VLANs shown with a <i class="mdi mdi-alert text-danger"></i>
|
||||
warning icon do not exist in the selected VLAN group in NetBox yet. Use the <strong>VLAN Sync</strong> tab to create them
|
||||
before syncing interfaces.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk VC Member Selection Modal -->
|
||||
<div class="modal fade" id="bulkVCMemberModal" tabindex="-1" aria-labelledby="bulkVCMemberModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="bulkVCMemberModalLabel">Set Virtual Chassis Member</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label for="bulk-vc-member-select">Select Virtual Chassis Member:</label>
|
||||
<select id="bulk-vc-member-select" class="form-select">
|
||||
{% for member in interface_sync.virtual_chassis_members %}
|
||||
<option value="{{ member.id }}">{{ member.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" id="apply-bulk-vc-member" data-bs-dismiss="modal">Apply</button>
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NetBox-only Interfaces Modal -->
|
||||
<div class="modal fade" id="netboxOnlyInterfacesModal" tabindex="-1" aria-labelledby="netboxOnlyInterfacesModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="netboxOnlyInterfacesModalLabel">
|
||||
NetBox-only Interfaces - {{ interface_sync.object.name }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="mdi mdi-alert me-2"></i>
|
||||
<strong>Warning:</strong> The following interfaces exist in NetBox but are not found in the LibreNMS
|
||||
data.
|
||||
Deleting these interfaces will permanently remove them from NetBox. This action cannot be undone.
|
||||
</div>
|
||||
|
||||
{% if interface_sync.netbox_only_interfaces %}
|
||||
<form id="delete-netbox-interfaces-form">
|
||||
{% csrf_token %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" id="select-all-netbox-interfaces"
|
||||
class="form-check-input">
|
||||
</th>
|
||||
<th>Interface Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for interface in interface_sync.netbox_only_interfaces %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="interface_ids" value="{{ interface.id }}"
|
||||
class="form-check-input netbox-interface-checkbox">
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ interface.url }}" target="_blank">{{ interface.name }}</a>
|
||||
</td>
|
||||
<td>{{ interface.type }}</td>
|
||||
<td>
|
||||
{% if interface.enabled %}
|
||||
<span class="text-success">Enabled</span>
|
||||
{% else %}
|
||||
<span class="text-danger">Disabled</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ interface.description|default:"-" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="mdi mdi-information me-2"></i>
|
||||
No interfaces found that exist only in NetBox.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{% if interface_sync.netbox_only_interfaces %}
|
||||
<button type="button" class="btn btn-danger" id="confirm-delete-interfaces">
|
||||
<i class="mdi mdi-delete"></i> Delete Selected Interfaces
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>IP Address Sync</h2>
|
||||
<div class="btn-list">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% if has_librenms_id %}
|
||||
{% with model_name=object|meta:"model_name" %}
|
||||
{% if model_name == "device" %}
|
||||
<button hx-post="{% url 'plugins:netbox_librenms_plugin:device_ipaddress_sync' pk=object.pk %}"
|
||||
hx-target="#ipaddress-sync-content"
|
||||
hx-include="[name='interface_name_field']"
|
||||
class="btn btn-outline-primary">
|
||||
Refresh IP Addresses
|
||||
</button>
|
||||
{% elif model_name == "virtualmachine" %}
|
||||
<button hx-post="{% url 'plugins:netbox_librenms_plugin:vm_ipaddress_sync' pk=object.pk %}"
|
||||
hx-target="#ipaddress-sync-content"
|
||||
hx-include="[name='interface_name_field']"
|
||||
class="btn btn-outline-primary">
|
||||
Refresh IP Addresses
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Action Buttons -->
|
||||
|
||||
<!-- Container for the interface sync content -->
|
||||
<div id="ipaddress-sync-content">
|
||||
{% include 'netbox_librenms_plugin/_ipaddress_sync_content.html' %}
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
{% load helpers %}
|
||||
{% include 'inc/messages.html' %}
|
||||
|
||||
<!-- IP Address Sync Table -->
|
||||
{% if ip_sync.table %}
|
||||
{% with model_name=ip_sync.object|meta:"model_name" %}
|
||||
<form method="post" action="{% url 'plugins:netbox_librenms_plugin:sync_device_ip_addresses' object_type=model_name pk=ip_sync.object.pk %}">
|
||||
{% endwith %}
|
||||
{% csrf_token %}
|
||||
{% if ip_sync.server_key %}<input type="hidden" name="server_key" value="{{ ip_sync.server_key }}">{% endif %}
|
||||
<input type="hidden" id="selected_ip" name="select" value="">
|
||||
<div class="noprint d-flex justify-content-between align-items-center mt-3 mb-3">
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="spinner spinner-border d-none" id="sync-spinner"></span>
|
||||
<span>Sync Selected IP Addresses</span>
|
||||
</button>
|
||||
</div>
|
||||
{% if ip_sync.cache_expiry %}
|
||||
<div id="ip-cache-countdown" class="me-3">
|
||||
Cache expires in: <span id="ip-countdown-timer" data-expiry="{{ ip_sync.cache_expiry|date:'c' }}"></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="d-flex justify-content-end align-items-center mb-3">
|
||||
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#ipFilterSection" aria-expanded="false" aria-controls="ipFilterSection">
|
||||
<i class="mdi mdi-filter"></i> Toggle Filters
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse mb-3" id="ipFilterSection">
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">
|
||||
<i class="mdi mdi-information"></i> Filters apply to currently displayed IP addresses.
|
||||
</small>
|
||||
</div>
|
||||
<div class="filter-container d-flex gap-2">
|
||||
<input type="text" id="filter-address" placeholder="Filter by Address" class="form-control">
|
||||
<input type="text" id="filter-prefix" placeholder="Filter by Prefix" class="form-control">
|
||||
<input type="text" id="filter-device" placeholder="Filter by Device" class="form-control">
|
||||
<input type="text" id="filter-interface" placeholder="Filter by Interface" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.ts-wrapper.multi .ts-control {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* VRF dropdown styles */
|
||||
td[data-col="vrf"] {
|
||||
width: 250px;
|
||||
min-width: 180px;
|
||||
}
|
||||
td[data-col="vrf"] .ts-wrapper {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure select elements in VRF column maintain consistent width */
|
||||
td[data-col="vrf"] select.form-select {
|
||||
width: 100%;
|
||||
min-width: 160px;
|
||||
}
|
||||
</style>
|
||||
<div class="card">
|
||||
{% include 'netbox_librenms_plugin/inc/paginator.html' with table=ip_sync.table %}
|
||||
{% include 'inc/table.html' with table=ip_sync.table %}
|
||||
{% include 'netbox_librenms_plugin/inc/paginator.html' with table=ip_sync.table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<div class="card-body text-center text-muted py-4">
|
||||
<i class="mdi mdi-sync-off mdi-48px"></i>
|
||||
<p class="mt-2 mb-0">No IP address data loaded. Click <strong>Refresh IP Addresses</strong> to fetch data from LibreNMS.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
|
||||
<!-- VLAN Sync Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>VLAN Sync</h2>
|
||||
<div class="btn-list">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% if has_librenms_id %}
|
||||
{% with model_name=object|meta:"model_name" %}
|
||||
{% if model_name == "device" %}
|
||||
<button hx-post="{% url 'plugins:netbox_librenms_plugin:device_vlan_sync' pk=object.pk %}"
|
||||
hx-target="#vlan-sync-content"
|
||||
hx-include="[name='interface_name_field']"
|
||||
class="btn btn-outline-primary">
|
||||
Refresh VLANs
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End VLAN Sync Header -->
|
||||
|
||||
<!-- Container for the VLAN sync content -->
|
||||
<div id="vlan-sync-content">
|
||||
{% include 'netbox_librenms_plugin/_vlan_sync_content.html' %}
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
{% load helpers %}
|
||||
{% include 'inc/messages.html' %}
|
||||
|
||||
{% if vlan_sync.error_message %}
|
||||
<!-- Error Message -->
|
||||
<div class="alert alert-danger">
|
||||
<i class="mdi mdi-alert-circle"></i> {{ vlan_sync.error_message }}
|
||||
</div>
|
||||
{% elif not vlan_sync.vlan_table or not vlan_sync.vlan_table.rows %}
|
||||
<!-- No VLAN Data -->
|
||||
<div class="card">
|
||||
<div class="card-body text-center text-muted py-4">
|
||||
<i class="mdi mdi-sync-off mdi-48px"></i>
|
||||
<p class="mt-2 mb-0">No VLAN data loaded. Click <strong>Refresh VLANs</strong> to fetch data from LibreNMS.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
{% with model_name=vlan_sync.object|meta:"model_name" %}
|
||||
<form method="post"
|
||||
action="{% url 'plugins:netbox_librenms_plugin:sync_selected_vlans' object_type=model_name object_id=vlan_sync.object.pk %}">
|
||||
{% endwith %}
|
||||
{% csrf_token %}
|
||||
{% if vlan_sync.server_key %}<input type="hidden" name="server_key" value="{{ vlan_sync.server_key }}">{% endif %}
|
||||
<input type="hidden" name="action" value="create_vlans">
|
||||
|
||||
<div class="noprint d-flex justify-content-between align-items-center mt-3 mb-3">
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="spinner spinner-border d-none" id="sync-spinner"></span>
|
||||
<span>Sync Selected VLANs</span>
|
||||
</button>
|
||||
<small class="text-muted ms-2">
|
||||
<i class="mdi mdi-information-outline"></i> Select a VLAN Group for each row, or leave empty for global VLANs
|
||||
</small>
|
||||
</div>
|
||||
<div class="ms-auto d-flex align-items-center">
|
||||
{% if vlan_sync.cache_expiry %}
|
||||
<div id="vlan-cache-countdown" class="me-3">
|
||||
Cache expires in: <span id="vlan-countdown-timer"
|
||||
data-expiry="{{ vlan_sync.cache_expiry|date:'c' }}"></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="color-key me-3">
|
||||
<span class="badge text-success text-white">Matching values</span>
|
||||
<span class="badge text-warning text-white">Mismatched values</span>
|
||||
<span class="badge text-danger text-white">Not present in NetBox</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
{% include 'netbox_librenms_plugin/inc/paginator.html' with table=vlan_sync.vlan_table %}
|
||||
{% include 'inc/table.html' with table=vlan_sync.vlan_table %}
|
||||
{% include 'netbox_librenms_plugin/inc/paginator.html' with table=vlan_sync.vlan_table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endif %}
|
||||
@@ -0,0 +1,212 @@
|
||||
{# Confirmation modal content for bulk imports #}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="mdi mdi-download"></i> Confirm Import
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-3">Review the devices that will be created in NetBox.</p>
|
||||
|
||||
{% if errors %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="mdi mdi-alert"></i>
|
||||
{{ errors|join:', ' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for entry in devices %}
|
||||
{% with vc=entry.validation.virtual_chassis %}
|
||||
<div class="border rounded-3 p-2 mb-2">
|
||||
{% if vc.is_stack and vc.members %}
|
||||
<button class="d-flex flex-wrap align-items-center gap-2 w-100 text-start border-0 bg-transparent p-0 collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#bulk-vc-{{ entry.device_id }}"
|
||||
aria-expanded="false">
|
||||
{% else %}
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
{% endif %}
|
||||
<i class="mdi mdi-{{ entry.is_vm|yesno:'cloud,server' }} fs-4 text-primary"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">{{ entry.device_name }}</div>
|
||||
<div class="small text-muted">{{ entry.is_vm|yesno:'Virtual Machine,Device' }}</div>
|
||||
</div>
|
||||
{% if entry.role %}
|
||||
<span class="badge text-white ms-1" style="background-color: #{{ entry.role.color|default:'6c757d' }};">{{ entry.role.name }}</span>
|
||||
{% endif %}
|
||||
{% if vc.is_stack %}
|
||||
<span class="badge bg-info text-white text-uppercase fw-semibold small">
|
||||
<i class="mdi mdi-switch"></i>
|
||||
VC{% if vc.member_count %} · {{ vc.member_count }}{% endif %}
|
||||
</span>
|
||||
{% if vc.members %}
|
||||
<i class="mdi mdi-chevron-down ms-auto"></i>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if vc.is_stack and vc.members %}
|
||||
</button>
|
||||
{% else %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="text-muted small mt-1 d-flex flex-wrap gap-3">
|
||||
{% if entry.cluster %}<span><strong>Cluster:</strong> {{ entry.cluster.name }}</span>{% endif %}
|
||||
{% if entry.rack %}
|
||||
<span>
|
||||
<strong>Rack:</strong>
|
||||
{% if entry.rack.location %}{{ entry.rack.location.name }} – {% endif %}
|
||||
{{ entry.rack.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if entry.validation.issues %}
|
||||
<div class="alert alert-warning py-2 px-3 mt-3 mb-0 small">
|
||||
<i class="mdi mdi-alert"></i>
|
||||
{{ entry.validation.issues|join:'; ' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if vc.is_stack and vc.members %}
|
||||
<div class="collapse mt-2" id="bulk-vc-{{ entry.device_id }}">
|
||||
<div class="border rounded">
|
||||
<div class="px-3 pb-2">
|
||||
<ul class="list-unstyled mb-0 small">
|
||||
{% for member in vc.members %}
|
||||
<li class="d-flex flex-wrap align-items-center py-2 {% if not forloop.last %}border-bottom{% endif %}">
|
||||
<span class="text-muted me-2">
|
||||
{% if member.position %}
|
||||
Pos {{ member.position }}
|
||||
{% else %}
|
||||
Pos —
|
||||
{% endif %}
|
||||
</span>
|
||||
<div class="flex-grow-1 d-flex align-items-center gap-2">
|
||||
<code class="flex-grow-1 mb-0">{{ member.suggested_name }}
|
||||
{% if member.is_master %}
|
||||
<span class="badge bg-success text-white">Master</span>
|
||||
{% endif %}
|
||||
</code>
|
||||
</div>
|
||||
{% if member.serial %}
|
||||
<span class="text-muted">SN {{ member.serial }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif vc.is_stack and vc.detection_error %}
|
||||
<div class="alert alert-warning py-2 px-3 mt-3 mb-0 small">
|
||||
<i class="mdi mdi-alert"></i>
|
||||
Unable to display virtual chassis members: {{ vc.detection_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="modal-body pt-0">
|
||||
<div class="p-3 bg-primary-subtle border border-primary rounded-1">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="use_background_job" id="use-background-job-checkbox" class="form-check-input" checked>
|
||||
<label class="form-check-label" for="use-background-job-checkbox">
|
||||
Run as background job
|
||||
</label>
|
||||
<small class="form-text text-muted d-block mt-1">
|
||||
Recommended: Jobs are logged and can be cancelled.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="mdi mdi-close"></i> Cancel
|
||||
</button>
|
||||
<form class="d-inline-flex align-items-center gap-2"
|
||||
id="bulk-import-confirm-form"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:bulk_import_devices' %}"
|
||||
hx-target="body"
|
||||
hx-swap="none">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="server_key" value="{{ server_key }}">
|
||||
<input type="hidden" name="vc_detection_enabled" value="{{ vc_detection_enabled|yesno:'on,off' }}">
|
||||
{% for entry in devices %}
|
||||
<input type="hidden" name="select" value="{{ entry.device_id }}">
|
||||
{% if entry.role %}<input type="hidden" name="role_{{ entry.device_id }}" value="{{ entry.role.pk }}">{% endif %}
|
||||
{% if entry.cluster %}<input type="hidden" name="cluster_{{ entry.device_id }}" value="{{ entry.cluster.pk }}">{% endif %}
|
||||
{% if entry.rack %}<input type="hidden" name="rack_{{ entry.device_id }}" value="{{ entry.rack.pk }}">{% endif %}
|
||||
{% endfor %}
|
||||
<input type="hidden" name="use_sysname" value="{{ use_sysname|yesno:'true,false' }}">
|
||||
<input type="hidden" name="strip_domain" value="{{ strip_domain|yesno:'true,false' }}">
|
||||
<input type="hidden" name="enable_vc_detection" value="{{ vc_detection_enabled|yesno:'true,false' }}">
|
||||
<input type="hidden" name="use_background_job" id="use-background-job-hidden" value="on">
|
||||
<button type="submit" class="btn btn-success d-inline-flex align-items-center gap-1" data-bulk-confirm-submit>
|
||||
<span data-state="idle" class="d-inline-flex align-items-center gap-1">
|
||||
<i class="mdi mdi-download"></i>
|
||||
Import {{ device_count }} device{{ device_count|pluralize }}
|
||||
</span>
|
||||
<span data-state="loading" class="d-none align-items-center gap-1">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
Importing...
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
if (window.__bulkConfirmHandlersAttached) {
|
||||
return;
|
||||
}
|
||||
window.__bulkConfirmHandlersAttached = true;
|
||||
|
||||
const formId = 'bulk-import-confirm-form';
|
||||
const checkboxId = 'use-background-job-checkbox';
|
||||
const hiddenId = 'use-background-job-hidden';
|
||||
|
||||
function toggleButtonState(isLoading) {
|
||||
const form = document.getElementById(formId);
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
const button = form.querySelector('[data-bulk-confirm-submit]');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
const idle = button.querySelector('[data-state="idle"]');
|
||||
const loading = button.querySelector('[data-state="loading"]');
|
||||
button.disabled = isLoading;
|
||||
if (idle) {
|
||||
idle.classList.toggle('d-none', isLoading);
|
||||
}
|
||||
if (loading) {
|
||||
loading.classList.toggle('d-none', !isLoading);
|
||||
}
|
||||
}
|
||||
|
||||
// Delegated so it survives HTMX re-renders of the modal fragment.
|
||||
document.addEventListener('change', function(event) {
|
||||
if (event.target && event.target.id === checkboxId) {
|
||||
const hiddenInput = document.getElementById(hiddenId);
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = event.target.checked ? 'on' : 'off';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||
if (event.target && event.target.id === formId) {
|
||||
toggleButtonState(true);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.target && event.target.id === formId) {
|
||||
toggleButtonState(false);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% if table.rows %}
|
||||
{% for row in table.rows %}
|
||||
<tr {{ row.attrs.as_html }} hx-swap-oob="true">
|
||||
{% for column, cell in row.items %}
|
||||
<td {{ column.attrs.td.as_html }}>{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr id="device-row-{{ record.device_id }}">
|
||||
<td colspan="13">
|
||||
<div class="alert alert-danger">
|
||||
ERROR: No table rows found for device {{ record.device_id }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{# Consume and clear any pending Django messages to prevent reappearing toasts #}
|
||||
<div id="django-messages" class="toast-container position-fixed bottom-0 end-0 p-3" hx-swap-oob="true">
|
||||
{% for _ in messages %}{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,625 @@
|
||||
{# HTMX template for device validation details modal #}
|
||||
{# Redesigned to match the sync page's clean table layout #}
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{% if validation.existing_device %}
|
||||
{% if validation.device_type_mismatch %}
|
||||
<i class="mdi mdi-alert-circle text-danger"></i>
|
||||
{% elif validation.existing_match_type == 'librenms_id' %}
|
||||
<i class="mdi mdi-check-decagram text-info"></i>
|
||||
{% else %}
|
||||
<i class="mdi mdi-alert text-warning"></i>
|
||||
{% endif %}
|
||||
{% elif validation.is_ready %}
|
||||
<i class="mdi mdi-check-circle text-success"></i>
|
||||
{% elif validation.can_import %}
|
||||
<i class="mdi mdi-alert text-warning"></i>
|
||||
{% else %}
|
||||
<i class="mdi mdi-close-circle text-danger"></i>
|
||||
{% endif %}
|
||||
Import Validation: {{ validation.resolved_name|default:libre_device.sysName|default:libre_device.hostname }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
{# Compute existing device URL once for use throughout #}
|
||||
{% if validation.existing_device %}
|
||||
{% if existing_device_model_name == "virtualmachine" %}
|
||||
{% url 'virtualization:virtualmachine' pk=validation.existing_device.pk as existing_device_url %}
|
||||
{% else %}
|
||||
{% url 'dcim:device' pk=validation.existing_device.pk as existing_device_url %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# Row: LibreNMS Status (left) + Device Info table (right) #}
|
||||
<div class="row mb-3">
|
||||
|
||||
{# Left: LibreNMS Status card #}
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<h6 class="card-header">LibreNMS Status</h6>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="padding-left: 0.75rem;">Status</th>
|
||||
<td>
|
||||
{% if libre_device.status == 1 or libre_device.status == "1" %}
|
||||
<span class="badge bg-success text-white"><i class="mdi mdi-check"></i> Up</span>
|
||||
{% elif libre_device.status == 0 or 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Right: Device Information table #}
|
||||
<div class="col-md-8">
|
||||
<div class="card h-100">
|
||||
<h6 class="card-header d-flex justify-content-between align-items-center">
|
||||
Device Information
|
||||
<span class="d-flex gap-1">
|
||||
{% if use_sysname %}
|
||||
<span class="badge bg-success-lt"><i class="mdi mdi-check-circle-outline"></i> sysName</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success-lt"><i class="mdi mdi-check-circle-outline"></i> hostname</span>
|
||||
{% endif %}
|
||||
{% if strip_domain %}
|
||||
<span class="badge bg-success-lt"><i class="mdi mdi-check-circle-outline"></i> Domain stripped</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</h6>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 22%; padding-left: 0.75rem;">Field</th>
|
||||
<th style="width: 39%;">NetBox Value</th>
|
||||
<th style="width: 39%;">LibreNMS Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{# Name row #}
|
||||
<tr>
|
||||
<td style="padding-left: 0.75rem;">Name</td>
|
||||
<td>
|
||||
{% if validation.existing_device %}
|
||||
<a href="{{ existing_device_url }}" target="_blank" rel="noopener noreferrer">
|
||||
{{ validation.existing_device.name }}
|
||||
</a>
|
||||
{% if validation.name_sync_available %}
|
||||
<form style="display:inline" class="ms-2"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:device_conflict_action' device_id=libre_device.device_id %}"
|
||||
hx-swap="none"
|
||||
hx-include="#use-sysname-toggle, #strip-domain-toggle">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="existing_device_id" value="{{ validation.existing_device.pk }}">
|
||||
<input type="hidden" name="existing_device_type" value="{{ existing_device_model_name|default:'device' }}">
|
||||
<input type="hidden" name="action" value="sync_name">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary py-0 px-1" title="Sync name to {{ validation.suggested_name }}" aria-label="Sync name to {{ validation.suggested_name }}">
|
||||
<i class="mdi mdi-sync"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">New device</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ validation.resolved_name|default:libre_device.sysName|default:libre_device.hostname }}</td>
|
||||
</tr>
|
||||
|
||||
{# Show only for non-VM contexts: true when no existing device (new import, not VM) OR when existing device is not a VM. #}
|
||||
{% if not validation.existing_device and not validation.import_as_vm or validation.existing_device and existing_device_model_name != "virtualmachine" %}
|
||||
<tr>
|
||||
<td style="padding-left: 0.75rem;">Site</td>
|
||||
<td>
|
||||
{% if validation.existing_device and validation.existing_device.site %}
|
||||
{{ validation.existing_device.site }}
|
||||
{% if validation.site.site and validation.existing_device.site.pk == validation.site.site.pk %}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
{% endif %}
|
||||
{% elif validation.site.site %}
|
||||
{{ validation.site.site.name }}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="mdi mdi-close-circle"></i> No matching site</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ libre_device.location|default:"—" }}</td>
|
||||
</tr>
|
||||
|
||||
{# Device Type row #}
|
||||
<tr>
|
||||
<td style="padding-left: 0.75rem;">Device Type</td>
|
||||
<td>
|
||||
{% if validation.device_type_mismatch %}
|
||||
<span class="text-danger">
|
||||
{{ validation.existing_device.device_type }}
|
||||
<i class="mdi mdi-alert-circle"></i>
|
||||
</span>
|
||||
<form style="display:inline" class="ms-1"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:device_conflict_action' device_id=libre_device.device_id %}"
|
||||
hx-swap="none"
|
||||
hx-include="#use-sysname-toggle, #strip-domain-toggle">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="existing_device_id" value="{{ validation.existing_device.pk }}">
|
||||
<input type="hidden" name="existing_device_type" value="{{ existing_device_model_name|default:'device' }}">
|
||||
<input type="hidden" name="action" value="update_type">
|
||||
<input type="hidden" name="force" value="on">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger py-0 px-1" title="Update to LibreNMS type" aria-label="Update to LibreNMS type">
|
||||
<i class="mdi mdi-sync"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% elif validation.existing_device and validation.existing_device.device_type %}
|
||||
{{ validation.existing_device.device_type }}
|
||||
{% if sync_info and not sync_info.device_type_synced and sync_info.librenms_device_type %}
|
||||
<form style="display:inline" class="ms-1"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:device_conflict_action' device_id=libre_device.device_id %}"
|
||||
hx-swap="none">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="existing_device_id" value="{{ validation.existing_device.pk }}">
|
||||
<input type="hidden" name="existing_device_type" value="{{ existing_device_model_name|default:'device' }}">
|
||||
<input type="hidden" name="action" value="sync_device_type">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary py-0 px-1" title="Sync device type" aria-label="Sync device type">
|
||||
<i class="mdi mdi-sync"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% elif validation.device_type.device_type %}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
{% endif %}
|
||||
{% elif validation.device_type.device_type %}
|
||||
<span {% if validation.device_type.match_type == 'chassis' %}title="Matched via chassis inventory ({{ validation.device_type.chassis_model }})"{% elif validation.device_type.match_type == 'mapping' %}title="Matched via device type mapping"{% endif %}>
|
||||
{{ validation.device_type.device_type }}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="mdi mdi-close-circle"></i> No matching type</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ libre_device.hardware|default:"—" }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{# Serial row (only for devices) #}
|
||||
{# Show only for non-VM contexts: true when no existing device (new import, not VM) OR when existing device is not a VM. #}
|
||||
{% if not validation.existing_device and not validation.import_as_vm or validation.existing_device and existing_device_model_name != "virtualmachine" %}
|
||||
<tr>
|
||||
<td style="padding-left: 0.75rem;">Serial</td>
|
||||
<td>
|
||||
{% if validation.existing_device %}
|
||||
{% if validation.existing_device.serial %}
|
||||
{{ validation.existing_device.serial }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not set</span>
|
||||
{% endif %}
|
||||
{% if sync_info and not sync_info.serial_synced %}
|
||||
{% if validation.serial_action == 'conflict' %}
|
||||
<span class="text-danger ms-1" title="Serial conflict — another device already has this serial">
|
||||
<i class="mdi mdi-alert-circle"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<form style="display:inline" class="ms-1"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:device_conflict_action' device_id=libre_device.device_id %}"
|
||||
hx-swap="none">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="existing_device_id" value="{{ validation.existing_device.pk }}">
|
||||
<input type="hidden" name="existing_device_type" value="{{ existing_device_model_name|default:'device' }}">
|
||||
<input type="hidden" name="action" value="sync_serial">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary py-0 px-1" title="Sync serial from LibreNMS" aria-label="Sync serial from LibreNMS">
|
||||
<i class="mdi mdi-sync"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% elif sync_info and sync_info.serial_synced and sync_info.librenms_serial != '-' %}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ libre_device.serial|default:"—" }}
|
||||
{% if validation.serial_duplicate %}
|
||||
<span class="badge bg-danger ms-1">Conflict</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{# Role row #}
|
||||
<tr>
|
||||
<td style="padding-left: 0.75rem;">
|
||||
{% if validation.import_as_vm %}Role{% else %}Device Role{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if validation.existing_device and validation.existing_device.role %}
|
||||
{{ validation.existing_device.role }}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
{% elif validation.device_role.role %}
|
||||
{{ validation.device_role.role.name }}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="mdi mdi-close-circle"></i> No role assigned</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="text-muted">—</span></td>
|
||||
</tr>
|
||||
|
||||
{# Platform row #}
|
||||
<tr>
|
||||
<td style="padding-left: 0.75rem;">Platform</td>
|
||||
<td>
|
||||
{% if validation.existing_device and validation.existing_device.platform %}
|
||||
{{ validation.existing_device.platform }}
|
||||
{% if sync_info and not sync_info.platform_synced %}
|
||||
{% if sync_info.platform_info.platform_exists %}
|
||||
<form style="display:inline" class="ms-1"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:device_conflict_action' device_id=libre_device.device_id %}"
|
||||
hx-swap="none">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="existing_device_id" value="{{ validation.existing_device.pk }}">
|
||||
<input type="hidden" name="existing_device_type" value="{{ existing_device_model_name|default:'device' }}">
|
||||
<input type="hidden" name="action" value="sync_platform">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary py-0 px-1" title="Sync platform" aria-label="Sync platform">
|
||||
<i class="mdi mdi-sync"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-muted ms-1" title="Platform '{{ sync_info.platform_info.librenms_os }}' not in NetBox">
|
||||
<i class="mdi mdi-alert-outline"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% elif sync_info and sync_info.platform_synced %}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
{% endif %}
|
||||
{% elif validation.platform.platform %}
|
||||
{{ validation.platform.platform.name }}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
{% elif sync_info and sync_info.platform_info.platform_exists %}
|
||||
<span class="text-muted">Not set</span>
|
||||
{% if validation.existing_device %}
|
||||
<form style="display:inline" class="ms-1"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:device_conflict_action' device_id=libre_device.device_id %}"
|
||||
hx-swap="none">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="existing_device_id" value="{{ validation.existing_device.pk }}">
|
||||
<input type="hidden" name="existing_device_type" value="{{ existing_device_model_name|default:'device' }}">
|
||||
<input type="hidden" name="action" value="sync_platform">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary py-0 px-1" title="Sync platform" aria-label="Sync platform">
|
||||
<i class="mdi mdi-sync"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">Optional</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ libre_device.os|default:"—" }}</td>
|
||||
</tr>
|
||||
|
||||
{% if validation.import_as_vm and not validation.existing_device or validation.existing_device and existing_device_model_name == "virtualmachine" %}
|
||||
{# Cluster row (VMs only) #}
|
||||
<tr>
|
||||
<td style="padding-left: 0.75rem;">Cluster</td>
|
||||
<td>
|
||||
{% if validation.cluster.cluster %}
|
||||
{{ validation.cluster.cluster.name }}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="mdi mdi-close-circle"></i> No cluster assigned</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="text-muted">—</span></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{# Rack row #}
|
||||
<tr>
|
||||
<td style="padding-left: 0.75rem;">Rack</td>
|
||||
<td>
|
||||
{% if validation.rack.rack %}
|
||||
{% if validation.rack.rack.location %}
|
||||
{{ validation.rack.rack.location.name }} — {{ validation.rack.rack.name }}
|
||||
{% else %}
|
||||
{{ validation.rack.rack.name }}
|
||||
{% endif %}
|
||||
<span class="text-success"><i class="mdi mdi-check-circle"></i></span>
|
||||
{% else %}
|
||||
<span class="text-muted">Optional</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="text-muted">—</span></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{# Primary IP row #}
|
||||
<tr>
|
||||
<td style="padding-left: 0.75rem;">Primary IP</td>
|
||||
<td>
|
||||
{% if validation.existing_device and validation.existing_device.primary_ip %}
|
||||
{{ validation.existing_device.primary_ip }}
|
||||
{% elif libre_device.ip %}
|
||||
{{ libre_device.ip }}
|
||||
{% else %}
|
||||
<span class="text-muted">No primary IP</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ libre_device.ip|default:"—" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Status & Actions #}
|
||||
{% if validation.existing_device %}
|
||||
{% if validation.existing_match_type == 'librenms_id' %}
|
||||
<div class="alert alert-info py-2 mb-3 d-flex flex-wrap align-items-center gap-1">
|
||||
{% if existing_id_servers %}
|
||||
{% for srv in existing_id_servers %}
|
||||
<span class="badge bg-info-lt me-1"><i class="mdi mdi-check-decagram"></i> Linked — ID {{ srv.device_id }} @ {{ srv.display_name }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="badge bg-info-lt me-1"><i class="mdi mdi-check-decagram"></i> Linked — ID {{ libre_device.device_id }}</span>
|
||||
{% endif %}
|
||||
{% if validation.name_matches %}
|
||||
<span class="badge bg-success-lt me-1"><i class="mdi mdi-check-circle-outline"></i> Name match</span>
|
||||
{% elif validation.name_sync_available %}
|
||||
<span class="badge bg-warning-lt me-1"><i class="mdi mdi-alert"></i> Name differs</span>
|
||||
{% endif %}
|
||||
{% if validation.serial_confirmed %}
|
||||
<span class="badge bg-success-lt me-1"><i class="mdi mdi-check-circle-outline"></i> Serial confirmed</span>
|
||||
{% elif validation.serial_action == 'conflict' %}
|
||||
<span class="badge bg-danger-lt me-1"><i class="mdi mdi-alert-circle"></i> Serial conflict</span>
|
||||
{% elif validation.serial_action == 'update_serial' %}
|
||||
<span class="badge bg-warning-lt me-1"><i class="mdi mdi-alert"></i> Serial differs</span>
|
||||
{% endif %}
|
||||
{% if validation.device_type_mismatch %}
|
||||
<span class="badge bg-danger-lt me-1"><i class="mdi mdi-alert-circle"></i> Type mismatch</span>
|
||||
{% endif %}
|
||||
{% if validation.librenms_id_needs_migration %}
|
||||
<span class="badge bg-warning-lt me-1"><i class="mdi mdi-database-alert"></i> Legacy ID format</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if validation.librenms_id_needs_migration %}
|
||||
<div class="mb-3">
|
||||
<form style="display:inline"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:device_conflict_action' device_id=libre_device.device_id %}"
|
||||
hx-swap="none">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="existing_device_id" value="{{ validation.existing_device.pk }}">
|
||||
<input type="hidden" name="existing_device_type" value="{{ existing_device_model_name|default:'device' }}">
|
||||
<input type="hidden" name="server_key" value="{{ server_key }}">
|
||||
<input type="hidden" name="action" value="migrate_librenms_id">
|
||||
{% if validation.existing_device.cluster %}
|
||||
<input type="hidden" name="cluster_{{ libre_device.device_id }}" value="{{ validation.existing_device.cluster.pk }}">
|
||||
{% endif %}
|
||||
{% if not validation.serial_confirmed %}
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="force" id="force-migrate-{{ libre_device.device_id }}"
|
||||
onchange="this.closest('form').querySelector('.migrate-btn').disabled = !this.checked">
|
||||
<label class="form-check-label text-warning" for="force-migrate-{{ libre_device.device_id }}">
|
||||
<i class="mdi mdi-alert"></i> Serial not confirmed — check to migrate anyway
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-sm btn-warning migrate-btn"{% if not validation.serial_confirmed %} disabled{% endif %}>
|
||||
<i class="mdi mdi-database-sync"></i> Migrate ID format
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif validation.existing_match_type == 'hostname' %}
|
||||
<div class="alert alert-warning py-2 mb-3 d-flex flex-wrap align-items-center gap-1">
|
||||
<span class="badge bg-warning-lt me-1"><i class="mdi mdi-link-variant"></i> Hostname match</span>
|
||||
{% if validation.serial_confirmed %}
|
||||
<span class="badge bg-success-lt me-1"><i class="mdi mdi-check-circle-outline"></i> Serial confirmed</span>
|
||||
{% elif validation.serial_action == 'conflict' %}
|
||||
<span class="badge bg-danger-lt me-1"><i class="mdi mdi-alert-circle"></i> Serial conflict</span>
|
||||
{% elif validation.serial_action == 'update_serial' %}
|
||||
<span class="badge bg-warning-lt me-1"><i class="mdi mdi-alert"></i> Serial differs</span>
|
||||
{% endif %}
|
||||
{% if validation.device_type_mismatch %}
|
||||
<span class="badge bg-danger-lt me-1"><i class="mdi mdi-alert-circle"></i> Type mismatch</span>
|
||||
{% endif %}
|
||||
<span class="text-muted ms-1">
|
||||
— Exists as
|
||||
<a href="{{ existing_device_url }}" target="_blank" rel="noopener noreferrer">{{ validation.existing_device.name }}</a>,
|
||||
not linked to LibreNMS.
|
||||
</span>
|
||||
</div>
|
||||
{% if validation.serial_action == 'conflict' %}
|
||||
<div class="alert alert-danger py-2 mb-3">
|
||||
<i class="mdi mdi-alert-circle"></i>
|
||||
Import blocked: The incoming serial number is already assigned to another device in NetBox.
|
||||
Resolve the duplicate serial before linking.
|
||||
</div>
|
||||
{% elif validation.import_as_vm or existing_device_model_name == "virtualmachine" %}
|
||||
<div class="alert alert-info py-2 mb-3">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
Hostname match found for a VM — use the import action to proceed.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-3">
|
||||
<form style="display:inline"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:device_conflict_action' device_id=libre_device.device_id %}"
|
||||
hx-swap="none"
|
||||
hx-include="#use-sysname-toggle, #strip-domain-toggle">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="existing_device_id" value="{{ validation.existing_device.pk }}">
|
||||
<input type="hidden" name="existing_device_type" value="{{ existing_device_model_name|default:'device' }}">
|
||||
{% if validation.device_type_mismatch %}
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="force" id="force-hostname-{{ libre_device.device_id }}"
|
||||
onchange="this.closest('form').querySelector('.force-action-btn').disabled = !this.checked">
|
||||
<label class="form-check-label text-danger" for="force-hostname-{{ libre_device.device_id }}">
|
||||
<i class="mdi mdi-alert-circle"></i> Device type mismatch — check to force
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if validation.serial_action == 'update_serial' %}
|
||||
<input type="hidden" name="action" value="update_serial">
|
||||
<button type="submit" class="btn btn-sm btn-warning{% if validation.device_type_mismatch %} force-action-btn{% endif %}"{% if validation.device_type_mismatch %} disabled{% endif %}>
|
||||
<i class="mdi mdi-swap-horizontal"></i> Update Serial & Link
|
||||
</button>
|
||||
{% else %}
|
||||
<input type="hidden" name="action" value="link">
|
||||
<button type="submit" class="btn btn-sm btn-primary{% if validation.device_type_mismatch %} force-action-btn{% endif %}"{% if validation.device_type_mismatch %} disabled{% endif %}>
|
||||
<i class="mdi mdi-link-plus"></i> Link to LibreNMS
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif validation.existing_match_type == 'serial' %}
|
||||
<div class="alert alert-warning py-2 mb-3 d-flex flex-wrap align-items-center gap-1">
|
||||
<span class="badge bg-warning-lt me-1"><i class="mdi mdi-barcode"></i> Serial match</span>
|
||||
{% if validation.serial_action == 'hostname_differs' %}
|
||||
<span class="badge bg-warning-lt me-1"><i class="mdi mdi-alert"></i> Name differs</span>
|
||||
{% elif validation.serial_action == 'link' %}
|
||||
<span class="badge bg-success-lt me-1"><i class="mdi mdi-check-circle-outline"></i> Name match</span>
|
||||
{% endif %}
|
||||
{% if validation.device_type_mismatch %}
|
||||
<span class="badge bg-danger-lt me-1"><i class="mdi mdi-alert-circle"></i> Type mismatch</span>
|
||||
{% endif %}
|
||||
<span class="text-muted ms-1">
|
||||
— Exists as
|
||||
<a href="{{ existing_device_url }}" target="_blank" rel="noopener noreferrer">{{ validation.existing_device.name }}</a>,
|
||||
not linked to LibreNMS.
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<form style="display:inline"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:device_conflict_action' device_id=libre_device.device_id %}"
|
||||
hx-swap="none"
|
||||
hx-include="#use-sysname-toggle, #strip-domain-toggle">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="existing_device_id" value="{{ validation.existing_device.pk }}">
|
||||
<input type="hidden" name="existing_device_type" value="{{ existing_device_model_name|default:'device' }}">
|
||||
{% if validation.device_type_mismatch %}
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="force" id="force-serial-{{ libre_device.device_id }}"
|
||||
onchange="this.closest('form').querySelector('.force-action-btn').disabled = !this.checked">
|
||||
<label class="form-check-label text-danger" for="force-serial-{{ libre_device.device_id }}">
|
||||
<i class="mdi mdi-alert-circle"></i> Device type mismatch — check to force
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if validation.serial_action == 'link' %}
|
||||
<input type="hidden" name="action" value="link">
|
||||
<button type="submit" class="btn btn-sm btn-primary{% if validation.device_type_mismatch %} force-action-btn{% endif %}"{% if validation.device_type_mismatch %} disabled{% endif %}>
|
||||
<i class="mdi mdi-link-plus"></i> Link to LibreNMS
|
||||
</button>
|
||||
{% elif validation.serial_action == 'hostname_differs' %}
|
||||
<button type="submit" name="action" value="update" class="btn btn-sm btn-warning{% if validation.device_type_mismatch %} force-action-btn{% endif %}"{% if validation.device_type_mismatch %} disabled{% endif %}>
|
||||
<i class="mdi mdi-pencil"></i> Update & Link
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% elif validation.existing_match_type == 'primary_ip' %}
|
||||
<div class="alert alert-warning py-2 mb-3 d-flex flex-wrap align-items-center gap-1">
|
||||
<span class="badge bg-warning-lt me-1"><i class="mdi mdi-ip-network"></i> IP match</span>
|
||||
<span class="text-muted ms-1">
|
||||
— Device with IP {{ libre_device.ip }} exists as
|
||||
<a href="{{ existing_device_url }}" target="_blank" rel="noopener noreferrer">{{ validation.existing_device.name }}</a>.
|
||||
Consider adding LibreNMS ID manually.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="alert alert-info py-2 mb-3">
|
||||
<i class="mdi mdi-link"></i>
|
||||
<strong>Exists</strong> — Device already exists as
|
||||
<a href="{{ existing_device_url }}" target="_blank" rel="noopener noreferrer">{{ validation.existing_device.name }}</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif validation.is_ready %}
|
||||
<div class="alert alert-success py-2 mb-3">
|
||||
<i class="mdi mdi-check-circle"></i>
|
||||
<strong>Ready to Import</strong> — All prerequisites are met.
|
||||
</div>
|
||||
{% elif validation.can_import %}
|
||||
<div class="alert alert-warning py-2 mb-3">
|
||||
<i class="mdi mdi-alert"></i>
|
||||
<strong>Can Import</strong> — Review warnings below before importing.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-danger py-2 mb-3">
|
||||
<i class="mdi mdi-close-circle"></i>
|
||||
<strong>Cannot Import</strong> — Validation issues prevent import.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Warnings #}
|
||||
{% if validation.warnings %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header py-2">
|
||||
<i class="mdi mdi-alert"></i> Warnings ({{ validation.warnings|length }})
|
||||
</div>
|
||||
<div class="card-body py-2">
|
||||
<ul class="mb-0 ps-3">
|
||||
{% for warning in validation.warnings %}
|
||||
<li><small>{{ warning }}</small></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
{% if validation.existing_device %}
|
||||
{% if existing_device_model_name == "virtualmachine" %}
|
||||
<a href="{{ existing_device_url }}"
|
||||
class="btn btn-primary btn-sm" target="_blank" rel="noopener noreferrer">
|
||||
<i class="mdi mdi-open-in-new"></i> View VM in NetBox
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ existing_device_url }}"
|
||||
class="btn btn-primary btn-sm" target="_blank" rel="noopener noreferrer">
|
||||
<i class="mdi mdi-open-in-new"></i> View in NetBox
|
||||
</a>
|
||||
{% if validation.existing_match_type == 'librenms_id' %}
|
||||
<a href="{% url 'plugins:netbox_librenms_plugin:device_librenms_sync' pk=validation.existing_device.pk %}"
|
||||
class="btn btn-outline-primary btn-sm" target="_blank" rel="noopener noreferrer">
|
||||
<i class="mdi mdi-sync"></i> Full Sync Page
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">
|
||||
<i class="mdi mdi-close"></i> Close
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,121 @@
|
||||
{# HTMX template for virtual chassis details modal #}
|
||||
{# Shows virtual chassis/stack information for a LibreNMS device #}
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="mdi mdi-server-network"></i> Virtual Chassis: {{ libre_device.hostname }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
{# Device Summary #}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="mdi mdi-server"></i> LibreNMS Device Information
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-3">Hostname:</dt>
|
||||
<dd class="col-sm-9"><strong>{{ libre_device.hostname }}</strong></dd>
|
||||
|
||||
<dt class="col-sm-3">System Name:</dt>
|
||||
<dd class="col-sm-9">{{ libre_device.sysName|default:"N/A" }}</dd>
|
||||
|
||||
<dt class="col-sm-3">LibreNMS ID:</dt>
|
||||
<dd class="col-sm-9">{{ libre_device.device_id }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Hardware:</dt>
|
||||
<dd class="col-sm-9">{{ libre_device.hardware|default:"N/A" }}</dd>
|
||||
|
||||
<dt class="col-sm-3">OS:</dt>
|
||||
<dd class="col-sm-9">{{ libre_device.os|default:"N/A" }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Virtual Chassis Detection Error #}
|
||||
{% if vc_data.detection_error %}
|
||||
<div class="alert alert-warning">
|
||||
<h6 class="alert-heading">
|
||||
<i class="mdi mdi-alert"></i> Virtual Chassis Detection Error
|
||||
</h6>
|
||||
<p class="mb-0">
|
||||
Unable to detect virtual chassis information: <strong>{{ vc_data.detection_error }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Virtual Chassis Details #}
|
||||
{% if vc_data.is_stack %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="mdi mdi-switch"></i> Virtual Chassis Stack
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-3">
|
||||
This device is part of a <strong>{{ vc_data.member_count }}-member</strong> stack/virtual chassis.
|
||||
</p>
|
||||
|
||||
{% if vc_data.members %}
|
||||
<h6 class="mb-3">Stack Members:</h6>
|
||||
<div class="border rounded">
|
||||
<div class="px-3 pb-2">
|
||||
<ul class="list-unstyled mb-0 small">
|
||||
{% for member in vc_data.members %}
|
||||
<li class="py-3 {% if not forloop.last %}border-bottom{% endif %}">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<span class="text-muted me-2">
|
||||
{% if member.position %}
|
||||
Pos {{ member.position }}
|
||||
{% else %}
|
||||
Pos —
|
||||
{% endif %}
|
||||
</span>
|
||||
<code class="flex-grow-1 mb-0">{{ member.suggested_name|default:member.name|default:"(unknown)" }}</code>
|
||||
{% if member.is_master %}
|
||||
<span class="badge bg-success text-white">Master</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-muted mt-2 d-flex flex-wrap gap-3">
|
||||
{% if member.model %}
|
||||
<span><strong>Model:</strong> {{ member.model }}</span>
|
||||
{% endif %}
|
||||
{% if member.serial %}
|
||||
<span><strong>Serial:</strong> <code>{{ member.serial }}</code></span>
|
||||
{% endif %}
|
||||
{% if not member.model and not member.serial %}
|
||||
<span>No additional metadata</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">
|
||||
<i class="mdi mdi-information-outline"></i> No member details available.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif not vc_data.detection_error %}
|
||||
<div class="alert alert-info">
|
||||
<h6 class="alert-heading">
|
||||
<i class="mdi mdi-information-outline"></i> Not a Virtual Chassis
|
||||
</h6>
|
||||
<p class="mb-0">
|
||||
This device does not appear to be part of a virtual chassis or stack.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="mdi mdi-close"></i> Close
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,59 @@
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% if table.page %}
|
||||
{% with page_param=table.prefix|stringformat:"s"|add:"page" %}
|
||||
<div class="d-flex justify-content-between align-items-center border-{% if placement == 'top' %}bottom{% else %}top{% endif %} p-2">
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
<nav aria-label="{% trans "Page selection" %}">
|
||||
<ul class="pagination mb-0">
|
||||
{% if table.page.has_previous %}
|
||||
<li class="page-item">
|
||||
<a href="?tab={{ table.tab }}&{{ page_param }}={{ table.page.previous_page_number }}&{{ table.prefix }}per_page={{ table.paginator.per_page }}&interface_name_field={{ interface_name_field }}" class="page-link">
|
||||
<i class="mdi mdi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for p in table.page.smart_pages %}
|
||||
<li class="page-item{% if table.page.number == p %} active{% endif %}">
|
||||
{% if p %}
|
||||
<a href="?tab={{ table.tab }}&{{ page_param }}={{ p }}&{{ table.prefix }}per_page={{ table.paginator.per_page }}&interface_name_field={{ interface_name_field }}" class="page-link">{{ p }}</a>
|
||||
{% else %}
|
||||
<span class="page-link" disabled>…</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if table.page.has_next %}
|
||||
<li class="page-item">
|
||||
<a href="?tab={{ table.tab }}&{{ page_param }}={{ table.page.next_page_number }}&{{ table.prefix }}per_page={{ table.paginator.per_page }}&interface_name_field={{ interface_name_field }}" class="page-link">
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<small class="text-end text-muted">
|
||||
{% blocktrans trimmed with start=table.page.start_index end=table.page.end_index total=table.paginator.count %}
|
||||
Showing {{ start }}-{{ end }} of {{ total }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
|
||||
<nav class="text-end" aria-label="{% trans "Pagination options" %}">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
{% trans "Per Page" %}
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
{% for n in table.paginator.get_page_lengths %}
|
||||
<a href="?tab={{ table.tab }}&{{ table.prefix }}per_page={{ n }}&{{ page_param }}={{ table.page.number }}&interface_name_field={{ interface_name_field }}" class="dropdown-item">{{ n }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<table class="table table-hover attr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>LibreNMS Type</th>
|
||||
<th>LibreNMS Speed (Kbps)</th>
|
||||
<th>NetBox Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ object.librenms_type }}</td>
|
||||
<td>{{ object.librenms_speed }}</td>
|
||||
<td>{{ object.get_netbox_type_display }}</td>
|
||||
<td>{{ object.description|default:"—" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="alert alert-info">
|
||||
<h4>Interface Type Mapping</h4>
|
||||
<p>This section allows you to map LibreNMS interface types to NetBox interface types.
|
||||
When synchronizing interfaces from LibreNMS, these mappings will be used to ensure
|
||||
correct interface type assignment in NetBox.</p>
|
||||
<p>Example: Map LibreNMS type "ethernetCsmacd" to NetBox type "1000base-t"</p>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,438 @@
|
||||
{% extends 'generic/_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'netbox_librenms_plugin/js/librenms_import.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block controls %}
|
||||
<div class="btn-list">
|
||||
{% plugin_list_buttons model %}
|
||||
{% action_buttons actions model %}
|
||||
</div>
|
||||
{% endblock controls %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{# Display Django messages #}
|
||||
{% include 'inc/messages.html' %}
|
||||
|
||||
{# LibreNMS Server Information #}
|
||||
{% if librenms_server_info %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body p-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="mdi mdi-server-network text-primary"></i>
|
||||
<strong>Active LibreNMS Server:</strong>
|
||||
{% if librenms_server_info.is_legacy %}
|
||||
{{ librenms_server_info.url }}
|
||||
{% else %}
|
||||
{{ librenms_server_info.display_name }} ({{ librenms_server_info.url }})
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not librenms_server_info.is_legacy %}
|
||||
<a href="{% url 'plugins:netbox_librenms_plugin:settings' %}" class="btn btn-sm btn-outline-primary ms-3">
|
||||
<i class="mdi mdi-cog"></i> Change Server
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Collapsible Filter Section #}
|
||||
{% if filter_form %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
Search Filters
|
||||
{% if filter_form.changed_data %}
|
||||
{% badge filter_form.changed_data|length bg_color="primary" %}
|
||||
{% endif %}
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#filter-collapse" aria-expanded="{% if filter_form.changed_data %}false{% else %}true{% endif %}" aria-controls="filter-collapse">
|
||||
<i class="mdi mdi-filter"></i> Toggle Filters
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse {% if not filter_form.changed_data %}show{% endif %}" id="filter-collapse">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{# Instructions column #}
|
||||
<div class="col-md-4">
|
||||
<h6 class="mb-3"><i class="mdi mdi-information-outline"></i> Search Instructions</h6>
|
||||
<p class="mb-3">
|
||||
<strong>At least one filter is required</strong> to search for devices.
|
||||
</p>
|
||||
|
||||
<p class="mb-3">
|
||||
The following matching rules apply:
|
||||
<ul class="mb-3" style="padding-left: 1.2rem; font-size: 0.9rem;">
|
||||
<li><strong>Location/Type/OS:</strong> Exact match</li>
|
||||
<li><strong>Hostname:</strong> Partial match</li>
|
||||
<li><strong>System Name:</strong> Exact match (or partial when combined with other filters)</li>
|
||||
</ul>
|
||||
|
||||
<div class="alert alert-info small mt-2 mb-0" role="status">
|
||||
<ul class="mb-0" style="padding-left: 1.2rem;">
|
||||
<strong>Performance note:</strong> <li>Results are cached (default: 5 minutes).</li>
|
||||
<li>Large LibreNMS datasets take time to process</li>
|
||||
<li>Repeating the same filter search will use cached data if available.</li>
|
||||
<li>Using background jobs is default and recommended.</li>
|
||||
<li>Background jobs can be cancelled if needed.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-muted small">
|
||||
<i class="mdi mdi-lightbulb-on-outline"></i> <strong>Tip:</strong> Start with Location and/or Type to narrow results, then refine with additional filters.
|
||||
</p>
|
||||
</div>
|
||||
{# Filters column #}
|
||||
<div class="col-md-8">
|
||||
<form method="get" class="form" id="librenms-import-filter-form">
|
||||
<input type="hidden" name="apply_filters" value="1">
|
||||
{% if show_filter_warning %}
|
||||
<div class="alert alert-warning alert-dismissible fade show border-warning" role="alert" style="border-left: 4px solid #ffc107;">
|
||||
<i class="mdi mdi-alert text-warning"></i>
|
||||
<strong>Filter Required:</strong> {{ filter_warning }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="{{ filter_form.librenms_location.id_for_label }}" class="form-label">{{ filter_form.librenms_location.label }}</label>
|
||||
{{ filter_form.librenms_location }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="{{ filter_form.librenms_type.id_for_label }}" class="form-label">{{ filter_form.librenms_type.label }}</label>
|
||||
{{ filter_form.librenms_type }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="{{ filter_form.librenms_os.id_for_label }}" class="form-label">{{ filter_form.librenms_os.label }}</label>
|
||||
{{ filter_form.librenms_os }}
|
||||
{% if filter_form.librenms_os.help_text %}
|
||||
<small class="form-text text-muted">{{ filter_form.librenms_os.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="{{ filter_form.librenms_hostname.id_for_label }}" class="form-label">{{ filter_form.librenms_hostname.label }}</label>
|
||||
{{ filter_form.librenms_hostname }}
|
||||
{% if filter_form.librenms_hostname.help_text %}
|
||||
<small class="form-text text-muted">{{ filter_form.librenms_hostname.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="{{ filter_form.librenms_sysname.id_for_label }}" class="form-label">{{ filter_form.librenms_sysname.label }}</label>
|
||||
{{ filter_form.librenms_sysname }}
|
||||
{% if filter_form.librenms_sysname.help_text %}
|
||||
<small class="form-text text-muted">{{ filter_form.librenms_sysname.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="{{ filter_form.librenms_hardware.id_for_label }}" class="form-label">{{ filter_form.librenms_hardware.label }}</label>
|
||||
{{ filter_form.librenms_hardware }}
|
||||
{% if filter_form.librenms_hardware.help_text %}
|
||||
<small class="form-text text-muted">{{ filter_form.librenms_hardware.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
{{ filter_form.exclude_existing }}
|
||||
<label class="form-check-label" for="{{ filter_form.exclude_existing.id_for_label }}">
|
||||
{{ filter_form.exclude_existing.label }}
|
||||
</label>
|
||||
{% if filter_form.exclude_existing.help_text %}
|
||||
<small class="form-text text-muted d-block">{{ filter_form.exclude_existing.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
{{ filter_form.show_disabled }}
|
||||
<label class="form-check-label" for="{{ filter_form.show_disabled.id_for_label }}">
|
||||
{{ filter_form.show_disabled.label }}
|
||||
</label>
|
||||
{% if filter_form.show_disabled.help_text %}
|
||||
<small class="form-text text-muted d-block">{{ filter_form.show_disabled.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
{{ filter_form.enable_vc_detection }}
|
||||
<label class="form-check-label" for="{{ filter_form.enable_vc_detection.id_for_label }}">
|
||||
{{ filter_form.enable_vc_detection.label }}
|
||||
</label>
|
||||
{% if filter_form.enable_vc_detection.help_text %}
|
||||
<small class="form-text text-muted d-block">{{ filter_form.enable_vc_detection.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
{{ filter_form.clear_cache }}
|
||||
<label class="form-check-label" for="{{ filter_form.clear_cache.id_for_label }}">
|
||||
{{ filter_form.clear_cache.label }}
|
||||
</label>
|
||||
{% if filter_form.clear_cache.help_text %}
|
||||
<small class="form-text text-muted d-block">{{ filter_form.clear_cache.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 my-3 bg-primary-subtle border border-primary rounded-1">
|
||||
<div class="form-check">
|
||||
{% if can_use_background_jobs %}
|
||||
{{ filter_form.use_background_job }}
|
||||
<label class="form-check-label" for="{{ filter_form.use_background_job.id_for_label }}">
|
||||
{{ filter_form.use_background_job.label }}
|
||||
</label>
|
||||
{% if filter_form.use_background_job.help_text %}
|
||||
<small class="form-text text-muted d-block">{{ filter_form.use_background_job.help_text }}</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<input type="checkbox" class="form-check-input" disabled title="Background jobs require superuser access">
|
||||
<label class="form-check-label text-muted">
|
||||
{{ filter_form.use_background_job.label }}
|
||||
</label>
|
||||
<small class="form-text text-muted d-block">
|
||||
<i class="mdi mdi-information-outline"></i> Background jobs require superuser access. Filters will process synchronously.
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<button type="submit" class="btn btn-primary" id="apply-filters-btn">
|
||||
<i class="mdi mdi-filter"></i> Apply Filters
|
||||
</button>
|
||||
<a href="{% url 'plugins:netbox_librenms_plugin:librenms_import' %}" class="btn btn-secondary">
|
||||
<i class="mdi mdi-close"></i> Clear
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Cached Searches Section #}
|
||||
{% if cached_searches %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="card-title mb-0">
|
||||
Active Cached Searches
|
||||
<span id="cached-searches-badge">{% badge cached_searches|length bg_color="primary" %}</span>
|
||||
</h6>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#cached-searches-collapse" aria-expanded="true" aria-controls="cached-searches-collapse">
|
||||
<i class="mdi mdi-clock-fast"></i> Toggle Cached Searches
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse show" id="cached-searches-collapse">
|
||||
<div class="card-body p-2">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for search in cached_searches %}
|
||||
<a href="?apply_filters=1{% for key, value in search.filters.items %}&librenms_{{ key }}={{ value|urlencode }}{% endfor %}{% if search.vc_enabled %}&enable_vc_detection=1{% endif %}"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
style="text-decoration: none;"
|
||||
title="Click to load this cached search">
|
||||
<i class="mdi mdi-filter-outline"></i>
|
||||
{% with display_data=search.display_filters|default:search.filters %}
|
||||
{% for key, value in display_data.items %}
|
||||
{% if not forloop.first %} <span class="text-muted">|</span> {% endif %}
|
||||
{{ value }}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% if search.vc_enabled %}
|
||||
<span class="text-muted">| VC</span>
|
||||
{% endif %}
|
||||
<span class="text-muted ms-2">({{ search.device_count }} devices, <span class="cached-search-countdown" data-cache-timestamp="{{ search.cached_at }}" data-cache-timeout="{{ search.cache_timeout }}">{{ search.remaining_seconds }}s</span> left)</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2">
|
||||
<i class="mdi mdi-information-outline"></i> Click any cached search to load those results again.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Applied filters #}
|
||||
{% if filter_form %}
|
||||
{% applied_filters model filter_form request.GET %}
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
/* Allow dropdown menus to extend beyond table container */
|
||||
.device-import-table-wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
/* Add space below table for dropdowns in last rows */
|
||||
padding-bottom: 200px;
|
||||
/* Pull up pagination to remove visual gap */
|
||||
margin-bottom: -200px;
|
||||
}
|
||||
|
||||
/* Ensure card doesn't clip overflowing dropdowns */
|
||||
.device-import-table-wrapper + .pagination {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
{# Results Section #}
|
||||
{% if table.page.paginator.count == 0 %}
|
||||
<div class="alert alert-warning mb-3">
|
||||
<h6 class="alert-heading">
|
||||
<i class="mdi mdi-alert"></i> No Results Found
|
||||
</h6>
|
||||
<p class="mb-0">
|
||||
No devices found matching your filters. Try adjusting your search criteria above.
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
{# Bulk import toolbar #}
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<button type="button" class="btn btn-outline-primary" id="select-all-ready">
|
||||
<i class="mdi mdi-checkbox-multiple-marked"></i> Select All Ready
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="select-none">
|
||||
<i class="mdi mdi-checkbox-multiple-blank-outline"></i> Deselect All
|
||||
</button>
|
||||
<span class="text-muted" id="selection-count">
|
||||
<i class="mdi mdi-information"></i> 0 devices selected
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
{# Cache expiration info - before Settings section #}
|
||||
{% if filters_submitted and table.rows %}
|
||||
{% if cache_metadata_missing %}
|
||||
<div class="d-flex align-items-center gap-1 text-warning small" title="Cache metadata not found. This may indicate a cache synchronization issue.">
|
||||
<i class="mdi mdi-alert"></i>
|
||||
<span>Cache status: <strong>unavailable</strong></span>
|
||||
</div>
|
||||
<span class="text-muted">|</span>
|
||||
{% elif cache_timestamp and cache_timeout %}
|
||||
<div class="d-flex align-items-center gap-1 text-muted small" id="cache-info-display"
|
||||
data-cache-timestamp="{{ cache_timestamp }}"
|
||||
data-cache-timeout="{{ cache_timeout }}">
|
||||
<i class="mdi mdi-clock-outline"></i>
|
||||
<span>Cache expires in <strong id="cache-expiry-countdown">{{ cache_timeout }}s</strong></span>
|
||||
</div>
|
||||
<span class="text-muted">|</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# Import Settings #}
|
||||
<div class="d-flex align-items-center gap-2 border-start ps-2"
|
||||
data-save-pref-url="{% url 'plugins:netbox_librenms_plugin:save_user_pref' %}">
|
||||
<strong class="small">Settings:</strong>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="use-sysname-toggle" name="use-sysname-toggle" {% if use_sysname %}checked{% endif %}
|
||||
data-bs-toggle="tooltip"
|
||||
title="Use SNMP sysName instead of LibreNMS hostname">
|
||||
<label class="form-check-label small" for="use-sysname-toggle">
|
||||
Use sysName
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="strip-domain-toggle" name="strip-domain-toggle" {% if strip_domain %}checked{% endif %}
|
||||
data-bs-toggle="tooltip"
|
||||
title="Remove domain suffix (e.g., 'switch01.example.com' → 'switch01'). IP addresses preserved.">
|
||||
<label class="form-check-label small" for="strip-domain-toggle">
|
||||
Strip domain
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success" id="bulk-import-btn" disabled
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:bulk_import_confirm' %}"
|
||||
hx-target="#htmx-modal-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="#import-selection-form, #use-sysname-toggle, #strip-domain-toggle">
|
||||
<span class="d-inline-flex align-items-center gap-1">
|
||||
<i class="mdi mdi-download"></i>
|
||||
<span>Import Selected (<span id="import-count">0</span>)</span>
|
||||
</span>
|
||||
<span class="spinner-border spinner-border-sm ms-2 htmx-indicator" role="status" aria-hidden="true" style="display: none;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="form form-horizontal" id="import-selection-form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
|
||||
<input type="hidden" name="enable_vc_detection" value="{{ vc_detection_enabled|yesno:'true,false' }}" />
|
||||
|
||||
{# Objects table #}
|
||||
<div class="card">
|
||||
<div class="htmx-container" id="object_list">
|
||||
{% include 'inc/paginator.html' with htmx=True table=table paginator=table.paginator page=table.page %}
|
||||
<div class="table-responsive device-import-table-wrapper">
|
||||
{% include 'inc/table.html' %}
|
||||
</div>
|
||||
{% include 'inc/paginator.html' with htmx=True table=table paginator=table.paginator page=table.page %}
|
||||
</div>
|
||||
</div>
|
||||
{# /Objects table #}
|
||||
|
||||
</form>
|
||||
|
||||
{# HTMX Modal for import actions #}
|
||||
<div class="modal fade" id="htmx-modal" tabindex="-1" aria-labelledby="htmxModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content" id="htmx-modal-content">
|
||||
{# Content loaded via HTMX #}
|
||||
<div class="modal-body text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Filter Processing Modal #}
|
||||
<div class="modal fade" id="filter-processing-modal" tabindex="-1" aria-labelledby="filterProcessingLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body text-center py-4">
|
||||
<div class="spinner-border text-primary mb-3" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5 class="mb-2">Applying Filters</h5>
|
||||
<p class="text-muted mb-1" id="filter-progress-message">Fetching LibreNMS data and processing filters...</p>
|
||||
<p class="text-info mb-3" id="filter-device-count" style="display: none;">
|
||||
<strong>LibreNMS Devices:</strong> <span id="filter-device-count-value">0</span>
|
||||
</p>
|
||||
{% if can_use_background_jobs %}
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="cancel-filter-btn">
|
||||
<i class="mdi mdi-close"></i> Cancel
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,339 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
{% block title %}Plugin Settings{% endblock %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="config-tab" data-bs-toggle="tab" data-bs-target="#config" type="button" role="tab" aria-controls="config" aria-selected="true">
|
||||
<i class="ti ti-server-2 me-1"></i> Server Config
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="import-settings-tab" data-bs-toggle="tab" data-bs-target="#import-settings" type="button" role="tab" aria-controls="import-settings" aria-selected="false">
|
||||
<i class="ti ti-download me-1"></i> Import Settings
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content %}
|
||||
<div class="tab-content" data-active-tab="{{ active_tab|default:'' }}">
|
||||
<!-- Server Config Tab -->
|
||||
<div class="tab-pane fade show active" id="config" role="tabpanel" aria-labelledby="config-tab">
|
||||
<form action="" method="post" class="form" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="form_type" value="server_config">
|
||||
|
||||
<div class="row justify-content-start">
|
||||
<div class="col-lg-5 col-xl-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="ti ti-server-2 me-2"></i>
|
||||
LibreNMS Server Settings
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="mb-3 flex-grow-1">
|
||||
<p class="text-muted mb-3">
|
||||
Configure which LibreNMS server to use for synchronization operations.
|
||||
Multiple servers can be configured in the NetBox configuration file.
|
||||
</p>
|
||||
{% render_field server_form.selected_server %}
|
||||
</div>
|
||||
<div class="text-end mt-auto">
|
||||
<button type="submit" id="save-server-btn" class="btn btn-primary" disabled>
|
||||
<i class="ti ti-check me-1"></i> Save Server Config
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Connection Test Card -->
|
||||
<div class="col-lg-5 col-xl-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="ti ti-network me-2"></i>
|
||||
Connection Test
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="flex-grow-1">
|
||||
<p class="text-muted mb-3">Test the connection to the selected LibreNMS server to verify configuration.</p>
|
||||
<button type="button"
|
||||
id="test-connection-btn"
|
||||
class="btn btn-outline-info w-100"
|
||||
hx-post="{% url 'plugins:netbox_librenms_plugin:test_connection' %}"
|
||||
hx-target="#test-result"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="[name='selected_server']">
|
||||
<i class="ti ti-network me-1"></i> Test Connection
|
||||
<span class="spinner-border spinner-border-sm ms-1 htmx-indicator" role="status" style="display: none;"></span>
|
||||
</button>
|
||||
<!-- Test result area -->
|
||||
<div id="test-result" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spacing between sections -->
|
||||
<div class="my-4"></div>
|
||||
|
||||
<!-- Configuration Example Section -->
|
||||
<div class="row">
|
||||
<div class="col-lg-10 col-xl-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="ti ti-code me-2"></i>
|
||||
Configuration Example
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-3">
|
||||
To configure multiple LibreNMS servers, update your NetBox
|
||||
<code>configuration.py</code>:
|
||||
</p>
|
||||
<div class="highlight">
|
||||
<pre class="mb-3"><code>PLUGINS_CONFIG = {
|
||||
'netbox_librenms_plugin': {
|
||||
'servers': {
|
||||
'production': {
|
||||
'display_name': 'Production LibreNMS',
|
||||
'librenms_url': 'https://librenms-prod.example.com',
|
||||
'api_token': 'your_production_token',
|
||||
'cache_timeout': 300,
|
||||
'verify_ssl': True,
|
||||
'interface_name_field': 'ifDescr'
|
||||
},
|
||||
'testing': {
|
||||
'display_name': 'Test LibreNMS',
|
||||
'librenms_url': 'https://librenms-test.example.com',
|
||||
'api_token': 'your_test_token',
|
||||
'cache_timeout': 300,
|
||||
'verify_ssl': False,
|
||||
'interface_name_field': 'ifName'
|
||||
}
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<i class="ti ti-info-circle me-2"></i>
|
||||
<strong>Note:</strong> For backward compatibility, the legacy single-server configuration
|
||||
is still supported if no "servers" configuration is provided.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Import Settings Tab -->
|
||||
<div class="tab-pane fade" id="import-settings" role="tabpanel" aria-labelledby="import-settings-tab">
|
||||
<form action="" method="post" class="form" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="form_type" value="import_settings">
|
||||
|
||||
<div class="row justify-content-start">
|
||||
<!-- Left Column: Device Naming and Virtual Chassis -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Device Naming Defaults Card -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="ti ti-edit me-2"></i>
|
||||
Device Naming Defaults
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-3">
|
||||
Configure default naming preferences for imported devices.
|
||||
</p>
|
||||
<div class="alert alert-info small mb-3">
|
||||
<i class="ti ti-info-circle me-1"></i>
|
||||
<strong>User preferences:</strong> These defaults apply to users who have not yet changed their own toggle settings on the import page. Once a user changes a toggle, their personal preference is saved and takes priority over these defaults.
|
||||
Saving these settings will also update your own preferences to match.
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group mb-3">
|
||||
<div class="form-check form-switch">
|
||||
{{ import_form.use_sysname_default }}
|
||||
<label class="form-check-label" for="{{ import_form.use_sysname_default.id_for_label }}">
|
||||
{{ import_form.use_sysname_default.label }}
|
||||
</label>
|
||||
{% if import_form.use_sysname_default.help_text %}
|
||||
<small class="form-text text-muted d-block">{{ import_form.use_sysname_default.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<div class="form-check form-switch">
|
||||
{{ import_form.strip_domain_default }}
|
||||
<label class="form-check-label" for="{{ import_form.strip_domain_default.id_for_label }}">
|
||||
{{ import_form.strip_domain_default.label }}
|
||||
</label>
|
||||
{% if import_form.strip_domain_default.help_text %}
|
||||
<small class="form-text text-muted d-block">{{ import_form.strip_domain_default.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Virtual Chassis Card -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="ti ti-switch me-2"></i>
|
||||
Virtual Chassis Member Naming
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-3">
|
||||
Configure how virtual chassis member devices are named during import.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ import_form.vc_member_name_pattern.id_for_label }}" class="form-label">
|
||||
{{ import_form.vc_member_name_pattern.label }}
|
||||
</label>
|
||||
{{ import_form.vc_member_name_pattern }}
|
||||
{% if import_form.vc_member_name_pattern.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ import_form.vc_member_name_pattern.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info mb-0">
|
||||
<p class="small mb-2"><strong>Available placeholders:</strong></p>
|
||||
<ul class="mb-3 small" style="padding-left: 1.2rem;">
|
||||
<li><code>{position}</code> - VC position number</li>
|
||||
<li><code>{serial}</code> - Member serial number</li>
|
||||
</ul>
|
||||
<p class="small mb-0"><strong>Note:</strong> The pattern is appended to the master device name. At least one placeholder is required to ensure each member gets a unique name.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p class="small mb-2"><strong>Examples:</strong></p>
|
||||
<ul class="mb-0 small" style="padding-left: 1.2rem;">
|
||||
<li><code>-M{position}</code> → switch01-M1, switch01-M2</li>
|
||||
<li><code> ({position})</code> → switch01 (1), switch01 (2)</li>
|
||||
<li><code>-SW{position}</code> → switch01-SW1, switch01-SW2</li>
|
||||
<li><code> [{serial}]</code> → switch01 [ABC123], switch01 [ABC124]</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="row justify-content-start">
|
||||
<div class="col-lg-6">
|
||||
<div class="text-end">
|
||||
<button type="submit" id="save-import-btn" class="btn btn-primary" disabled>
|
||||
<i class="ti ti-check me-1"></i> Save Import Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spacing -->
|
||||
<div class="my-4"></div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Auto-select the correct tab if there was a validation error
|
||||
// Uses the same pattern as librenms_sync.js for consistency
|
||||
const tabContent = document.querySelector('.tab-content');
|
||||
const activeTab = tabContent ? tabContent.dataset.activeTab : '';
|
||||
|
||||
if (activeTab === 'import_settings') {
|
||||
const importTab = document.getElementById('import-settings-tab');
|
||||
const importPane = document.getElementById('import-settings');
|
||||
const configTab = document.getElementById('config-tab');
|
||||
const configPane = document.getElementById('config');
|
||||
|
||||
if (importTab && importPane && configTab && configPane) {
|
||||
// Deactivate config tab
|
||||
configTab.classList.remove('active');
|
||||
configTab.setAttribute('aria-selected', 'false');
|
||||
configPane.classList.remove('show', 'active');
|
||||
|
||||
// Activate import settings tab
|
||||
importTab.classList.add('active');
|
||||
importTab.setAttribute('aria-selected', 'true');
|
||||
importPane.classList.add('show', 'active');
|
||||
}
|
||||
}
|
||||
|
||||
const serverSelect = document.getElementById('id_selected_server');
|
||||
const vcPatternInput = document.getElementById('id_vc_member_name_pattern');
|
||||
const saveServerBtn = document.getElementById('save-server-btn');
|
||||
const saveImportBtn = document.getElementById('save-import-btn');
|
||||
const useSysnameCheckbox = document.getElementById('id_use_sysname_default');
|
||||
const stripDomainCheckbox = document.getElementById('id_strip_domain_default');
|
||||
|
||||
// Store the initial values to detect changes
|
||||
const initialServerValue = serverSelect.value;
|
||||
const initialVcPatternValue = vcPatternInput.value;
|
||||
const initialUseSysnameValue = useSysnameCheckbox ? useSysnameCheckbox.checked : true;
|
||||
const initialStripDomainValue = stripDomainCheckbox ? stripDomainCheckbox.checked : false;
|
||||
|
||||
// Enable/disable server save button based on changes
|
||||
function updateServerSaveButton() {
|
||||
const hasChanges = serverSelect.value !== initialServerValue;
|
||||
saveServerBtn.disabled = !hasChanges;
|
||||
saveServerBtn.classList.toggle('btn-primary', hasChanges);
|
||||
saveServerBtn.classList.toggle('btn-secondary', !hasChanges);
|
||||
}
|
||||
|
||||
// Enable/disable import save button based on changes
|
||||
function updateImportSaveButton() {
|
||||
const vcPatternChanged = vcPatternInput.value !== initialVcPatternValue;
|
||||
const useSysnameChanged = useSysnameCheckbox ? (useSysnameCheckbox.checked !== initialUseSysnameValue) : false;
|
||||
const stripDomainChanged = stripDomainCheckbox ? (stripDomainCheckbox.checked !== initialStripDomainValue) : false;
|
||||
const hasChanges = vcPatternChanged || useSysnameChanged || stripDomainChanged;
|
||||
saveImportBtn.disabled = !hasChanges;
|
||||
saveImportBtn.classList.toggle('btn-primary', hasChanges);
|
||||
saveImportBtn.classList.toggle('btn-secondary', !hasChanges);
|
||||
}
|
||||
|
||||
// Listen for changes on respective form fields
|
||||
serverSelect.addEventListener('change', updateServerSaveButton);
|
||||
vcPatternInput.addEventListener('input', updateImportSaveButton);
|
||||
if (useSysnameCheckbox) useSysnameCheckbox.addEventListener('change', updateImportSaveButton);
|
||||
if (stripDomainCheckbox) stripDomainCheckbox.addEventListener('change', updateImportSaveButton);
|
||||
|
||||
// Initialize button states
|
||||
updateServerSaveButton();
|
||||
updateImportSaveButton();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,51 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block header %}
|
||||
<div class="container-fluid mt-2 d-print-none">
|
||||
<div>
|
||||
<h1 class="page-title mt-1 mb-2">Site and Location Sync</h1>
|
||||
<p>This page displays the synchronization status between NetBox sites and LibreNMS locations. It allows you to update or create locations in LibreNMS based on NetBox data.</p>
|
||||
<p class="text-muted">Note: Only LibreNMS locations that match a Netbox site are shown on this page.</p>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<form method="get">
|
||||
<div class="d-flex">
|
||||
<div class="search-box" style="width: 300px">
|
||||
{% for field in filter_form %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="buttons m-2">
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
<a href="." class="btn btn-secondary">Clear</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
{% render_table table 'inc/table.html' %}
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
|
||||
{% block title %}LibreNMS Status Check{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if table.page.paginator.count == 0 %}
|
||||
<div class="text-muted mb-3">
|
||||
Select filters to check device status in LibreNMS. Click the status to view device LibreNMS sync details.
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user