diff --git a/netbox_interface_sync/__init__.py b/netbox_interface_sync/__init__.py index de916a5..6046865 100644 --- a/netbox_interface_sync/__init__.py +++ b/netbox_interface_sync/__init__.py @@ -4,17 +4,24 @@ from extras.plugins import PluginConfig class Config(PluginConfig): name = 'netbox_interface_sync' verbose_name = 'NetBox interface synchronization' - description = 'Syncing interfaces with the interfaces from device type for NetBox devices' + description = 'Compare and synchronize components (interfaces, ports, outlets, etc.) between NetBox device types ' \ + 'and devices' version = '0.2.0' author = 'Victor Golovanenko' author_email = 'drygdryg2014@yandex.ru' default_settings = { + # Ignore case and spaces in names when matching components between device type and device + 'name_comparison': { + 'case-insensitive': True, + 'space-insensitive': True + }, + # Exclude virtual interfaces (bridge, link aggregation group (LAG), "virtual") from comparison 'exclude_virtual_interfaces': True, + # Add a panel with information about the number of interfaces to the device page 'include_interfaces_panel': False, - # Compare description during diff - # If compare is true, description will also be synced to device - # Otherwise not. - 'compare_description': True + # Consider component descriptions when comparing. If this option is set to True, then take into account + # component descriptions when comparing components and synchronizing their attributes, otherwise - ignore + 'sync_descriptions': True } diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py index aa10687..d643f9d 100644 --- a/netbox_interface_sync/comparison.py +++ b/netbox_interface_sync/comparison.py @@ -1,25 +1,30 @@ +from typing import Optional + import attr from attrs import fields - from django.conf import settings +from netbox.models import PrimaryModel + config = settings.PLUGINS_CONFIG["netbox_interface_sync"] -COMPARE_DESCRIPTIONS: bool = config["compare_description"] +SYNC_DESCRIPTIONS: bool = config["sync_descriptions"] @attr.s(frozen=True, auto_attribs=True) class BaseComparison: """Common fields of a device component""" - # Do not compare IDs - id: int = attr.ib(eq=False, hash=False, metadata={'printable': False}) + id: int = attr.ib(eq=False, metadata={'printable': False, 'netbox_exportable': False}) # Compare names case-insensitively and spaces-insensitively - name: str = attr.ib(eq=lambda name: name.lower().replace(" ", ""), metadata={'printable': False}) - label: str = attr.ib(hash=False) + name: str = attr.ib(metadata={'printable': False}) + label: str = attr.ib() # Compare descriptions if it is set by the configuration - description: str = attr.ib(eq=COMPARE_DESCRIPTIONS, hash=False) + description: str = attr.ib(eq=SYNC_DESCRIPTIONS, metadata={'synced': SYNC_DESCRIPTIONS}) # Do not compare `is_template` properties - is_template: bool = attr.ib(kw_only=True, default=False, eq=False, hash=False, metadata={'printable': False}) + is_template: bool = attr.ib( + default=False, kw_only=True, eq=False, + metadata={'printable': False, 'netbox_exportable': False} + ) @property def fields_display(self) -> str: @@ -32,46 +37,31 @@ class BaseComparison: if not field_value: continue field_caption = field.metadata.get('displayed_caption') or field.name.replace('_', ' ').capitalize() + if isinstance(field_value, BaseComparison): + field_value = f'{field_value.name} (ID: {field_value.id})' fields_to_display.append(f'{field_caption}: {field_value}') return '\n'.join(fields_to_display) + def get_fields_for_netbox_component(self, sync=False): + """ + Returns a dict of fields and values for creating or updating a NetBox component object + :param sync: if True, returns fields for syncing an existing component, otherwise - for creating a new one. + """ + + def field_filter(field: attr.Attribute, _): + result = field.metadata.get('netbox_exportable', True) + if sync: + result &= field.metadata.get('synced', True) + return result + + return attr.asdict(self, recurse=True, filter=field_filter) + @attr.s(frozen=True, auto_attribs=True) class BaseTypedComparison(BaseComparison): """Common fields of a device typed component""" - - type: str = attr.ib(hash=False, metadata={'printable': False}) - type_display: str = attr.ib(eq=False, hash=False, metadata={'displayed_caption': 'Type'}) - - -@attr.s(frozen=True, auto_attribs=True) -class DeviceBayComparison(BaseComparison): - """A unified way to represent the device bay and device bay template""" - pass - - -@attr.s(frozen=True, auto_attribs=True) -class InterfaceComparison(BaseTypedComparison): - """A unified way to represent the interface and interface template""" - - mgmt_only: bool = attr.ib(hash=False) - - -@attr.s(frozen=True, auto_attribs=True) -class FrontPortComparison(BaseTypedComparison): - """A unified way to represent the front port and front port template""" - - color: str = attr.ib(hash=False) - # rear_port_id: int - rear_port_position: int = attr.ib(hash=False, metadata={'displayed_caption': 'Position'}) - - -@attr.s(frozen=True, auto_attribs=True) -class RearPortComparison(BaseTypedComparison): - """A unified way to represent the rear port and rear port template""" - - color: str = attr.ib(hash=False) - positions: int = attr.ib(hash=False) + type: str = attr.ib(metadata={'printable': False}) + type_display: str = attr.ib(eq=False, metadata={'displayed_caption': 'Type', 'netbox_exportable': False}) @attr.s(frozen=True, auto_attribs=True) @@ -89,14 +79,78 @@ class ConsoleServerPortComparison(BaseTypedComparison): @attr.s(frozen=True, auto_attribs=True) class PowerPortComparison(BaseTypedComparison): """A unified way to represent the power port and power port template""" - - maximum_draw: str = attr.ib(hash=False) - allocated_draw: str = attr.ib(hash=False) + maximum_draw: str = attr.ib() + allocated_draw: str = attr.ib() @attr.s(frozen=True, auto_attribs=True) class PowerOutletComparison(BaseTypedComparison): """A unified way to represent the power outlet and power outlet template""" + power_port: PowerPortComparison = attr.ib() + feed_leg: str = attr.ib() - power_port_name: str = attr.ib(hash=False) - feed_leg: str = attr.ib(hash=False) + +@attr.s(frozen=True, auto_attribs=True) +class InterfaceComparison(BaseTypedComparison): + """A unified way to represent the interface and interface template""" + mgmt_only: bool = attr.ib() + + +@attr.s(frozen=True, auto_attribs=True) +class FrontPortComparison(BaseTypedComparison): + """A unified way to represent the front port and front port template""" + color: str = attr.ib() + # rear_port_id: int + rear_port_position: int = attr.ib(metadata={'displayed_caption': 'Position'}) + + +@attr.s(frozen=True, auto_attribs=True) +class RearPortComparison(BaseTypedComparison): + """A unified way to represent the rear port and rear port template""" + color: str = attr.ib() + positions: int = attr.ib() + + +@attr.s(frozen=True, auto_attribs=True) +class DeviceBayComparison(BaseComparison): + """A unified way to represent the device bay and device bay template""" + pass + + +def from_netbox_object(netbox_object: PrimaryModel) -> Optional[BaseComparison]: + """Makes a comparison object from the NetBox object""" + type_map = { + "DeviceBay": DeviceBayComparison, + "Interface": InterfaceComparison, + "FrontPort": FrontPortComparison, + "RearPort": RearPortComparison, + "ConsolePort": ConsolePortComparison, + "ConsoleServerPort": ConsoleServerPortComparison, + "PowerPort": PowerPortComparison, + "PowerOutlet": PowerOutletComparison + } + + obj_name = netbox_object._meta.object_name + if obj_name.endswith("Template"): + is_template = True + obj_name = obj_name[:-8] # TODO: use `removesuffix` introduced in Python 3.9 + else: + is_template = False + + comparison = type_map.get(obj_name) + if not comparison: + return + + values = {} + for field in fields(comparison): + if field.name == "is_template": + continue + if field.name == "type_display": + values[field.name] = netbox_object.get_type_display() + else: + field_value = getattr(netbox_object, field.name) + if isinstance(field_value, PrimaryModel): + field_value = from_netbox_object(field_value) + values[field.name] = field_value + + return comparison(**values, is_template=is_template) diff --git a/netbox_interface_sync/forms.py b/netbox_interface_sync/forms.py deleted file mode 100644 index 45a39db..0000000 --- a/netbox_interface_sync/forms.py +++ /dev/null @@ -1,6 +0,0 @@ -from django import forms - - -class ComponentComparisonForm(forms.Form): - add_to_device = forms.BooleanField(required=False) - remove_from_device = forms.BooleanField(required=False) 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 index e095442..c451520 100644 --- a/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html +++ b/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html @@ -1,9 +1,9 @@ {% if perms.dcim.change_device %}