mirror of
https://github.com/drygdryg/netbox-plugin-interface-sync
synced 2024-11-29 18:20:52 +03:00
Compare commits
No commits in common. "c3b7e561b0d705ff18b30bdb5084f7eb0877b1f2" and "51b75bc6d587593a1a5cd3c21d157e41d080dbff" have entirely different histories.
c3b7e561b0
...
51b75bc6d5
@ -1,6 +1,7 @@
|
|||||||
# netbox-interface-sync
|
# netbox-interface-sync
|
||||||
|
[Русская версия](./README_ru.md)
|
||||||
## Overview
|
## Overview
|
||||||
This plugin allows you to compare and synchronize interfaces between devices and device types in NetBox 4. It can be useful for finding and correcting inconsistencies between interfaces.
|
This plugin allows you to compare and synchronize interfaces between devices and device types in NetBox. It can be useful for finding and correcting inconsistencies between interfaces.
|
||||||
Tested with NetBox version 4.0
|
Tested with NetBox version 4.0
|
||||||
## Installation
|
## Installation
|
||||||
If your NetBox installation uses virtualenv, activate it like this:
|
If your NetBox installation uses virtualenv, activate it like this:
|
||||||
|
44
README_ru.md
Normal file
44
README_ru.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# netbox-interface-sync
|
||||||
|
[English version](./README.md)
|
||||||
|
## Обзор
|
||||||
|
Плагин для NetBox, позволяющий сравнивать и синхронизировать интерфейсы между устройствами (devices) и типами устройств (device types). Полезен для поиска и исправления несоответствий между интерфейсами. Работа проверена с NetBox версий 4.0
|
||||||
|
## Установка
|
||||||
|
Если NetBox использует virtualenv, то активируйте его, например, так:
|
||||||
|
```
|
||||||
|
source /opt/netbox/venv/bin/activate
|
||||||
|
```
|
||||||
|
Установите плагин из репозитория PyPI:
|
||||||
|
```
|
||||||
|
pip install netbox-interface-sync
|
||||||
|
```
|
||||||
|
или клонируйте этот репозиторий, затем перейдите в папку с ним и установите плагин:
|
||||||
|
```
|
||||||
|
pip install .
|
||||||
|
```
|
||||||
|
Включите плагин в файле `configuration.py` (обычно он находится в `/opt/netbox/netbox/netbox/`), добавьте его имя в список `PLUGINS`:
|
||||||
|
```
|
||||||
|
PLUGINS = [
|
||||||
|
'netbox_interface_sync'
|
||||||
|
]
|
||||||
|
```
|
||||||
|
Перезапустите NetBox:
|
||||||
|
```
|
||||||
|
sudo systemctl restart netbox
|
||||||
|
```
|
||||||
|
## Использование
|
||||||
|
Для того чтобы сравнить интерфейсы, откройте страницу нужного устройства и найдите кнопку "Interface sync" справа сверху:
|
||||||
|
![Device page](docs/images/1_device_page.png)
|
||||||
|
Отметьте требуемые действия напротив интерфейсов флажками и нажмите "Apply".
|
||||||
|
![Interface comparison](docs/images/2_interface_comparison.png)
|
||||||
|
### Настройки плагина
|
||||||
|
Если вы хотите переопределить значения по умолчанию, настройте переменную `PLUGINS_CONFIG` в вашем файле `configuration.py`:
|
||||||
|
```
|
||||||
|
PLUGINS_CONFIG = {
|
||||||
|
'netbox_interface_sync': {
|
||||||
|
'exclude_virtual_interfaces': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
| Настройка | Значение по умолчанию | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| exclude_virtual_interfaces | `True` | Не учитывать виртуальные интерфейсы (VLAN, LAG) при сравнении
|
Binary file not shown.
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
Binary file not shown.
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 115 KiB |
@ -3,13 +3,25 @@ from netbox.plugins import PluginConfig
|
|||||||
|
|
||||||
class Config(PluginConfig):
|
class Config(PluginConfig):
|
||||||
name = 'netbox_interface_sync'
|
name = 'netbox_interface_sync'
|
||||||
verbose_name = 'NetBox interface synchronization'
|
verbose_name = 'NetBox 4 Interface Synchronization'
|
||||||
description = 'Syncing interfaces with the interfaces from device type for NetBox 4'
|
description = 'Compare and synchronize components (interfaces, ports, outlets, etc.) between NetBox 4 device types ' \
|
||||||
version = '0.4.1'
|
'and devices'
|
||||||
author = 'Keith Knowles'
|
version = '0.4.0'
|
||||||
author_email = 'mkknowles@outlook.com'
|
author = 'based on work by Victor Golovanenko'
|
||||||
|
author_email = 'drygdryg2014@yandex.ru'
|
||||||
default_settings = {
|
default_settings = {
|
||||||
'exclude_virtual_interfaces': True
|
# 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
156
netbox_interface_sync/comparison.py
Normal file
156
netbox_interface_sync/comparison.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
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)
|
@ -1,6 +0,0 @@
|
|||||||
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"
|
model = "dcim.device"
|
||||||
|
|
||||||
def buttons(self):
|
def buttons(self):
|
||||||
"""Implements a compare interfaces button at the top of the page"""
|
"""Implements a compare button at the top of the page"""
|
||||||
obj = self.context['object']
|
obj = self.context['object']
|
||||||
return self.render("netbox_interface_sync/compare_interfaces_button.html", extra_context={
|
return self.render("netbox_interface_sync/compare_components_button.html", extra_context={
|
||||||
"device": obj
|
"device": obj
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
{% 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 %}
|
@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
@ -1,3 +0,0 @@
|
|||||||
<a href="{% url 'plugins:netbox_interface_sync:interface_comparison' device_id=device.id %}" class="btn btn-purple">
|
|
||||||
Interface Sync
|
|
||||||
</a>
|
|
@ -0,0 +1,158 @@
|
|||||||
|
{% 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 %}
|
@ -1,161 +0,0 @@
|
|||||||
{% 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 %}
|
|
@ -1,161 +0,0 @@
|
|||||||
{% 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 %}
|
|
@ -1,161 +0,0 @@
|
|||||||
{% 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,4 +1,5 @@
|
|||||||
<div class="card">
|
{% if config.include_interfaces_panel %}
|
||||||
|
<div class="card">
|
||||||
<h5 class="card-header">Number of interfaces</h5>
|
<h5 class="card-header">Number of interfaces</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
Total interfaces: {{ interfaces|length }}<br>
|
Total interfaces: {{ interfaces|length }}<br>
|
||||||
@ -7,4 +8,5 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
Interfaces in the assigned device type: {{ interface_templates|length }}
|
Interfaces in the assigned device type: {{ interface_templates|length }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
@ -6,5 +6,44 @@ from . import views
|
|||||||
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
|
# 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.
|
# a specific view so that it can be accessed by users.
|
||||||
urlpatterns = (
|
urlpatterns = (
|
||||||
path("interface-comparison/<int:device_id>/", views.InterfaceComparisonView.as_view(), name="interface_comparison"),
|
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",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import re
|
import re
|
||||||
from typing import Iterable
|
from typing import Iterable, List
|
||||||
from dataclasses import dataclass
|
from django.conf import settings
|
||||||
|
|
||||||
|
config = settings.PLUGINS_CONFIG['netbox_interface_sync']
|
||||||
|
|
||||||
|
|
||||||
def split(s):
|
def split(s):
|
||||||
for x, y in re.findall(r'(\d*)(\D*)', s):
|
for x, y in re.findall(r"(\d*)(\D*)", s):
|
||||||
yield '', int(x or '0')
|
yield "", int(x or "0")
|
||||||
yield y, 0
|
yield y, 0
|
||||||
|
|
||||||
|
|
||||||
@ -17,19 +19,21 @@ def human_sorted(iterable: Iterable):
|
|||||||
return sorted(iterable, key=natural_keys)
|
return sorted(iterable, key=natural_keys)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
def make_integer_list(lst: List[str]):
|
||||||
class UnifiedInterface:
|
return [int(i) for i in lst if i.isdigit()]
|
||||||
"""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 __hash__(self):
|
def get_permissions_for_model(model, actions: Iterable[str]) -> List[str]:
|
||||||
# Ignore some fields when hashing; ignore interface name case and whitespaces
|
"""
|
||||||
return hash((self.name.lower().replace(' ', ''), self.type))
|
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
|
||||||
|
@ -1,114 +1,461 @@
|
|||||||
from django.shortcuts import get_object_or_404, render, redirect
|
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.views.generic import View
|
from django.views.generic import View
|
||||||
from dcim.models import Device, Interface, InterfaceTemplate
|
from dcim.models import (Device, Interface, InterfaceTemplate, PowerPort, PowerPortTemplate, ConsolePort,
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, DeviceBay,
|
||||||
|
DeviceBayTemplate, FrontPort, FrontPortTemplate, PowerOutlet, PowerOutletTemplate, RearPort,
|
||||||
|
RearPortTemplate)
|
||||||
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
|
||||||
from .utils import UnifiedInterface, natural_keys
|
from netbox.models import PrimaryModel
|
||||||
from .forms import InterfaceComparisonForm
|
from dcim.constants import VIRTUAL_IFACE_TYPES
|
||||||
|
|
||||||
|
from . import comparison
|
||||||
|
from .utils import get_permissions_for_model, make_integer_list, human_sorted
|
||||||
|
|
||||||
config = settings.PLUGINS_CONFIG['netbox_interface_sync']
|
config = settings.PLUGINS_CONFIG['netbox_interface_sync']
|
||||||
|
ComparisonTableRow = namedtuple('ComparisonTableRow', ('component_template', 'component'))
|
||||||
|
|
||||||
|
|
||||||
class InterfaceComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
class GenericComparisonView(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")
|
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()))
|
||||||
|
)
|
||||||
|
|
||||||
def get(self, request, device_id):
|
def get(self, request, device_id):
|
||||||
device = get_object_or_404(Device.objects.filter(id=device_id))
|
self._fetch_comparison_objects(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)
|
|
||||||
|
|
||||||
unified_interfaces = [UnifiedInterface(i.id, i.name, i.type, i.get_type_display()) for i in interfaces]
|
return render(request, "netbox_interface_sync/components_comparison.html", {
|
||||||
unified_interface_templates = [
|
"component_type_name": self.obj_model._meta.verbose_name_plural,
|
||||||
UnifiedInterface(i.id, i.name, i.type, i.get_type_display(), is_template=True) for i in interface_templates]
|
"comparison_items": self.comparison_table,
|
||||||
|
"templates_count": len(self.comparison_component_templates),
|
||||||
# List of interfaces and interface templates presented in the unified format
|
"components_count": len(self.comparison_components),
|
||||||
overall_interfaces = list(set(unified_interface_templates + unified_interfaces))
|
"device": self.device,
|
||||||
overall_interfaces.sort(key=lambda o: natural_keys(o.name))
|
})
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request, device_id):
|
def post(self, request, device_id):
|
||||||
form = InterfaceComparisonForm(request.POST)
|
components_to_add = make_integer_list(request.POST.getlist("add"))
|
||||||
if form.is_valid():
|
components_to_delete = make_integer_list(request.POST.getlist("remove"))
|
||||||
device = get_object_or_404(Device.objects.filter(id=device_id))
|
components_to_sync = make_integer_list(request.POST.getlist("sync"))
|
||||||
interfaces = device.vc_interfaces()
|
if not any((components_to_add, components_to_delete, components_to_sync)):
|
||||||
if config["exclude_virtual_interfaces"]:
|
messages.warning(request, "No actions selected")
|
||||||
interfaces = interfaces.exclude(type__in=["virtual", "lag"])
|
return redirect(request.path)
|
||||||
interface_templates = InterfaceTemplate.objects.filter(device_type=device.device_type)
|
|
||||||
|
|
||||||
# Manually validating interfaces and interface templates lists
|
self._fetch_comparison_objects(device_id)
|
||||||
add_to_device = filter(
|
|
||||||
lambda i: i in interface_templates.values_list("id", flat=True),
|
component_ids_to_delete = []
|
||||||
map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device")))
|
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())
|
||||||
)
|
)
|
||||||
remove_from_device = filter(
|
elif component and (component.id in components_to_delete):
|
||||||
lambda i: i in interfaces.values_list("id", flat=True),
|
# Delete component from the device
|
||||||
map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_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)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove selected interfaces from the device and count them
|
deleted_count = self.obj_model.objects.filter(id__in=component_ids_to_delete).delete()[0]
|
||||||
interfaces_deleted = Interface.objects.filter(id__in=remove_from_device).delete()[0]
|
created_count = len(self.obj_model.objects.bulk_create(components_to_bulk_create))
|
||||||
|
|
||||||
# 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
|
|
||||||
]))
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Generating result message
|
# Generating result message
|
||||||
|
component_type_name = self.obj_model._meta.verbose_name_plural
|
||||||
message = []
|
message = []
|
||||||
if interfaces_created > 0:
|
if synced_count > 0:
|
||||||
message.append(f"created {interfaces_created} interfaces")
|
message.append(f"synced {synced_count} {component_type_name}")
|
||||||
if interfaces_deleted > 0:
|
if created_count > 0:
|
||||||
message.append(f"deleted {interfaces_deleted} interfaces")
|
message.append(f"created {created_count} {component_type_name}")
|
||||||
if interfaces_fixed > 0:
|
if deleted_count > 0:
|
||||||
message.append(f"fixed {interfaces_fixed} interfaces")
|
message.append(f"deleted {deleted_count} {component_type_name}")
|
||||||
messages.success(request, "; ".join(message).capitalize())
|
messages.success(request, "; ".join(message).capitalize())
|
||||||
|
|
||||||
return redirect(request.path)
|
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))
|
||||||
|
|
||||||
|
poweroutlets = device.poweroutlets.all()
|
||||||
|
poweroutlets_templates = PowerOutletTemplate.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 poweroutlets.values_list("id", flat=True),
|
||||||
|
map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove")))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove selected power outlets from the device and count them
|
||||||
|
deleted = PowerOutlet.objects.filter(id__in=remove_from_device).delete()[0]
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
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")))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add selected component to the device and count them
|
||||||
|
add_to_device_component = PowerOutletTemplate.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 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
|
||||||
|
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")
|
||||||
|
|
||||||
|
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)
|
||||||
|
6
setup.py
6
setup.py
@ -5,12 +5,12 @@ with open('README.md', encoding='utf-8') as f:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='netbox-interface-sync',
|
name='netbox-interface-sync',
|
||||||
version='0.4.1',
|
version='0.4.0',
|
||||||
description='Syncing interfaces with the interfaces from device type for NetBox 4 devices',
|
description='Syncing interfaces with the interfaces from device type for NetBox 4 devices',
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type='text/markdown',
|
||||||
author='Keith Knowles',
|
author='Based on work by Victor Golovanenko',
|
||||||
author_email='mkknowles@outlook.com',
|
author_email='drygdryg2014@yandex.com',
|
||||||
license='GPL-3.0',
|
license='GPL-3.0',
|
||||||
install_requires=['attrs>=21.1.0'],
|
install_requires=['attrs>=21.1.0'],
|
||||||
packages=["netbox_interface_sync"],
|
packages=["netbox_interface_sync"],
|
||||||
|
Loading…
Reference in New Issue
Block a user