mirror of
https://github.com/drygdryg/netbox-plugin-interface-sync
synced 2024-11-29 18:20:52 +03:00
Ready for netboxcommunity
This plugin has been re-developed for Netbox 4. The sync button has been redesigned as well as the colors of the sync table.
This commit is contained in:
parent
51b75bc6d5
commit
b48cfaca4b
@ -3,25 +3,13 @@ from netbox.plugins import PluginConfig
|
||||
|
||||
class Config(PluginConfig):
|
||||
name = 'netbox_interface_sync'
|
||||
verbose_name = 'NetBox 4 Interface Synchronization'
|
||||
description = 'Compare and synchronize components (interfaces, ports, outlets, etc.) between NetBox 4 device types ' \
|
||||
'and devices'
|
||||
verbose_name = 'NetBox interface synchronization'
|
||||
description = 'Syncing interfaces with the interfaces from device type for NetBox 4'
|
||||
version = '0.4.0'
|
||||
author = 'based on work by Victor Golovanenko'
|
||||
author_email = 'drygdryg2014@yandex.ru'
|
||||
author = 'Keith Knowles'
|
||||
author_email = 'mkknowles@outlook.com'
|
||||
default_settings = {
|
||||
# Ignore case and spaces in names when matching components between device type and device
|
||||
'name_comparison': {
|
||||
'case-insensitive': True,
|
||||
'space-insensitive': True
|
||||
},
|
||||
# Exclude virtual interfaces (bridge, link aggregation group (LAG), "virtual") from comparison
|
||||
'exclude_virtual_interfaces': True,
|
||||
# Add a panel with information about the number of interfaces to the device page
|
||||
'include_interfaces_panel': False,
|
||||
# Consider component descriptions when comparing. If this option is set to True, then take into account
|
||||
# component descriptions when comparing components and synchronizing their attributes, otherwise - ignore
|
||||
'sync_descriptions': True
|
||||
'exclude_virtual_interfaces': True
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,156 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
import attr
|
||||
from attrs import fields
|
||||
from django.conf import settings
|
||||
|
||||
from netbox.models import PrimaryModel
|
||||
|
||||
config = settings.PLUGINS_CONFIG["netbox_interface_sync"]
|
||||
SYNC_DESCRIPTIONS: bool = config["sync_descriptions"]
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class BaseComparison:
|
||||
"""Common fields of a device component"""
|
||||
# Do not compare IDs
|
||||
id: int = attr.ib(eq=False, metadata={'printable': False, 'netbox_exportable': False})
|
||||
# Compare names case-insensitively and spaces-insensitively
|
||||
name: str = attr.ib(metadata={'printable': False})
|
||||
label: str = attr.ib()
|
||||
# Compare descriptions if it is set by the configuration
|
||||
description: str = attr.ib(eq=SYNC_DESCRIPTIONS, metadata={'synced': SYNC_DESCRIPTIONS})
|
||||
# Do not compare `is_template` properties
|
||||
is_template: bool = attr.ib(
|
||||
default=False, kw_only=True, eq=False,
|
||||
metadata={'printable': False, 'netbox_exportable': False}
|
||||
)
|
||||
|
||||
@property
|
||||
def fields_display(self) -> str:
|
||||
"""Generate human-readable list of printable fields to display in the comparison table"""
|
||||
fields_to_display = []
|
||||
for field in fields(self.__class__):
|
||||
if not field.metadata.get('printable', True):
|
||||
continue
|
||||
field_value = getattr(self, field.name)
|
||||
if not field_value:
|
||||
continue
|
||||
field_caption = field.metadata.get('displayed_caption') or field.name.replace('_', ' ').capitalize()
|
||||
if isinstance(field_value, BaseComparison):
|
||||
field_value = f'{field_value.name} (ID: {field_value.id})'
|
||||
fields_to_display.append(f'{field_caption}: {field_value}')
|
||||
return '\n'.join(fields_to_display)
|
||||
|
||||
def get_fields_for_netbox_component(self, sync=False):
|
||||
"""
|
||||
Returns a dict of fields and values for creating or updating a NetBox component object
|
||||
:param sync: if True, returns fields for syncing an existing component, otherwise - for creating a new one.
|
||||
"""
|
||||
|
||||
def field_filter(field: attr.Attribute, _):
|
||||
result = field.metadata.get('netbox_exportable', True)
|
||||
if sync:
|
||||
result &= field.metadata.get('synced', True)
|
||||
return result
|
||||
|
||||
return attr.asdict(self, recurse=True, filter=field_filter)
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class BaseTypedComparison(BaseComparison):
|
||||
"""Common fields of a device typed component"""
|
||||
type: str = attr.ib(metadata={'printable': False})
|
||||
type_display: str = attr.ib(eq=False, metadata={'displayed_caption': 'Type', 'netbox_exportable': False})
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class ConsolePortComparison(BaseTypedComparison):
|
||||
"""A unified way to represent the consoleport and consoleport template"""
|
||||
pass
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class ConsoleServerPortComparison(BaseTypedComparison):
|
||||
"""A unified way to represent the consoleserverport and consoleserverport template"""
|
||||
pass
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class PowerPortComparison(BaseTypedComparison):
|
||||
"""A unified way to represent the power port and power port template"""
|
||||
maximum_draw: str = attr.ib()
|
||||
allocated_draw: str = attr.ib()
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class PowerOutletComparison(BaseTypedComparison):
|
||||
"""A unified way to represent the power outlet and power outlet template"""
|
||||
power_port: PowerPortComparison = attr.ib()
|
||||
feed_leg: str = attr.ib()
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class InterfaceComparison(BaseTypedComparison):
|
||||
"""A unified way to represent the interface and interface template"""
|
||||
mgmt_only: bool = attr.ib()
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class FrontPortComparison(BaseTypedComparison):
|
||||
"""A unified way to represent the front port and front port template"""
|
||||
color: str = attr.ib()
|
||||
# rear_port_id: int
|
||||
rear_port_position: int = attr.ib(metadata={'displayed_caption': 'Position'})
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class RearPortComparison(BaseTypedComparison):
|
||||
"""A unified way to represent the rear port and rear port template"""
|
||||
color: str = attr.ib()
|
||||
positions: int = attr.ib()
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class DeviceBayComparison(BaseComparison):
|
||||
"""A unified way to represent the device bay and device bay template"""
|
||||
pass
|
||||
|
||||
|
||||
def from_netbox_object(netbox_object: PrimaryModel) -> Optional[BaseComparison]:
|
||||
"""Makes a comparison object from the NetBox object"""
|
||||
type_map = {
|
||||
"DeviceBay": DeviceBayComparison,
|
||||
"Interface": InterfaceComparison,
|
||||
"FrontPort": FrontPortComparison,
|
||||
"RearPort": RearPortComparison,
|
||||
"ConsolePort": ConsolePortComparison,
|
||||
"ConsoleServerPort": ConsoleServerPortComparison,
|
||||
"PowerPort": PowerPortComparison,
|
||||
"PowerOutlet": PowerOutletComparison
|
||||
}
|
||||
|
||||
obj_name = netbox_object._meta.object_name
|
||||
if obj_name.endswith("Template"):
|
||||
is_template = True
|
||||
obj_name = obj_name[:-8] # TODO: use `removesuffix` introduced in Python 3.9
|
||||
else:
|
||||
is_template = False
|
||||
|
||||
comparison = type_map.get(obj_name)
|
||||
if not comparison:
|
||||
return
|
||||
|
||||
values = {}
|
||||
for field in fields(comparison):
|
||||
if field.name == "is_template":
|
||||
continue
|
||||
if field.name == "type_display":
|
||||
values[field.name] = netbox_object.get_type_display()
|
||||
else:
|
||||
field_value = getattr(netbox_object, field.name)
|
||||
if isinstance(field_value, PrimaryModel):
|
||||
field_value = from_netbox_object(field_value)
|
||||
values[field.name] = field_value
|
||||
|
||||
return comparison(**values, is_template=is_template)
|
6
netbox_interface_sync/forms.py
Normal file
6
netbox_interface_sync/forms.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
class InterfaceComparisonForm(forms.Form):
|
||||
add_to_device = forms.BooleanField(required=False)
|
||||
remove_from_device = forms.BooleanField(required=False)
|
@ -6,9 +6,9 @@ class DeviceViewExtension(PluginTemplateExtension):
|
||||
model = "dcim.device"
|
||||
|
||||
def buttons(self):
|
||||
"""Implements a compare button at the top of the page"""
|
||||
"""Implements a compare interfaces button at the top of the page"""
|
||||
obj = self.context['object']
|
||||
return self.render("netbox_interface_sync/compare_components_button.html", extra_context={
|
||||
return self.render("netbox_interface_sync/compare_interfaces_button.html", extra_context={
|
||||
"device": obj
|
||||
})
|
||||
|
||||
|
@ -1,65 +0,0 @@
|
||||
{% if perms.dcim.change_device %}
|
||||
<div class="dropdown">
|
||||
<button id="device-type-sync" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Device type sync
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labeled-by="device-type-sync">
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'plugins:netbox_interface_sync:consoleport_comparison' device_id=device.id %}">
|
||||
Console Ports
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'plugins:netbox_interface_sync:consoleserverport_comparison' device_id=device.id %}">
|
||||
Console Server Ports
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'plugins:netbox_interface_sync:powerport_comparison' device_id=device.id %}">
|
||||
Power Ports
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'plugins:netbox_interface_sync:poweroutlet_comparison' device_id=device.id %}">
|
||||
Power Outlets
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'plugins:netbox_interface_sync:interface_comparison' device_id=device.id %}">
|
||||
Interfaces
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{# {% if perms.dcim.add_frontport %}#}
|
||||
{# <li>#}
|
||||
{# <a class="dropdown-item" href="{% url 'plugins:netbox_interface_sync:frontport_comparison' device_id=device.id %}">#}
|
||||
{# Front Ports#}
|
||||
{# </a>#}
|
||||
{# </li>#}
|
||||
{# {% endif %}#}
|
||||
{% if perms.dcim.add_rearport %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'plugins:netbox_interface_sync:rearport_comparison' device_id=device.id %}">
|
||||
Rear Ports
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'plugins:netbox_interface_sync:devicebay_comparison' device_id=device.id %}">
|
||||
Device Bays
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
@ -0,0 +1,3 @@
|
||||
<a href="{% url 'plugins:netbox_interface_sync:interface_comparison' device_id=device.id %}" class="btn btn-purple"><i class="mdi mdi-sync" aria-hidden="true"></i>
|
||||
Interface Sync
|
||||
</a>
|
@ -0,0 +1,3 @@
|
||||
<a href="{% url 'plugins:netbox_interface_sync:interface_comparison' device_id=device.id %}" class="btn btn-purple">
|
||||
Interface Sync
|
||||
</a>
|
@ -1,158 +0,0 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
|
||||
{% block title %}{{ device }} - {{ component_type_name|capfirst }} comparison{% endblock %}
|
||||
{% block header %}
|
||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device_list' %}?site={{ device.site.slug }}">{{ device.site }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device' pk=device.id %}">{{ device }}</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.caption-red {
|
||||
caption-side: top;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.caption-green {
|
||||
caption-side: top;
|
||||
color: green;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function toggle(event) {
|
||||
event = event || window.event;
|
||||
const src = event.target || event.srcElement || event;
|
||||
const checkboxes = document.getElementsByName(src.id);
|
||||
for(const checkbox of checkboxes) checkbox.checked = src.checked;
|
||||
}
|
||||
|
||||
function uncheck(event) {
|
||||
event = event || window.event;
|
||||
const src = event.target || event.srcElement || event;
|
||||
if (src.checked === false) {
|
||||
document.getElementById(src.name).checked = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="table-responsive-xl">
|
||||
<table class="table table-hover table-bordered">
|
||||
{% if templates_count == components_count %}
|
||||
<caption class="caption-green">
|
||||
The device and device type have the same number of {{ component_type_name }}.
|
||||
</caption>
|
||||
{% else %}
|
||||
<caption class="caption-red">
|
||||
The device and device type have different number of {{ component_type_name }}.<br>
|
||||
Device: {{ components_count }}<br>
|
||||
Device type: {{ templates_count }}
|
||||
</caption>
|
||||
{% endif %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" colspan="2">Device type</th>
|
||||
<th scope="col">Actions</th>
|
||||
<th scope="col" colspan="2">Device</th>
|
||||
<th scope="col" colspan="2">Actions</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Attributes</th>
|
||||
<th scope="col">
|
||||
<label>
|
||||
<input type="checkbox" id="add" onclick="toggle(this)">
|
||||
Add to the device
|
||||
</label>
|
||||
</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Attributes</th>
|
||||
<th scope="col">
|
||||
<label>
|
||||
<input type="checkbox" id="remove" onclick="toggle(this)">
|
||||
Remove
|
||||
</label>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<label>
|
||||
<input type="checkbox" id="sync" onclick="toggle(this)">
|
||||
Sync attributes
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for component_template, component in comparison_items %}
|
||||
<tr>
|
||||
{% if component_template %}
|
||||
<th scope="row" {% if not component %}class="table-danger"{% endif %}>
|
||||
{% if component and component_template.name != component.name %}
|
||||
<span style="background-color: #eab2b2">{{ component_template.name }}</span>
|
||||
{% else %}
|
||||
{{ component_template.name }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<td style="white-space:pre" {% if not component %}class="table-danger"{% endif %}>{{ component_template.fields_display }}</td>
|
||||
<td {% if not component %}class="table-danger"{% endif %}>
|
||||
{% if not component %}
|
||||
<label>
|
||||
<input type="checkbox" name="add" value="{{ component_template.id }}" onclick="uncheck(this)">
|
||||
Add to device
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<th scope="row"> </th>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
{% endif %}
|
||||
|
||||
{% if component %}
|
||||
<th scope="row" {% if not component_template %}class="table-success"{% endif %}>
|
||||
{% if component_template and component_template.name != component.name %}
|
||||
<span style="background-color: #cde8c2">{{ component.name }}</span>
|
||||
{% else %}
|
||||
{{ component.name }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<td style="white-space:pre" {% if not component_template %}class="table-success"{% endif %}>{{ component.fields_display }}</td>
|
||||
<td {% if not component_template %}class="table-success"{% endif %}>
|
||||
{% if not component_template %}
|
||||
<label>
|
||||
<input type="checkbox" name="remove" value="{{ component.id }}" onclick="uncheck(this)">
|
||||
Remove
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td {% if not component_template %}class="table-success"{% endif %}>
|
||||
{% if component_template and component_template != component %}
|
||||
<label>
|
||||
<input type="checkbox" name="sync" value="{{ component.id }}" onclick="uncheck(this)">
|
||||
Sync attributes
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Apply" class="btn btn-primary" style="float: right;">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,161 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
|
||||
{% block title %}{{ device }} - Interface comparison{% endblock %}
|
||||
{% block header %}
|
||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device_list' %}?site={{ device.site.slug }}">{{ device.site }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device' pk=device.id %}">{{ device }}</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.checkbox-group {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function toggle(event) {
|
||||
event = event || window.event;
|
||||
var src = event.target || event.srcElement || event;
|
||||
checkboxes = document.getElementsByName(src.id);
|
||||
for(var checkbox of checkboxes) checkbox.checked = src.checked;
|
||||
}
|
||||
|
||||
function uncheck(event) {
|
||||
event = event || window.event;
|
||||
var src = event.target || event.srcElement || event;
|
||||
if (src.checked == false) {
|
||||
document.getElementById(src.name).checked = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>
|
||||
{% if templates_count == interfaces_count %}
|
||||
The device and device type have the same number of interfaces.
|
||||
{% else %}
|
||||
The device and device type have a different number of interfaces.<br>
|
||||
Device: {{ interfaces_count }}<br>
|
||||
Device type: {{ templates_count }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
<!-- Interface templates -->
|
||||
{% csrf_token %}
|
||||
<table class="table" style="width: 50%; float: left;">
|
||||
<tr>
|
||||
<th colspan="2">Device Type</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="add_to_device" onclick="toggle(this)">
|
||||
Add To The Device
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
{% for template, interface in comparison_items %}
|
||||
{% if template %}
|
||||
<tr {% if not interface %}class="success" data-mark-connected="true"{% endif %}>
|
||||
<td>
|
||||
{% if interface and template.name != interface.name %}
|
||||
<span style="background-color: #cde8c2">{{ template.name }}</span>
|
||||
{% else %}
|
||||
{{ template.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ template.type_display }}</td>
|
||||
<td>
|
||||
{% if not interface %}
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" name="add_to_device" value="{{ template.id }}" onclick="uncheck(this)">
|
||||
Add To Device
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<table class="table" style="width: 50%; float: right;">
|
||||
<!-- Interfaces -->
|
||||
<tr>
|
||||
<th colspan="2">Device</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="remove_from_device" onclick="toggle(this)">
|
||||
Remove
|
||||
</label>
|
||||
</th>
|
||||
<th>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="fix_name" onclick="toggle(this)">
|
||||
Fix The Name
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
{% for template, interface in comparison_items %}
|
||||
{% if interface %}
|
||||
<tr {% if not template %}class="danger" data-enabled="disabled"{% endif %}>
|
||||
<td>
|
||||
{% if template and template.name != interface.name %}
|
||||
<span style="background-color: #eab2b2">{{ interface.name }}</span>
|
||||
{% else %}
|
||||
{{ interface.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ interface.type_display }}</td>
|
||||
<td>
|
||||
{% if not template %}
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" name="remove_from_device" value="{{ interface.id }}" onclick="uncheck(this)">
|
||||
Remove
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if template and template.name != interface.name %}
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" name="fix_name" value="{{ interface.id }}" onclick="uncheck(this)">
|
||||
Fix Name
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="text-right">
|
||||
<input type="submit" value="Apply" class="btn btn-primary">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,161 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
|
||||
{% block title %}{{ device }} - Interface comparison{% endblock %}
|
||||
{% block header %}
|
||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device_list' %}?site={{ device.site.slug }}">{{ device.site }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device' pk=device.id %}">{{ device }}</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.checkbox-group {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function toggle(event) {
|
||||
event = event || window.event;
|
||||
var src = event.target || event.srcElement || event;
|
||||
checkboxes = document.getElementsByName(src.id);
|
||||
for(var checkbox of checkboxes) checkbox.checked = src.checked;
|
||||
}
|
||||
|
||||
function uncheck(event) {
|
||||
event = event || window.event;
|
||||
var src = event.target || event.srcElement || event;
|
||||
if (src.checked == false) {
|
||||
document.getElementById(src.name).checked = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>
|
||||
{% if templates_count == interfaces_count %}
|
||||
The device and device type have the same number of interfaces.
|
||||
{% else %}
|
||||
The device and device type have a different number of interfaces.<br>
|
||||
Device: {{ interfaces_count }}<br>
|
||||
Device type: {{ templates_count }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
<!-- Interface templates -->
|
||||
{% csrf_token %}
|
||||
<table class="table" style="width: 50%; float: left;">
|
||||
<tr>
|
||||
<th colspan="2">Device Type</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="add_to_device" onclick="toggle(this)">
|
||||
Add To The Device
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
{% for template, interface in comparison_items %}
|
||||
{% if template %}
|
||||
<tr {% if not interface %}class="success" data-enabled="enabled"{% endif %}>
|
||||
<td>
|
||||
{% if interface and template.name != interface.name %}
|
||||
<span style="background-color: #cde8c2">{{ template.name }}</span>
|
||||
{% else %}
|
||||
{{ template.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ template.type_display }}</td>
|
||||
<td>
|
||||
{% if not interface %}
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" name="add_to_device" value="{{ template.id }}" onclick="uncheck(this)">
|
||||
Add To Device
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<table class="table" style="width: 50%; float: right;">
|
||||
<!-- Interfaces -->
|
||||
<tr>
|
||||
<th colspan="2">Device</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="remove_from_device" onclick="toggle(this)">
|
||||
Remove
|
||||
</label>
|
||||
</th>
|
||||
<th>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="fix_name" onclick="toggle(this)">
|
||||
Fix The Name
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
{% for template, interface in comparison_items %}
|
||||
{% if interface %}
|
||||
<tr {% if not template %}class="danger"{% endif %}>
|
||||
<td>
|
||||
{% if template and template.name != interface.name %}
|
||||
<span style="background-color: #cde8c2">{{ interface.name }}</span>
|
||||
{% else %}
|
||||
{{ interface.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ interface.type_display }}</td>
|
||||
<td>
|
||||
{% if not template %}
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" name="remove_from_device" value="{{ interface.id }}" onclick="uncheck(this)">
|
||||
Remove
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if template and template.name != interface.name %}
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" name="fix_name" value="{{ interface.id }}" onclick="uncheck(this)">
|
||||
Fix Name
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="text-right">
|
||||
<input type="submit" value="Apply" class="btn btn-primary">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,161 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
|
||||
{% block title %}{{ device }} - Interface comparison{% endblock %}
|
||||
{% block header %}
|
||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device_list' %}?site={{ device.site.slug }}">{{ device.site }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:device' pk=device.id %}">{{ device }}</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.checkbox-group {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function toggle(event) {
|
||||
event = event || window.event;
|
||||
var src = event.target || event.srcElement || event;
|
||||
checkboxes = document.getElementsByName(src.id);
|
||||
for(var checkbox of checkboxes) checkbox.checked = src.checked;
|
||||
}
|
||||
|
||||
function uncheck(event) {
|
||||
event = event || window.event;
|
||||
var src = event.target || event.srcElement || event;
|
||||
if (src.checked == false) {
|
||||
document.getElementById(src.name).checked = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>
|
||||
{% if templates_count == interfaces_count %}
|
||||
The device and device type have the same number of interfaces.
|
||||
{% else %}
|
||||
The device and device type have a different number of interfaces.<br>
|
||||
Device: {{ interfaces_count }}<br>
|
||||
Device type: {{ templates_count }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
<!-- Interface templates -->
|
||||
{% csrf_token %}
|
||||
<table class="table" style="width: 50%; float: left;">
|
||||
<tr>
|
||||
<th colspan="2">Device Type</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="add_to_device" onclick="toggle(this)">
|
||||
Add To The Device
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
{% for template, interface in comparison_items %}
|
||||
{% if template %}
|
||||
<tr {% if not interface %}class="danger"{% endif %}>
|
||||
<td>
|
||||
{% if interface and template.name != interface.name %}
|
||||
<span style="background-color: #eab2b2">{{ template.name }}</span>
|
||||
{% else %}
|
||||
{{ template.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ template.type_display }}</td>
|
||||
<td>
|
||||
{% if not interface %}
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" name="add_to_device" value="{{ template.id }}" onclick="uncheck(this)">
|
||||
Add To Device
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<table class="table" style="width: 50%; float: right;">
|
||||
<!-- Interfaces -->
|
||||
<tr>
|
||||
<th colspan="2">Device</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="remove_from_device" onclick="toggle(this)">
|
||||
Remove
|
||||
</label>
|
||||
</th>
|
||||
<th>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="fix_name" onclick="toggle(this)">
|
||||
Fix The Name
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
{% for template, interface in comparison_items %}
|
||||
{% if interface %}
|
||||
<tr {% if not template %}class="success"{% endif %}>
|
||||
<td>
|
||||
{% if template and template.name != interface.name %}
|
||||
<span style="background-color: #cde8c2">{{ interface.name }}</span>
|
||||
{% else %}
|
||||
{{ interface.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ interface.type_display }}</td>
|
||||
<td>
|
||||
{% if not template %}
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" name="remove_from_device" value="{{ interface.id }}" onclick="uncheck(this)">
|
||||
Remove
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if template and template.name != interface.name %}
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" name="fix_name" value="{{ interface.id }}" onclick="uncheck(this)">
|
||||
Fix Name
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="text-right">
|
||||
<input type="submit" value="Apply" class="btn btn-primary">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -1,12 +1,10 @@
|
||||
{% if config.include_interfaces_panel %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Number of interfaces</h5>
|
||||
<div class="card-body">
|
||||
Total interfaces: {{ interfaces|length }}<br>
|
||||
{% if config.exclude_virtual_interfaces %}
|
||||
Non-virtual interfaces: {{ real_interfaces|length }}<br>
|
||||
{% endif %}
|
||||
Interfaces in the assigned device type: {{ interface_templates|length }}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">Number of interfaces</h5>
|
||||
<div class="card-body">
|
||||
Total interfaces: {{ interfaces|length }}<br>
|
||||
{% if config.exclude_virtual_interfaces %}
|
||||
Non-virtual interfaces: {{ real_interfaces|length }}<br>
|
||||
{% endif %}
|
||||
Interfaces in the assigned device type: {{ interface_templates|length }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -6,44 +6,5 @@ from . import views
|
||||
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
|
||||
# a specific view so that it can be accessed by users.
|
||||
urlpatterns = (
|
||||
path(
|
||||
"consoleport-comparison/<int:device_id>/",
|
||||
views.ConsolePortComparisonView.as_view(),
|
||||
name="consoleport_comparison",
|
||||
),
|
||||
path(
|
||||
"consoleserverport-comparison/<int:device_id>/",
|
||||
views.ConsoleServerPortComparisonView.as_view(),
|
||||
name="consoleserverport_comparison",
|
||||
),
|
||||
path(
|
||||
"interface-comparison/<int:device_id>/",
|
||||
views.InterfaceComparisonView.as_view(),
|
||||
name="interface_comparison",
|
||||
),
|
||||
path(
|
||||
"powerport-comparison/<int:device_id>/",
|
||||
views.PowerPortComparisonView.as_view(),
|
||||
name="powerport_comparison",
|
||||
),
|
||||
path(
|
||||
"poweroutlet-comparison/<int:device_id>/",
|
||||
views.PowerOutletComparisonView.as_view(),
|
||||
name="poweroutlet_comparison",
|
||||
),
|
||||
# path(
|
||||
# "frontport-comparison/<int:device_id>/",
|
||||
# views.FrontPortComparisonView.as_view(),
|
||||
# name="frontport_comparison",
|
||||
# ),
|
||||
path(
|
||||
"rearport-comparison/<int:device_id>/",
|
||||
views.RearPortComparisonView.as_view(),
|
||||
name="rearport_comparison",
|
||||
),
|
||||
path(
|
||||
"devicebay-comparison/<int:device_id>/",
|
||||
views.DeviceBayComparisonView.as_view(),
|
||||
name="devicebay_comparison",
|
||||
),
|
||||
path("interface-comparison/<int:device_id>/", views.InterfaceComparisonView.as_view(), name="interface_comparison"),
|
||||
)
|
||||
|
@ -1,13 +1,11 @@
|
||||
import re
|
||||
from typing import Iterable, List
|
||||
from django.conf import settings
|
||||
|
||||
config = settings.PLUGINS_CONFIG['netbox_interface_sync']
|
||||
from typing import Iterable
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
def split(s):
|
||||
for x, y in re.findall(r"(\d*)(\D*)", s):
|
||||
yield "", int(x or "0")
|
||||
for x, y in re.findall(r'(\d*)(\D*)', s):
|
||||
yield '', int(x or '0')
|
||||
yield y, 0
|
||||
|
||||
|
||||
@ -19,21 +17,19 @@ def human_sorted(iterable: Iterable):
|
||||
return sorted(iterable, key=natural_keys)
|
||||
|
||||
|
||||
def make_integer_list(lst: List[str]):
|
||||
return [int(i) for i in lst if i.isdigit()]
|
||||
@dataclass(frozen=True)
|
||||
class UnifiedInterface:
|
||||
"""A unified way to represent the interface and interface template"""
|
||||
id: int
|
||||
name: str
|
||||
type: str
|
||||
type_display: str
|
||||
is_template: bool = False
|
||||
|
||||
def __eq__(self, other):
|
||||
# Ignore some fields when comparing; ignore interface name case and whitespaces
|
||||
return (self.name.lower().replace(' ', '') == other.name.lower().replace(' ', '')) and (self.type == other.type)
|
||||
|
||||
def get_permissions_for_model(model, actions: Iterable[str]) -> List[str]:
|
||||
"""
|
||||
Resolve a list of permissions for a given model (or instance).
|
||||
|
||||
:param model: A model or instance
|
||||
:param actions: List of actions: view, add, change, or delete
|
||||
"""
|
||||
permissions = []
|
||||
for action in actions:
|
||||
if action not in ("view", "add", "change", "delete"):
|
||||
raise ValueError(f"Unsupported action: {action}")
|
||||
permissions.append(f'{model._meta.app_label}.{action}_{model._meta.model_name}')
|
||||
|
||||
return permissions
|
||||
def __hash__(self):
|
||||
# Ignore some fields when hashing; ignore interface name case and whitespaces
|
||||
return hash((self.name.lower().replace(' ', ''), self.type))
|
||||
|
@ -1,461 +1,114 @@
|
||||
from collections import namedtuple
|
||||
from typing import Type, Tuple
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.views.generic import View
|
||||
from dcim.models import (Device, Interface, InterfaceTemplate, PowerPort, PowerPortTemplate, ConsolePort,
|
||||
ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, DeviceBay,
|
||||
DeviceBayTemplate, FrontPort, FrontPortTemplate, PowerOutlet, PowerOutletTemplate, RearPort,
|
||||
RearPortTemplate)
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from dcim.models import Device, Interface, InterfaceTemplate
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
|
||||
from netbox.models import PrimaryModel
|
||||
from dcim.constants import VIRTUAL_IFACE_TYPES
|
||||
|
||||
from . import comparison
|
||||
from .utils import get_permissions_for_model, make_integer_list, human_sorted
|
||||
from .utils import UnifiedInterface, natural_keys
|
||||
from .forms import InterfaceComparisonForm
|
||||
|
||||
config = settings.PLUGINS_CONFIG['netbox_interface_sync']
|
||||
ComparisonTableRow = namedtuple('ComparisonTableRow', ('component_template', 'component'))
|
||||
|
||||
|
||||
class GenericComparisonView(PermissionRequiredMixin, View):
|
||||
"""
|
||||
Generic object comparison view
|
||||
|
||||
obj_model: Model of the object involved in the comparison (for example, Interface)
|
||||
obj_template_model: Model of the object template involved in the comparison (for example, InterfaceTemplate)
|
||||
"""
|
||||
obj_model: Type[PrimaryModel] = None
|
||||
obj_template_model: Type[PrimaryModel] = None
|
||||
|
||||
def get_permission_required(self):
|
||||
# User must have permission to view the device whose components are being compared
|
||||
permissions = ["dcim.view_device"]
|
||||
|
||||
# Resolve permissions related to the object and the object template
|
||||
permissions.extend(get_permissions_for_model(self.obj_model, ("view", "add", "change", "delete")))
|
||||
permissions.extend(get_permissions_for_model(self.obj_template_model, ("view",)))
|
||||
|
||||
return permissions
|
||||
|
||||
@staticmethod
|
||||
def filter_comparison_components(component_templates: QuerySet, components: QuerySet) -> Tuple[QuerySet, QuerySet]:
|
||||
"""Override this in the inherited View to implement special comparison objects filtering logic"""
|
||||
return component_templates, components
|
||||
|
||||
def _fetch_comparison_objects(self, device_id: int):
|
||||
self.device = get_object_or_404(Device, id=device_id)
|
||||
component_templates = self.obj_template_model.objects.filter(device_type_id=self.device.device_type.id)
|
||||
components = self.obj_model.objects.filter(device_id=device_id)
|
||||
self.component_templates, self.components = self.filter_comparison_components(component_templates, components)
|
||||
self.comparison_component_templates = [comparison.from_netbox_object(obj) for obj in self.component_templates]
|
||||
self.comparison_components = [comparison.from_netbox_object(obj) for obj in self.components]
|
||||
|
||||
name_comparison_config = config['name_comparison']
|
||||
|
||||
def name_key(obj_name: str) -> str:
|
||||
name = obj_name
|
||||
if name_comparison_config.get('case-insensitive'):
|
||||
name = name.lower()
|
||||
if name_comparison_config.get('space-insensitive'):
|
||||
name = name.replace(' ', '')
|
||||
return name
|
||||
|
||||
component_templates_dict = {name_key(obj.name): obj for obj in self.comparison_component_templates}
|
||||
components_dict = {name_key(obj.name): obj for obj in self.comparison_components}
|
||||
|
||||
self.comparison_table = tuple(
|
||||
ComparisonTableRow(
|
||||
component_template=component_templates_dict.get(component_name),
|
||||
component=components_dict.get(component_name)
|
||||
)
|
||||
for component_name in human_sorted(set().union(component_templates_dict.keys(), components_dict.keys()))
|
||||
)
|
||||
class InterfaceComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""Comparison of interfaces between a device and a device type and beautiful visualization"""
|
||||
permission_required = ("dcim.view_interface", "dcim.add_interface", "dcim.change_interface", "dcim.delete_interface")
|
||||
|
||||
def get(self, request, device_id):
|
||||
self._fetch_comparison_objects(device_id)
|
||||
|
||||
return render(request, "netbox_interface_sync/components_comparison.html", {
|
||||
"component_type_name": self.obj_model._meta.verbose_name_plural,
|
||||
"comparison_items": self.comparison_table,
|
||||
"templates_count": len(self.comparison_component_templates),
|
||||
"components_count": len(self.comparison_components),
|
||||
"device": self.device,
|
||||
})
|
||||
|
||||
def post(self, request, device_id):
|
||||
components_to_add = make_integer_list(request.POST.getlist("add"))
|
||||
components_to_delete = make_integer_list(request.POST.getlist("remove"))
|
||||
components_to_sync = make_integer_list(request.POST.getlist("sync"))
|
||||
if not any((components_to_add, components_to_delete, components_to_sync)):
|
||||
messages.warning(request, "No actions selected")
|
||||
return redirect(request.path)
|
||||
|
||||
self._fetch_comparison_objects(device_id)
|
||||
|
||||
component_ids_to_delete = []
|
||||
components_to_bulk_create = []
|
||||
synced_count = 0
|
||||
for template, component in self.comparison_table:
|
||||
if template and (template.id in components_to_add):
|
||||
# Add component to the device from the template
|
||||
components_to_bulk_create.append(
|
||||
self.obj_model(device=self.device, **template.get_fields_for_netbox_component())
|
||||
)
|
||||
elif component and (component.id in components_to_delete):
|
||||
# Delete component from the device
|
||||
component_ids_to_delete.append(component.id)
|
||||
elif (template and component) and (component.id in components_to_sync):
|
||||
# Update component attributes from the template
|
||||
synced_count += self.components.filter(id=component.id).update(
|
||||
**template.get_fields_for_netbox_component(sync=True)
|
||||
)
|
||||
|
||||
deleted_count = self.obj_model.objects.filter(id__in=component_ids_to_delete).delete()[0]
|
||||
created_count = len(self.obj_model.objects.bulk_create(components_to_bulk_create))
|
||||
|
||||
# Generating result message
|
||||
component_type_name = self.obj_model._meta.verbose_name_plural
|
||||
message = []
|
||||
if synced_count > 0:
|
||||
message.append(f"synced {synced_count} {component_type_name}")
|
||||
if created_count > 0:
|
||||
message.append(f"created {created_count} {component_type_name}")
|
||||
if deleted_count > 0:
|
||||
message.append(f"deleted {deleted_count} {component_type_name}")
|
||||
messages.success(request, "; ".join(message).capitalize())
|
||||
|
||||
return redirect(request.path)
|
||||
|
||||
|
||||
class ConsolePortComparisonView(GenericComparisonView):
|
||||
"""Comparison of console ports between a device and a device type and beautiful visualization"""
|
||||
obj_model = ConsolePort
|
||||
obj_template_model = ConsolePortTemplate
|
||||
|
||||
|
||||
class ConsoleServerPortComparisonView(GenericComparisonView):
|
||||
"""Comparison of console server ports between a device and a device type and beautiful visualization"""
|
||||
obj_model = ConsoleServerPort
|
||||
obj_template_model = ConsoleServerPortTemplate
|
||||
|
||||
|
||||
class InterfaceComparisonView(GenericComparisonView):
|
||||
"""Comparison of interfaces between a device and a device type and beautiful visualization"""
|
||||
obj_model = Interface
|
||||
obj_template_model = InterfaceTemplate
|
||||
|
||||
@staticmethod
|
||||
def filter_comparison_components(component_templates: QuerySet, components: QuerySet) -> Tuple[QuerySet, QuerySet]:
|
||||
if config["exclude_virtual_interfaces"]:
|
||||
components = components.exclude(type__in=VIRTUAL_IFACE_TYPES)
|
||||
component_templates = component_templates.exclude(type__in=VIRTUAL_IFACE_TYPES)
|
||||
return component_templates, components
|
||||
|
||||
|
||||
class PowerPortComparisonView(GenericComparisonView):
|
||||
"""Comparison of power ports between a device and a device type and beautiful visualization"""
|
||||
obj_model = PowerPort
|
||||
obj_template_model = PowerPortTemplate
|
||||
|
||||
|
||||
class PowerOutletComparisonView(GenericComparisonView):
|
||||
"""Comparison of power outlets between a device and a device type and beautiful visualization"""
|
||||
obj_model = PowerOutlet
|
||||
obj_template_model = PowerOutletTemplate
|
||||
|
||||
def post(self, request, device_id):
|
||||
device = get_object_or_404(Device.objects.filter(id=device_id))
|
||||
interfaces = device.vc_interfaces()
|
||||
if config["exclude_virtual_interfaces"]:
|
||||
interfaces = list(filter(lambda i: not i.is_virtual, interfaces))
|
||||
interface_templates = InterfaceTemplate.objects.filter(device_type=device.device_type)
|
||||
|
||||
poweroutlets = device.poweroutlets.all()
|
||||
poweroutlets_templates = PowerOutletTemplate.objects.filter(device_type=device.device_type)
|
||||
unified_interfaces = [UnifiedInterface(i.id, i.name, i.type, i.get_type_display()) for i in interfaces]
|
||||
unified_interface_templates = [
|
||||
UnifiedInterface(i.id, i.name, i.type, i.get_type_display(), is_template=True) for i in interface_templates]
|
||||
|
||||
# Generating result message
|
||||
message = []
|
||||
created = 0
|
||||
updated = 0
|
||||
fixed = 0
|
||||
# List of interfaces and interface templates presented in the unified format
|
||||
overall_interfaces = list(set(unified_interface_templates + unified_interfaces))
|
||||
overall_interfaces.sort(key=lambda o: natural_keys(o.name))
|
||||
|
||||
remove_from_device = filter(
|
||||
lambda i: i in poweroutlets.values_list("id", flat=True),
|
||||
map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove")))
|
||||
comparison_templates = []
|
||||
comparison_interfaces = []
|
||||
for i in overall_interfaces:
|
||||
try:
|
||||
comparison_templates.append(unified_interface_templates[unified_interface_templates.index(i)])
|
||||
except ValueError:
|
||||
comparison_templates.append(None)
|
||||
|
||||
try:
|
||||
comparison_interfaces.append(unified_interfaces[unified_interfaces.index(i)])
|
||||
except ValueError:
|
||||
comparison_interfaces.append(None)
|
||||
|
||||
comparison_items = list(zip(comparison_templates, comparison_interfaces))
|
||||
return render(
|
||||
request, "netbox_interface_sync/interface_comparison.html",
|
||||
{
|
||||
"comparison_items": comparison_items,
|
||||
"templates_count": len(interface_templates),
|
||||
"interfaces_count": len(interfaces),
|
||||
"device": device
|
||||
}
|
||||
)
|
||||
|
||||
# Remove selected power outlets from the device and count them
|
||||
deleted = PowerOutlet.objects.filter(id__in=remove_from_device).delete()[0]
|
||||
def post(self, request, device_id):
|
||||
form = InterfaceComparisonForm(request.POST)
|
||||
if form.is_valid():
|
||||
device = get_object_or_404(Device.objects.filter(id=device_id))
|
||||
interfaces = device.vc_interfaces()
|
||||
if config["exclude_virtual_interfaces"]:
|
||||
interfaces = interfaces.exclude(type__in=["virtual", "lag"])
|
||||
interface_templates = InterfaceTemplate.objects.filter(device_type=device.device_type)
|
||||
|
||||
# Get device power ports to check dependency between power outlets
|
||||
device_pp = PowerPort.objects.filter(device_id=device.id)
|
||||
|
||||
matching = {}
|
||||
mismatch = False
|
||||
for i in poweroutlets_templates:
|
||||
found = False
|
||||
if i.power_port_id is not None:
|
||||
ppt = PowerPortTemplate.objects.get(id=i.power_port_id)
|
||||
for pp in device_pp:
|
||||
if pp.name == ppt.name:
|
||||
# Save matching to add the correct power port later
|
||||
matching[i.id] = pp.id
|
||||
found = True
|
||||
|
||||
# If at least one power port is not found in device there is a dependency
|
||||
# Better not to sync at all
|
||||
if not found:
|
||||
mismatch = True
|
||||
break
|
||||
|
||||
if not mismatch:
|
||||
# Manually validating interfaces and interface templates lists
|
||||
add_to_device = filter(
|
||||
lambda i: i in poweroutlets_templates.values_list("id", flat=True),
|
||||
map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add")))
|
||||
lambda i: i in interface_templates.values_list("id", flat=True),
|
||||
map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device")))
|
||||
)
|
||||
remove_from_device = filter(
|
||||
lambda i: i in interfaces.values_list("id", flat=True),
|
||||
map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device")))
|
||||
)
|
||||
|
||||
# Add selected component to the device and count them
|
||||
add_to_device_component = PowerOutletTemplate.objects.filter(id__in=add_to_device)
|
||||
# Remove selected interfaces from the device and count them
|
||||
interfaces_deleted = Interface.objects.filter(id__in=remove_from_device).delete()[0]
|
||||
|
||||
bulk_create = []
|
||||
updated = 0
|
||||
keys_to_avoid = ["id"]
|
||||
# Add selected interfaces to the device and count them
|
||||
add_to_device_interfaces = InterfaceTemplate.objects.filter(id__in=add_to_device)
|
||||
interfaces_created = len(Interface.objects.bulk_create([
|
||||
Interface(device=device, name=i.name, type=i.type) for i in add_to_device_interfaces
|
||||
]))
|
||||
|
||||
if not config["compare_description"]:
|
||||
keys_to_avoid.append("description")
|
||||
|
||||
for i in add_to_device_component.values():
|
||||
to_create = False
|
||||
# Getting and validating a list of interfaces to rename
|
||||
fix_name_interfaces = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), interfaces)
|
||||
# Casting interface templates into UnifiedInterface objects for proper comparison with interfaces for renaming
|
||||
unified_interface_templates = [
|
||||
UnifiedInterface(i.id, i.name, i.type, i.get_type_display()) for i in interface_templates]
|
||||
|
||||
# Rename selected interfaces
|
||||
interfaces_fixed = 0
|
||||
for interface in fix_name_interfaces:
|
||||
unified_interface = UnifiedInterface(interface.id, interface.name, interface.type, interface.get_type_display())
|
||||
try:
|
||||
# If power outlets already exists, update and do not recreate
|
||||
po = device.poweroutlets.get(name=i["name"])
|
||||
except PowerOutlet.DoesNotExist:
|
||||
po = PowerOutlet()
|
||||
po.device = device
|
||||
to_create = True
|
||||
|
||||
# Copy all fields from template
|
||||
for k in i.keys():
|
||||
if k not in keys_to_avoid:
|
||||
setattr(po, k, i[k])
|
||||
po.power_port_id = matching.get(i["id"], None)
|
||||
|
||||
if to_create:
|
||||
bulk_create.append(po)
|
||||
else:
|
||||
po.save()
|
||||
updated += 1
|
||||
|
||||
created = len(PowerOutlet.objects.bulk_create(bulk_create))
|
||||
|
||||
# Getting and validating a list of components to rename
|
||||
fix_name_components = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), poweroutlets)
|
||||
|
||||
# Casting component templates into Unified objects for proper comparison with component for renaming
|
||||
unified_component_templates = [
|
||||
PowerOutletComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(),
|
||||
power_port_name=PowerPortTemplate.objects.get(id=i.power_port_id).name
|
||||
if i.power_port_id is not None else "",
|
||||
feed_leg=i.feed_leg, is_template=True)
|
||||
for i in poweroutlets_templates]
|
||||
|
||||
# Rename selected power outlets
|
||||
fixed = 0
|
||||
for component in fix_name_components:
|
||||
unified_poweroutlet = PowerOutletComparison(
|
||||
component.id, component.name, component.label, component.description, component.type,
|
||||
component.get_type_display(),
|
||||
power_port_name=PowerPort.objects.get(id=component.power_port_id).name
|
||||
if component.power_port_id is not None else "",
|
||||
feed_leg=component.feed_leg
|
||||
)
|
||||
try:
|
||||
# Try to extract a component template with the corresponding name
|
||||
corresponding_template = unified_component_templates[
|
||||
unified_component_templates.index(unified_poweroutlet)
|
||||
]
|
||||
component.name = corresponding_template.name
|
||||
component.save()
|
||||
fixed += 1
|
||||
# Try to extract an interface template with the corresponding name
|
||||
corresponding_template = unified_interface_templates[unified_interface_templates.index(unified_interface)]
|
||||
interface.name = corresponding_template.name
|
||||
interface.save()
|
||||
interfaces_fixed += 1
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
messages.error(request, "Dependency detected, sync power ports first!")
|
||||
|
||||
if created > 0:
|
||||
message.append(f"created {created} power outlets")
|
||||
if updated > 0:
|
||||
message.append(f"updated {updated} power outlets")
|
||||
if deleted > 0:
|
||||
message.append(f"deleted {deleted} power outlets")
|
||||
if fixed > 0:
|
||||
message.append(f"fixed {fixed} power outlets")
|
||||
# Generating result message
|
||||
message = []
|
||||
if interfaces_created > 0:
|
||||
message.append(f"created {interfaces_created} interfaces")
|
||||
if interfaces_deleted > 0:
|
||||
message.append(f"deleted {interfaces_deleted} interfaces")
|
||||
if interfaces_fixed > 0:
|
||||
message.append(f"fixed {interfaces_fixed} interfaces")
|
||||
messages.success(request, "; ".join(message).capitalize())
|
||||
|
||||
messages.info(request, "; ".join(message).capitalize())
|
||||
|
||||
return redirect(request.path)
|
||||
|
||||
|
||||
class RearPortComparisonView(GenericComparisonView):
|
||||
"""Comparison of rear ports between a device and a device type and beautiful visualization"""
|
||||
obj_model = RearPort
|
||||
obj_template_model = RearPortTemplate
|
||||
|
||||
|
||||
class DeviceBayComparisonView(GenericComparisonView):
|
||||
"""Comparison of device bays between a device and a device type and beautiful visualization"""
|
||||
obj_model = DeviceBay
|
||||
obj_template_model = DeviceBayTemplate
|
||||
#
|
||||
#
|
||||
# class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
# """Comparison of front ports between a device and a device type and beautiful visualization"""
|
||||
# permission_required = get_permissions_for_object("dcim", "frontport")
|
||||
#
|
||||
# def get(self, request, device_id):
|
||||
#
|
||||
# device = get_object_or_404(Device.objects.filter(id=device_id))
|
||||
#
|
||||
# frontports = device.frontports.all()
|
||||
# frontports_templates = FrontPortTemplate.objects.filter(device_type=device.device_type)
|
||||
#
|
||||
# unified_frontports = [
|
||||
# FrontPortComparison(
|
||||
# i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color, i.rear_port_position)
|
||||
# for i in frontports]
|
||||
# unified_frontports_templates = [
|
||||
# FrontPortComparison(
|
||||
# i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color,
|
||||
# i.rear_port_position, is_template=True)
|
||||
# for i in frontports_templates]
|
||||
#
|
||||
# return get_components(request, device, frontports, unified_frontports, unified_frontports_templates)
|
||||
#
|
||||
# def post(self, request, device_id):
|
||||
# form = ComponentComparisonForm(request.POST)
|
||||
# if form.is_valid():
|
||||
# device = get_object_or_404(Device.objects.filter(id=device_id))
|
||||
#
|
||||
# frontports = device.frontports.all()
|
||||
# frontports_templates = FrontPortTemplate.objects.filter(device_type=device.device_type)
|
||||
#
|
||||
# # Generating result message
|
||||
# message = []
|
||||
# created = 0
|
||||
# updated = 0
|
||||
# fixed = 0
|
||||
#
|
||||
# remove_from_device = filter(
|
||||
# lambda i: i in frontports.values_list("id", flat=True),
|
||||
# map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device")))
|
||||
# )
|
||||
#
|
||||
# # Remove selected front ports from the device and count them
|
||||
# deleted = FrontPort.objects.filter(id__in=remove_from_device).delete()[0]
|
||||
#
|
||||
# # Get device rear ports to check dependency between front ports
|
||||
# device_rp = RearPort.objects.filter(device_id=device.id)
|
||||
#
|
||||
# matching = {}
|
||||
# mismatch = False
|
||||
# for i in frontports_templates:
|
||||
# found = False
|
||||
# if i.rear_port_id is not None:
|
||||
# rpt = RearPortTemplate.objects.get(id=i.rear_port_id)
|
||||
# for rp in device_rp:
|
||||
# if rp.name == rpt.name:
|
||||
# # Save matching to add the correct rear port later
|
||||
# matching[i.id] = rp.id
|
||||
# found = True
|
||||
#
|
||||
# # If at least one rear port is not found in device there is a dependency
|
||||
# # Better not to sync at all
|
||||
# if not found:
|
||||
# mismatch = True
|
||||
# break
|
||||
#
|
||||
# if not mismatch:
|
||||
# add_to_device = filter(
|
||||
# lambda i: i in frontports_templates.values_list("id", flat=True),
|
||||
# map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device")))
|
||||
# )
|
||||
#
|
||||
# # Add selected component to the device and count them
|
||||
# add_to_device_component = FrontPortTemplate.objects.filter(id__in=add_to_device)
|
||||
#
|
||||
# bulk_create = []
|
||||
# updated = 0
|
||||
# keys_to_avoid = ["id"]
|
||||
#
|
||||
# if not config["compare_description"]:
|
||||
# keys_to_avoid.append("description")
|
||||
#
|
||||
# for i in add_to_device_component.values():
|
||||
# to_create = False
|
||||
#
|
||||
# try:
|
||||
# # If front port already exists, update and do not recreate
|
||||
# fp = device.frontports.get(name=i["name"])
|
||||
# except FrontPort.DoesNotExist:
|
||||
# fp = FrontPort()
|
||||
# fp.device = device
|
||||
# to_create = True
|
||||
#
|
||||
# # Copy all fields from template
|
||||
# for k in i.keys():
|
||||
# if k not in keys_to_avoid:
|
||||
# setattr(fp, k, i[k])
|
||||
# fp.rear_port_id = matching.get(i["id"], None)
|
||||
#
|
||||
# if to_create:
|
||||
# bulk_create.append(fp)
|
||||
# else:
|
||||
# fp.save()
|
||||
# updated += 1
|
||||
#
|
||||
# created = len(FrontPort.objects.bulk_create(bulk_create))
|
||||
#
|
||||
# # Getting and validating a list of components to rename
|
||||
# fix_name_components = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), frontports)
|
||||
#
|
||||
# # Casting component templates into Unified objects for proper comparison with component for renaming
|
||||
# unified_frontports_templates = [
|
||||
# FrontPortComparison(
|
||||
# i.id, i.name, i.label, i.description, i.type, i.get_type_display(),
|
||||
# i.color, i.rear_port_position, is_template=True)
|
||||
# for i in frontports_templates]
|
||||
# # Rename selected front ports
|
||||
# fixed = 0
|
||||
# for component in fix_name_components:
|
||||
# unified_frontport = FrontPortComparison(
|
||||
# component.id, component.name, component.label, component.description, component.type,
|
||||
# component.get_type_display(), component.color, component.rear_port_position
|
||||
# )
|
||||
#
|
||||
# try:
|
||||
# # Try to extract a component template with the corresponding name
|
||||
# corresponding_template = unified_frontports_templates[
|
||||
# unified_frontports_templates.index(unified_frontport)
|
||||
# ]
|
||||
# component.name = corresponding_template.name
|
||||
# component.save()
|
||||
# fixed += 1
|
||||
# except ValueError:
|
||||
# pass
|
||||
# else:
|
||||
# messages.error(request, "Dependency detected, sync rear ports first!")
|
||||
#
|
||||
# if created > 0:
|
||||
# message.append(f"created {created} front ports")
|
||||
# if updated > 0:
|
||||
# message.append(f"updated {updated} front ports")
|
||||
# if deleted > 0:
|
||||
# message.append(f"deleted {deleted} front ports")
|
||||
# if fixed > 0:
|
||||
# message.append(f"fixed {fixed} front ports")
|
||||
#
|
||||
# messages.info(request, "; ".join(message).capitalize())
|
||||
#
|
||||
# return redirect(request.path)
|
||||
return redirect(request.path)
|
||||
|
Loading…
Reference in New Issue
Block a user