diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py index 10cf63a..8784601 100644 --- a/netbox_interface_sync/comparison.py +++ b/netbox_interface_sync/comparison.py @@ -3,30 +3,39 @@ from dataclasses import dataclass @dataclass(frozen=True) class ParentComparison: + """Common fields of a device component""" + id: int name: str label: str description: str def __eq__(self, other): - # Ignore some fields when comparing; ignore interface name case and whitespaces - return self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "") + return ( + (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) + and (self.label == other.label) + and (self.description == other.description) + ) def __hash__(self): - # Ignore some fields when hashing; ignore interface name case and whitespaces return hash(self.name.lower().replace(" ", "")) @dataclass(frozen=True) class ParentTypedComparison(ParentComparison): + """Common fields of a device typed component""" + type: str type_display: str 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) + (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) + and (self.label == other.label) + and (self.description == other.description) + and (self.type == other.type) + ) def __hash__(self): # Ignore some fields when hashing; ignore interface name case and whitespaces @@ -34,28 +43,78 @@ class ParentTypedComparison(ParentComparison): @dataclass(frozen=True) -class UnifiedInterface: +class InterfaceComparison(ParentTypedComparison): """A unified way to represent the interface and interface template""" - id: int - name: str - type: str = "" - type_display: str = "" + mgmt_only: bool 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): - # Ignore some fields when hashing; ignore interface name case and whitespaces - return hash((self.name.lower().replace(" ", ""), self.type)) + (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) + and (self.label == other.label) + and (self.description == other.description) + and (self.type == other.type) + and (self.mgmt_only == other.mgmt_only) + ) @dataclass(frozen=True) -class ComparisonPowerOutlet(ParentTypedComparison): +class FrontPortComparison(ParentTypedComparison): + """A unified way to represent the front port and front port template""" + + color: str + rearports: str + is_template: bool = False + + +@dataclass(frozen=True) +class RearPortComparison(ParentTypedComparison): + """A unified way to represent the rear port and rear port template""" + + color: str + positions: str + is_template: bool = False + + +@dataclass(frozen=True, eq=False) +class ConsolePortComparison(ParentTypedComparison): + """A unified way to represent the consoleport and consoleport template""" + + is_template: bool = False + + +@dataclass(frozen=True, eq=False) +class ConsoleServerPortComparison(ParentTypedComparison): + """A unified way to represent the consoleserverport and consoleserverport template""" + + is_template: bool = False + + +@dataclass(frozen=True) +class PowerPortComparison(ParentTypedComparison): + """A unified way to represent the power port and power port template""" + + maximum_draw: str + allocated_draw: 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.label == other.label) + and (self.description == other.description) + and (self.type == other.type) + and (self.maximum_draw == other.maximum_draw) + and (self.allocated_draw == other.allocated_draw) + ) + + +@dataclass(frozen=True) +class PowerOutletComparison(ParentTypedComparison): + """A unified way to represent the power outlet and power outlet template""" power_port_name: str = "" feed_leg: str = "" @@ -77,3 +136,31 @@ class ComparisonPowerOutlet(ParentTypedComparison): return hash( (self.name.lower().replace(" ", ""), self.type, self.power_port_name) ) + + +@dataclass(frozen=True, eq=False) +class DeviceBayComparison(ParentComparison): + """A unified way to represent the interface and interface template""" + + is_template: bool = False + + +@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 __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/utils.py b/netbox_interface_sync/utils.py index c04bc75..8254c51 100644 --- a/netbox_interface_sync/utils.py +++ b/netbox_interface_sync/utils.py @@ -2,11 +2,12 @@ import re from typing import Iterable from django.shortcuts import render, redirect from django.contrib import messages -from .comparison import UnifiedInterface, ComparisonPowerOutlet +from .comparison import UnifiedInterface + 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 @@ -17,18 +18,19 @@ def natural_keys(c): def human_sorted(iterable: Iterable): return sorted(iterable, key=natural_keys) -def get_components(request, device, components, component_templates): - try: - unified_components = [UnifiedInterface(i.id, i.name, i.type, i.get_type_display()) for i in components] - except AttributeError: - unified_components = [UnifiedInterface(i.id, i.name) for i in components] - try: - unified_component_templates = [ - UnifiedInterface(i.id, i.name, i.type, i.get_type_display(), is_template=True) for i in component_templates] - except AttributeError: - unified_component_templates = [ - UnifiedInterface(i.id, i.name, is_template=True) for i in component_templates] +def get_components(request, device, components, unified_components, unified_component_templates): + # try: + # unified_components = [UnifiedInterface(i.id, i.name, i.type, i.get_type_display()) for i in components] + # except AttributeError: + # unified_components = [UnifiedInterface(i.id, i.name) for i in components] + + # try: + # unified_component_templates = [ + # UnifiedInterface(i.id, i.name, i.type, i.get_type_display(), is_template=True) for i in component_templates] + # except AttributeError: + # unified_component_templates = [ + # UnifiedInterface(i.id, i.name, is_template=True) for i in component_templates] # List of interfaces and interface templates presented in the unified format overall_powers = list(set(unified_component_templates + unified_components)) @@ -38,35 +40,46 @@ def get_components(request, device, components, component_templates): comparison_interfaces = [] for i in overall_powers: try: - comparison_templates.append(unified_component_templates[unified_component_templates.index(i)]) + comparison_templates.append( + unified_component_templates[unified_component_templates.index(i)] + ) except ValueError: comparison_templates.append(None) try: - comparison_interfaces.append(unified_components[unified_components.index(i)]) + comparison_interfaces.append( + unified_components[unified_components.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", + request, + "netbox_interface_sync/interface_comparison.html", { "comparison_items": comparison_items, "templates_count": len(unified_component_templates), "interfaces_count": len(components), - "device": device - } + "device": device, + }, ) -def post_components(request, device, components, component_templates, ObjectType, ObjectTemplateType): + +def post_components( + request, device, components, component_templates, ObjectType, ObjectTemplateType +): # Manually validating interfaces and interface templates lists add_to_device = filter( lambda i: i in component_templates.values_list("id", flat=True), - map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device"))) + map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device"))), ) remove_from_device = filter( lambda i: i in components.values_list("id", flat=True), - map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device"))) + map( + int, + filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device")), + ), ) # Remove selected interfaces from the device and count them @@ -89,26 +102,38 @@ def post_components(request, device, components, component_templates, ObjectType created = len(ObjectType.objects.bulk_create(bulk_create)) # Getting and validating a list of interfaces to rename - fix_name_components = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), components) + fix_name_components = filter( + lambda i: str(i.id) in request.POST.getlist("fix_name"), components + ) # Casting interface templates into UnifiedInterface objects for proper comparison with interfaces for renaming try: unified_component_templates = [ - UnifiedInterface(i.id, i.name, i.type, i.get_type_display()) for i in component_templates] + UnifiedInterface(i.id, i.name, i.type, i.get_type_display()) + for i in component_templates + ] except AttributeError: unified_component_templates = [ - UnifiedInterface(i.id, i.name) for i in component_templates] + UnifiedInterface(i.id, i.name) for i in component_templates + ] # Rename selected interfaces fixed = 0 for component in fix_name_components: try: - unified_component = UnifiedInterface(component.id, component.name, component.type, component.get_type_display()) + unified_component = UnifiedInterface( + component.id, + component.name, + component.type, + component.get_type_display(), + ) except AttributeError: unified_component = UnifiedInterface(component.id, component.name) try: # Try to extract an interface template with the corresponding name - corresponding_template = unified_component_templates[unified_component_templates.index(unified_component)] + corresponding_template = unified_component_templates[ + unified_component_templates.index(unified_component) + ] component.name = corresponding_template.name component.save() fixed += 1 diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py index b553b13..5486fe9 100644 --- a/netbox_interface_sync/views.py +++ b/netbox_interface_sync/views.py @@ -8,7 +8,7 @@ from django.conf import settings from django.contrib import messages from .utils import natural_keys, get_components, post_components -from .comparison import ComparisonPowerOutlet, UnifiedInterface +from .comparison import PowerPortComparison, PowerOutletComparison, InterfaceComparison, ConsolePortComparison, ConsoleServerPortComparison, DeviceBayComparison from .forms import InterfaceComparisonForm config = settings.PLUGINS_CONFIG['netbox_interface_sync'] @@ -25,7 +25,11 @@ class InterfaceComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) interfaces = list(filter(lambda i: not i.is_virtual, interfaces)) interface_templates = InterfaceTemplate.objects.filter(device_type=device.device_type) - return get_components(request, device, interfaces, interface_templates) + unified_interfaces = [InterfaceComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.mgmt_only) for i in interfaces] + unified_interface_templates = [ + InterfaceComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.mgmt_only, is_template=True) for i in interface_templates] + + return get_components(request, device, interfaces, unified_interfaces, unified_interface_templates) def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -47,8 +51,12 @@ class PowerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) powerports = device.powerports.all() powerports_templates = PowerPortTemplate.objects.filter(device_type=device.device_type) + + unified_powerports = [PowerPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.maximum_draw, i.allocated_draw) for i in powerports] + unified_powerport_templates = [ + PowerPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.maximum_draw, i.allocated_draw, is_template=True) for i in powerports_templates] - return get_components(request, device, powerports, powerports_templates) + return get_components(request, device, powerports, unified_powerports, unified_powerport_templates) def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -70,7 +78,11 @@ class ConsolePortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie consoleports = device.consoleports.all() consoleports_templates = ConsolePortTemplate.objects.filter(device_type=device.device_type) - return get_components(request, device, consoleports, consoleports_templates) + unified_consoleports = [ConsolePortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display()) for i in consoleports] + unified_consoleport_templates = [ + ConsolePortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), is_template=True) for i in consoleports_templates] + + return get_components(request, device, consoleports, unified_consoleports, unified_consoleport_templates) def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -92,7 +104,11 @@ class ConsoleServerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixi consoleserverports = device.consoleserverports.all() consoleserverports_templates = ConsoleServerPortTemplate.objects.filter(device_type=device.device_type) - return get_components(request, device, consoleserverports, consoleserverports_templates) + unified_consoleserverports = [ConsoleServerPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display()) for i in consoleserverports] + unified_consoleserverport_templates = [ + ConsoleServerPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), is_template=True) for i in consoleserverports_templates] + + return get_components(request, device, consoleserverports, unified_consoleserverports, unified_consoleserverport_templates) def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -114,38 +130,11 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie poweroutlets = device.poweroutlets.all() poweroutlets_templates = PowerOutletTemplate.objects.filter(device_type=device.device_type) - - unified_components = [ComparisonPowerOutlet(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), power_port_name=PowerPort.objects.get(id=i.power_port_id).name if i.power_port_id is not None else "", feed_leg=i.feed_leg) for i in poweroutlets] - unified_component_templates = [ - ComparisonPowerOutlet(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] + unified_poweroutlets = [PowerOutletComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), power_port_name=PowerPort.objects.get(id=i.power_port_id).name if i.power_port_id is not None else "", feed_leg=i.feed_leg) for i in poweroutlets] + unified_poweroutlet_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] - # List of interfaces and interface templates presented in the unified format - overall_powers = list(set(unified_component_templates + unified_components)) - overall_powers.sort(key=lambda o: natural_keys(o.name)) - - comparison_templates = [] - comparison_interfaces = [] - for i in overall_powers: - try: - comparison_templates.append(unified_component_templates[unified_component_templates.index(i)]) - except ValueError: - comparison_templates.append(None) - - try: - comparison_interfaces.append(unified_components[unified_components.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(unified_component_templates), - "interfaces_count": len(poweroutlets), - "device": device - } - ) + return get_components(request, device, poweroutlets, unified_poweroutlets, unified_poweroutlet_templates) def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -234,12 +223,12 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie # Casting interface templates into UnifiedInterface objects for proper comparison with interfaces for renaming unified_component_templates = [ - ComparisonPowerOutlet(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] + 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 interfaces fixed = 0 for component in fix_name_components: - unified_component = [ComparisonPowerOutlet(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), power_port_name=PowerPort.objects.get(id=i.power_port_id).name if i.power_port_id is not None else "", feed_leg=i.feed_leg) for i in poweroutlets] + unified_component = [PowerOutletComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), power_port_name=PowerPort.objects.get(id=i.power_port_id).name if i.power_port_id is not None else "", feed_leg=i.feed_leg) for i in poweroutlets] try: # Try to extract an interface template with the corresponding name @@ -318,8 +307,12 @@ class DeviceBayComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) devicebays = device.devicebays.all() devicebays_templates = DeviceBayTemplate.objects.filter(device_type=device.device_type) - - return get_components(request, device, devicebays, devicebays_templates) + + unified_devicebays = [DeviceBayComparison(i.id, i.name, i.label, i.description) for i in devicebays] + unified_devicebay_templates = [ + DeviceBayComparison(i.id, i.name, i.label, i.description, is_template=True) for i in devicebays_templates] + + return get_components(request, device, devicebays, unified_devicebays, unified_devicebay_templates) def post(self, request, device_id): form = InterfaceComparisonForm(request.POST)