diff --git a/netbox_interface_sync/__init__.py b/netbox_interface_sync/__init__.py index 51b9b7a..de916a5 100644 --- a/netbox_interface_sync/__init__.py +++ b/netbox_interface_sync/__init__.py @@ -9,7 +9,12 @@ 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, + # Compare description during diff + # If compare is true, description will also be synced to device + # Otherwise not. + 'compare_description': True } diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py new file mode 100644 index 0000000..32aae37 --- /dev/null +++ b/netbox_interface_sync/comparison.py @@ -0,0 +1,225 @@ +from dataclasses import dataclass +from django.conf import settings + +config = settings.PLUGINS_CONFIG["netbox_interface_sync"] + + +@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 component name case and whitespaces + 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 + return hash(self.name.lower().replace(" ", "")) + + def __str__(self): + return f"Label: {self.label}\nDescription: {self.description}" + + +@dataclass(frozen=True) +class ParentTypedComparison(ParentComparison): + """Common fields of a device typed component""" + + type: str + type_display: str + + def __eq__(self, other): + eq = ( + (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) + and (self.label == other.label) + 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)) + + def __str__(self): + return f"{super().__str__()}\nType: {self.type_display}" + + +@dataclass(frozen=True) +class InterfaceComparison(ParentTypedComparison): + """A unified way to represent the interface and interface template""" + + mgmt_only: bool + is_template: bool = False + + def __eq__(self, other): + eq = ( + (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) + and (self.label == other.label) + 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)) + + def __str__(self): + return f"{super().__str__()}\nManagement only: {self.mgmt_only}" + + +@dataclass(frozen=True) +class FrontPortComparison(ParentTypedComparison): + """A unified way to represent the front port and front port template""" + + color: str + # rear_port_id: int + rear_port_position: int + is_template: bool = False + + def __eq__(self, other): + eq = ( + (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) + and (self.label == other.label) + 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)) + + def __str__(self): + return f"{super().__str__()}\nColor: {self.color}\nPosition: {self.rear_port_position}" + + +@dataclass(frozen=True) +class RearPortComparison(ParentTypedComparison): + """A unified way to represent the rear port and rear port template""" + + color: str + positions: int + is_template: bool = False + + def __eq__(self, other): + eq = ( + (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) + and (self.label == other.label) + 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)) + + def __str__(self): + return f"{super().__str__()}\nColor: {self.color}\nPositions: {self.positions}" + + +@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): + eq = ( + (self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")) + and (self.label == other.label) + 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)) + + def __str__(self): + return f"{super().__str__()}\nMaximum draw: {self.maximum_draw}\nAllocated draw: {self.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 = "" + is_template: bool = False + + def __eq__(self, other): + 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) + ) + + 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) + ) + + 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): + """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 be5c5a4..4e95d3c 100644 --- a/netbox_interface_sync/template_content.py +++ b/netbox_interface_sync/template_content.py @@ -6,9 +6,9 @@ class DeviceViewExtension(PluginTemplateExtension): model = "dcim.device" def buttons(self): - """Implements a compare 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_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_components_button.html b/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html new file mode 100644 index 0000000..e095442 --- /dev/null +++ b/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html @@ -0,0 +1,65 @@ +{% if perms.dcim.change_device %} + +{% endif %} \ No newline at end of file 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 deleted file mode 100644 index dd2c179..0000000 --- a/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html +++ /dev/null @@ -1,3 +0,0 @@ - - Interface sync - \ No newline at end of file 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..2276463 --- /dev/null +++ b/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html @@ -0,0 +1,158 @@ +{% 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/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 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..31d53f4 100644 --- a/netbox_interface_sync/utils.py +++ b/netbox_interface_sync/utils.py @@ -1,11 +1,16 @@ import re from typing import Iterable -from dataclasses import dataclass +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): - 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,19 +22,117 @@ def human_sorted(iterable: Iterable): return sorted(iterable, key=natural_keys) -@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 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)) - 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) + comparison_templates = [] + comparison_components = [] + for i in overall_powers: + try: + comparison_templates.append( + unified_component_templates[unified_component_templates.index(i)] + ) + except ValueError: + comparison_templates.append(None) - def __hash__(self): - # Ignore some fields when hashing; ignore interface name case and whitespaces - return hash((self.name.lower().replace(' ', ''), self.type)) + try: + comparison_components.append( + unified_components[unified_components.index(i)] + ) + except ValueError: + comparison_components.append(None) + + comparison_items = list(zip(comparison_templates, comparison_components)) + return render( + request, + "netbox_interface_sync/components_comparison.html", + { + "component_type": component_type, + "comparison_items": comparison_items, + "templates_count": len(unified_component_templates), + "components_count": len(components), + "device": device, + }, + ) + + +def post_components( + request, device, components, component_templates, ObjectType, ObjectTemplateType, unified_component, unified_component_templates, component_type +): + # 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"))), + ) + 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 component from the device and count them + deleted = ObjectType.objects.filter(id__in=remove_from_device).delete()[0] + + # Add selected components to the device and count them + add_to_device_component = ObjectTemplateType.objects.filter(id__in=add_to_device) + + bulk_create = [] + updated = 0 + keys_to_avoid = ["id"] + + if not config["compare_description"]: + keys_to_avoid.append("description") + + for i in add_to_device_component.values(): + to_create = False + + try: + 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]) + + if to_create: + bulk_create.append(tmp) + else: + tmp.save() + updated += 1 + + created = len(ObjectType.objects.bulk_create(bulk_create)) + + # Rename selected components + fixed = 0 + for component, component_comparison in unified_component: + try: + # Try to extract a component template with the corresponding name + corresponding_template = unified_component_templates[ + unified_component_templates.index(component_comparison) + ] + component.name = corresponding_template.name + component.save() + fixed += 1 + except ValueError: + pass + + # Generating result message + message = [] + if created > 0: + message.append(f"created {created} {component_type}") + if updated > 0: + message.append(f"updated {updated} {component_type}") + if deleted > 0: + message.append(f"deleted {deleted} {component_type}") + if fixed > 0: + 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 55adfcd..427bc54 100644 --- a/netbox_interface_sync/views.py +++ b/netbox_interface_sync/views.py @@ -1,12 +1,13 @@ -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.models import Device, Interface, InterfaceTemplate +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 .forms import InterfaceComparisonForm +from .utils import get_components, post_components +from .comparison import FrontPortComparison, PowerPortComparison, PowerOutletComparison, InterfaceComparison, ConsolePortComparison, ConsoleServerPortComparison, DeviceBayComparison, RearPortComparison +from .forms import ComponentComparisonForm config = settings.PLUGINS_CONFIG['netbox_interface_sync'] @@ -22,40 +23,14 @@ 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_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 = [ - UnifiedInterface(i.id, i.name, i.type, i.get_type_display(), is_template=True) for i in 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] - # 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, 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() @@ -63,52 +38,534 @@ class InterfaceComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View) interfaces = interfaces.exclude(type__in=["virtual", "lag"]) interface_templates = InterfaceTemplate.objects.filter(device_type=device.device_type) - # Manually validating interfaces and interface templates lists - add_to_device = filter( - lambda i: i in interface_templates.values_list("id", flat=True), - map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device"))) - ) - remove_from_device = filter( - lambda i: i in interfaces.values_list("id", flat=True), - map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device"))) - ) - - # Remove selected interfaces from the device and count them - interfaces_deleted = Interface.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 - ])) - # Getting and validating a list of interfaces to rename - fix_name_interfaces = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), interfaces) - # Casting interface templates into UnifiedInterface objects for proper comparison with interfaces for renaming - unified_interface_templates = [ - UnifiedInterface(i.id, i.name, i.type, i.get_type_display()) for i in interface_templates] + fix_name_components = filter( + lambda i: str(i.id) in request.POST.getlist("fix_name"), interfaces + ) - # Rename selected interfaces - interfaces_fixed = 0 - for interface in fix_name_interfaces: - unified_interface = UnifiedInterface(interface.id, interface.name, interface.type, interface.get_type_display()) - try: - # Try to extract an interface template with the corresponding name - corresponding_template = unified_interface_templates[unified_interface_templates.index(unified_interface)] - interface.name = corresponding_template.name - interface.save() - interfaces_fixed += 1 - except ValueError: - pass + 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] + + 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, "interfaces") + +class PowerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): + """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)) + + 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, unified_powerports, unified_powerport_templates, "Power ports") + + def post(self, request, device_id): + form = ComponentComparisonForm(request.POST) + if form.is_valid(): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + powerports = device.powerports.all() + powerports_templates = PowerPortTemplate.objects.filter(device_type=device.device_type) + + # 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 + ) + + 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, unified_powerports, unified_powerport_templates, "power ports") + +class ConsolePortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): + """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)) + + consoleports = device.consoleports.all() + consoleports_templates = ConsolePortTemplate.objects.filter(device_type=device.device_type) + + 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, "Console ports") + + def post(self, request, device_id): + form = ComponentComparisonForm(request.POST) + if form.is_valid(): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + consoleports = device.consoleports.all() + consoleports_templates = ConsolePortTemplate.objects.filter(device_type=device.device_type) + + # 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 + ) + + 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, unified_consoleports, unified_consoleport_templates, "console ports") + +class ConsoleServerPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): + """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)) + + consoleserverports = device.consoleserverports.all() + consoleserverports_templates = ConsoleServerPortTemplate.objects.filter(device_type=device.device_type) + + 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, "Console server ports") + + def post(self, request, device_id): + form = ComponentComparisonForm(request.POST) + if form.is_valid(): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + consoleserverports = device.consoleserverports.all() + consoleserverports_templates = ConsoleServerPortTemplate.objects.filter(device_type=device.device_type) + + # 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 + ) + + 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, unified_consoleserverports, unified_consoleserverport_templates, "console server ports") + +class PowerOutletComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): + """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)) + + poweroutlets = device.poweroutlets.all() + poweroutlets_templates = PowerOutletTemplate.objects.filter(device_type=device.device_type) + + 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] + + return get_components(request, device, poweroutlets, unified_poweroutlets, unified_poweroutlet_templates, "Power outlets") + + def post(self, request, device_id): + form = ComponentComparisonForm(request.POST) + if form.is_valid(): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + poweroutlets = device.poweroutlets.all() + poweroutlets_templates = PowerOutletTemplate.objects.filter(device_type=device.device_type) # Generating result message message = [] - if interfaces_created > 0: - message.append(f"created {interfaces_created} interfaces") - if interfaces_deleted > 0: - message.append(f"deleted {interfaces_deleted} interfaces") - if interfaces_fixed > 0: - message.append(f"fixed {interfaces_fixed} interfaces") - messages.success(request, "; ".join(message).capitalize()) + 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 power outlets from the device and count them + deleted = PowerOutlet.objects.filter(id__in=remove_from_device).delete()[0] + + # Get device power ports to check dependency between power outlets + device_pp = PowerPort.objects.filter(device_id=device.id) + + matching = {} + mismatch = False + for i in poweroutlets_templates: + found = False + if i.power_port_id is not None: + ppt = PowerPortTemplate.objects.get(id=i.power_port_id) + for pp in device_pp: + if pp.name == ppt.name: + # Save matching to add the correct power port later + matching[i.id] = pp.id + found = True + + # If at least one power port is not found in device there is a dependency + # Better not to sync at all + if not found: + mismatch = True + break + + if not mismatch: + add_to_device = filter( + lambda i: i in poweroutlets_templates.values_list("id", flat=True), + map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device"))) + ) + + # Add selected component to the device and count them + add_to_device_component = PowerOutletTemplate.objects.filter(id__in=add_to_device) + + bulk_create = [] + updated = 0 + keys_to_avoid = ["id"] + + if not config["compare_description"]: + keys_to_avoid.append("description") + + for i in add_to_device_component.values(): + to_create = False + + try: + # If power outlets already exists, update and do not recreate + po = device.poweroutlets.get(name=i["name"]) + except PowerOutlet.DoesNotExist: + po = PowerOutlet() + po.device = device + to_create = True + + # Copy all fields from template + for k in i.keys(): + if k not in keys_to_avoid: + setattr(po, k, i[k]) + po.power_port_id = matching.get(i["id"], None) + + if to_create: + bulk_create.append(po) + else: + po.save() + updated += 1 + + created = len(PowerOutlet.objects.bulk_create(bulk_create)) + + # Getting and validating a list of components to rename + fix_name_components = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), poweroutlets) + + # Casting component templates into Unified objects for proper comparison with component for renaming + unified_component_templates = [ + PowerOutletComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), power_port_name=PowerPortTemplate.objects.get(id=i.power_port_id).name if i.power_port_id is not None else "", feed_leg=i.feed_leg, is_template=True) for i in poweroutlets_templates] + + # Rename selected power outlets + fixed = 0 + for component in fix_name_components: + unified_poweroutlet = PowerOutletComparison(component.id, component.name, component.label, component.description, component.type, component.get_type_display(), power_port_name=PowerPort.objects.get(id=component.power_port_id).name if component.power_port_id is not None else "", feed_leg=component.feed_leg) + + try: + # Try to extract a component template with the corresponding name + corresponding_template = unified_component_templates[unified_component_templates.index(unified_poweroutlet)] + component.name = corresponding_template.name + component.save() + fixed += 1 + except ValueError: + pass + else: + messages.error(request, "Dependecy detected, sync power ports first!") + + if created > 0: + message.append(f"created {created} power outlets") + if updated > 0: + message.append(f"updated {updated} power outlets") + if deleted > 0: + message.append(f"deleted {deleted} power outlets") + if fixed > 0: + message.append(f"fixed {fixed} power outlets") + + messages.info(request, "; ".join(message).capitalize()) return redirect(request.path) + +class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): + """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): + + device = get_object_or_404(Device.objects.filter(id=device_id)) + + frontports = device.frontports.all() + frontports_templates = FrontPortTemplate.objects.filter(device_type=device.device_type) + + unified_frontports = [FrontPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color, i.rear_port_position) for i in frontports] + unified_frontports_templates = [ + FrontPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color, i.rear_port_position, is_template=True) for i in frontports_templates] + + return get_components(request, device, frontports, unified_frontports, unified_frontports_templates, "Front ports") + + def post(self, request, device_id): + form = ComponentComparisonForm(request.POST) + if form.is_valid(): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + frontports = device.frontports.all() + frontports_templates = FrontPortTemplate.objects.filter(device_type=device.device_type) + + # Generating result message + message = [] + created = 0 + updated = 0 + fixed = 0 + + remove_from_device = filter( + lambda i: i in frontports.values_list("id", flat=True), + map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device"))) + ) + + # Remove selected front ports from the device and count them + deleted = FrontPort.objects.filter(id__in=remove_from_device).delete()[0] + + # Get device rear ports to check dependency between front ports + device_rp = RearPort.objects.filter(device_id=device.id) + + matching = {} + mismatch = False + for i in frontports_templates: + found = False + if i.rear_port_id is not None: + rpt = RearPortTemplate.objects.get(id=i.rear_port_id) + for rp in device_rp: + if rp.name == rpt.name: + # Save matching to add the correct rear port later + matching[i.id] = rp.id + found = True + + # If at least one rear port is not found in device there is a dependency + # Better not to sync at all + if not found: + mismatch = True + break + + if not mismatch: + add_to_device = filter( + lambda i: i in frontports_templates.values_list("id", flat=True), + map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device"))) + ) + + # Add selected component to the device and count them + add_to_device_component = FrontPortTemplate.objects.filter(id__in=add_to_device) + + bulk_create = [] + updated = 0 + keys_to_avoid = ["id"] + + if not config["compare_description"]: + keys_to_avoid.append("description") + + for i in add_to_device_component.values(): + to_create = False + + try: + # If fron port already exists, update and do not recreate + fp = device.frontports.get(name=i["name"]) + except FrontPort.DoesNotExist: + fp = FrontPort() + fp.device = device + to_create = True + + # Copy all fields from template + for k in i.keys(): + if k not in keys_to_avoid: + setattr(fp, k, i[k]) + fp.rear_port_id = matching.get(i["id"], None) + + if to_create: + bulk_create.append(fp) + else: + fp.save() + updated += 1 + + created = len(FrontPort.objects.bulk_create(bulk_create)) + + # Getting and validating a list of components to rename + fix_name_components = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), frontports) + + # Casting component templates into Unified objects for proper comparison with component for renaming + unified_frontports_templates = [ + FrontPortComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color, i.rear_port_position, is_template=True) for i in frontports_templates] + # Rename selected front ports + fixed = 0 + for component in fix_name_components: + unified_frontport = FrontPortComparison(component.id, component.name, component.label, component.description, component.type, component.get_type_display(), component.color, component.rear_port_position) + + try: + # Try to extract a component template with the corresponding name + corresponding_template = unified_frontports_templates[unified_frontports_templates.index(unified_frontport)] + component.name = corresponding_template.name + component.save() + fixed += 1 + except ValueError: + pass + else: + messages.error(request, "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 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)) + + rearports = device.rearports.all() + rearports_templates = RearPortTemplate.objects.filter(device_type=device.device_type) + + 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, "Rear ports") + + def post(self, request, device_id): + form = ComponentComparisonForm(request.POST) + if form.is_valid(): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + rearports = device.rearports.all() + rearports_templates = RearPortTemplate.objects.filter(device_type=device.device_type) + + # 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 + ) + + 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, "rear ports") + + + 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) + + return post_components(request, device, rearports, rearports_templates, RearPort, RearPortTemplate) + +class DeviceBayComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View): + """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)) + + devicebays = device.devicebays.all() + devicebays_templates = DeviceBayTemplate.objects.filter(device_type=device.device_type) + + 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, "Device bays") + + def post(self, request, device_id): + form = ComponentComparisonForm(request.POST) + if form.is_valid(): + device = get_object_or_404(Device.objects.filter(id=device_id)) + + 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] + + 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, "device bays")