From b48cfaca4bd24b99a009af80b3739c81feaaeb6f Mon Sep 17 00:00:00 2001
From: NetTech2001 <128771411+NetTech2001@users.noreply.github.com>
Date: Fri, 17 May 2024 16:00:56 -0600
Subject: [PATCH] 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.
---
netbox_interface_sync/__init__.py | 22 +-
netbox_interface_sync/comparison.py | 156 ------
netbox_interface_sync/forms.py | 6 +
netbox_interface_sync/template_content.py | 4 +-
.../compare_components_button.html | 65 ---
.../compare_interfaces_button.html | 3 +
.../compare_interfaces_button.html-old | 3 +
.../components_comparison.html | 158 ------
.../interface_comparison.html | 161 ++++++
.../interface_comparison.html-keith | 161 ++++++
.../interface_comparison.html-old | 161 ++++++
.../number_of_interfaces_panel.html | 20 +-
netbox_interface_sync/urls.py | 41 +-
netbox_interface_sync/utils.py | 40 +-
netbox_interface_sync/views.py | 519 +++---------------
15 files changed, 616 insertions(+), 904 deletions(-)
delete mode 100644 netbox_interface_sync/comparison.py
create mode 100644 netbox_interface_sync/forms.py
delete mode 100644 netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html
create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html
create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html-old
delete mode 100644 netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html
create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html
create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-keith
create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-old
diff --git a/netbox_interface_sync/__init__.py b/netbox_interface_sync/__init__.py
index f7ceee8..20abc48 100644
--- a/netbox_interface_sync/__init__.py
+++ b/netbox_interface_sync/__init__.py
@@ -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
}
diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py
deleted file mode 100644
index d643f9d..0000000
--- a/netbox_interface_sync/comparison.py
+++ /dev/null
@@ -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)
diff --git a/netbox_interface_sync/forms.py b/netbox_interface_sync/forms.py
new file mode 100644
index 0000000..0799ebe
--- /dev/null
+++ b/netbox_interface_sync/forms.py
@@ -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)
diff --git a/netbox_interface_sync/template_content.py b/netbox_interface_sync/template_content.py
index 71a4cc9..750ee09 100644
--- a/netbox_interface_sync/template_content.py
+++ b/netbox_interface_sync/template_content.py
@@ -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
})
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html b/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html
deleted file mode 100644
index 3682f71..0000000
--- a/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{% if perms.dcim.change_device %}
-
-
- Device type sync
-
-
-
-{% endif %}
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html b/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html
new file mode 100644
index 0000000..5594df4
--- /dev/null
+++ b/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html
@@ -0,0 +1,3 @@
+
+ Interface Sync
+
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html-old b/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html-old
new file mode 100644
index 0000000..a64fec2
--- /dev/null
+++ b/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html-old
@@ -0,0 +1,3 @@
+
+ Interface Sync
+
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html b/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html
deleted file mode 100644
index 7ca570c..0000000
--- a/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html
+++ /dev/null
@@ -1,158 +0,0 @@
-{% extends 'base/layout.html' %}
-
-{% block title %}{{ device }} - {{ component_type_name|capfirst }} comparison{% endblock %}
-{% block header %}
-
-
- Devices
- {{ device.site }}
- {{ device }}
-
-
- {{ block.super }}
-{% endblock %}
-
-{% block content %}
-
-
-
-
-
-{% endblock %}
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html
new file mode 100644
index 0000000..eda27e1
--- /dev/null
+++ b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html
@@ -0,0 +1,161 @@
+{% extends 'base/layout.html' %}
+
+{% block title %}{{ device }} - Interface comparison{% endblock %}
+{% block header %}
+
+
+ Devices
+ {{ device.site }}
+ {{ device }}
+
+
+ {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+{% 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.
+ Device: {{ interfaces_count }}
+ Device type: {{ templates_count }}
+{% endif %}
+
+
+
+
+{% endblock %}
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-keith b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-keith
new file mode 100644
index 0000000..54d102d
--- /dev/null
+++ b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-keith
@@ -0,0 +1,161 @@
+{% extends 'base/layout.html' %}
+
+{% block title %}{{ device }} - Interface comparison{% endblock %}
+{% block header %}
+
+
+ Devices
+ {{ device.site }}
+ {{ device }}
+
+
+ {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+{% 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.
+ Device: {{ interfaces_count }}
+ Device type: {{ templates_count }}
+{% endif %}
+
+
+
+
+{% endblock %}
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-old b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-old
new file mode 100644
index 0000000..063634d
--- /dev/null
+++ b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-old
@@ -0,0 +1,161 @@
+{% extends 'base/layout.html' %}
+
+{% block title %}{{ device }} - Interface comparison{% endblock %}
+{% block header %}
+
+
+ Devices
+ {{ device.site }}
+ {{ device }}
+
+
+ {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+{% 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.
+ Device: {{ interfaces_count }}
+ Device type: {{ templates_count }}
+{% endif %}
+
+
+
+
+{% endblock %}
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/number_of_interfaces_panel.html b/netbox_interface_sync/templates/netbox_interface_sync/number_of_interfaces_panel.html
index 8d9b2ad..98bc1ef 100644
--- a/netbox_interface_sync/templates/netbox_interface_sync/number_of_interfaces_panel.html
+++ b/netbox_interface_sync/templates/netbox_interface_sync/number_of_interfaces_panel.html
@@ -1,12 +1,10 @@
-{% if config.include_interfaces_panel %}
-
-
-
- Total interfaces: {{ interfaces|length }}
- {% if config.exclude_virtual_interfaces %}
- Non-virtual interfaces: {{ real_interfaces|length }}
- {% endif %}
- Interfaces in the assigned device type: {{ interface_templates|length }}
-
+
+
+
+ Total interfaces: {{ interfaces|length }}
+ {% if config.exclude_virtual_interfaces %}
+ Non-virtual interfaces: {{ real_interfaces|length }}
+ {% endif %}
+ Interfaces in the assigned device type: {{ interface_templates|length }}
-{% endif %}
\ No newline at end of file
+
\ No newline at end of file
diff --git a/netbox_interface_sync/urls.py b/netbox_interface_sync/urls.py
index 847e18c..cef81f2 100644
--- a/netbox_interface_sync/urls.py
+++ b/netbox_interface_sync/urls.py
@@ -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/
/",
- views.ConsolePortComparisonView.as_view(),
- name="consoleport_comparison",
- ),
- path(
- "consoleserverport-comparison//",
- views.ConsoleServerPortComparisonView.as_view(),
- name="consoleserverport_comparison",
- ),
- path(
- "interface-comparison//",
- views.InterfaceComparisonView.as_view(),
- name="interface_comparison",
- ),
- path(
- "powerport-comparison//",
- views.PowerPortComparisonView.as_view(),
- name="powerport_comparison",
- ),
- path(
- "poweroutlet-comparison//",
- views.PowerOutletComparisonView.as_view(),
- name="poweroutlet_comparison",
- ),
- # path(
- # "frontport-comparison//",
- # views.FrontPortComparisonView.as_view(),
- # name="frontport_comparison",
- # ),
- path(
- "rearport-comparison//",
- views.RearPortComparisonView.as_view(),
- name="rearport_comparison",
- ),
- path(
- "devicebay-comparison//",
- views.DeviceBayComparisonView.as_view(),
- name="devicebay_comparison",
- ),
+ path("interface-comparison//", views.InterfaceComparisonView.as_view(), name="interface_comparison"),
)
diff --git a/netbox_interface_sync/utils.py b/netbox_interface_sync/utils.py
index e6b0bd6..19d7fd1 100644
--- a/netbox_interface_sync/utils.py
+++ b/netbox_interface_sync/utils.py
@@ -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))
diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py
index fd46a84..55adfcd 100644
--- a/netbox_interface_sync/views.py
+++ b/netbox_interface_sync/views.py
@@ -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)