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 %} - -{% 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 %} - - {{ block.super }} -{% endblock %} - -{% block content %} - - - -
- {% csrf_token %} -
- - {% if templates_count == components_count %} - - {% else %} - - {% endif %} - - - - - - - - - - - - - - - - - - - {% for component_template, component in comparison_items %} - - {% if component_template %} - - - - {% else %} - - - - {% endif %} - - {% if component %} - - - - - {% else %} - - - - - {% endif %} - - {% endfor %} - -
- The device and device type have the same number of {{ component_type_name }}. - - The device and device type have different number of {{ component_type_name }}.
- Device: {{ components_count }}
- Device type: {{ templates_count }} -
Device typeActionsDeviceActions
NameAttributes - - NameAttributes - - - -
- {% if component and component_template.name != component.name %} - {{ component_template.name }} - {% else %} - {{ component_template.name }} - {% endif %} - {{ component_template.fields_display }} - {% if not component %} - - {% endif %} -     - {% if component_template and component_template.name != component.name %} - {{ component.name }} - {% else %} - {{ component.name }} - {% endif %} - {{ component.fields_display }} - {% if not component_template %} - - {% endif %} - - {% if component_template and component_template != component %} - - {% endif %} -     
-
-
- -
-
- -{% 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 %} + + {{ 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 %} +

+ +
+ + {% csrf_token %} + + + + + + + + + + + {% for template, interface in comparison_items %} + {% if template %} + + + + + + {% else %} + + + + + + {% endif %} + {% endfor %} +
Device TypeActions
NameType + +
+ {% if interface and template.name != interface.name %} + {{ template.name }} + {% else %} + {{ template.name }} + {% endif %} + {{ template.type_display }} + {% if not interface %} + + {% endif %} +
   
+ + + + + + + + + + + + + + {% for template, interface in comparison_items %} + {% if interface %} + + + + + + + {% else %} + + + + + + + {% endif %} + {% endfor %} +
DeviceActions
NameType + + + +
+ {% if template and template.name != interface.name %} + {{ interface.name }} + {% else %} + {{ interface.name }} + {% endif %} + {{ interface.type_display }} + {% if not template %} + + {% endif %} + + {% if template and template.name != interface.name %} + + {% 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 %} + + {{ 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 %} +

+ +
+ + {% csrf_token %} + + + + + + + + + + + {% for template, interface in comparison_items %} + {% if template %} + + + + + + {% else %} + + + + + + {% endif %} + {% endfor %} +
Device TypeActions
NameType + +
+ {% if interface and template.name != interface.name %} + {{ template.name }} + {% else %} + {{ template.name }} + {% endif %} + {{ template.type_display }} + {% if not interface %} + + {% endif %} +
   
+ + + + + + + + + + + + + + {% for template, interface in comparison_items %} + {% if interface %} + + + + + + + {% else %} + + + + + + + {% endif %} + {% endfor %} +
DeviceActions
NameType + + + +
+ {% if template and template.name != interface.name %} + {{ interface.name }} + {% else %} + {{ interface.name }} + {% endif %} + {{ interface.type_display }} + {% if not template %} + + {% endif %} + + {% if template and template.name != interface.name %} + + {% 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 %} + + {{ 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 %} +

+ +
+ + {% csrf_token %} + + + + + + + + + + + {% for template, interface in comparison_items %} + {% if template %} + + + + + + {% else %} + + + + + + {% endif %} + {% endfor %} +
Device TypeActions
NameType + +
+ {% if interface and template.name != interface.name %} + {{ template.name }} + {% else %} + {{ template.name }} + {% endif %} + {{ template.type_display }} + {% if not interface %} + + {% endif %} +
   
+ + + + + + + + + + + + + + {% for template, interface in comparison_items %} + {% if interface %} + + + + + + + {% else %} + + + + + + + {% endif %} + {% endfor %} +
DeviceActions
NameType + + + +
+ {% if template and template.name != interface.name %} + {{ interface.name }} + {% else %} + {{ interface.name }} + {% endif %} + {{ interface.type_display }} + {% if not template %} + + {% endif %} + + {% if template and template.name != interface.name %} + + {% 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 %} -
-
Number of interfaces
-
- 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 }} -
+
+
Number of interfaces
+
+ 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)