From 2f104442c321e583bcfb0dcf54258ed9aa693319 Mon Sep 17 00:00:00 2001 From: rizlas Date: Mon, 27 Dec 2021 17:37:56 +0100 Subject: [PATCH 01/11] Added sync for all components. WIP on PowerOutlets. --- .../compare_interfaces_button.html | 68 +++- netbox_interface_sync/urls.py | 41 ++- netbox_interface_sync/utils.py | 132 +++++++- netbox_interface_sync/views.py | 307 ++++++++++++++---- 4 files changed, 484 insertions(+), 64 deletions(-) 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 index dd2c179..e095442 100644 --- a/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html +++ b/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html @@ -1,3 +1,65 @@ - - Interface sync - \ No newline at end of file +{% if perms.dcim.change_device %} + +{% endif %} \ No newline at end of file diff --git a/netbox_interface_sync/urls.py b/netbox_interface_sync/urls.py index cef81f2..b51488f 100644 --- a/netbox_interface_sync/urls.py +++ b/netbox_interface_sync/urls.py @@ -6,5 +6,44 @@ 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("interface-comparison//", views.InterfaceComparisonView.as_view(), name="interface_comparison"), + path( + "interface-comparison//", + views.InterfaceComparisonView.as_view(), + name="interface_comparison", + ), + path( + "powerport-comparison//", + views.PowerPortComparisonView.as_view(), + name="powerport_comparison", + ), + path( + "consoleport-comparison//", + views.ConsolePortComparisonView.as_view(), + name="consoleport_comparison", + ), + path( + "consoleserverport-comparison//", + views.ConsoleServerPortComparisonView.as_view(), + name="consoleserverport_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", + ), ) diff --git a/netbox_interface_sync/utils.py b/netbox_interface_sync/utils.py index 19d7fd1..97f3462 100644 --- a/netbox_interface_sync/utils.py +++ b/netbox_interface_sync/utils.py @@ -1,7 +1,8 @@ -import re +import re, copy from typing import Iterable from dataclasses import dataclass - +from django.shortcuts import render, redirect +from django.contrib import messages def split(s): for x, y in re.findall(r'(\d*)(\D*)', s): @@ -16,14 +17,124 @@ 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] + + # 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(components), + "device": device + } + ) + +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"))) + ) + 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"))) + ) + + # Remove selected interfaces from the device and count them + deleted = ObjectType.objects.filter(id__in=remove_from_device).delete()[0] + + # Add selected interfaces to the device and count them + add_to_device_component = ObjectTemplateType.objects.filter(id__in=add_to_device) + + bulk_create = [] + keys_to_avoid = ["id"] + + for i in add_to_device_component.values(): + tmp = ObjectType() + tmp.device = device + for k in i.keys(): + if k not in keys_to_avoid: + setattr(tmp, k, i[k]) + bulk_create.append(tmp) + + 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) + # 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] + except AttributeError: + unified_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()) + 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)] + component.name = corresponding_template.name + component.save() + fixed += 1 + except ValueError: + pass + + # Generating result message + message = [] + if created > 0: + message.append(f"created {created} interfaces") + if deleted > 0: + message.append(f"deleted {deleted} interfaces") + if fixed > 0: + message.append(f"fixed {fixed} interfaces") + messages.success(request, "; ".join(message).capitalize()) + + return redirect(request.path) + @dataclass(frozen=True) class UnifiedInterface: """A unified way to represent the interface and interface template""" id: int name: str - type: str - type_display: str + type: str = "" + type_display: str = "" is_template: bool = False def __eq__(self, other): @@ -33,3 +144,16 @@ class UnifiedInterface: def __hash__(self): # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(' ', ''), self.type)) + +@dataclass(frozen=True) +class ComparisonPowerOutlet(UnifiedInterface): + + power_port_name: 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) and (self.power_port_name == other.power_port_name) + + def __hash__(self): + # Ignore some fields when hashing; ignore interface name case and whitespaces + return hash((self.name.lower().replace(' ', ''), self.type, self.power_port_name)) \ No newline at end of file diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py index 55adfcd..0d092ad 100644 --- a/netbox_interface_sync/views.py +++ b/netbox_interface_sync/views.py @@ -1,11 +1,13 @@ +from django.db.models.fields.related import ForeignKey from django.shortcuts import get_object_or_404, render, redirect from django.views.generic import View -from dcim.models import Device, Interface, InterfaceTemplate +from dcim.choices import PortTypeChoices +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 LoginRequiredMixin, PermissionRequiredMixin from django.conf import settings from django.contrib import messages -from .utils import UnifiedInterface, natural_keys +from .utils import ComparisonPowerOutlet, UnifiedInterface, natural_keys, get_components, post_components from .forms import InterfaceComparisonForm config = settings.PLUGINS_CONFIG['netbox_interface_sync'] @@ -22,37 +24,7 @@ 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) - 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] - - # 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)) - - 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 - } - ) + return get_components(request, device, interfaces, interface_templates) def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -62,53 +34,276 @@ class InterfaceComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) if config["exclude_virtual_interfaces"]: interfaces = interfaces.exclude(type__in=["virtual", "lag"]) interface_templates = InterfaceTemplate.objects.filter(device_type=device.device_type) + + return post_components(request, device, interfaces, interface_templates, Interface, InterfaceTemplate) +class PowerPortComparisonView(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): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + powerports = device.powerports.all() + powerports_templates = PowerPortTemplate.objects.filter(device_type=device.device_type) + + return get_components(request, device, powerports, powerports_templates) + + 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)) + + powerports = device.powerports.all() + powerports_templates = PowerPortTemplate.objects.filter(device_type=device.device_type) + + return post_components(request, device, powerports, powerports_templates, PowerPort, PowerPortTemplate) + +class ConsolePortComparisonView(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): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + consoleports = device.consoleports.all() + consoleports_templates = ConsolePortTemplate.objects.filter(device_type=device.device_type) + + return get_components(request, device, consoleports, consoleports_templates) + + 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)) + + consoleports = device.consoleports.all() + consoleports_templates = ConsolePortTemplate.objects.filter(device_type=device.device_type) + + return post_components(request, device, consoleports, consoleports_templates, ConsolePort, ConsolePortTemplate) + +class ConsoleServerPortComparisonView(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): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + consoleserverports = device.consoleserverports.all() + consoleserverports_templates = ConsoleServerPortTemplate.objects.filter(device_type=device.device_type) + + return get_components(request, device, consoleserverports, consoleserverports_templates) + + 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)) + + consoleserverports = device.consoleserverports.all() + consoleserverports_templates = ConsoleServerPortTemplate.objects.filter(device_type=device.device_type) + + return post_components(request, device, consoleserverports, consoleserverports_templates, ConsoleServerPort, ConsoleServerPortTemplate) + +class PowerOutletComparisonView(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): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + poweroutlets = device.poweroutlets.all() + poweroutlets_templates = PowerOutletTemplate.objects.filter(device_type=device.device_type) + + unified_components = [ComparisonPowerOutlet(i.id, i.name, 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 "") for i in poweroutlets] + unified_component_templates = [ + ComparisonPowerOutlet(i.id, i.name, i.type, i.get_type_display(), is_template=True, power_port_name=PowerPortTemplate.objects.get(id=i.power_port_id).name if i.power_port_id is not None else "") 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 + } + ) + + 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)) + + poweroutlets = device.poweroutlets.all() + poweroutlets_templates = PowerOutletTemplate.objects.filter(device_type=device.device_type) + + #se il template ha una power port che non ho nel device fisico stop + 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: + matching[i.id] = pp.id + found = True + + if not found: + mismatch = True + break + + if not mismatch: # Manually validating interfaces and interface templates lists + with open("/tmp/ciccio.log", "w") as f: + f.write(str(request.POST.getlist("add_to_device"))) + add_to_device = filter( - lambda i: i in interface_templates.values_list("id", flat=True), + lambda i: i in poweroutlets_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), + lambda i: i in poweroutlets.values_list("id", flat=True), map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device"))) ) # Remove selected interfaces from the device and count them - interfaces_deleted = Interface.objects.filter(id__in=remove_from_device).delete()[0] + deleted = PowerOutlet.objects.filter(id__in=remove_from_device).delete()[0] # 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 - ])) + add_to_device_component = PowerOutletTemplate.objects.filter(id__in=add_to_device) + + bulk_create = [] + keys_to_avoid = ["id"] + + for i in add_to_device_component.values(): + tmp = PowerOutlet() + tmp.device = device + for k in i.keys(): + if k not in keys_to_avoid: + setattr(tmp, k, i[k]) + tmp.power_port_id = matching.get(i["id"], None) + bulk_create.append(tmp) + + created = len(PowerOutlet.objects.bulk_create(bulk_create)) # 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) + fix_name_components = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), poweroutlets) + # 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] + unified_component_templates = [ + ComparisonPowerOutlet(i.id, i.name, i.type, i.get_type_display(), is_template=True, power_port_name=PowerPortTemplate.objects.get(id=i.power_port_id).name if i.power_port_id is not None else "") for i in poweroutlets_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()) + fixed = 0 + for component in fix_name_components: + unified_component = [ComparisonPowerOutlet(i.id, i.name, 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 "") for i in poweroutlets] + try: # Try to extract an interface template with the corresponding name - corresponding_template = unified_interface_templates[unified_interface_templates.index(unified_interface)] - interface.name = corresponding_template.name - interface.save() - interfaces_fixed += 1 + corresponding_template = unified_component_templates[unified_component_templates.index(unified_component)] + component.name = corresponding_template.name + component.save() + fixed += 1 except ValueError: pass # 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") + if created > 0: + message.append(f"created {created} interfaces") + if deleted > 0: + message.append(f"deleted {deleted} interfaces") + if fixed > 0: + message.append(f"fixed {fixed} interfaces") messages.success(request, "; ".join(message).capitalize()) return redirect(request.path) + else: + messages.error(request, "Fai prima le power ports") + return redirect(request.path) + +class FrontPortComparisonView(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): + 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) + + return get_components(request, device, frontports, frontports_templates) + + 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)) + + frontports = device.frontports.all() + frontports_templates = FrontPortTemplate.objects.filter(device_type=device.device_type) + + return post_components(request, device, frontports, frontports_templates, FrontPort, FrontPortTemplate) + +class RearPortComparisonView(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): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + rearports = device.rearports.all() + rearports_templates = RearPortTemplate.objects.filter(device_type=device.device_type) + + return get_components(request, device, rearports, rearports_templates) + + 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)) + + rearports = device.rearports.all() + rearports_templates = RearPortTemplate.objects.filter(device_type=device.device_type) + + return post_components(request, device, rearports, rearports_templates, RearPort, RearPortTemplate) + +class DeviceBayComparisonView(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): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + devicebays = device.devicebays.all() + devicebays_templates = DeviceBayTemplate.objects.filter(device_type=device.device_type) + + return get_components(request, device, devicebays, devicebays_templates) + + 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)) + + devicebays = device.devicebays.all() + devicebays_templates = DeviceBayTemplate.objects.filter(device_type=device.device_type) + + return post_components(request, device, devicebays, devicebays_templates, DeviceBay, DeviceBayTemplate) From b6bdbf9028d542eefce90112b44af7caf0dd69cd Mon Sep 17 00:00:00 2001 From: rizlas Date: Tue, 28 Dec 2021 15:43:30 +0100 Subject: [PATCH 02/11] Comparison classes in different files. Inerithance with a parent comparison class. Check all fields in poweroutlet object for sync. Added update strategy if an object already exists. --- netbox_interface_sync/comparison.py | 79 +++++++++++++++++++++++ netbox_interface_sync/utils.py | 35 +---------- netbox_interface_sync/views.py | 97 ++++++++++++++++++----------- 3 files changed, 141 insertions(+), 70 deletions(-) create mode 100644 netbox_interface_sync/comparison.py diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py new file mode 100644 index 0000000..10cf63a --- /dev/null +++ b/netbox_interface_sync/comparison.py @@ -0,0 +1,79 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ParentComparison: + 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(" ", "") + + 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): + 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) + + def __hash__(self): + # Ignore some fields when hashing; ignore interface name case and whitespaces + return hash((self.name.lower().replace(" ", ""), self.type)) + + +@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)) + + +@dataclass(frozen=True) +class ComparisonPowerOutlet(ParentTypedComparison): + + power_port_name: str = "" + feed_leg: 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.type == other.type) + and (self.power_port_name == other.power_port_name) + and (self.feed_leg == other.feed_leg) + 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(" ", ""), self.type, self.power_port_name) + ) diff --git a/netbox_interface_sync/utils.py b/netbox_interface_sync/utils.py index 97f3462..c04bc75 100644 --- a/netbox_interface_sync/utils.py +++ b/netbox_interface_sync/utils.py @@ -1,8 +1,8 @@ -import re, copy +import re from typing import Iterable -from dataclasses import dataclass from django.shortcuts import render, redirect from django.contrib import messages +from .comparison import UnifiedInterface, ComparisonPowerOutlet def split(s): for x, y in re.findall(r'(\d*)(\D*)', s): @@ -126,34 +126,3 @@ def post_components(request, device, components, component_templates, ObjectType messages.success(request, "; ".join(message).capitalize()) return redirect(request.path) - - -@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)) - -@dataclass(frozen=True) -class ComparisonPowerOutlet(UnifiedInterface): - - power_port_name: 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) and (self.power_port_name == other.power_port_name) - - def __hash__(self): - # Ignore some fields when hashing; ignore interface name case and whitespaces - return hash((self.name.lower().replace(' ', ''), self.type, self.power_port_name)) \ No newline at end of file diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py index 0d092ad..b553b13 100644 --- a/netbox_interface_sync/views.py +++ b/netbox_interface_sync/views.py @@ -7,7 +7,8 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix from django.conf import settings from django.contrib import messages -from .utils import ComparisonPowerOutlet, UnifiedInterface, natural_keys, get_components, post_components +from .utils import natural_keys, get_components, post_components +from .comparison import ComparisonPowerOutlet, UnifiedInterface from .forms import InterfaceComparisonForm config = settings.PLUGINS_CONFIG['netbox_interface_sync'] @@ -113,9 +114,10 @@ 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.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 "") for i in poweroutlets] + + 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.type, i.get_type_display(), is_template=True, power_port_name=PowerPortTemplate.objects.get(id=i.power_port_id).name if i.power_port_id is not None else "") for i in poweroutlets_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] # List of interfaces and interface templates presented in the unified format overall_powers = list(set(unified_component_templates + unified_components)) @@ -153,7 +155,21 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie poweroutlets = device.poweroutlets.all() poweroutlets_templates = PowerOutletTemplate.objects.filter(device_type=device.device_type) - #se il template ha una power port che non ho nel device fisico stop + # Generating result message + message = [] + created = 0 + updated = 0 + fixed = 0 + + remove_from_device = filter( + lambda i: i in poweroutlets.values_list("id", flat=True), + map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device"))) + ) + + # Remove selected interfaces from the device and count them + deleted = PowerOutlet.objects.filter(id__in=remove_from_device).delete()[0] + + # Get device power ports to check dependency between power outlets device_pp = PowerPort.objects.filter(device_id=device.id) matching = {} @@ -164,44 +180,52 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie 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 on power port is found 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 - with open("/tmp/ciccio.log", "w") as f: - f.write(str(request.POST.getlist("add_to_device"))) - + # 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_to_device"))) ) - 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_from_device"))) - ) - - # Remove selected interfaces from the device and count them - deleted = PowerOutlet.objects.filter(id__in=remove_from_device).delete()[0] # Add selected interfaces to the device and count them add_to_device_component = PowerOutletTemplate.objects.filter(id__in=add_to_device) bulk_create = [] + updated = 0 keys_to_avoid = ["id"] for i in add_to_device_component.values(): - tmp = PowerOutlet() - tmp.device = device + to_create = False + + try: + # If power outlets already exists, update and do not recreate + po = device.poweroutlets.get(name=i["name"]) + except PowerOutlet.DoesNotExist: + po = PowerOutlet() + po.device = device + to_create = True + + # Copy all fields from template for k in i.keys(): if k not in keys_to_avoid: - setattr(tmp, k, i[k]) - tmp.power_port_id = matching.get(i["id"], None) - bulk_create.append(tmp) + 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)) @@ -210,13 +234,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.type, i.get_type_display(), is_template=True, power_port_name=PowerPortTemplate.objects.get(id=i.power_port_id).name if i.power_port_id is not None else "") for i in poweroutlets_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] # Rename selected interfaces fixed = 0 for component in fix_name_components: - unified_component = [ComparisonPowerOutlet(i.id, i.name, 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 "") for i in poweroutlets] + 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] try: # Try to extract an interface template with the corresponding name @@ -226,21 +249,21 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie fixed += 1 except ValueError: pass - - # Generating result message - message = [] - if created > 0: - message.append(f"created {created} interfaces") - if deleted > 0: - message.append(f"deleted {deleted} interfaces") - if fixed > 0: - message.append(f"fixed {fixed} interfaces") - messages.success(request, "; ".join(message).capitalize()) - - return redirect(request.path) else: - messages.error(request, "Fai prima le power ports") - return redirect(request.path) + message.append("Dependecy detected, sync power ports first!") + + if created > 0: + message.append(f"created {created} interfaces") + if updated > 0: + message.append(f"updated {updated} interfaces") + if deleted > 0: + message.append(f"deleted {deleted} interfaces") + if fixed > 0: + message.append(f"fixed {fixed} interfaces") + + messages.info(request, "; ".join(message).capitalize()) + + return redirect(request.path) class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): """Comparison of interfaces between a device and a device type and beautiful visualization""" From 80112869e05a57012200d9ef12fd95844bac12dd Mon Sep 17 00:00:00 2001 From: rizlas Date: Tue, 28 Dec 2021 18:32:33 +0100 Subject: [PATCH 03/11] Fix comparison for every get. Changed signature in centralized get --- netbox_interface_sync/comparison.py | 121 ++++++++++++++++++++++++---- netbox_interface_sync/utils.py | 79 +++++++++++------- netbox_interface_sync/views.py | 73 ++++++++--------- 3 files changed, 189 insertions(+), 84 deletions(-) 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) From d1b2b82d1366f417d2b0a1f9e9adf08df31db1df Mon Sep 17 00:00:00 2001 From: rizlas Date: Tue, 28 Dec 2021 20:29:37 +0100 Subject: [PATCH 04/11] Fix interface comparison. Modified global post --- netbox_interface_sync/comparison.py | 25 +-- netbox_interface_sync/utils.py | 66 ++---- netbox_interface_sync/views.py | 319 ++++++++++++++++++---------- 3 files changed, 234 insertions(+), 176 deletions(-) diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py index 8784601..c3ef9ec 100644 --- a/netbox_interface_sync/comparison.py +++ b/netbox_interface_sync/comparison.py @@ -59,6 +59,10 @@ class InterfaceComparison(ParentTypedComparison): and (self.mgmt_only == other.mgmt_only) ) + def __hash__(self): + # Ignore some fields when hashing; ignore interface name case and whitespaces + return hash((self.name.lower().replace(" ", ""), self.type)) + @dataclass(frozen=True) class FrontPortComparison(ParentTypedComparison): @@ -143,24 +147,3 @@ 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 8254c51..0d4186e 100644 --- a/netbox_interface_sync/utils.py +++ b/netbox_interface_sync/utils.py @@ -2,7 +2,7 @@ import re from typing import Iterable from django.shortcuts import render, redirect from django.contrib import messages -from .comparison import UnifiedInterface +from django.core.exceptions import ObjectDoesNotExist def split(s): @@ -20,18 +20,6 @@ def human_sorted(iterable: Iterable): 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)) overall_powers.sort(key=lambda o: natural_keys(o.name)) @@ -67,7 +55,7 @@ def get_components(request, device, components, unified_components, unified_comp def post_components( - request, device, components, component_templates, ObjectType, ObjectTemplateType + request, device, components, component_templates, ObjectType, ObjectTemplateType, unified_component, unified_component_templates ): # Manually validating interfaces and interface templates lists add_to_device = filter( @@ -89,50 +77,38 @@ def post_components( add_to_device_component = ObjectTemplateType.objects.filter(id__in=add_to_device) bulk_create = [] + updated = 0 keys_to_avoid = ["id"] for i in add_to_device_component.values(): - tmp = ObjectType() - tmp.device = device + to_create = False + + try: + tmp = components.get(name=i["name"]) + except ObjectDoesNotExist: + tmp = ObjectType() + tmp.device = device + to_create = True + for k in i.keys(): if k not in keys_to_avoid: setattr(tmp, k, i[k]) - bulk_create.append(tmp) + + if to_create: + bulk_create.append(tmp) + else: + tmp.save() + updated += 1 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 - ) - # 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 - ] - except AttributeError: - unified_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(), - ) - except AttributeError: - unified_component = UnifiedInterface(component.id, component.name) - + for component, component_comparison in unified_component: try: # Try to extract an interface template with the corresponding name corresponding_template = unified_component_templates[ - unified_component_templates.index(unified_component) + unified_component_templates.index(component_comparison) ] component.name = corresponding_template.name component.save() @@ -144,6 +120,8 @@ def post_components( message = [] if created > 0: message.append(f"created {created} interfaces") + if updated > 0: + message.append(f"updated {updated} interfaces") if deleted > 0: message.append(f"deleted {deleted} interfaces") if fixed > 0: diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py index 5486fe9..5fd2ec4 100644 --- a/netbox_interface_sync/views.py +++ b/netbox_interface_sync/views.py @@ -39,8 +39,28 @@ class InterfaceComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) if config["exclude_virtual_interfaces"]: interfaces = interfaces.exclude(type__in=["virtual", "lag"]) interface_templates = InterfaceTemplate.objects.filter(device_type=device.device_type) + + # Getting and validating a list of interfaces to rename + fix_name_components = filter( + lambda i: str(i.id) in request.POST.getlist("fix_name"), 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 post_components(request, device, interfaces, interface_templates, Interface, InterfaceTemplate) + unified_interfaces = [] + + for component in fix_name_components: + unified_interfaces.append((component, InterfaceComparison( + component.id, + component.name, + component.label, + component.description, + component.type, + component.get_type_display(), + component.mgmt_only))) + + return post_components(request, device, interfaces, interface_templates, Interface, InterfaceTemplate, unified_interfaces, unified_interface_templates) class PowerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): """Comparison of interfaces between a device and a device type and beautiful visualization""" @@ -65,8 +85,29 @@ class PowerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) powerports = device.powerports.all() powerports_templates = PowerPortTemplate.objects.filter(device_type=device.device_type) + + # Getting and validating a list of interfaces to rename + fix_name_components = filter( + lambda i: str(i.id) in request.POST.getlist("fix_name"), 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] + + unified_powerports = [] + + for component in fix_name_components: + unified_powerports.append((component, PowerPortComparison( + component.id, + component.name, + component.label, + component.description, + component.type, + component.get_type_display(), + component.maximum_draw, + component.allocated_draw))) - return post_components(request, device, powerports, powerports_templates, PowerPort, PowerPortTemplate) + return post_components(request, device, powerports, powerports_templates, PowerPort, PowerPortTemplate, unified_powerports, unified_powerport_templates) class ConsolePortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): """Comparison of interfaces between a device and a device type and beautiful visualization""" @@ -91,8 +132,27 @@ class ConsolePortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie consoleports = device.consoleports.all() consoleports_templates = ConsolePortTemplate.objects.filter(device_type=device.device_type) + + # Getting and validating a list of interfaces to rename + fix_name_components = filter( + lambda i: str(i.id) in request.POST.getlist("fix_name"), 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] + + unified_consoleports = [] + + for component in fix_name_components: + unified_consoleports.append((component, ConsolePortComparison( + component.id, + component.name, + component.label, + component.description, + component.type, + component.get_type_display()))) - return post_components(request, device, consoleports, consoleports_templates, ConsolePort, ConsolePortTemplate) + return post_components(request, device, consoleports, consoleports_templates, ConsolePort, ConsolePortTemplate, unified_consoleports, unified_consoleport_templates) class ConsoleServerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): """Comparison of interfaces between a device and a device type and beautiful visualization""" @@ -117,8 +177,27 @@ class ConsoleServerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixi consoleserverports = device.consoleserverports.all() consoleserverports_templates = ConsoleServerPortTemplate.objects.filter(device_type=device.device_type) + + # Getting and validating a list of interfaces to rename + fix_name_components = filter( + lambda i: str(i.id) in request.POST.getlist("fix_name"), 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] + + unified_consoleserverports = [] + + for component in fix_name_components: + unified_consoleserverports.append((component, ConsoleServerPortComparison( + component.id, + component.name, + component.label, + component.description, + component.type, + component.get_type_display()))) - return post_components(request, device, consoleserverports, consoleserverports_templates, ConsoleServerPort, ConsoleServerPortTemplate) + return post_components(request, device, consoleserverports, consoleserverports_templates, ConsoleServerPort, ConsoleServerPortTemplate, unified_consoleserverports, unified_consoleserverport_templates) class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): """Comparison of interfaces between a device and a device type and beautiful visualization""" @@ -141,118 +220,118 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) - poweroutlets = device.poweroutlets.all() - poweroutlets_templates = PowerOutletTemplate.objects.filter(device_type=device.device_type) + poweroutlets = device.poweroutlets.all() + poweroutlets_templates = PowerOutletTemplate.objects.filter(device_type=device.device_type) - # Generating result message - message = [] - created = 0 - updated = 0 - fixed = 0 - - remove_from_device = filter( - lambda i: i in poweroutlets.values_list("id", flat=True), - map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device"))) - ) - - # Remove selected interfaces from the device and count them - deleted = PowerOutlet.objects.filter(id__in=remove_from_device).delete()[0] - - # Get device power ports to check dependency between power outlets - device_pp = PowerPort.objects.filter(device_id=device.id) - - matching = {} - mismatch = False - for i in poweroutlets_templates: - found = False - if i.power_port_id is not None: - ppt = PowerPortTemplate.objects.get(id=i.power_port_id) - for pp in device_pp: - if pp.name == ppt.name: - # Save matching to add the correct power port later - matching[i.id] = pp.id - found = True - - # If at least on power port is found 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_to_device"))) + # Generating result message + message = [] + created = 0 + updated = 0 + fixed = 0 + + remove_from_device = filter( + lambda i: i in poweroutlets.values_list("id", flat=True), + map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device"))) ) - # Add selected interfaces 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 + deleted = PowerOutlet.objects.filter(id__in=remove_from_device).delete()[0] - bulk_create = [] - updated = 0 - keys_to_avoid = ["id"] + # Get device power ports to check dependency between power outlets + device_pp = PowerPort.objects.filter(device_id=device.id) - for i in add_to_device_component.values(): - to_create = False + 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 on power port is found 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_to_device"))) + ) - 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 + # Add selected interfaces to the device and count them + add_to_device_component = PowerOutletTemplate.objects.filter(id__in=add_to_device) - # 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) + bulk_create = [] + updated = 0 + keys_to_avoid = ["id"] - if to_create: - bulk_create.append(po) - else: - po.save() - updated += 1 + for i in add_to_device_component.values(): + to_create = False - created = len(PowerOutlet.objects.bulk_create(bulk_create)) + 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 - # Getting and validating a list of interfaces to rename - fix_name_components = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), poweroutlets) + # 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) - # Casting interface templates into UnifiedInterface objects for proper comparison with interfaces 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] + if to_create: + bulk_create.append(po) + else: + po.save() + updated += 1 - # Rename selected interfaces - fixed = 0 - for component in fix_name_components: - 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] + created = len(PowerOutlet.objects.bulk_create(bulk_create)) - try: - # Try to extract an interface template with the corresponding name - corresponding_template = unified_component_templates[unified_component_templates.index(unified_component)] - component.name = corresponding_template.name - component.save() - fixed += 1 - except ValueError: - pass - else: - message.append("Dependecy detected, sync power ports first!") + # Getting and validating a list of interfaces to rename + fix_name_components = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), poweroutlets) - if created > 0: - message.append(f"created {created} interfaces") - if updated > 0: - message.append(f"updated {updated} interfaces") - if deleted > 0: - message.append(f"deleted {deleted} interfaces") - if fixed > 0: - message.append(f"fixed {fixed} interfaces") + # Casting interface templates into UnifiedInterface objects for proper comparison with interfaces 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] - messages.info(request, "; ".join(message).capitalize()) + # Rename selected interfaces + fixed = 0 + for component in fix_name_components: + 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] - return redirect(request.path) + try: + # Try to extract an interface template with the corresponding name + corresponding_template = unified_component_templates[unified_component_templates.index(unified_component)] + component.name = corresponding_template.name + component.save() + fixed += 1 + except ValueError: + pass + else: + message.append("Dependecy detected, sync power ports first!") + + if created > 0: + message.append(f"created {created} interfaces") + if updated > 0: + message.append(f"updated {updated} interfaces") + if deleted > 0: + message.append(f"deleted {deleted} interfaces") + if fixed > 0: + message.append(f"fixed {fixed} interfaces") + + messages.info(request, "; ".join(message).capitalize()) + + return redirect(request.path) class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): """Comparison of interfaces between a device and a device type and beautiful visualization""" @@ -271,10 +350,10 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) 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) - - return post_components(request, device, frontports, frontports_templates, FrontPort, FrontPortTemplate) + frontports = device.frontports.all() + frontports_templates = FrontPortTemplate.objects.filter(device_type=device.device_type) + + return post_components(request, device, frontports, frontports_templates, FrontPort, FrontPortTemplate) class RearPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): """Comparison of interfaces between a device and a device type and beautiful visualization""" @@ -293,10 +372,10 @@ class RearPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) - rearports = device.rearports.all() - rearports_templates = RearPortTemplate.objects.filter(device_type=device.device_type) - - return post_components(request, device, rearports, rearports_templates, RearPort, RearPortTemplate) + rearports = device.rearports.all() + rearports_templates = RearPortTemplate.objects.filter(device_type=device.device_type) + + return post_components(request, device, rearports, rearports_templates, RearPort, RearPortTemplate) class DeviceBayComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): """Comparison of interfaces between a device and a device type and beautiful visualization""" @@ -319,7 +398,25 @@ class DeviceBayComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) - devicebays = device.devicebays.all() - devicebays_templates = DeviceBayTemplate.objects.filter(device_type=device.device_type) + devicebays = device.devicebays.all() + devicebays_templates = DeviceBayTemplate.objects.filter(device_type=device.device_type) + + # Getting and validating a list of devicebays to rename + fix_name_components = filter( + lambda i: str(i.id) in request.POST.getlist("fix_name"), devicebays + ) + + unified_devicebay_templates = [ + DeviceBayComparison(i.id, i.name, i.label, i.description, is_template=True) for i in devicebays_templates] - return post_components(request, device, devicebays, devicebays_templates, DeviceBay, DeviceBayTemplate) + unified_devicebays = [] + + for component in fix_name_components: + unified_devicebays.append((component, DeviceBayComparison( + component.id, + component.name, + component.label, + component.description + ))) + + return post_components(request, device, devicebays, devicebays_templates, DeviceBay, DeviceBayTemplate, unified_devicebays, unified_devicebay_templates) From 836d8504014ba8e8e3e6cb52f469321ad0971266 Mon Sep 17 00:00:00 2001 From: rizlas Date: Tue, 11 Jan 2022 13:47:43 +0100 Subject: [PATCH 05/11] Added front ports and rear ports sync --- netbox_interface_sync/comparison.py | 35 +++++- netbox_interface_sync/views.py | 181 +++++++++++++++++++++++++--- 2 files changed, 196 insertions(+), 20 deletions(-) diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py index c3ef9ec..41d420c 100644 --- a/netbox_interface_sync/comparison.py +++ b/netbox_interface_sync/comparison.py @@ -69,18 +69,49 @@ class FrontPortComparison(ParentTypedComparison): """A unified way to represent the front port and front port template""" color: str - rearports: str + # rear_port_id: int + rear_port_position: int 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.color == other.color) + and (self.rear_port_position == other.rear_port_position) + ) + + def __hash__(self): + # Ignore some fields when hashing; ignore interface name case and whitespaces + return hash((self.name.lower().replace(" ", ""), self.type)) + @dataclass(frozen=True) class RearPortComparison(ParentTypedComparison): """A unified way to represent the rear port and rear port template""" color: str - positions: str + positions: int 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.color == other.color) + and (self.positions == other.positions) + ) + + def __hash__(self): + # Ignore some fields when hashing; ignore interface name case and whitespaces + return hash((self.name.lower().replace(" ", ""), self.type)) + @dataclass(frozen=True, eq=False) class ConsolePortComparison(ParentTypedComparison): diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py index 5fd2ec4..59ff5aa 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 PowerPortComparison, PowerOutletComparison, InterfaceComparison, ConsolePortComparison, ConsoleServerPortComparison, DeviceBayComparison +from .comparison import FrontPortComparison, PowerPortComparison, PowerOutletComparison, InterfaceComparison, ConsolePortComparison, ConsoleServerPortComparison, DeviceBayComparison, RearPortComparison from .forms import InterfaceComparisonForm config = settings.PLUGINS_CONFIG['netbox_interface_sync'] @@ -252,20 +252,19 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie matching[i.id] = pp.id found = True - # If at least on power port is found there is a dependency + # 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 + if not mismatch: add_to_device = filter( lambda i: i in poweroutlets_templates.values_list("id", flat=True), map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device"))) ) - # Add selected interfaces to the device and count them + # Add selected component to the device and count them add_to_device_component = PowerOutletTemplate.objects.filter(id__in=add_to_device) bulk_create = [] @@ -297,21 +296,21 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie created = len(PowerOutlet.objects.bulk_create(bulk_create)) - # Getting and validating a list of interfaces to rename + # 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 interface templates into UnifiedInterface objects for proper comparison with interfaces for renaming + # Casting component templates into UnifiedInterface 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 interfaces fixed = 0 for component in fix_name_components: - 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] + 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 an interface template with the corresponding name - corresponding_template = unified_component_templates[unified_component_templates.index(unified_component)] + # 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 @@ -321,13 +320,13 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie message.append("Dependecy detected, sync power ports first!") if created > 0: - message.append(f"created {created} interfaces") + message.append(f"created {created} power outlet") if updated > 0: - message.append(f"updated {updated} interfaces") + message.append(f"updated {updated} power outlet") if deleted > 0: - message.append(f"deleted {deleted} interfaces") + message.append(f"deleted {deleted} power outlet") if fixed > 0: - message.append(f"fixed {fixed} interfaces") + message.append(f"fixed {fixed} power outlet") messages.info(request, "; ".join(message).capitalize()) @@ -338,12 +337,17 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) permission_required = ("dcim.view_interface", "dcim.add_interface", "dcim.change_interface", "dcim.delete_interface") 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) - return get_components(request, device, frontports, frontports_templates) + 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 = InterfaceComparisonForm(request.POST) @@ -352,8 +356,114 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) frontports = device.frontports.all() frontports_templates = FrontPortTemplate.objects.filter(device_type=device.device_type) - - return post_components(request, device, frontports, frontports_templates, FrontPort, FrontPortTemplate) + + # 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 interfaces from the device and count them + deleted = FrontPort.objects.filter(id__in=remove_from_device).delete()[0] + + # Get device power ports to check dependency between power outlets + 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 power port later + matching[i.id] = rp.id + found = True + + # If at least one power port is not found in device there is a dependency + # Better not to sync at all + if not found: + mismatch = True + break + + if not mismatch: + add_to_device = filter( + lambda i: i in 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"] + + for i in add_to_device_component.values(): + to_create = False + + try: + # If power outlets 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 UnifiedInterface 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 interfaces + 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: + message.append("Dependecy 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) class RearPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): """Comparison of interfaces between a device and a device type and beautiful visualization""" @@ -365,9 +475,44 @@ class RearPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): rearports = device.rearports.all() rearports_templates = RearPortTemplate.objects.filter(device_type=device.device_type) - return get_components(request, device, rearports, rearports_templates) + unified_rearports = [RearPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color, i.positions) for i in rearports] + unified_rearports_templates = [ + RearPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color, i.positions, is_template=True) for i in rearports_templates] + + return get_components(request, device, rearports, unified_rearports, unified_rearports_templates) 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)) + + rearports = device.rearports.all() + rearports_templates = RearPortTemplate.objects.filter(device_type=device.device_type) + + # Getting and validating a list of interfaces to rename + fix_name_components = filter( + lambda i: str(i.id) in request.POST.getlist("fix_name"), rearports + ) + + unified_rearports_templates = [ + RearPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color, i.positions, is_template=True) for i in rearports_templates] + + unified_rearports = [] + + for component in fix_name_components: + unified_rearports.append((component, RearPortComparison( + component.id, + component.name, + component.label, + component.description, + component.type, + component.get_type_display(), + component.color, + component.positions))) + + return post_components(request, device, rearports, rearports_templates, RearPort, RearPortTemplate, unified_rearports, unified_rearports_templates) + + form = InterfaceComparisonForm(request.POST) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) From 00cedbd589bd6add97d9acde7531541f3aa5e693 Mon Sep 17 00:00:00 2001 From: rizlas Date: Tue, 11 Jan 2022 14:09:46 +0100 Subject: [PATCH 06/11] Minor fixes --- netbox_interface_sync/comparison.py | 4 ++++ netbox_interface_sync/views.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py index 41d420c..ec0ce61 100644 --- a/netbox_interface_sync/comparison.py +++ b/netbox_interface_sync/comparison.py @@ -146,6 +146,10 @@ class PowerPortComparison(ParentTypedComparison): and (self.allocated_draw == other.allocated_draw) ) + def __hash__(self): + # Ignore some fields when hashing; ignore interface name case and whitespaces + return hash((self.name.lower().replace(" ", ""), self.type)) + @dataclass(frozen=True) class PowerOutletComparison(ParentTypedComparison): diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py index 59ff5aa..6c0b03a 100644 --- a/netbox_interface_sync/views.py +++ b/netbox_interface_sync/views.py @@ -317,7 +317,7 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie except ValueError: pass else: - message.append("Dependecy detected, sync power ports first!") + messages.error(request, "Dependecy detected, sync power ports first!") if created > 0: message.append(f"created {created} power outlet") @@ -450,7 +450,7 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) except ValueError: pass else: - message.append("Dependecy detected, sync rear ports first!") + messages.error(request, "Dependecy detected, sync power ports first!") if created > 0: message.append(f"created {created} front ports") From 729218f5770a0ff03db0f973854ffd2b93f362d7 Mon Sep 17 00:00:00 2001 From: rizlas Date: Tue, 11 Jan 2022 16:21:11 +0100 Subject: [PATCH 07/11] Added possibility to exclude interfaces panel --- netbox_interface_sync/__init__.py | 3 ++- .../number_of_interfaces_panel.html | 20 ++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/netbox_interface_sync/__init__.py b/netbox_interface_sync/__init__.py index 51b9b7a..eed32d5 100644 --- a/netbox_interface_sync/__init__.py +++ b/netbox_interface_sync/__init__.py @@ -9,7 +9,8 @@ class Config(PluginConfig): author = 'Victor Golovanenko' author_email = 'drygdryg2014@yandex.ru' default_settings = { - 'exclude_virtual_interfaces': True + 'exclude_virtual_interfaces': True, + 'include_interfaces_panel': False } 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 98bc1ef..8d9b2ad 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,10 +1,12 @@ -
-
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 }} +{% 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 }} +
-
\ No newline at end of file +{% endif %} \ No newline at end of file From 861eff8a6111bd9711b7bc0809cf04da6d14f114 Mon Sep 17 00:00:00 2001 From: rizlas Date: Wed, 12 Jan 2022 12:59:05 +0100 Subject: [PATCH 08/11] Styling rendered table with bootstrap 4. Removed the double for loop. Added str method to comparison classes. --- netbox_interface_sync/comparison.py | 21 +++ netbox_interface_sync/template_content.py | 2 +- ...on.html => compare_components_button.html} | 0 .../components_comparison.html | 159 +++++++++++++++++ .../interface_comparison.html | 161 ------------------ netbox_interface_sync/utils.py | 17 +- netbox_interface_sync/views.py | 16 +- 7 files changed, 198 insertions(+), 178 deletions(-) rename netbox_interface_sync/templates/netbox_interface_sync/{compare_interfaces_button.html => compare_components_button.html} (100%) create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html delete mode 100644 netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py index ec0ce61..af12877 100644 --- a/netbox_interface_sync/comparison.py +++ b/netbox_interface_sync/comparison.py @@ -20,6 +20,9 @@ class ParentComparison: def __hash__(self): return hash(self.name.lower().replace(" ", "")) + def __str__(self): + return f"Label: {self.label}\nDescription: {self.description}" + @dataclass(frozen=True) class ParentTypedComparison(ParentComparison): @@ -41,6 +44,9 @@ class ParentTypedComparison(ParentComparison): # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(" ", ""), self.type)) + def __str__(self): + return f"{super().__str__()}\nType: {self.type_display}" + @dataclass(frozen=True) class InterfaceComparison(ParentTypedComparison): @@ -63,6 +69,9 @@ class InterfaceComparison(ParentTypedComparison): # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(" ", ""), self.type)) + def __str__(self): + return f"{super().__str__()}\nManagement only: {self.mgmt_only}" + @dataclass(frozen=True) class FrontPortComparison(ParentTypedComparison): @@ -88,6 +97,9 @@ class FrontPortComparison(ParentTypedComparison): # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(" ", ""), self.type)) + def __str__(self): + return f"{super().__str__()}\nColor: {self.color}\nPosition: {self.rear_port_position}" + @dataclass(frozen=True) class RearPortComparison(ParentTypedComparison): @@ -112,6 +124,9 @@ class RearPortComparison(ParentTypedComparison): # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(" ", ""), self.type)) + def __str__(self): + return f"{super().__str__()}\nColor: {self.color}\nPositions: {self.positions}" + @dataclass(frozen=True, eq=False) class ConsolePortComparison(ParentTypedComparison): @@ -150,6 +165,9 @@ class PowerPortComparison(ParentTypedComparison): # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(" ", ""), self.type)) + def __str__(self): + return f"{super().__str__()}\nMaximum draw: {self.maximum_draw}\nAllocated draw: {self.allocated_draw}" + @dataclass(frozen=True) class PowerOutletComparison(ParentTypedComparison): @@ -176,6 +194,9 @@ class PowerOutletComparison(ParentTypedComparison): (self.name.lower().replace(" ", ""), self.type, self.power_port_name) ) + def __str__(self): + return f"{super().__str__()}\nPower port name: {self.power_port_name}\nFeed leg: {self.feed_leg}" + @dataclass(frozen=True, eq=False) class DeviceBayComparison(ParentComparison): diff --git a/netbox_interface_sync/template_content.py b/netbox_interface_sync/template_content.py index be5c5a4..8bda0d7 100644 --- a/netbox_interface_sync/template_content.py +++ b/netbox_interface_sync/template_content.py @@ -8,7 +8,7 @@ class DeviceViewExtension(PluginTemplateExtension): def buttons(self): """Implements a compare interfaces button at the top of the page""" obj = self.context['object'] - return self.render("netbox_interface_sync/compare_interfaces_button.html", extra_context={ + return self.render("netbox_interface_sync/compare_components_button.html", extra_context={ "device": obj }) diff --git a/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html b/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html similarity index 100% rename from netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html rename to netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html diff --git a/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html b/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html new file mode 100644 index 0000000..f7668ae --- /dev/null +++ b/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html @@ -0,0 +1,159 @@ +{% extends 'base/layout.html' %} + +{% block title %}{{ device }} - {{ component_type }} 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 }}. + + The device and device type have different number of {{ component_type|lower }}.
+ 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 }} + {% if not component %} + + {% endif %} +     + {% if component_template and component_template.name != component.name %} + {{ component.name }} + {% else %} + {{ component.name }} + {% endif %} + {{ component }} + {% if not component_template %} + + {% endif %} + + {% if component_template and component_template.name != component.name %} + + {% 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 deleted file mode 100644 index 4befb9d..0000000 --- a/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html +++ /dev/null @@ -1,161 +0,0 @@ -{% 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 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/utils.py b/netbox_interface_sync/utils.py index 0d4186e..739a40c 100644 --- a/netbox_interface_sync/utils.py +++ b/netbox_interface_sync/utils.py @@ -19,13 +19,13 @@ def human_sorted(iterable: Iterable): return sorted(iterable, key=natural_keys) -def get_components(request, device, components, unified_components, unified_component_templates): - # List of interfaces and interface templates presented in the unified format +def get_components(request, device, components, unified_components, unified_component_templates, component_type): + # List of components and components 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 = [] + comparison_components = [] for i in overall_powers: try: comparison_templates.append( @@ -35,20 +35,21 @@ def get_components(request, device, components, unified_components, unified_comp comparison_templates.append(None) try: - comparison_interfaces.append( + comparison_components.append( unified_components[unified_components.index(i)] ) except ValueError: - comparison_interfaces.append(None) + comparison_components.append(None) - comparison_items = list(zip(comparison_templates, comparison_interfaces)) + comparison_items = list(zip(comparison_templates, comparison_components)) return render( request, - "netbox_interface_sync/interface_comparison.html", + "netbox_interface_sync/components_comparison.html", { + "component_type": component_type, "comparison_items": comparison_items, "templates_count": len(unified_component_templates), - "interfaces_count": len(components), + "components_count": len(components), "device": device, }, ) diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py index 6c0b03a..f2256e6 100644 --- a/netbox_interface_sync/views.py +++ b/netbox_interface_sync/views.py @@ -29,7 +29,7 @@ class InterfaceComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) 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) + return get_components(request, device, interfaces, unified_interfaces, unified_interface_templates, "Interfaces") def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -76,7 +76,7 @@ class PowerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) 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, unified_powerports, unified_powerport_templates) + return get_components(request, device, powerports, unified_powerports, unified_powerport_templates, "Power ports") def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -123,7 +123,7 @@ class ConsolePortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie 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) + return get_components(request, device, consoleports, unified_consoleports, unified_consoleport_templates, "Console ports") def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -168,7 +168,7 @@ class ConsoleServerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixi 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) + return get_components(request, device, consoleserverports, unified_consoleserverports, unified_consoleserverport_templates, "Console server ports") def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -213,7 +213,7 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie 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] - return get_components(request, device, poweroutlets, unified_poweroutlets, unified_poweroutlet_templates) + return get_components(request, device, poweroutlets, unified_poweroutlets, unified_poweroutlet_templates, "Power outlets") def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -347,7 +347,7 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) 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) + return get_components(request, device, frontports, unified_frontports, unified_frontports_templates, "Front ports") def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -479,7 +479,7 @@ class RearPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): unified_rearports_templates = [ RearPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color, i.positions, is_template=True) for i in rearports_templates] - return get_components(request, device, rearports, unified_rearports, unified_rearports_templates) + return get_components(request, device, rearports, unified_rearports, unified_rearports_templates, "Rear ports") def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) @@ -536,7 +536,7 @@ class DeviceBayComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) 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) + return get_components(request, device, devicebays, unified_devicebays, unified_devicebay_templates, "Device bays") def post(self, request, device_id): form = InterfaceComparisonForm(request.POST) From e50d8e86334f378366c1105dc0613f2f69d105ae Mon Sep 17 00:00:00 2001 From: rizlas Date: Wed, 12 Jan 2022 14:36:55 +0100 Subject: [PATCH 09/11] Clean and renaming --- netbox_interface_sync/comparison.py | 16 +-- netbox_interface_sync/forms.py | 2 +- netbox_interface_sync/template_content.py | 2 +- .../components_comparison.html | 1 - netbox_interface_sync/utils.py | 20 ++-- netbox_interface_sync/views.py | 104 +++++++++--------- 6 files changed, 66 insertions(+), 79 deletions(-) diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py index af12877..b0896d1 100644 --- a/netbox_interface_sync/comparison.py +++ b/netbox_interface_sync/comparison.py @@ -11,6 +11,7 @@ class ParentComparison: description: str def __eq__(self, other): + # Ignore some fields when comparing; ignore component name case and whitespaces return ( (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) and (self.label == other.label) @@ -18,6 +19,7 @@ class ParentComparison: ) def __hash__(self): + # Ignore some fields when hashing; ignore component name case and whitespaces return hash(self.name.lower().replace(" ", "")) def __str__(self): @@ -32,7 +34,6 @@ class ParentTypedComparison(ParentComparison): 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.label == other.label) @@ -41,7 +42,6 @@ class ParentTypedComparison(ParentComparison): ) def __hash__(self): - # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(" ", ""), self.type)) def __str__(self): @@ -56,7 +56,6 @@ class InterfaceComparison(ParentTypedComparison): 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) @@ -66,7 +65,6 @@ class InterfaceComparison(ParentTypedComparison): ) def __hash__(self): - # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(" ", ""), self.type)) def __str__(self): @@ -83,7 +81,6 @@ class FrontPortComparison(ParentTypedComparison): 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) @@ -94,7 +91,6 @@ class FrontPortComparison(ParentTypedComparison): ) def __hash__(self): - # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(" ", ""), self.type)) def __str__(self): @@ -110,7 +106,6 @@ class RearPortComparison(ParentTypedComparison): 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) @@ -121,7 +116,6 @@ class RearPortComparison(ParentTypedComparison): ) def __hash__(self): - # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(" ", ""), self.type)) def __str__(self): @@ -151,7 +145,6 @@ class PowerPortComparison(ParentTypedComparison): 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) @@ -162,7 +155,6 @@ class PowerPortComparison(ParentTypedComparison): ) def __hash__(self): - # Ignore some fields when hashing; ignore interface name case and whitespaces return hash((self.name.lower().replace(" ", ""), self.type)) def __str__(self): @@ -178,7 +170,6 @@ class PowerOutletComparison(ParentTypedComparison): 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) @@ -189,7 +180,6 @@ class PowerOutletComparison(ParentTypedComparison): ) def __hash__(self): - # Ignore some fields when hashing; ignore interface name case and whitespaces return hash( (self.name.lower().replace(" ", ""), self.type, self.power_port_name) ) @@ -200,6 +190,6 @@ class PowerOutletComparison(ParentTypedComparison): @dataclass(frozen=True, eq=False) class DeviceBayComparison(ParentComparison): - """A unified way to represent the interface and interface template""" + """A unified way to represent the device bay and device bay template""" is_template: bool = False diff --git a/netbox_interface_sync/forms.py b/netbox_interface_sync/forms.py index fcdf5a5..45a39db 100644 --- a/netbox_interface_sync/forms.py +++ b/netbox_interface_sync/forms.py @@ -1,6 +1,6 @@ from django import forms -class InterfaceComparisonForm(forms.Form): +class ComponentComparisonForm(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 8bda0d7..4e95d3c 100644 --- a/netbox_interface_sync/template_content.py +++ b/netbox_interface_sync/template_content.py @@ -6,7 +6,7 @@ class DeviceViewExtension(PluginTemplateExtension): model = "dcim.device" def buttons(self): - """Implements a compare interfaces button at the top of the page""" + """Implements a compare button at the top of the page""" obj = self.context['object'] return self.render("netbox_interface_sync/compare_components_button.html", extra_context={ "device": obj diff --git a/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html b/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html index f7668ae..2276463 100644 --- a/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html +++ b/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html @@ -42,7 +42,6 @@ function uncheck(event) {
- {% csrf_token %}
diff --git a/netbox_interface_sync/utils.py b/netbox_interface_sync/utils.py index 739a40c..2073c37 100644 --- a/netbox_interface_sync/utils.py +++ b/netbox_interface_sync/utils.py @@ -56,9 +56,9 @@ def get_components(request, device, components, unified_components, unified_comp def post_components( - request, device, components, component_templates, ObjectType, ObjectTemplateType, unified_component, unified_component_templates + request, device, components, component_templates, ObjectType, ObjectTemplateType, unified_component, unified_component_templates, component_type ): - # Manually validating interfaces and interface templates lists + # Manually validating components and component 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"))), @@ -71,10 +71,10 @@ def post_components( ), ) - # Remove selected interfaces from the device and count them + # Remove selected component from the device and count them deleted = ObjectType.objects.filter(id__in=remove_from_device).delete()[0] - # Add selected interfaces to the device and count them + # Add selected components to the device and count them add_to_device_component = ObjectTemplateType.objects.filter(id__in=add_to_device) bulk_create = [] @@ -103,11 +103,11 @@ def post_components( created = len(ObjectType.objects.bulk_create(bulk_create)) - # Rename selected interfaces + # Rename selected components fixed = 0 for component, component_comparison in unified_component: try: - # Try to extract an interface template with the corresponding name + # Try to extract a component template with the corresponding name corresponding_template = unified_component_templates[ unified_component_templates.index(component_comparison) ] @@ -120,13 +120,13 @@ def post_components( # Generating result message message = [] if created > 0: - message.append(f"created {created} interfaces") + message.append(f"created {created} {component_type}") if updated > 0: - message.append(f"updated {updated} interfaces") + message.append(f"updated {updated} {component_type}") if deleted > 0: - message.append(f"deleted {deleted} interfaces") + message.append(f"deleted {deleted} {component_type}") if fixed > 0: - message.append(f"fixed {fixed} interfaces") + message.append(f"fixed {fixed} {component_type}") messages.success(request, "; ".join(message).capitalize()) return redirect(request.path) diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py index f2256e6..a093062 100644 --- a/netbox_interface_sync/views.py +++ b/netbox_interface_sync/views.py @@ -1,15 +1,13 @@ -from django.db.models.fields.related import ForeignKey -from django.shortcuts import get_object_or_404, render, redirect +from django.shortcuts import get_object_or_404, redirect from django.views.generic import View -from dcim.choices import PortTypeChoices 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 LoginRequiredMixin, PermissionRequiredMixin from django.conf import settings from django.contrib import messages -from .utils import natural_keys, get_components, post_components +from .utils import get_components, post_components from .comparison import FrontPortComparison, PowerPortComparison, PowerOutletComparison, InterfaceComparison, ConsolePortComparison, ConsoleServerPortComparison, DeviceBayComparison, RearPortComparison -from .forms import InterfaceComparisonForm +from .forms import ComponentComparisonForm config = settings.PLUGINS_CONFIG['netbox_interface_sync'] @@ -32,7 +30,7 @@ class InterfaceComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) return get_components(request, device, interfaces, unified_interfaces, unified_interface_templates, "Interfaces") def post(self, request, device_id): - form = InterfaceComparisonForm(request.POST) + form = ComponentComparisonForm(request.POST) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) interfaces = device.vc_interfaces() @@ -60,11 +58,11 @@ class InterfaceComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) component.get_type_display(), component.mgmt_only))) - return post_components(request, device, interfaces, interface_templates, Interface, InterfaceTemplate, unified_interfaces, unified_interface_templates) + return post_components(request, device, interfaces, interface_templates, Interface, InterfaceTemplate, unified_interfaces, unified_interface_templates, "interfaces") class PowerPortComparisonView(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") + """Comparison of power ports between a device and a device type and beautiful visualization""" + permission_required = ("dcim.view_powerport", "dcim.add_powerport", "dcim.change_powerport", "dcim.delete_powerport") def get(self, request, device_id): device = get_object_or_404(Device.objects.filter(id=device_id)) @@ -79,14 +77,14 @@ class PowerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) return get_components(request, device, powerports, unified_powerports, unified_powerport_templates, "Power ports") def post(self, request, device_id): - form = InterfaceComparisonForm(request.POST) + form = ComponentComparisonForm(request.POST) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) powerports = device.powerports.all() powerports_templates = PowerPortTemplate.objects.filter(device_type=device.device_type) - # Getting and validating a list of interfaces to rename + # Getting and validating a list of power ports to rename fix_name_components = filter( lambda i: str(i.id) in request.POST.getlist("fix_name"), powerports ) @@ -107,11 +105,11 @@ class PowerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) component.maximum_draw, component.allocated_draw))) - return post_components(request, device, powerports, powerports_templates, PowerPort, PowerPortTemplate, unified_powerports, unified_powerport_templates) + return post_components(request, device, powerports, powerports_templates, PowerPort, PowerPortTemplate, unified_powerports, unified_powerport_templates, "power ports") class ConsolePortComparisonView(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") + """Comparison of console ports between a device and a device type and beautiful visualization""" + permission_required = ("dcim.view_consoleport", "dcim.add_consoleport", "dcim.change_consoleport", "dcim.delete_consoleport") def get(self, request, device_id): device = get_object_or_404(Device.objects.filter(id=device_id)) @@ -126,14 +124,14 @@ class ConsolePortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie return get_components(request, device, consoleports, unified_consoleports, unified_consoleport_templates, "Console ports") def post(self, request, device_id): - form = InterfaceComparisonForm(request.POST) + form = ComponentComparisonForm(request.POST) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) consoleports = device.consoleports.all() consoleports_templates = ConsolePortTemplate.objects.filter(device_type=device.device_type) - # Getting and validating a list of interfaces to rename + # Getting and validating a list of console ports to rename fix_name_components = filter( lambda i: str(i.id) in request.POST.getlist("fix_name"), consoleports ) @@ -152,11 +150,11 @@ class ConsolePortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie component.type, component.get_type_display()))) - return post_components(request, device, consoleports, consoleports_templates, ConsolePort, ConsolePortTemplate, unified_consoleports, unified_consoleport_templates) + return post_components(request, device, consoleports, consoleports_templates, ConsolePort, ConsolePortTemplate, unified_consoleports, unified_consoleport_templates, "console ports") class ConsoleServerPortComparisonView(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") + """Comparison of console server ports between a device and a device type and beautiful visualization""" + permission_required = ("dcim.view_consoleserverport", "dcim.add_consoleserverport", "dcim.change_consoleserverport", "dcim.delete_consoleserverport") def get(self, request, device_id): device = get_object_or_404(Device.objects.filter(id=device_id)) @@ -171,14 +169,14 @@ class ConsoleServerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixi return get_components(request, device, consoleserverports, unified_consoleserverports, unified_consoleserverport_templates, "Console server ports") def post(self, request, device_id): - form = InterfaceComparisonForm(request.POST) + form = ComponentComparisonForm(request.POST) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) consoleserverports = device.consoleserverports.all() consoleserverports_templates = ConsoleServerPortTemplate.objects.filter(device_type=device.device_type) - # Getting and validating a list of interfaces to rename + # Getting and validating a list of console server ports to rename fix_name_components = filter( lambda i: str(i.id) in request.POST.getlist("fix_name"), consoleserverports ) @@ -197,11 +195,11 @@ class ConsoleServerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixi component.type, component.get_type_display()))) - return post_components(request, device, consoleserverports, consoleserverports_templates, ConsoleServerPort, ConsoleServerPortTemplate, unified_consoleserverports, unified_consoleserverport_templates) + return post_components(request, device, consoleserverports, consoleserverports_templates, ConsoleServerPort, ConsoleServerPortTemplate, unified_consoleserverports, unified_consoleserverport_templates, "console server ports") class PowerOutletComparisonView(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") + """Comparison of power outlets between a device and a device type and beautiful visualization""" + permission_required = ("dcim.view_poweroutlet", "dcim.add_poweroutlet", "dcim.change_poweroutlet", "dcim.delete_poweroutlet") def get(self, request, device_id): device = get_object_or_404(Device.objects.filter(id=device_id)) @@ -216,7 +214,7 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie return get_components(request, device, poweroutlets, unified_poweroutlets, unified_poweroutlet_templates, "Power outlets") def post(self, request, device_id): - form = InterfaceComparisonForm(request.POST) + form = ComponentComparisonForm(request.POST) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) @@ -234,7 +232,7 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device"))) ) - # Remove selected interfaces from the device and count them + # Remove selected power outlets from the device and count them deleted = PowerOutlet.objects.filter(id__in=remove_from_device).delete()[0] # Get device power ports to check dependency between power outlets @@ -299,11 +297,11 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie # 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 UnifiedInterface objects for proper comparison with component for renaming + # 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 interfaces + # 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) @@ -320,21 +318,21 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie messages.error(request, "Dependecy detected, sync power ports first!") if created > 0: - message.append(f"created {created} power outlet") + message.append(f"created {created} power outlets") if updated > 0: - message.append(f"updated {updated} power outlet") + message.append(f"updated {updated} power outlets") if deleted > 0: - message.append(f"deleted {deleted} power outlet") + message.append(f"deleted {deleted} power outlets") if fixed > 0: - message.append(f"fixed {fixed} power outlet") + message.append(f"fixed {fixed} power outlets") messages.info(request, "; ".join(message).capitalize()) return redirect(request.path) class FrontPortComparisonView(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") + """Comparison of front ports between a device and a device type and beautiful visualization""" + permission_required = ("dcim.view_frontport", "dcim.add_frontport", "dcim.change_frontport", "dcim.delete_frontport") def get(self, request, device_id): @@ -350,7 +348,7 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) return get_components(request, device, frontports, unified_frontports, unified_frontports_templates, "Front ports") def post(self, request, device_id): - form = InterfaceComparisonForm(request.POST) + form = ComponentComparisonForm(request.POST) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) @@ -368,10 +366,10 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device"))) ) - # Remove selected interfaces from the device and count them + # Remove selected front ports from the device and count them deleted = FrontPort.objects.filter(id__in=remove_from_device).delete()[0] - # Get device power ports to check dependency between power outlets + # Get device rear ports to check dependency between front ports device_rp = RearPort.objects.filter(device_id=device.id) matching = {} @@ -382,11 +380,11 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) 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 power port later + # Save matching to add the correct rear port later matching[i.id] = rp.id found = True - # If at least one power port is not found in device there is a dependency + # 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 @@ -409,7 +407,7 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) to_create = False try: - # If power outlets already exists, update and do not recreate + # If fron port already exists, update and do not recreate fp = device.frontports.get(name=i["name"]) except FrontPort.DoesNotExist: fp = FrontPort() @@ -433,10 +431,10 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) # 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 UnifiedInterface objects for proper comparison with component for renaming + # 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 interfaces + # 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) @@ -450,7 +448,7 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) except ValueError: pass else: - messages.error(request, "Dependecy detected, sync power ports first!") + messages.error(request, "Dependecy detected, sync rear ports first!") if created > 0: message.append(f"created {created} front ports") @@ -466,8 +464,8 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) return redirect(request.path) class RearPortComparisonView(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") + """Comparison of rear ports between a device and a device type and beautiful visualization""" + permission_required = ("dcim.view_rearport", "dcim.add_rearport", "dcim.change_rearport", "dcim.delete_rearport") def get(self, request, device_id): device = get_object_or_404(Device.objects.filter(id=device_id)) @@ -482,14 +480,14 @@ class RearPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): return get_components(request, device, rearports, unified_rearports, unified_rearports_templates, "Rear ports") def post(self, request, device_id): - form = InterfaceComparisonForm(request.POST) + form = ComponentComparisonForm(request.POST) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) rearports = device.rearports.all() rearports_templates = RearPortTemplate.objects.filter(device_type=device.device_type) - # Getting and validating a list of interfaces to rename + # Getting and validating a list of rear ports to rename fix_name_components = filter( lambda i: str(i.id) in request.POST.getlist("fix_name"), rearports ) @@ -510,10 +508,10 @@ class RearPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): component.color, component.positions))) - return post_components(request, device, rearports, rearports_templates, RearPort, RearPortTemplate, unified_rearports, unified_rearports_templates) + return post_components(request, device, rearports, rearports_templates, RearPort, RearPortTemplate, unified_rearports, unified_rearports_templates, "rear ports") - form = InterfaceComparisonForm(request.POST) + form = ComponentComparisonForm(request.POST) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) @@ -523,8 +521,8 @@ class RearPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): return post_components(request, device, rearports, rearports_templates, RearPort, RearPortTemplate) class DeviceBayComparisonView(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") + """Comparison of device bays between a device and a device type and beautiful visualization""" + permission_required = ("dcim.view_devicebay", "dcim.add_devicebay", "dcim.change_devicebay", "dcim.delete_devicebay") def get(self, request, device_id): device = get_object_or_404(Device.objects.filter(id=device_id)) @@ -539,7 +537,7 @@ class DeviceBayComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) return get_components(request, device, devicebays, unified_devicebays, unified_devicebay_templates, "Device bays") def post(self, request, device_id): - form = InterfaceComparisonForm(request.POST) + form = ComponentComparisonForm(request.POST) if form.is_valid(): device = get_object_or_404(Device.objects.filter(id=device_id)) @@ -564,4 +562,4 @@ class DeviceBayComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) component.description ))) - return post_components(request, device, devicebays, devicebays_templates, DeviceBay, DeviceBayTemplate, unified_devicebays, unified_devicebay_templates) + return post_components(request, device, devicebays, devicebays_templates, DeviceBay, DeviceBayTemplate, unified_devicebays, unified_devicebay_templates, "device bays") From 1bb75cfe69e30fa4956ed0ffef303b96ee5c8b6e Mon Sep 17 00:00:00 2001 From: rizlas Date: Tue, 18 Jan 2022 17:56:57 +0100 Subject: [PATCH 10/11] Possibility to choose if description is used during diff and also if it should be synced in device --- netbox_interface_sync/__init__.py | 6 ++- netbox_interface_sync/comparison.py | 64 +++++++++++++++++++++-------- netbox_interface_sync/utils.py | 6 +++ netbox_interface_sync/views.py | 6 +++ 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/netbox_interface_sync/__init__.py b/netbox_interface_sync/__init__.py index eed32d5..23ec7a1 100644 --- a/netbox_interface_sync/__init__.py +++ b/netbox_interface_sync/__init__.py @@ -10,7 +10,11 @@ class Config(PluginConfig): author_email = 'drygdryg2014@yandex.ru' default_settings = { 'exclude_virtual_interfaces': True, - 'include_interfaces_panel': False + 'include_interfaces_panel': False, + # Compare description during diff + 'compare_description': False, + # Sync or not description from device type + 'exclude_description': False } diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py index b0896d1..32aae37 100644 --- a/netbox_interface_sync/comparison.py +++ b/netbox_interface_sync/comparison.py @@ -1,4 +1,7 @@ from dataclasses import dataclass +from django.conf import settings + +config = settings.PLUGINS_CONFIG["netbox_interface_sync"] @dataclass(frozen=True) @@ -12,11 +15,14 @@ class ParentComparison: def __eq__(self, other): # Ignore some fields when comparing; ignore component name case and whitespaces - return ( - (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) - and (self.label == other.label) - and (self.description == other.description) - ) + eq = ( + self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "") + ) and (self.label == other.label) + + if config["compare_description"]: + eq = eq and (self.description == other.description) + + return eq def __hash__(self): # Ignore some fields when hashing; ignore component name case and whitespaces @@ -34,13 +40,17 @@ class ParentTypedComparison(ParentComparison): type_display: str def __eq__(self, other): - return ( + eq = ( (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) and (self.label == other.label) - and (self.description == other.description) and (self.type == other.type) ) + if config["compare_description"]: + eq = eq and (self.description == other.description) + + return eq + def __hash__(self): return hash((self.name.lower().replace(" ", ""), self.type)) @@ -56,14 +66,18 @@ class InterfaceComparison(ParentTypedComparison): is_template: bool = False def __eq__(self, other): - return ( + eq = ( (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) ) + if config["compare_description"]: + eq = eq and (self.description == other.description) + + return eq + def __hash__(self): return hash((self.name.lower().replace(" ", ""), self.type)) @@ -81,15 +95,19 @@ class FrontPortComparison(ParentTypedComparison): is_template: bool = False def __eq__(self, other): - return ( + eq = ( (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.color == other.color) and (self.rear_port_position == other.rear_port_position) ) + if config["compare_description"]: + eq = eq and (self.description == other.description) + + return eq + def __hash__(self): return hash((self.name.lower().replace(" ", ""), self.type)) @@ -106,15 +124,19 @@ class RearPortComparison(ParentTypedComparison): is_template: bool = False def __eq__(self, other): - return ( + eq = ( (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.color == other.color) and (self.positions == other.positions) ) + if config["compare_description"]: + eq = eq and (self.description == other.description) + + return eq + def __hash__(self): return hash((self.name.lower().replace(" ", ""), self.type)) @@ -145,15 +167,19 @@ class PowerPortComparison(ParentTypedComparison): is_template: bool = False def __eq__(self, other): - return ( + eq = ( (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) ) + if config["compare_description"]: + eq = eq and (self.description == other.description) + + return eq + def __hash__(self): return hash((self.name.lower().replace(" ", ""), self.type)) @@ -170,15 +196,19 @@ class PowerOutletComparison(ParentTypedComparison): is_template: bool = False def __eq__(self, other): - return ( + eq = ( (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) and (self.label == other.label) and (self.type == other.type) and (self.power_port_name == other.power_port_name) and (self.feed_leg == other.feed_leg) - and (self.description == other.description) ) + if config["compare_description"]: + eq = eq and (self.description == other.description) + + return eq + def __hash__(self): return hash( (self.name.lower().replace(" ", ""), self.type, self.power_port_name) diff --git a/netbox_interface_sync/utils.py b/netbox_interface_sync/utils.py index 2073c37..d46ab9e 100644 --- a/netbox_interface_sync/utils.py +++ b/netbox_interface_sync/utils.py @@ -3,6 +3,9 @@ from typing import Iterable from django.shortcuts import render, redirect from django.contrib import messages from django.core.exceptions import ObjectDoesNotExist +from django.conf import settings + +config = settings.PLUGINS_CONFIG['netbox_interface_sync'] def split(s): @@ -81,6 +84,9 @@ def post_components( updated = 0 keys_to_avoid = ["id"] + if config["exclude_description"]: + keys_to_avoid.append("description") + for i in add_to_device_component.values(): to_create = False diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py index a093062..0762bb8 100644 --- a/netbox_interface_sync/views.py +++ b/netbox_interface_sync/views.py @@ -269,6 +269,9 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie updated = 0 keys_to_avoid = ["id"] + if config["exclude_description"]: + keys_to_avoid.append("description") + for i in add_to_device_component.values(): to_create = False @@ -403,6 +406,9 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) updated = 0 keys_to_avoid = ["id"] + if config["exclude_description"]: + keys_to_avoid.append("description") + for i in add_to_device_component.values(): to_create = False From e5573f9a8c61748ad91e348e61d8ac22bfeb42a3 Mon Sep 17 00:00:00 2001 From: rizlas Date: Tue, 18 Jan 2022 18:26:15 +0100 Subject: [PATCH 11/11] Only compare description config --- netbox_interface_sync/__init__.py | 6 +++--- netbox_interface_sync/utils.py | 2 +- netbox_interface_sync/views.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/netbox_interface_sync/__init__.py b/netbox_interface_sync/__init__.py index 23ec7a1..de916a5 100644 --- a/netbox_interface_sync/__init__.py +++ b/netbox_interface_sync/__init__.py @@ -12,9 +12,9 @@ class Config(PluginConfig): 'exclude_virtual_interfaces': True, 'include_interfaces_panel': False, # Compare description during diff - 'compare_description': False, - # Sync or not description from device type - 'exclude_description': False + # If compare is true, description will also be synced to device + # Otherwise not. + 'compare_description': True } diff --git a/netbox_interface_sync/utils.py b/netbox_interface_sync/utils.py index d46ab9e..31d53f4 100644 --- a/netbox_interface_sync/utils.py +++ b/netbox_interface_sync/utils.py @@ -84,7 +84,7 @@ def post_components( updated = 0 keys_to_avoid = ["id"] - if config["exclude_description"]: + if not config["compare_description"]: keys_to_avoid.append("description") for i in add_to_device_component.values(): diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py index 0762bb8..427bc54 100644 --- a/netbox_interface_sync/views.py +++ b/netbox_interface_sync/views.py @@ -269,7 +269,7 @@ class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, Vie updated = 0 keys_to_avoid = ["id"] - if config["exclude_description"]: + if not config["compare_description"]: keys_to_avoid.append("description") for i in add_to_device_component.values(): @@ -406,7 +406,7 @@ class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) updated = 0 keys_to_avoid = ["id"] - if config["exclude_description"]: + if not config["compare_description"]: keys_to_avoid.append("description") for i in add_to_device_component.values():