netbox-plugin-interface-sync/netbox_interface_sync/comparison.py
2022-03-03 12:39:16 +03:00

157 lines
5.6 KiB
Python

from typing import Optional
import attr
from attrs import fields
from django.conf import settings
from netbox.models import PrimaryModel
config = settings.PLUGINS_CONFIG["netbox_interface_sync"]
SYNC_DESCRIPTIONS: bool = config["sync_descriptions"]
@attr.s(frozen=True, auto_attribs=True)
class BaseComparison:
"""Common fields of a device component"""
# Do not compare IDs
id: int = attr.ib(eq=False, metadata={'printable': False, 'netbox_exportable': False})
# Compare names case-insensitively and spaces-insensitively
name: str = attr.ib(metadata={'printable': False})
label: str = attr.ib()
# Compare descriptions if it is set by the configuration
description: str = attr.ib(eq=SYNC_DESCRIPTIONS, metadata={'synced': SYNC_DESCRIPTIONS})
# Do not compare `is_template` properties
is_template: bool = attr.ib(
default=False, kw_only=True, eq=False,
metadata={'printable': False, 'netbox_exportable': False}
)
@property
def fields_display(self) -> str:
"""Generate human-readable list of printable fields to display in the comparison table"""
fields_to_display = []
for field in fields(self.__class__):
if not field.metadata.get('printable', True):
continue
field_value = getattr(self, field.name)
if not field_value:
continue
field_caption = field.metadata.get('displayed_caption') or field.name.replace('_', ' ').capitalize()
if isinstance(field_value, BaseComparison):
field_value = f'{field_value.name} (ID: {field_value.id})'
fields_to_display.append(f'{field_caption}: {field_value}')
return '\n'.join(fields_to_display)
def get_fields_for_netbox_component(self, sync=False):
"""
Returns a dict of fields and values for creating or updating a NetBox component object
:param sync: if True, returns fields for syncing an existing component, otherwise - for creating a new one.
"""
def field_filter(field: attr.Attribute, _):
result = field.metadata.get('netbox_exportable', True)
if sync:
result &= field.metadata.get('synced', True)
return result
return attr.asdict(self, recurse=True, filter=field_filter)
@attr.s(frozen=True, auto_attribs=True)
class BaseTypedComparison(BaseComparison):
"""Common fields of a device typed component"""
type: str = attr.ib(metadata={'printable': False})
type_display: str = attr.ib(eq=False, metadata={'displayed_caption': 'Type', 'netbox_exportable': False})
@attr.s(frozen=True, auto_attribs=True)
class ConsolePortComparison(BaseTypedComparison):
"""A unified way to represent the consoleport and consoleport template"""
pass
@attr.s(frozen=True, auto_attribs=True)
class ConsoleServerPortComparison(BaseTypedComparison):
"""A unified way to represent the consoleserverport and consoleserverport template"""
pass
@attr.s(frozen=True, auto_attribs=True)
class PowerPortComparison(BaseTypedComparison):
"""A unified way to represent the power port and power port template"""
maximum_draw: str = attr.ib()
allocated_draw: str = attr.ib()
@attr.s(frozen=True, auto_attribs=True)
class PowerOutletComparison(BaseTypedComparison):
"""A unified way to represent the power outlet and power outlet template"""
power_port: PowerPortComparison = attr.ib()
feed_leg: str = attr.ib()
@attr.s(frozen=True, auto_attribs=True)
class InterfaceComparison(BaseTypedComparison):
"""A unified way to represent the interface and interface template"""
mgmt_only: bool = attr.ib()
@attr.s(frozen=True, auto_attribs=True)
class FrontPortComparison(BaseTypedComparison):
"""A unified way to represent the front port and front port template"""
color: str = attr.ib()
# rear_port_id: int
rear_port_position: int = attr.ib(metadata={'displayed_caption': 'Position'})
@attr.s(frozen=True, auto_attribs=True)
class RearPortComparison(BaseTypedComparison):
"""A unified way to represent the rear port and rear port template"""
color: str = attr.ib()
positions: int = attr.ib()
@attr.s(frozen=True, auto_attribs=True)
class DeviceBayComparison(BaseComparison):
"""A unified way to represent the device bay and device bay template"""
pass
def from_netbox_object(netbox_object: PrimaryModel) -> Optional[BaseComparison]:
"""Makes a comparison object from the NetBox object"""
type_map = {
"DeviceBay": DeviceBayComparison,
"Interface": InterfaceComparison,
"FrontPort": FrontPortComparison,
"RearPort": RearPortComparison,
"ConsolePort": ConsolePortComparison,
"ConsoleServerPort": ConsoleServerPortComparison,
"PowerPort": PowerPortComparison,
"PowerOutlet": PowerOutletComparison
}
obj_name = netbox_object._meta.object_name
if obj_name.endswith("Template"):
is_template = True
obj_name = obj_name[:-8] # TODO: use `removesuffix` introduced in Python 3.9
else:
is_template = False
comparison = type_map.get(obj_name)
if not comparison:
return
values = {}
for field in fields(comparison):
if field.name == "is_template":
continue
if field.name == "type_display":
values[field.name] = netbox_object.get_type_display()
else:
field_value = getattr(netbox_object, field.name)
if isinstance(field_value, PrimaryModel):
field_value = from_netbox_object(field_value)
values[field.name] = field_value
return comparison(**values, is_template=is_template)