From 87f48e830b8eacb512f3dcdfeb72d0e5db6df268 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Tue, 10 Dec 2024 16:27:04 +0100 Subject: [PATCH] revamps SSoT cookie for contrib pattern - collapses both adapters into one file, asthe Nautobot adapter is significantly simpler with contrib collapse all models into one class per Nautobot content type -> users are of course free to change this but I found that for most SSoTs this is unnecessary complexity - the differentation between base, SoR and Nautobot seems not necessary because SSoTs that sync both way simply don't exist - add a "direction_of_sync" cookiecutter variable to define whether contrib classes are used or not depending on direction of sync --- nautobot-app-ssot/cookiecutter.json | 6 +- .../diffsync/__init__.py | 1 + ...ystem_of_record_slug }}.py => adapters.py} | 24 ++++++-- .../diffsync/adapters/__init__.py | 1 - .../diffsync/adapters/nautobot.py | 28 --------- .../diffsync/models.py | 46 ++++++++++++++ .../diffsync/models/__init__.py | 1 - .../diffsync/models/base.py | 29 --------- .../diffsync/models/nautobot.py | 60 ------------------- ...{ cookiecutter.system_of_record_slug }}.py | 20 ------- .../{{ cookiecutter.app_name }}/jobs.py | 3 +- 11 files changed, 72 insertions(+), 147 deletions(-) create mode 100644 nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/__init__.py rename nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/{adapters/{{ cookiecutter.system_of_record_slug }}.py => adapters.py} (55%) delete mode 100644 nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/__init__.py delete mode 100644 nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/nautobot.py create mode 100644 nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models.py delete mode 100644 nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/__init__.py delete mode 100644 nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/base.py delete mode 100644 nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/nautobot.py delete mode 100644 nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/{{ cookiecutter.system_of_record_slug }}.py diff --git a/nautobot-app-ssot/cookiecutter.json b/nautobot-app-ssot/cookiecutter.json index 08df17af..3706f61c 100644 --- a/nautobot-app-ssot/cookiecutter.json +++ b/nautobot-app-ssot/cookiecutter.json @@ -6,6 +6,11 @@ "system_of_record": "System of Record", "system_of_record_camel": "{{ cookiecutter.system_of_record.title().replace(' ', '').replace('_', '').replace('-', '') }}", "system_of_record_slug": "{{ cookiecutter.system_of_record.lower().replace(' ', '_').replace('-', '_') }}", + "model_class_name": "None", + "direction_of_sync": [ + "To Nautobot", + "From Nautobot" + ], "app_name": "nautobot_ssot_{{ cookiecutter.system_of_record.lower().replace(' ', '_').replace('-', '_') }}", "verbose_name": "{{ cookiecutter.app_name.title().replace('_', ' ') }}", "app_slug": "{{ cookiecutter.app_name.lower().replace(' ', '-').replace('_', '-') }}", @@ -16,7 +21,6 @@ "max_nautobot_version": "2.9999", "camel_name": "{{ cookiecutter.app_slug.title().replace(' ', '').replace('-', '') }}", "project_short_description": "{{ cookiecutter.verbose_name }}", - "model_class_name": "None", "open_source_license": [ "Apache-2.0", "Not open source" diff --git a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/__init__.py b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/__init__.py new file mode 100644 index 00000000..b458ee99 --- /dev/null +++ b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/__init__.py @@ -0,0 +1 @@ +"""Diffsync components for {{ cookiecutter.app_name }}.""" diff --git a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/{{ cookiecutter.system_of_record_slug }}.py b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters.py similarity index 55% rename from nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/{{ cookiecutter.system_of_record_slug }}.py rename to nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters.py index 9a98851a..8f6781d2 100644 --- a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/{{ cookiecutter.system_of_record_slug }}.py +++ b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters.py @@ -1,14 +1,14 @@ -"""{{ cookiecutter.verbose_name }} Adapter for {{ cookiecutter.system_of_record }} SSoT app.""" +"""Diffsync adapters for {{ cookiecutter.app_name }}.""" from diffsync import Adapter -from {{ cookiecutter.app_name }}.diffsync.models.{{ cookiecutter.system_of_record_slug }} import {{ cookiecutter.system_of_record_camel }}Device +from {{ cookiecutter.app_name }}.diffsync.models import DiffsyncDevice -class {{ cookiecutter.system_of_record_camel }}Adapter(Adapter): +class {{ cookiecutter.system_of_record_camel }}RemoteAdapter(Adapter): """DiffSync adapter for {{ cookiecutter.system_of_record }}.""" - device = {{ cookiecutter.system_of_record_camel }}Device + device = DiffsyncDevice top_level = ["device"] @@ -27,4 +27,18 @@ def __init__(self, *args, job=None, sync=None, client=None, **kwargs): def load(self): """Load data from {{ cookiecutter.system_of_record }} into DiffSync models.""" - raise NotImplementedError + raise NotImplementedError() + + +class {{ cookiecutter.system_of_record_camel }}NautobotAdapter(NautobotAdapter): + """DiffSync adapter for Nautobot.""" + + device = NautobotDevice + + top_level = ["device"] + + {% if cookiecutter.direction_of_sync == "From Nautobot" %} + def load(self): + """Load data from Nautobot into DiffSync models.""" + raise NotImplementedError() + {% endif %} diff --git a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/__init__.py b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/__init__.py deleted file mode 100644 index da6cd024..00000000 --- a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Adapter classes for loading DiffSyncModels with data from {{ cookiecutter.system_of_record }} or Nautobot.""" diff --git a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/nautobot.py b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/nautobot.py deleted file mode 100644 index 73cb33e9..00000000 --- a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/adapters/nautobot.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Nautobot Adapter for {{ cookiecutter.system_of_record }} SSoT app.""" - -from diffsync import Adapter - -from {{ cookiecutter.app_name }}.diffsync.models.nautobot import NautobotDevice - - -class NautobotAdapter(Adapter): - """DiffSync adapter for Nautobot.""" - - device = NautobotDevice - - top_level = ["device"] - - def __init__(self, *args, job=None, sync=None, **kwargs): - """Initialize Nautobot. - - Args: - job (object, optional): Nautobot job. Defaults to None. - sync (object, optional): Nautobot DiffSync. Defaults to None. - """ - super().__init__(*args, **kwargs) - self.job = job - self.sync = sync - - def load(self): - """Load data from Nautobot into DiffSync models.""" - raise NotImplementedError diff --git a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models.py b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models.py new file mode 100644 index 00000000..6cca313f --- /dev/null +++ b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models.py @@ -0,0 +1,46 @@ +"""Diffsync models for {{ cookiecutter.app_name }}.""" +from typing import Optional, Annotated + +{% if cookiecutter.direction_of_sync == "From Nautobot" %} +from diffsync import DiffSyncModel +{% endif %} +from nautobot.dcim.models import Device +{% if cookiecutter.direction_of_sync == "To Nautobot" %} +from nautobot_ssot.contrib import CustomFieldAnnotation, NautobotModel +{% endif %} + +class DiffsyncDevice({{ "NautobotModel" if cookiecutter.direction_of_sync == "To Nautobot" else "DiffSyncModel" }}): + """DiffSync model for {{ cookiecutter.system_of_record }} devices.""" + + {% if cookiecutter.direction_of_sync == "To Nautobot" %}_model = Device{% endif %} + _modelname = "device" + _identifiers = ("name",) + _attributes = ( + "status__name", + "role__name", + "device_type__name", + "location__name", + "example_custom_field" + ) + + name: str + status__name: Optional[str] = None + role__name: Optional[str] = None + device_type__name: Optional[str] = None + location__name: Optional[str] = None + ip_address: Optional[str] = None + example_custom_field: Annotated[str, CustomFieldAnnotation(key="my_example_custom_field")] + {% if cookiecutter.direction_of_sync == "From Nautobot" %} + @classmethod + def create(cls, adapter, ids, attrs): + """Create device in {{ cookiecutter.system_of_record }}.""" + raise NotImplementedError("Device creation is not implemented.") + + def update(self, attrs): + """Update device in {{ cookiecutter.system_of_record }}.""" + raise NotImplementedError("Device updates are not implemented.") + + def delete(self): + """Delete device in {{ cookiecutter.system_of_record }}.""" + raise NotImplementedError("Device deletion is not implemented.") + {% endif %} \ No newline at end of file diff --git a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/__init__.py b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/__init__.py deleted file mode 100644 index 132bb086..00000000 --- a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""DiffSync models and adapters for the {{ cookiecutter.system_of_record }} SSoT app.""" diff --git a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/base.py b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/base.py deleted file mode 100644 index 5436c8c4..00000000 --- a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/base.py +++ /dev/null @@ -1,29 +0,0 @@ -"""DiffSyncModel subclasses for Nautobot-to-{{ cookiecutter.system_of_record }} data sync.""" - -from typing import Optional -from uuid import UUID -from diffsync import DiffSyncModel - - -class Device(DiffSyncModel): - """DiffSync model for {{ cookiecutter.system_of_record }} devices.""" - - _modelname = "device" - _identifiers = ("name",) - _attributes = ( - "status", - "role", - "model", - "location", - "ip_address", - ) - _children = {} - - name: str - status: Optional[str] = None - role: Optional[str] = None - model: Optional[str] = None - location: Optional[str] = None - ip_address: Optional[str] = None - - uuid: Optional[UUID] = None diff --git a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/nautobot.py b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/nautobot.py deleted file mode 100644 index c53eca86..00000000 --- a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/nautobot.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Nautobot DiffSync models for {{ cookiecutter.system_of_record }} SSoT.""" - -from django.contrib.contenttypes.models import ContentType - -from nautobot.dcim.models import Device as NewDevice, Location, LocationType, DeviceType -from nautobot.extras.models import Status, Role -from {{ cookiecutter.app_name }}.diffsync.models.base import Device - - -def ensure_location(location_name): - """Safely returns a Location with a LocationType that support Devices.""" - location_type, _ = LocationType.objects.get_or_create(name="Site") - content_type = ContentType.objects.get_for_model(NewDevice) - location_type.content_types.add(content_type) - status = Status.objects.get(name="Active") - return Location.objects.get_or_create(name=location_name, location_type=location_type, status=status)[0] - - -def ensure_role(role_name): - """Safely returns a Role that support Devices.""" - content_type = ContentType.objects.get_for_model(NewDevice) - role, _ = Role.objects.get_or_create(name=role_name) - role.content_types.add(content_type) - return role - - -class NautobotDevice(Device): - """Nautobot implementation of {{ cookiecutter.system_of_record }} Device model.""" - - @classmethod - def create(cls, adapter, ids, attrs): - """Create Device in Nautobot from NautobotDevice object.""" - new_device = NewDevice( - name=ids["name"], - device_type=DeviceType.objects.get_or_create(model=attrs["model"])[0], - status=Status.objects.get_or_create(name=attrs["status"])[0], - role=ensure_role(attrs["role"]), - location=ensure_location(attrs["location"]), - ) - new_device.validated_save() - return super().create(adapter=adapter, ids=ids, attrs=attrs) - - def update(self, attrs): - """Update Device in Nautobot from NautobotDevice object.""" - device = NewDevice.objects.get(id=self.uuid) - if "status" in attrs: - device.status = Status.objects.get_or_create(name=attrs["status"])[0] - if "role" in attrs: - device.role = ensure_role(attrs["role"]) - if "location" in attrs: - device.location = ensure_location(attrs["location"]) - device.validated_save() - return super().update(attrs) - - def delete(self): - """Delete Device in Nautobot from NautobotDevice object.""" - dev = NewDevice.objects.get(id=self.uuid) - super().delete() - dev.delete() - return self diff --git a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/{{ cookiecutter.system_of_record_slug }}.py b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/{{ cookiecutter.system_of_record_slug }}.py deleted file mode 100644 index 15d4182b..00000000 --- a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/diffsync/models/{{ cookiecutter.system_of_record_slug }}.py +++ /dev/null @@ -1,20 +0,0 @@ -"""{{ cookiecutter.verbose_name }} DiffSync models for {{ cookiecutter.verbose_name }} SSoT.""" - -from {{ cookiecutter.app_name }}.diffsync.models.base import Device - - -class {{ cookiecutter.system_of_record_camel }}Device(Device): - """{{ cookiecutter.system_of_record }} implementation of Device DiffSync model.""" - - @classmethod - def create(cls, diffsync, ids, attrs): - """Create Device in {{ cookiecutter.system_of_record }} from {{ cookiecutter.system_of_record_camel }}Device object.""" - return super().create(diffsync=diffsync, ids=ids, attrs=attrs) - - def update(self, attrs): - """Update Device in {{ cookiecutter.system_of_record }} from {{ cookiecutter.system_of_record_camel }}Device object.""" - return super().update(attrs) - - def delete(self): - """Delete Device in {{ cookiecutter.system_of_record }} from {{ cookiecutter.system_of_record_camel }}Device object.""" - return self diff --git a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/jobs.py b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/jobs.py index 7ffd0b59..dcde584b 100644 --- a/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/jobs.py +++ b/nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/jobs.py @@ -3,8 +3,7 @@ from nautobot.apps.jobs import BooleanVar, register_jobs from nautobot_ssot.jobs.base import DataSource, DataTarget -from {{ cookiecutter.app_name }}.diffsync.adapters import {{ cookiecutter.system_of_record_slug }}, nautobot - +from {{ cookiecutter.app_name }}.diffsync.adapters import {{ cookiecutter.system_of_record_camel }}RemoteAdapter, {{ cookiecutter.system_of_record_camel }}NautobotAdapter name = "{{ cookiecutter.system_of_record }} SSoT" # pylint: disable=invalid-name