From a861ccb87956df3da81a7aa24b80f5e50a722e0c Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Tue, 22 Aug 2023 10:41:21 +0000 Subject: [PATCH 01/33] chore: Revert ChatOps dependency removal --- development/nautobot_config.py | 20 +++++++++---------- .../integrations/ipfabric/workers.py | 6 ------ pyproject.toml | 10 ++++------ tasks.py | 2 +- 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 79bec4371..b218e55de 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -133,8 +133,7 @@ # Enable installed plugins. Add the name of each plugin to the list. PLUGINS = [ - # Enable chatops after dropping Python 3.7 support - # "nautobot_chatops", + "nautobot_chatops", "nautobot_device_lifecycle_mgmt", "nautobot_ssot", ] @@ -142,15 +141,14 @@ # Plugins configuration settings. These settings are used by various plugins that the user may have installed. # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. PLUGINS_CONFIG = { - # Enable chatops after dropping Python 3.7 support - # "nautobot_chatops": { - # "enable_slack": True, - # "slack_api_token": os.getenv("SLACK_API_TOKEN"), - # "slack_signing_secret": os.getenv("SLACK_SIGNING_SECRET"), - # "session_cache_timeout": 3600, - # "ipfabric_api_token": os.getenv("IPFABRIC_API_TOKEN"), - # "ipfabric_host": os.getenv("IPFABRIC_HOST"), - # }, + "nautobot_chatops": { + "enable_slack": True, + "slack_api_token": os.getenv("SLACK_API_TOKEN"), + "slack_signing_secret": os.getenv("SLACK_SIGNING_SECRET"), + "session_cache_timeout": 3600, + "ipfabric_api_token": os.getenv("IPFABRIC_API_TOKEN"), + "ipfabric_host": os.getenv("IPFABRIC_HOST"), + }, "nautobot_ssot": { # URL and credentials should be configured as environment variables on the host system "aci_apics": {x: os.environ[x] for x in os.environ if "APIC" in x}, diff --git a/nautobot_ssot/integrations/ipfabric/workers.py b/nautobot_ssot/integrations/ipfabric/workers.py index d2bab1a34..a1e22fdfa 100644 --- a/nautobot_ssot/integrations/ipfabric/workers.py +++ b/nautobot_ssot/integrations/ipfabric/workers.py @@ -7,14 +7,8 @@ from django_rq import job from nautobot.core.settings_funcs import is_truthy from nautobot.extras.models import JobResult - -# pylint: disable-next=import-error from nautobot_chatops.choices import CommandStatusChoices - -# pylint: disable-next=import-error from nautobot_chatops.dispatchers import Dispatcher - -# pylint: disable-next=import-error from nautobot_chatops.workers import handle_subcommands, subcommand_of from nautobot_ssot.integrations.ipfabric.jobs import IpFabricDataSource diff --git a/pyproject.toml b/pyproject.toml index 9bb24942d..1bebb6708 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ netutils = { version = "^1.0.0", optional = true } oauthlib = { version = ">=3.1.0", optional = true } packaging = ">=21.3, <24" prometheus-client = "~0.14.1" -python = ">=3.7.2,<=3.11" +python = ">=3.8,<=3.11" python-magic = { version = ">=0.4.15", optional = true } pytz = { version = ">=2019.3", optional = true } requests = { version = ">=2.21.0", optional = true } @@ -81,8 +81,7 @@ mkdocstrings-python = "0.8.3" requests-mock = "^1.10.0" parameterized = "^0.8.1" myst-parser = "^0.15.2" -# Enable chatops after dropping Python 3.7 support -# nautobot-chatops = { version = "^2.0.2", extras = ["ipfabric"] } +nautobot-chatops = { version = "^2.0.2", extras = ["ipfabric"] } responses = "^0.14.0" [tool.poetry.plugins."nautobot_ssot.data_sources"] @@ -91,9 +90,8 @@ responses = "^0.14.0" [tool.poetry.plugins."nautobot_ssot.data_targets"] "example" = "nautobot_ssot.sync.example:ExampleSyncWorker" -# Enable chatops after dropping Python 3.7 support -# [tool.poetry.plugins."nautobot.workers"] -# "ipfabric" = "nautobot_ssot.integrations.ipfabric.workers:ipfabric" +[tool.poetry.plugins."nautobot.workers"] +"ipfabric" = "nautobot_ssot.integrations.ipfabric.workers:ipfabric" [tool.poetry.extras] aci = [ diff --git a/tasks.py b/tasks.py index d56d05f2a..4bae04d6d 100644 --- a/tasks.py +++ b/tasks.py @@ -45,7 +45,7 @@ def is_truthy(arg): namespace.configure( { "nautobot_ssot": { - "nautobot_ver": "1.5.1", + "nautobot_ver": "1.5.4", "project_name": "nautobot_ssot", "python_ver": "3.8", "local": False, From 8eb3cd684f11a9becef4e980b408d9b8771407bf Mon Sep 17 00:00:00 2001 From: Adam Byczkowski <38091261+qduk@users.noreply.github.com> Date: Tue, 12 Sep 2023 12:30:32 -0500 Subject: [PATCH 02/33] Migrated PR from child repo --- nautobot_ssot/integrations/aristacv/utils/cloudvision.py | 4 +++- nautobot_ssot/tests/aristacv/test_utils_cloudvision.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nautobot_ssot/integrations/aristacv/utils/cloudvision.py b/nautobot_ssot/integrations/aristacv/utils/cloudvision.py index 64087fcc9..a6ae2d574 100644 --- a/nautobot_ssot/integrations/aristacv/utils/cloudvision.py +++ b/nautobot_ssot/integrations/aristacv/utils/cloudvision.py @@ -307,12 +307,14 @@ def get_tags_by_type(client, creator_type: int = tag_models.CREATOR_TYPE_USER): def get_device_tags(client, device_id: str): """Get tags for specific device.""" - tag_stub = tag_services.TagAssignmentConfigServiceStub(client) + tag_stub = tag_services.TagAssignmentServiceStub(client) req = tag_services.TagAssignmentConfigStreamRequest( partial_eq_filter=[ tag_models.TagAssignmentConfig( key=tag_models.TagAssignmentKey( device_id=StringValue(value=device_id), + element_type=tag_models.ELEMENT_TYPE_DEVICE, + workspace_id=StringValue(value=""), ) ) ] diff --git a/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py b/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py index ce61d3630..398c8459d 100644 --- a/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py +++ b/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py @@ -128,7 +128,7 @@ def test_get_device_tags(self): mock_tag.value.device_id.value = "JPE12345678" tag_stub = MagicMock() - tag_stub.TagAssignmentConfigServiceStub.return_value.GetAll.return_value = [mock_tag] + tag_stub.TagAssignmentServiceStub.return_value.GetAll.return_value = [mock_tag] with patch("nautobot_ssot.integrations.aristacv.utils.cloudvision.tag_services", tag_stub): results = cloudvision.get_device_tags(client=self.client, device_id="JPE12345678") From bdffb6cd040730dc141f310ee4f5eb86ba0177c1 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 18 Sep 2023 15:19:24 +0200 Subject: [PATCH 03/33] fix: :bug: Remove opinionated slug name for device-role --- nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py | 2 +- nautobot_ssot/integrations/aci/diffsync/models/nautobot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py index 2835ac74e..42f7338e9 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py @@ -161,7 +161,7 @@ def load_interfaces(self): def load_deviceroles(self): """Method to load Device Roles from Nautobot.""" - for nbdevicerole in DeviceRole.objects.filter(slug__contains="-ssot-aci"): + for nbdevicerole in DeviceRole.objects.all(): _devicerole = self.device_role( name=nbdevicerole.name, description=nbdevicerole.description, diff --git a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py index 25ad625c5..8d6044585 100644 --- a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py @@ -149,7 +149,7 @@ class NautobotDeviceRole(DeviceRole): def create(cls, diffsync, ids, attrs): """Create DeviceRole object in Nautobot.""" _ids_name = ids["name"] - _devicerole = OrmDeviceRole(name=_ids_name, slug=f"{_ids_name}-ssot-aci", description=attrs["description"]) + _devicerole = OrmDeviceRole(name=_ids_name, slug=slugify(_ids_name)", description=attrs["description"]) _devicerole.validated_save() return super().create(ids=ids, diffsync=diffsync, attrs=attrs) From 885e9ff9b5754fc2091d4dbda2d47fb67aa4210e Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 18 Sep 2023 15:27:14 +0200 Subject: [PATCH 04/33] Update nautobot.py --- nautobot_ssot/integrations/aci/diffsync/models/nautobot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py index 8d6044585..6e70a7094 100644 --- a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py @@ -149,7 +149,7 @@ class NautobotDeviceRole(DeviceRole): def create(cls, diffsync, ids, attrs): """Create DeviceRole object in Nautobot.""" _ids_name = ids["name"] - _devicerole = OrmDeviceRole(name=_ids_name, slug=slugify(_ids_name)", description=attrs["description"]) + _devicerole = OrmDeviceRole(name=_ids_name, slug=slugify(_ids_name), description=attrs["description"]) _devicerole.validated_save() return super().create(ids=ids, diffsync=diffsync, attrs=attrs) From 26d509da63ed4be3b091963482b04eb8132bdd04 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 18 Sep 2023 15:51:47 +0200 Subject: [PATCH 05/33] Manage device roles existing --- nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py | 2 ++ nautobot_ssot/integrations/aci/diffsync/models/nautobot.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py index 42f7338e9..64e4b82cc 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py @@ -6,6 +6,7 @@ from diffsync import DiffSync from django.db.models import ProtectedError from django.utils.text import slugify +from diffsync.enum import DiffSyncModelFlags from nautobot.tenancy.models import Tenant from nautobot.dcim.models import DeviceType, DeviceRole, Device, InterfaceTemplate, Interface from nautobot.ipam.models import IPAddress, Prefix, VRF @@ -166,6 +167,7 @@ def load_deviceroles(self): name=nbdevicerole.name, description=nbdevicerole.description, ) + _devicerole.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST self.add(_devicerole) def load_devices(self): diff --git a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py index 8d6044585..6e70a7094 100644 --- a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py @@ -149,7 +149,7 @@ class NautobotDeviceRole(DeviceRole): def create(cls, diffsync, ids, attrs): """Create DeviceRole object in Nautobot.""" _ids_name = ids["name"] - _devicerole = OrmDeviceRole(name=_ids_name, slug=slugify(_ids_name)", description=attrs["description"]) + _devicerole = OrmDeviceRole(name=_ids_name, slug=slugify(_ids_name), description=attrs["description"]) _devicerole.validated_save() return super().create(ids=ids, diffsync=diffsync, attrs=attrs) From 7c3ef681d79447e315e0e982b4f4ed46ae939936 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 18 Sep 2023 15:57:07 +0200 Subject: [PATCH 06/33] Use active status for interfaces as a mandatory field --- nautobot_ssot/integrations/aci/diffsync/models/nautobot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py index 6e70a7094..21e612827 100644 --- a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py @@ -268,6 +268,7 @@ def create(cls, diffsync, ids, attrs): device=OrmDevice.objects.get(name=ids["device"], site=Site.objects.get(name=ids["site"])), description=attrs["description"], type=attrs["type"], + status=Status.objects.get(name="Active"), ) _interface.custom_field_data["gbic_vendor"] = attrs["gbic_vendor"] _interface.custom_field_data["gbic_sn"] = attrs["gbic_sn"] From 19899a6dddc24efa21ceaa15bd122ac46db5bde5 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 18 Sep 2023 16:03:54 +0200 Subject: [PATCH 07/33] fix log message --- nautobot_ssot/integrations/aci/diffsync/models/nautobot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py index 21e612827..ed456ee15 100644 --- a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py @@ -436,7 +436,7 @@ def create(cls, diffsync, ids, attrs): try: vrf_tenant = OrmTenant.objects.get(name=attrs["vrf_tenant"]) except OrmTenant.DoesNotExist: - diffsync.job.log_warning(message=f"Tenant {attrs['vrf_tenant']} not found for VRF {attrs['vrf']}") + diffsync.job.log_warning(message=f"Tenant {attrs['vrf_tenant']} not found for VRF {ids['vrf']}") vrf_tenant = None if ids["vrf"] and vrf_tenant: From 8d64274f41a26407843d4ec9459df0fd43d6241e Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Mon, 18 Sep 2023 18:01:52 +0200 Subject: [PATCH 08/33] keep adding fixes --- .../aci/diffsync/adapters/nautobot.py | 2 +- .../aci/diffsync/models/nautobot.py | 36 ++++++++++++++----- nautobot_ssot/integrations/aci/jobs.py | 3 ++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py index 64e4b82cc..219efa79b 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py @@ -240,7 +240,7 @@ def load_prefixes(self): status=nbprefix.status.name, site=self.site, description=nbprefix.description, - tenant=nbprefix.tenant.name, + tenant=nbprefix.tenant.name if nbprefix.tenant else None, vrf=vrf, vrf_tenant=vrf_tenant, site_tag=self.site, diff --git a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py index ed456ee15..9d4e2e826 100644 --- a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py @@ -349,10 +349,12 @@ def create(cls, diffsync, ids, attrs): else: obj_type = None obj_id = None - if ids["tenant"]: - tenant_name = OrmTenant.objects.get(name=ids["tenant"]) - else: - tenant_name = None + + try: + tenant = OrmTenant.objects.get(name=ids["tenant"]) + except OrmTenant.DoesNotExist: + tenant = None + try: vrf_tenant = OrmTenant.objects.get(name=attrs["vrf_tenant"]) except OrmTenant.DoesNotExist: @@ -373,7 +375,7 @@ def create(cls, diffsync, ids, attrs): address=ids["address"], status=Status.objects.get(name=attrs["status"]), description=attrs["description"], - tenant=tenant_name, + tenant=tenant, assigned_object_type=obj_type, assigned_object_id=obj_id, vrf=vrf_name, @@ -418,9 +420,15 @@ def delete(self): """Delete IPAddress object in Nautobot.""" self.diffsync.job.log_warning(f"IP Address {self.address} will be deleted.") super().delete() + + try: + tenant = OrmTenant.objects.get(name=self.tenant) + except OrmTenant.DoesNotExist: + tenant= None + _ipaddress = OrmIPAddress.objects.get( address=self.get_identifiers()["address"], - tenant=OrmTenant.objects.get(name=self.tenant), + tenant=tenant, vrf=OrmVrf.objects.get(name=self.vrf, tenant=OrmTenant.objects.get(name=self.vrf_tenant)), ) self.diffsync.objects_to_delete["ipaddress"].append(_ipaddress) # pylint: disable=protected-access @@ -447,11 +455,17 @@ def create(cls, diffsync, ids, attrs): vrf = None else: vrf = None + + try: + prefix_tenant = OrmTenant.objects.get(name=ids["tenant"]) + except OrmTenant.DoesNotExist: + prefix_tenant = None + _prefix = OrmPrefix( prefix=ids["prefix"], status=Status.objects.get(name=attrs["status"]), description=attrs["description"], - tenant=OrmTenant.objects.get(name=ids["tenant"]), + tenant=prefix_tenant, site=Site.objects.get(name=ids["site"]), vrf=vrf, ) @@ -482,9 +496,15 @@ def delete(self): """Delete Prefix object in Nautobot.""" self.diffsync.job.log_warning(f"Prefix {self.prefix} will be deleted.") super().delete() + + try: + tenant = OrmTenant.objects.get(name=self.tenant) + except OrmTenant.DoesNotExist: + tenant= None + _prefix = OrmPrefix.objects.get( prefix=self.get_identifiers()["prefix"], - tenant=OrmTenant.objects.get(name=self.tenant), + tenant=tenant, vrf=OrmVrf.objects.get(name=self.vrf, tenant=OrmTenant.objects.get(name=self.vrf_tenant)), ) self.diffsync.objects_to_delete["prefix"].append(_prefix) # pylint: disable=protected-access diff --git a/nautobot_ssot/integrations/aci/jobs.py b/nautobot_ssot/integrations/aci/jobs.py index ebefadd66..75e502e1d 100644 --- a/nautobot_ssot/integrations/aci/jobs.py +++ b/nautobot_ssot/integrations/aci/jobs.py @@ -1,6 +1,7 @@ """Jobs for ACI SSoT plugin.""" from django.templatetags.static import static from django.urls import reverse +from diffsync import DiffSyncFlags from nautobot.core.settings_funcs import is_truthy from nautobot.extras.jobs import BooleanVar, ChoiceVar, Job from nautobot_ssot.jobs.base import DataMapping, DataSource @@ -39,6 +40,8 @@ class AciDataSource(DataSource, Job): # pylint: disable=abstract-method debug = BooleanVar(description="Enable for verbose debug logging.") + diffsync_flags = DiffSyncFlags.SKIP_UNMATCHED_DST + class Meta: # pylint: disable=too-few-public-methods """Information about the Job.""" From bfc3a22fcaf232cef48eb4f8d5e4cdd3b00b1c26 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Tue, 19 Sep 2023 11:18:34 +0200 Subject: [PATCH 09/33] extra fixes --- .../integrations/aci/diffsync/adapters/aci.py | 20 ++++++++++++++++--- .../aci/diffsync/models/nautobot.py | 14 +++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py index 7b04f3945..4e4128df9 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py @@ -106,7 +106,7 @@ def load_ipaddresses(self): node_dict = self.conn.get_nodes() # Leaf/Spine management IP addresses for node in node_dict.values(): - if node["oob_ip"] and not node["oob_ip"] == "0.0.0.0": # nosec + if "oob_ip" in node and node["oob_ip"] and not node["oob_ip"] == "0.0.0.0": # nosec new_ipaddress = self.ip_address( address=f"{node['oob_ip']}/32", device=node["name"], @@ -342,6 +342,7 @@ def load_devices(self): """Load devices from ACI device data.""" devicetype_file_path = os.path.join(os.path.dirname(__file__), "..", "device-types") for key, value in self.devices.items(): + model = "" if f"{value['model']}.yaml" in os.listdir(devicetype_file_path): device_specs = load_yamlfile( os.path.join( @@ -350,12 +351,25 @@ def load_devices(self): ) ) model = device_specs["model"] - else: + + if not model: + try: + self.get("device_type", {"model": value['model'], "part_nbr": ""}) + except ObjectNotFound: + _devicetype = self.device_type( + model=value["model"], + manufacturer="Cisco", + part_nbr="", + u_height=1, + comments="", + ) + self.add(_devicetype) model = value["model"] + new_device = self.device( name=value["name"], device_type=model, - device_role=value["role"], + device_role=value["model"], serial=value["serial"], comments=PLUGIN_CFG.get("comments", ""), node_id=int(key), diff --git a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py index 9d4e2e826..d572af64c 100644 --- a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py @@ -426,10 +426,15 @@ def delete(self): except OrmTenant.DoesNotExist: tenant= None + try: + vrf_tenant = OrmTenant.objects.get(name=self.vrf_tenant) + except OrmTenant.DoesNotExist: + vrf_tenant= None + _ipaddress = OrmIPAddress.objects.get( address=self.get_identifiers()["address"], tenant=tenant, - vrf=OrmVrf.objects.get(name=self.vrf, tenant=OrmTenant.objects.get(name=self.vrf_tenant)), + vrf=OrmVrf.objects.get(name=self.vrf, tenant=vrf_tenant), ) self.diffsync.objects_to_delete["ipaddress"].append(_ipaddress) # pylint: disable=protected-access return self @@ -502,10 +507,15 @@ def delete(self): except OrmTenant.DoesNotExist: tenant= None + try: + vrf_tenant = OrmTenant.objects.get(name=self.vrf_tenant) + except OrmTenant.DoesNotExist: + vrf_tenant= None + _prefix = OrmPrefix.objects.get( prefix=self.get_identifiers()["prefix"], tenant=tenant, - vrf=OrmVrf.objects.get(name=self.vrf, tenant=OrmTenant.objects.get(name=self.vrf_tenant)), + vrf=OrmVrf.objects.get(name=self.vrf, tenant=vrf_tenant), ) self.diffsync.objects_to_delete["prefix"].append(_prefix) # pylint: disable=protected-access return self From 17686df2eec969dadfa327c57dba0f0392b03814 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Tue, 19 Sep 2023 12:24:36 +0200 Subject: [PATCH 10/33] fix pylint --- nautobot_ssot/integrations/aci/diffsync/adapters/aci.py | 2 +- .../integrations/aci/diffsync/adapters/nautobot.py | 2 +- .../integrations/aci/diffsync/models/nautobot.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py index 4e4128df9..6be87c0ff 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py @@ -354,7 +354,7 @@ def load_devices(self): if not model: try: - self.get("device_type", {"model": value['model'], "part_nbr": ""}) + self.get("device_type", {"model": value["model"], "part_nbr": ""}) except ObjectNotFound: _devicetype = self.device_type( model=value["model"], diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py index 219efa79b..3ec2f8db8 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py @@ -4,9 +4,9 @@ import logging from collections import defaultdict from diffsync import DiffSync +from diffsync.enum import DiffSyncModelFlags from django.db.models import ProtectedError from django.utils.text import slugify -from diffsync.enum import DiffSyncModelFlags from nautobot.tenancy.models import Tenant from nautobot.dcim.models import DeviceType, DeviceRole, Device, InterfaceTemplate, Interface from nautobot.ipam.models import IPAddress, Prefix, VRF diff --git a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py index d572af64c..b29c5eeb0 100644 --- a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py @@ -424,12 +424,12 @@ def delete(self): try: tenant = OrmTenant.objects.get(name=self.tenant) except OrmTenant.DoesNotExist: - tenant= None + tenant = None try: vrf_tenant = OrmTenant.objects.get(name=self.vrf_tenant) except OrmTenant.DoesNotExist: - vrf_tenant= None + vrf_tenant = None _ipaddress = OrmIPAddress.objects.get( address=self.get_identifiers()["address"], @@ -505,12 +505,12 @@ def delete(self): try: tenant = OrmTenant.objects.get(name=self.tenant) except OrmTenant.DoesNotExist: - tenant= None + tenant = None try: vrf_tenant = OrmTenant.objects.get(name=self.vrf_tenant) except OrmTenant.DoesNotExist: - vrf_tenant= None + vrf_tenant = None _prefix = OrmPrefix.objects.get( prefix=self.get_identifiers()["prefix"], From 5bdbaa93d69bc4f242a8f6b7d2ce620a85069b49 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Tue, 19 Sep 2023 13:59:47 +0200 Subject: [PATCH 11/33] Update nautobot_ssot/integrations/aci/diffsync/adapters/aci.py --- nautobot_ssot/integrations/aci/diffsync/adapters/aci.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py index 6be87c0ff..744148134 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py @@ -369,7 +369,7 @@ def load_devices(self): new_device = self.device( name=value["name"], device_type=model, - device_role=value["model"], + device_role=value["role"], serial=value["serial"], comments=PLUGIN_CFG.get("comments", ""), node_id=int(key), From 17486eaa3d6144861058f44b9aa48e0f16ccde18 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Tue, 19 Sep 2023 14:24:36 +0200 Subject: [PATCH 12/33] Update nautobot_ssot/integrations/aci/diffsync/adapters/aci.py Co-authored-by: Leo Kirchner --- .../integrations/aci/diffsync/adapters/aci.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py index 744148134..e34bd95df 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py @@ -353,17 +353,7 @@ def load_devices(self): model = device_specs["model"] if not model: - try: - self.get("device_type", {"model": value["model"], "part_nbr": ""}) - except ObjectNotFound: - _devicetype = self.device_type( - model=value["model"], - manufacturer="Cisco", - part_nbr="", - u_height=1, - comments="", - ) - self.add(_devicetype) + self.get_or_instantiate("device_type", ids={"model": value["model"], "part_nbr": ""}, attrs={"manufacturer": "Cisco", "u_height": 1, "comments": ""}) model = value["model"] new_device = self.device( From 2e20bd4851836ef3d6f3d9844153d6b07da05898 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Tue, 19 Sep 2023 14:27:37 +0200 Subject: [PATCH 13/33] fix black --- nautobot_ssot/integrations/aci/diffsync/adapters/aci.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py index e34bd95df..210ac53a6 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py @@ -353,7 +353,11 @@ def load_devices(self): model = device_specs["model"] if not model: - self.get_or_instantiate("device_type", ids={"model": value["model"], "part_nbr": ""}, attrs={"manufacturer": "Cisco", "u_height": 1, "comments": ""}) + self.get_or_instantiate( + "device_type", + ids={"model": value["model"], "part_nbr": ""}, + attrs={"manufacturer": "Cisco", "u_height": 1, "comments": ""}, + ) model = value["model"] new_device = self.device( From db40ac7d202300278b2edf5d8466433f0d30367e Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Wed, 20 Sep 2023 16:46:00 +0200 Subject: [PATCH 14/33] change flags --- nautobot_ssot/integrations/aci/jobs.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nautobot_ssot/integrations/aci/jobs.py b/nautobot_ssot/integrations/aci/jobs.py index 75e502e1d..1989e4d76 100644 --- a/nautobot_ssot/integrations/aci/jobs.py +++ b/nautobot_ssot/integrations/aci/jobs.py @@ -40,8 +40,6 @@ class AciDataSource(DataSource, Job): # pylint: disable=abstract-method debug = BooleanVar(description="Enable for verbose debug logging.") - diffsync_flags = DiffSyncFlags.SKIP_UNMATCHED_DST - class Meta: # pylint: disable=too-few-public-methods """Information about the Job.""" @@ -50,6 +48,13 @@ class Meta: # pylint: disable=too-few-public-methods data_source_icon = static("nautobot_ssot_aci/aci.png") description = "Sync information from ACI to Nautobot" + def __init__(self): + """Initialize ExampleYAMLDataSource.""" + super().__init__() + self.diffsync_flags = ( + self.diffsync_flags | DiffSyncFlags.SKIP_UNMATCHED_DST + ) + @classmethod def data_mappings(cls): """Shows mapping of models between ACI and Nautobot.""" From 79762d90712483e81f3115e9f914bcbdaef11d8c Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Wed, 20 Sep 2023 17:02:42 +0200 Subject: [PATCH 15/33] fix black --- nautobot_ssot/integrations/aci/jobs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nautobot_ssot/integrations/aci/jobs.py b/nautobot_ssot/integrations/aci/jobs.py index 1989e4d76..0d9855510 100644 --- a/nautobot_ssot/integrations/aci/jobs.py +++ b/nautobot_ssot/integrations/aci/jobs.py @@ -51,9 +51,7 @@ class Meta: # pylint: disable=too-few-public-methods def __init__(self): """Initialize ExampleYAMLDataSource.""" super().__init__() - self.diffsync_flags = ( - self.diffsync_flags | DiffSyncFlags.SKIP_UNMATCHED_DST - ) + self.diffsync_flags = self.diffsync_flags | DiffSyncFlags.SKIP_UNMATCHED_DST @classmethod def data_mappings(cls): From 27853eff24860fbdea942d27c282b2e6fa3073d8 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Wed, 20 Sep 2023 17:19:44 +0200 Subject: [PATCH 16/33] ix pylint --- nautobot_ssot/integrations/aci/jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautobot_ssot/integrations/aci/jobs.py b/nautobot_ssot/integrations/aci/jobs.py index 0d9855510..fd062ba5b 100644 --- a/nautobot_ssot/integrations/aci/jobs.py +++ b/nautobot_ssot/integrations/aci/jobs.py @@ -51,7 +51,7 @@ class Meta: # pylint: disable=too-few-public-methods def __init__(self): """Initialize ExampleYAMLDataSource.""" super().__init__() - self.diffsync_flags = self.diffsync_flags | DiffSyncFlags.SKIP_UNMATCHED_DST + self.diffsync_flags = self.diffsync_flags | DiffSyncFlags.SKIP_UNMATCHED_DST # pylint: disable=unsupported-binary-operation @classmethod def data_mappings(cls): From 7e84e638aaca7fa0979ca5ddc85dbc88d6cd376b Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Fri, 22 Sep 2023 12:58:02 +0200 Subject: [PATCH 17/33] fixes issues where contrib.NautobotModel wasn't returning objects on update/delete --- nautobot_ssot/contrib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nautobot_ssot/contrib.py b/nautobot_ssot/contrib.py index 6f548679e..bf7cf9fa1 100644 --- a/nautobot_ssot/contrib.py +++ b/nautobot_ssot/contrib.py @@ -271,10 +271,12 @@ def update(self, attrs): """Update the ORM object corresponding to this diffsync object.""" obj = self.get_from_db() self._update_obj_with_parameters(obj, attrs) + return super().update(attrs) def delete(self): """Delete the ORM object corresponding to this diffsync object.""" self.get_from_db().delete() + return super().delete() @classmethod def create(cls, diffsync, ids, attrs): From d861de95cd903da72001d3e16f830620998b3b99 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Fri, 22 Sep 2023 14:24:45 +0200 Subject: [PATCH 18/33] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6663598e..620a5947c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,7 +189,7 @@ jobs: - name: "Upload binaries to release" uses: "svenstaro/upload-release-action@v2" with: - repo_token: "${{ secrets.NTC_GITHUB_TOKEN }}" + repo_token: "${{ secrets.GH_NAUTOBOT_BOT_TOKEN }}" file: "dist/*" tag: "${{ github.ref }}" overwrite: true From bfe3cac19d220a877900906e20db6937f1a3db58 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:30:38 -0500 Subject: [PATCH 19/33] =?UTF-8?q?feat:=20=E2=9C=A8=20Adding=20Device42=20i?= =?UTF-8?q?ntegration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrating integration into main SSoT repo. --- development/development.env | 5 + development/nautobot_config.py | 17 + .../integrations/device42/__init__.py | 1 + .../integrations/device42/constant.py | 94 ++ .../device42/diffsync/adapters/__init__.py | 1 + .../device42/diffsync/adapters/device42.py | 1099 +++++++++++++++++ .../device42/diffsync/adapters/nautobot.py | 1004 +++++++++++++++ .../device42/diffsync/models/__init__.py | 72 ++ .../device42/diffsync/models/base/__init__.py | 1 + .../device42/diffsync/models/base/assets.py | 67 + .../device42/diffsync/models/base/circuits.py | 60 + .../device42/diffsync/models/base/dcim.py | 200 +++ .../device42/diffsync/models/base/ipam.py | 80 ++ .../diffsync/models/nautobot/__init__.py | 1 + .../diffsync/models/nautobot/assets.py | 244 ++++ .../diffsync/models/nautobot/circuits.py | 217 ++++ .../device42/diffsync/models/nautobot/dcim.py | 970 +++++++++++++++ .../device42/diffsync/models/nautobot/ipam.py | 363 ++++++ nautobot_ssot/integrations/device42/jobs.py | 149 +++ .../device42/migrations/__init__.py | 0 .../nautobot_ssot_device42/d42_logo.png | Bin 0 -> 8304 bytes .../integrations/device42/utils/__init__.py | 0 .../integrations/device42/utils/device42.py | 704 +++++++++++ .../integrations/device42/utils/nautobot.py | 371 ++++++ nautobot_ssot/tests/device42/__init__.py | 1 + .../tests/device42/fixtures/__init__.py | 0 .../fixtures/get_all_custom_fields_recv.json | 17 + .../fixtures/get_all_custom_fields_sent.json | 17 + .../fixtures/get_building_pks_recv.json | 68 + .../fixtures/get_building_pks_sent.json | 68 + .../device42/fixtures/get_buildings.json | 70 ++ .../device42/fixtures/get_buildings_recv.json | 65 + .../fixtures/get_cluster_members_recv.json | 23 + .../fixtures/get_cluster_members_sent.json | 20 + .../fixtures/get_customer_pks_recv.json | 32 + .../fixtures/get_customer_pks_sent.json | 32 + .../fixtures/get_device_pks_recv.json | 14 + .../fixtures/get_device_pks_sent.json | 14 + .../device42/fixtures/get_devices_recv.json | 366 ++++++ .../device42/fixtures/get_devices_sent.json | 371 ++++++ .../fixtures/get_hardware_models_recv.json | 394 ++++++ .../fixtures/get_hardware_models_sent.json | 399 ++++++ .../tests/device42/fixtures/get_ip_addrs.json | 26 + .../get_ipaddr_custom_fields_recv.json | 14 + .../get_ipaddr_custom_fields_sent.json | 16 + ...get_ipaddr_default_custom_fields_recv.json | 12 + ...get_ipaddr_default_custom_fields_sent.json | 12 + .../get_patch_panel_port_pks_recv.json | 54 + .../get_patch_panel_port_pks_sent.json | 54 + .../device42/fixtures/get_patch_panels.json | 80 ++ .../fixtures/get_port_connections.json | 14 + .../fixtures/get_port_custom_fields_recv.json | 16 + .../fixtures/get_port_custom_fields_sent.json | 16 + .../device42/fixtures/get_port_pks_recv.json | 14 + .../device42/fixtures/get_port_pks_sent.json | 14 + .../fixtures/get_ports_with_vlans_recv.json | 19 + .../fixtures/get_ports_with_vlans_sent.json | 19 + .../fixtures/get_ports_wo_vlans_recv.json | 58 + .../fixtures/get_ports_wo_vlans_sent.json | 58 + .../device42/fixtures/get_rack_pks_recv.json | 74 ++ .../device42/fixtures/get_rack_pks_sent.json | 74 ++ .../tests/device42/fixtures/get_racks.json | 101 ++ .../device42/fixtures/get_racks_recv.json | 96 ++ .../device42/fixtures/get_room_pks_recv.json | 47 + .../device42/fixtures/get_room_pks_sent.json | 47 + .../tests/device42/fixtures/get_rooms.json | 83 ++ .../device42/fixtures/get_rooms_recv.json | 78 ++ .../get_subnet_custom_fields_recv.json | 26 + .../get_subnet_custom_fields_sent.json | 18 + ...get_subnet_default_custom_fields_recv.json | 12 + ...get_subnet_default_custom_fields_sent.json | 18 + .../tests/device42/fixtures/get_subnets.json | 23 + .../device42/fixtures/get_telcocircuits.json | 64 + .../fixtures/get_vendor_pks_recv.json | 50 + .../fixtures/get_vendor_pks_sent.json | 50 + .../device42/fixtures/get_vendors_recv.json | 41 + .../device42/fixtures/get_vendors_sent.json | 46 + .../fixtures/get_vlan_info_cfields.json | 14 + .../device42/fixtures/get_vlan_info_recv.json | 28 + .../fixtures/get_vlan_info_vlaninfo.json | 17 + .../fixtures/get_vlans_with_location.json | 20 + .../device42/fixtures/get_vrfgroups_recv.json | 36 + .../device42/fixtures/get_vrfgroups_sent.json | 41 + .../tests/device42/fixtures/merged_ports.json | 36 + .../device42/fixtures/ports_with_vlans.json | 36 + .../device42/fixtures/ports_wo_vlans.json | 28 + nautobot_ssot/tests/device42/unit/__init__.py | 0 .../device42/unit/test_device42_adapter.py | 422 +++++++ .../device42/unit/test_utils_device42.py | 772 ++++++++++++ .../device42/unit/test_utils_nautobot.py | 234 ++++ pyproject.toml | 3 + 91 files changed, 10822 insertions(+) create mode 100644 nautobot_ssot/integrations/device42/__init__.py create mode 100644 nautobot_ssot/integrations/device42/constant.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/adapters/__init__.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/adapters/device42.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/adapters/nautobot.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/__init__.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/base/__init__.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/base/assets.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/base/circuits.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/base/dcim.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/base/ipam.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/nautobot/__init__.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/nautobot/assets.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/nautobot/circuits.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/nautobot/dcim.py create mode 100644 nautobot_ssot/integrations/device42/diffsync/models/nautobot/ipam.py create mode 100644 nautobot_ssot/integrations/device42/jobs.py create mode 100644 nautobot_ssot/integrations/device42/migrations/__init__.py create mode 100644 nautobot_ssot/integrations/device42/static/nautobot_ssot_device42/d42_logo.png create mode 100644 nautobot_ssot/integrations/device42/utils/__init__.py create mode 100644 nautobot_ssot/integrations/device42/utils/device42.py create mode 100644 nautobot_ssot/integrations/device42/utils/nautobot.py create mode 100644 nautobot_ssot/tests/device42/__init__.py create mode 100644 nautobot_ssot/tests/device42/fixtures/__init__.py create mode 100644 nautobot_ssot/tests/device42/fixtures/get_all_custom_fields_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_all_custom_fields_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_building_pks_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_building_pks_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_buildings.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_buildings_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_cluster_members_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_cluster_members_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_customer_pks_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_customer_pks_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_device_pks_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_device_pks_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_devices_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_devices_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_hardware_models_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_hardware_models_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_ip_addrs.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_ipaddr_default_custom_fields_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_ipaddr_default_custom_fields_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_patch_panel_port_pks_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_patch_panel_port_pks_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_patch_panels.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_port_connections.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_port_pks_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_port_pks_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_rack_pks_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_rack_pks_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_racks.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_racks_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_room_pks_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_room_pks_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_rooms.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_rooms_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_subnets.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_telcocircuits.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_vendor_pks_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_vendor_pks_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_vendors_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_vendors_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_vlan_info_cfields.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_vlan_info_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_vlan_info_vlaninfo.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_vlans_with_location.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_vrfgroups_recv.json create mode 100644 nautobot_ssot/tests/device42/fixtures/get_vrfgroups_sent.json create mode 100644 nautobot_ssot/tests/device42/fixtures/merged_ports.json create mode 100644 nautobot_ssot/tests/device42/fixtures/ports_with_vlans.json create mode 100644 nautobot_ssot/tests/device42/fixtures/ports_wo_vlans.json create mode 100644 nautobot_ssot/tests/device42/unit/__init__.py create mode 100644 nautobot_ssot/tests/device42/unit/test_device42_adapter.py create mode 100644 nautobot_ssot/tests/device42/unit/test_utils_device42.py create mode 100644 nautobot_ssot/tests/device42/unit/test_utils_nautobot.py diff --git a/development/development.env b/development/development.env index 09468330c..688624e4f 100644 --- a/development/development.env +++ b/development/development.env @@ -70,6 +70,11 @@ NAUTOBOT_ARISTACV_IMPORT_ACTIVE="False" NAUTOBOT_ARISTACV_IMPORT_TAG="False" NAUTOBOT_ARISTACV_VERIFY=True +NAUTOBOT_SSOT_ENABLE_DEVICE42="True" +NAUTOBOT_SSOT_DEVICE42_HOST="" +NAUTOBOT_SSOT_DEVICE42_USERNAME="" +NAUTOBOT_SSOT_DEVICE42_PASSWORD="" + NAUTOBOT_SSOT_ENABLE_INFOBLOX="True" NAUTOBOT_SSOT_INFOBLOX_DEFAULT_STATUS="active" NAUTOBOT_SSOT_INFOBLOX_ENABLE_RFC1918_NETWORK_CONTAINERS="True" diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 79bec4371..71e91c8ba 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -200,10 +200,27 @@ "aristacv_verify": is_truthy(os.getenv("NAUTOBOT_ARISTACV_VERIFY", True)), "enable_aci": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_ACI")), "enable_aristacv": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_ARISTACV")), + "enable_device42": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_DEVICE42")), "enable_infoblox": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_INFOBLOX")), "enable_ipfabric": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_IPFABRIC")), "enable_servicenow": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_SERVICENOW")), "hide_example_jobs": is_truthy(os.getenv("NAUTOBOT_SSOT_HIDE_EXAMPLE_JOBS")), + "device42_host": os.getenv("NAUTOBOT_SSOT_DEVICE42_HOST", ""), + "device42_username": os.getenv("NAUTOBOT_SSOT_DEVICE42_USERNAME", ""), + "device42_password": os.getenv("NAUTOBOT_SSOT_DEVICE42_PASSWORD", ""), + "device42_verify_ssl": False, + "device42_defaults": { + "site_status": "Active", + "rack_status": "Active", + "device_role": "Unknown", + }, + "device42_delete_on_sync": False, + "device42_use_dns": True, + "device42_customer_is_facility": True, + "device42_facility_prepend": "sitecode-", + "device42_role_prepend": "nautobot-", + "device42_ignore_tag": "", + "device42_hostname_mapping": [], "infoblox_default_status": os.getenv("NAUTOBOT_SSOT_INFOBLOX_DEFAULT_STATUS", "active"), "infoblox_enable_rfc1918_network_containers": is_truthy( os.getenv("NAUTOBOT_SSOT_INFOBLOX_ENABLE_RFC1918_NETWORK_CONTAINERS") diff --git a/nautobot_ssot/integrations/device42/__init__.py b/nautobot_ssot/integrations/device42/__init__.py new file mode 100644 index 000000000..254eaaf0d --- /dev/null +++ b/nautobot_ssot/integrations/device42/__init__.py @@ -0,0 +1 @@ +"""Base module for Device42 integration.""" diff --git a/nautobot_ssot/integrations/device42/constant.py b/nautobot_ssot/integrations/device42/constant.py new file mode 100644 index 000000000..623a0a335 --- /dev/null +++ b/nautobot_ssot/integrations/device42/constant.py @@ -0,0 +1,94 @@ +"""Storage of data that will not change throughout the life cycle of the application.""" + +from django.conf import settings + +# Import config vars from nautobot_config.py +PLUGIN_CFG = settings.PLUGINS_CONFIG["nautobot_ssot"] + +DEFAULTS = PLUGIN_CFG.get("device42_defaults") + +PHY_INTF_MAP = { # pylint: disable=invalid-name + "100 Mbps": "100base-tx", + "1.0 Gbps": "1000base-t", + "10 Gbps": "10gbase-t", + "25 Gbps": "25gbase-x-sfp28", + "40 Gbps": "40gbase-x-qsfpp", + "50 Gbps": "50gbase-x-sfp28", + "100 Gbps": "100gbase-x-qsfp28", + "200 Gbps": "200gbase-x-qsfp56", + "400 Gbps": "400gbase-x-qsfpdd", + "1000000": "1000base-t", + "10000000": "10gbase-t", + "1000000000": "100gbase-x-qsfp28", +} + +FC_INTF_MAP = { # pylint: disable=invalid-name + "1.0 Gbps": "1gfc-sfp", + "2.0 Gbps": "2gfc-sfp", + "4.0 Gbps": "4gfc-sfp", + "4 Gbps": "4gfc-sfp", + "8.0 Gbps": "8gfc-sfpp", + "16.0 Gbps": "16gfc-sfpp", + "32.0 Gbps": "32gfc-sfp28", + "64.0 Gbps": "64gfc-qsfpp", + "128.0 Gbps": "128gfc-sfp28", +} + +INTF_NAME_MAP = { + "Ethernet": { + "itype": "1000base-t", + "ex_speed": "1.0 Gbps", + }, + "FastEthernet": { + "itype": "100base-tx", + "ex_speed": "100 Mbps", + }, + "GigabitEthernet": { + "itype": "1000base-t", + "ex_speed": "1.0 Gbps", + }, + "TenGigabitEthernet": { + "itype": "10gbase-t", + "ex_speed": "10 Gbps", + }, + "TwentyFiveGigE": { + "itype": "25gbase-x-sfp28", + "ex_speed": "25 Gbps", + }, + "FortyGigabitEthernet": { + "itype": "40gbase-x-qsfpp", + "ex_speed": "40 Gbps", + }, + "HundredGigabitEthernet": { + "itype": "100gbase-x-qsfp28", + "ex_speed": "100 Gbps", + }, +} + +# Map Interface type to max speed in Kbps +INTF_SPEED_MAP = { + "100base-tx": 100000, + "1000base-t": 1000000, + "2.5gbase-t": 2500000, + "5gbase-t": 5000000, + "10gbase-t": 10000000, + "10gbase-cx4": 10000000, + "1000base-x-gbic": 100000000, + "1000base-x-sfp": 100000000, + "10gbase-x-sfpp": 10000000, + "10gbase-x-xfp": 10000000, + "10gbase-x-xenpak": 10000000, + "10gbase-x-x2": 10000000, + "25gbase-x-sfp28": 25000000, + "40gbase-x-qsfpp": 40000000, + "50gbase-x-sfp28": 50000000, + "100gbase-x-cfp": 100000000, + "100gbase-x-cfp2": 100000000, + "200gbase-x-cfp2": 200000000, + "100gbase-x-cfp4": 100000000, + "100gbase-x-cpak": 100000000, + "100gbase-x-qsfp28": 100000000, + "200gbase-x-qsfp56": 200000000, + "400gbase-x-qsfpdd": 400000000, + "400gbase-x-osfp,": 400000000, +} diff --git a/nautobot_ssot/integrations/device42/diffsync/adapters/__init__.py b/nautobot_ssot/integrations/device42/diffsync/adapters/__init__.py new file mode 100644 index 000000000..4717bb178 --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/adapters/__init__.py @@ -0,0 +1 @@ +"""Adapter classes for loading DiffSyncModels with data from Device42 or Nautobot.""" diff --git a/nautobot_ssot/integrations/device42/diffsync/adapters/device42.py b/nautobot_ssot/integrations/device42/diffsync/adapters/device42.py new file mode 100644 index 000000000..ff2f414b2 --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/adapters/device42.py @@ -0,0 +1,1099 @@ +"""DiffSync adapter for Device42.""" + +import re +from decimal import Decimal +from typing import List + +from diffsync import DiffSync +from diffsync.exceptions import ObjectAlreadyExists, ObjectNotFound +from django.utils.text import slugify +from nautobot.core.settings_funcs import is_truthy +from netutils.bandwidth import name_to_bits +from netutils.dns import fqdn_to_ip, is_fqdn_resolvable +from nautobot.extras.jobs import Job +from nautobot_ssot.integrations.device42.constant import PLUGIN_CFG +from nautobot_ssot.integrations.device42.diffsync.models.base import assets, circuits, dcim, ipam +from nautobot_ssot.integrations.device42.utils.device42 import ( + get_facility, + get_intf_type, + get_intf_status, + get_netmiko_platform, + get_custom_field_dict, + load_vlan, +) +from nautobot_ssot.integrations.device42.utils.nautobot import determine_vc_position + + +def sanitize_string(san_str: str): + """Sanitize string to ensure it doesn't have invisible characters.""" + return san_str.replace("\u200b", "").replace("\r", "").rstrip("-") + + +def get_circuit_status(status: str) -> str: + """Map Device42 Status to Nautobot Status. + + Args: + status (str): Device42 Status to be mapped. + + Returns: + str: Device42 mapped Status. + """ + STATUS_MAP = { + "Production": "Active", + "Provisioning": "Provisioning", + "Canceled": "Deprovisioning", + "Decommissioned": "Decommissioned", + } + if status in STATUS_MAP: + return STATUS_MAP[status] + else: + return "Offline" + + +def get_site_from_mapping(device_name: str) -> str: + """Method to map a Device to a Site based upon their name using a regex pattern in the settings. + + This works in conjunction with the `hostname_mapping` setting to have a Device assigned to a Site by hostname. This is done using a regex pattern mapped to the Site slug. + + Args: + device_name (str): Name of the Device to be matched. Must match one of the regex patterns provided to get a response. + + Returns: + str: The Site slug of the associated Site for the Device in the mapping. Returns blank string if match not found. + """ + for _entry in PLUGIN_CFG["hostname_mapping"]: + for _mapping, _slug in _entry.items(): + site_match = re.match(_mapping, device_name) + if site_match: + return _slug + return "" + + +def get_dns_a_record(dev_name: str): + """Method to obtain A record for a Device. + + Args: + dev_name (str): Name of Device to perform DNS query for. + + Returns: + str: A record for Device if exists, else False. + """ + if is_fqdn_resolvable(dev_name): + return fqdn_to_ip(dev_name) + else: + return False + + +class Device42Adapter(DiffSync): + """DiffSync adapter using requests to communicate to Device42 server.""" + + building = dcim.Building + room = dcim.Room + rack = dcim.Rack + vendor = dcim.Vendor + hardware = dcim.Hardware + cluster = dcim.Cluster + device = dcim.Device + port = dcim.Port + vrf = ipam.VRFGroup + subnet = ipam.Subnet + ipaddr = ipam.IPAddress + vlan = ipam.VLAN + conn = dcim.Connection + provider = circuits.Provider + circuit = circuits.Circuit + patchpanel = assets.PatchPanel + patchpanelfrontport = assets.PatchPanelFrontPort + patchpanelrearport = assets.PatchPanelRearPort + + top_level = [ + "vrf", + "subnet", + "vendor", + "hardware", + "building", + "vlan", + "cluster", + "device", + "patchpanel", + "patchpanelrearport", + "patchpanelfrontport", + "ipaddr", + "provider", + "circuit", + "conn", + ] + + def __init__(self, *args, job: Job, sync=None, client, **kwargs): + """Initialize Device42Adapter. + + Args: + job (Job): Nautobot Job. + sync (object, optional): Nautobot DiffSync. Defaults to None. + client (object): Device42API client connection object. + """ + super().__init__(*args, **kwargs) + self.job = job + self.sync = sync + self.device42_hardware_dict = {} + self.device42 = client + self.device42_clusters = self.device42.get_cluster_members() + self.rack_elevations = {} + + # mapping of SiteCode (facility) to Building name + self.d42_building_sitecode_map = {} + # mapping of Building PK to Building info + self.d42_building_map = self.device42.get_building_pks() + # mapping of Customer PK to Customer info + self.d42_customer_map = self.device42.get_customer_pks() + # mapping of Room PK to Room info + self.d42_room_map = self.device42.get_room_pks() + # mapping of Rack PK to Rack info + self.d42_rack_map = self.device42.get_rack_pks() + # mapping of VLAN PK to VLAN name and ID + self.d42_vlan_map = self.device42.get_vlan_info() + # mapping of Device PK to Device name + self.d42_device_map = self.device42.get_device_pks() + # mapping of Port PK to Port name + self.d42_port_map = self.device42.get_port_pks() + # mapping of Vendor PK to Vendor info + self.d42_vendor_map = self.device42.get_vendor_pks() + # default custom fields for IP Address + self.d42_ipaddr_default_cfs = self.device42.get_ipaddr_default_custom_fields() + + def get_building_for_device(self, dev_record: dict) -> str: + """Method to determine the Building (Site) for a Device. + + Args: + dev_record (dict): Dictionary of Device information from Device42. Needs to have name, customer, and building keys depending upon enabled plugin settings. + + Returns: + str: Slugified version of the Building (Site) for a Device. + """ + _building = False + if PLUGIN_CFG.get("hostname_mapping") and len(PLUGIN_CFG["hostname_mapping"]) > 0: + _building = get_site_from_mapping(device_name=dev_record["name"]) + + if not _building: + if ( + PLUGIN_CFG.get("customer_is_facility") + and dev_record.get("customer") + and dev_record["customer"] in self.d42_building_sitecode_map + ): + _building = self.d42_building_sitecode_map[dev_record["customer"].upper()] + else: + _building = dev_record.get("building") + if _building is not None: + return slugify(_building) + return "" + + def load_buildings(self): + """Load Device42 buildings.""" + for record in self.device42.get_buildings(): + self.job.log_info(message=f"Loading {record['name']} building from Device42.") + _tags = record["tags"] if record.get("tags") else [] + if len(_tags) > 1: + _tags.sort() + building = self.building( + name=record["name"], + address=sanitize_string(record["address"]) if record.get("address") else "", + latitude=float(round(Decimal(record["latitude"] if record["latitude"] else 0.0), 6)), + longitude=float(round(Decimal(record["longitude"] if record["longitude"] else 0.0), 6)), + contact_name=record["contact_name"] if record.get("contact_name") else "", + contact_phone=record["contact_phone"] if record.get("contact_phone") else "", + rooms=record["rooms"] if record.get("rooms") else [], + custom_fields=get_custom_field_dict(record["custom_fields"]), + tags=_tags, + uuid=None, + ) + _facility = get_facility(diffsync=self, tags=_tags) + if _facility: + self.d42_building_sitecode_map[_facility.upper()] = record["name"] + try: + self.add(building) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=f"{record['name']} is already loaded. {err}") + + def load_rooms(self): + """Load Device42 rooms.""" + for record in self.device42.get_rooms(): + self.job.log_info(message=f"Loading {record['name']} room from Device42.") + _tags = record["tags"] if record.get("tags") else [] + if len(_tags) > 1: + _tags.sort() + if record.get("building"): + if record["building"] not in self.rack_elevations: + self.rack_elevations[record["building"]] = {} + if record["name"] not in self.rack_elevations[record["building"]]: + self.rack_elevations[record["building"]][record["name"]] = {} + room = self.room( + name=record["name"], + building=record["building"], + notes=record["notes"] if record.get("notes") else "", + custom_fields=get_custom_field_dict(record["custom_fields"]), + tags=_tags, + uuid=None, + ) + try: + self.add(room) + _site = self.get(self.building, record.get("building")) + _site.add_child(child=room) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=f"{record['name']} is already loaded. {err}") + else: + self.job.log_warning(message=f"{record['name']} is missing Building and won't be imported.") + continue + + def load_racks(self): + """Load Device42 racks.""" + self.job.log_info(message="Loading racks from Device42.") + for record in self.device42.get_racks(): + _tags = record["tags"] if record.get("tags") else [] + if len(_tags) > 1: + _tags.sort() + if record.get("building") and record.get("room"): + self.rack_elevations[record["building"]][record["room"]][record["name"]] = { + slot: [] for slot in range(1, record["size"] + 1) + } + rack = self.rack( + name=record["name"], + building=record["building"], + room=record["room"], + height=record["size"] if record.get("size") else 1, + numbering_start_from_bottom=record["numbering_start_from_bottom"], + custom_fields=get_custom_field_dict(record["custom_fields"]), + tags=_tags, + uuid=None, + ) + try: + self.add(rack) + _room = self.get( + self.room, {"name": record["room"], "building": record["building"], "room": record["room"]} + ) + _room.add_child(child=rack) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=f"Rack {record['name']} already exists. {err}") + else: + self.job.log_warning(message=f"{record['name']} is missing Building and Room and won't be imported.") + continue + + def load_vendors(self): + """Load Device42 vendors.""" + for _vendor in self.device42.get_vendors(): + self.job.log_info(message=f"Loading vendor {_vendor['name']} from Device42.") + vendor = self.vendor( + name=_vendor["name"], + custom_fields=get_custom_field_dict(_vendor["custom_fields"]), + uuid=None, + ) + self.add(vendor) + + def load_hardware_models(self): + """Load Device42 hardware models.""" + for _model in self.device42.get_hardware_models(): + self.job.log_info(message=f"Loading hardware model {_model['name']} from Device42.") + if _model.get("manufacturer"): + model = self.hardware( + name=sanitize_string(_model["name"]), + manufacturer=_model["manufacturer"] if _model.get("manufacturer") else "Unknown", + size=float(round(_model["size"])) if _model.get("size") else 1.0, + depth=_model["depth"] if _model.get("depth") else "Half Depth", + part_number=_model["part_no"] if _model.get("part_no") else "", + custom_fields=get_custom_field_dict(_model["custom_fields"]), + uuid=None, + ) + try: + self.add(model) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=f"Hardware model already exists. {err}") + continue + + def get_cluster_host(self, device: str) -> str: + """Get name of cluster host if device is in a cluster. + + Args: + device (str): Name of device to see if part of cluster. + + Returns: + str: Name of cluster device is part of or empty string. + """ + for _cluster, _info in self.device42_clusters.items(): + if device in _info["members"]: + return _cluster + return "" + + def load_cluster(self, cluster_info: dict): + """Load Device42 clusters into DiffSync model. + + Args: + cluster_info (dict): Information of cluster to be added to DiffSync model. + + Returns: + models.Cluster: Cluster model that has been created or found. + """ + try: + _cluster = self.get(self.cluster, cluster_info["name"][:64]) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=f"Cluster {cluster_info['name']} already has been added. {err}") + except ObjectNotFound: + self.job.log_info(message=f"Cluster {cluster_info['name']} being loaded from Device42.") + _clus = self.device42_clusters[cluster_info["name"]] + _tags = cluster_info["tags"] if cluster_info.get("tags") else [] + if PLUGIN_CFG.get("ignore_tag") and PLUGIN_CFG["ignore_tag"] in _tags: + self.job.log_warning(message=f"Cluster {cluster_info['name']} has ignore tag so skipping.") + return + if len(_tags) > 1: + _tags.sort() + _cluster = self.cluster( + name=cluster_info["name"][:64], + members=_clus["members"], + tags=_tags, + custom_fields=get_custom_field_dict(cluster_info["custom_fields"]), + uuid=None, + ) + self.add(_cluster) + # Add master device to hold stack info like intfs and IPs + _building = self.get_building_for_device(dev_record={**_clus, **cluster_info}) + _device = self.device( + name=cluster_info["name"][:64], + building=_building if _building else "", + rack="", + rack_orientation="rear", + room="", + hardware=sanitize_string(_clus["hardware"]), + os=get_netmiko_platform(_clus["os"][:100]) if _clus.get("os") else "", + in_service=cluster_info.get("in_service"), + tags=_tags, + cluster_host=cluster_info["name"][:64], + master_device=True, + serial_no="", + custom_fields=get_custom_field_dict(cluster_info["custom_fields"]), + rack_position=None, + os_version="", + vc_position=1, + uuid=None, + ) + self.add(_device) + + def load_devices_and_clusters(self): + """Load Device42 devices.""" + self.job.log_info(message="Retrieving devices from Device42.") + _devices = self.device42.get_devices() + + # Add all Clusters first + for _record in _devices: + if _record.get("type") == "cluster" and _record.get("name") in self.device42_clusters: + self.load_cluster(_record) + + # Then iterate through again and add Devices + for _record in _devices: + rack_position, model = None, None + self.job.log_info(message=f"Device {_record['name']} being loaded.") + _building = self.get_building_for_device(dev_record=_record) + # only consider devices that have a Building + if _building == "": + self.job.log_warning( + message=f"Device {_record['name']} can't be loaded as we're unable to find associated Building." + ) + continue + if _record.get("type") != "cluster" and _record.get("hw_model"): + try: + model = self.get(self.hardware, sanitize_string(_record["hw_model"])) + except ObjectNotFound as err: + self.job.log_warning( + message=f"Unable to find hardware model {_record['hw_model']} for {_record['name']} so it will not be loaded. {err}" + ) + continue + _tags = _record["tags"] if _record.get("tags") else [] + if PLUGIN_CFG.get("ignore_tag") and PLUGIN_CFG["ignore_tag"] in _tags: + self.job.log_warning( + message=f"Skipping loading {_record['name']} as it has the specified ignore tag." + ) + continue + if len(_tags) > 1: + _tags.sort() + # Get size of model to ensure appropriate number of rack Us are filled + if model: + model_size = int(model.size) + if _record.get("start_at"): + rack_position = int(_record["start_at"]) + for slot in range(rack_position, rack_position + model_size + 1): + if _building not in self.rack_elevations: + self.rack_elevations[_building] = {} + + if _record["room"] not in self.rack_elevations[_building]: + self.rack_elevations[_building][_record["room"]] = {} + + if _record["rack"] not in self.rack_elevations[_building][_record["room"]]: + self.rack_elevations[_building][_record["room"]][_record["rack"]] = {} + + if slot not in self.rack_elevations[_building][_record["room"]][_record["rack"]]: + self.rack_elevations[_building][_record["room"]][_record["rack"]][slot] = [] + + self.rack_elevations[_building][_record["room"]][_record["rack"]][slot].append( + _record["name"][:64] + ) + + if ( + len( + self.rack_elevations[_building][_record["room"]][_record["rack"]][ + int(_record["start_at"]) + ] + ) + > 1 + ): + rack_position = None + _device = self.device( + name=_record["name"][:64], + building=_building, + room=_record["room"] if _record.get("room") else "", + rack=_record["rack"] if _record.get("rack") else "", + rack_position=rack_position, + rack_orientation="front" if _record.get("orientation") == 1 else "rear", + hardware=sanitize_string(_record["hw_model"]), + os=get_netmiko_platform(_record["os"][:100]) if _record.get("os") else "", + os_version=re.sub(r"^[a-zA-Z]+\s", "", _record["osver"]) if _record.get("osver") else "", + in_service=_record.get("in_service"), + serial_no=_record["serial_no"], + master_device=False, + tags=_tags, + custom_fields=get_custom_field_dict(_record["custom_fields"]), + cluster_host=None, + vc_position=None, + uuid=None, + ) + self.assign_cluster_host(_record, _device) + try: + self.add(_device) + except ObjectAlreadyExists as err: + self.job.log_warning(message=f"Duplicate device attempting to be added. {err}") + continue + elif _record.get("type") != "cluster" and not _record.get("hw_model"): + self.job.log_warning(message=f"Device {_record['name']}'s hardware isn't specified so won't be loaded.") + + def assign_cluster_host(self, _record, _device): + """Assign cluster host to loaded Device if found. + + Args: + _record (dict): Device record from Device42 API. + _device (NautobotDevice): NautobotDevice DiffSync model. + """ + cluster_host = self.get_cluster_host(_record["name"]) + if cluster_host: + if not is_truthy(self.device42_clusters[cluster_host]["is_network"]): + self.job.log_warning( + message=f"{cluster_host} has network device members but isn't marked as network device. This should be corrected in Device42." + ) + _device.cluster_host = cluster_host + if _device.name == cluster_host: + _device.master_device = True + _device.vc_position = 1 + else: + _device.vc_position = determine_vc_position( + vc_map=self.device42_clusters, virtual_chassis=cluster_host, device_name=_record["name"] + ) + + def assign_version_to_master_devices(self): + """Update all Master Devices in Cluster to have OS Version of first device in stack.""" + for cluster in self.get_all(self.cluster): + try: + first_in_stack = self.get(self.device, self.device42_clusters[cluster.name]["members"][0]) + try: + master_device = self.get(self.device, cluster.name) + if first_in_stack.os_version != "": + self.job.log_info( + message=f"Assigning {first_in_stack.os_version} version to {master_device.name}." + ) + master_device.os_version = first_in_stack.os_version + else: + self.job.log_info( + message=f"Software version for {first_in_stack.name} is blank so will not assign version to {master_device.name}." + ) + except ObjectNotFound: + self.job.log_warning(message=f"Unable to find VC Master Device {cluster.name} to assign version.") + except KeyError as err: + self.job.log_warning(message=f"Unable to find cluster host in device42_clusters dictionary. {err}") + + def load_ports(self): + """Load Device42 ports.""" + vlan_ports = self.device42.get_ports_with_vlans() + no_vlan_ports = self.device42.get_ports_wo_vlans() + merged_ports = self.filter_ports(vlan_ports, no_vlan_ports) + default_cfs = self.device42.get_port_default_custom_fields() + _cfs = self.device42.get_port_custom_fields() + for _port in merged_ports: + if _port.get("second_device_fk"): + _device_name = self.d42_device_map[_port["second_device_fk"]]["name"] + else: + _device_name = _port["device_name"] + if _port.get("port_name"): + _port_name = _port["port_name"][:63].strip() + else: + _port_name = _port["hwaddress"] + try: + _dev = self.get(self.device, _device_name) + except ObjectNotFound: + if self.job.kwargs.get("debug"): + self.job.log_warning( + message=f"Skipping loading of Port {_port_name} for Device {_device_name} as device was not loaded." + ) + continue + if self.job.kwargs.get("debug"): + self.job.log_info(message=f"Loading Port {_port_name} for Device {_device_name}") + _tags = _port["tags"].split(",") if _port.get("tags") else [] + if len(_tags) > 1: + _tags.sort() + _status = get_intf_status(port=_port) + try: + self.get(self.port, {"device": _device_name, "name": _port_name}) + except ObjectNotFound: + new_port = self.port( + name=_port_name, + device=_device_name, + enabled=is_truthy(_port["up_admin"]), + mtu=_port["mtu"] if _port.get("mtu") in range(1, 65537) else 1500, + description=_port["description"], + mac_addr=_port["hwaddress"][:13], + type=get_intf_type(intf_record=_port), + tags=_tags, + mode="access", + status=_status, + vlans=[], + custom_fields=default_cfs, + uuid=None, + ) + if _port.get("vlan_pks"): + _vlans = [] + for _pk in _port["vlan_pks"]: + if _pk in self.d42_vlan_map and self.d42_vlan_map[_pk]["vid"] != 0: + # Need to ensure that there's a VLAN loaded for every one that's being tagged. + try: + self.get( + self.vlan, {"vlan_id": self.d42_vlan_map[_pk]["vid"], "building": _dev.building} + ) + except ObjectNotFound: + load_vlan(diffsync=self, vlan_id=self.d42_vlan_map[_pk]["vid"], site_name=_dev.building) + _vlans.append(self.d42_vlan_map[_pk]["vid"]) + new_port.vlans = sorted(set(_vlans)) + if len(_vlans) > 1: + new_port.mode = "tagged" + if _device_name in _cfs and _cfs[_device_name].get(_port_name): + new_port.custom_fields = _cfs[_device_name][_port_name] + self.add(new_port) + _dev.add_child(new_port) + + @staticmethod + def filter_ports(vlan_ports: List[dict], no_vlan_ports: List[dict]) -> List[dict]: + """Method to combine lists of ports while removing duplicates. + + Args: + vlan_ports (List[dict]): List of Ports with tagged VLANs. + no_vlan_ports (List[dict]): List of Ports without VLANs. + + Returns: + List[dict]: Merged list of Ports with duplicates removed. + """ + no_vlan_ports_only = [] + for no_vlan_port in no_vlan_ports: + for vlan_port in vlan_ports: + if no_vlan_port["netport_pk"] == vlan_port["netport_pk"]: + break + else: + no_vlan_ports_only.append(no_vlan_port) + return vlan_ports + no_vlan_ports_only + + def load_vrfgroups(self): + """Load Device42 VRFGroups.""" + for _grp in self.device42.get_vrfgroups(): + self.job.log_info(message=f"Loading VRF group {_grp['name']} from Device42.") + try: + _tags = _grp["tags"] if _grp.get("tags") else [] + if len(_tags) > 1: + _tags.sort() + new_vrf = self.vrf( + name=_grp["name"], + description=_grp["description"], + tags=_tags, + custom_fields=get_custom_field_dict(_grp["custom_fields"]), + uuid=None, + ) + self.add(new_vrf) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=f"VRF Group {_grp['name']} already exists. {err}") + continue + + def load_subnets(self): + """Load Device42 Subnets.""" + self.job.log_info(message="Loading Subnets from Device42.") + default_cfs = self.device42.get_subnet_default_custom_fields() + _cfs = self.device42.get_subnet_custom_fields() + for _pf in self.device42.get_subnets(): + _tags = _pf["tags"].split(",") if _pf.get("tags") else [] + if len(_tags) > 1: + _tags.sort() + if _pf["mask_bits"] != 0: + try: + new_pf = self.subnet( + network=_pf["network"], + mask_bits=_pf["mask_bits"], + description=_pf["name"], + vrf=_pf["vrf"], + tags=_tags, + custom_fields=default_cfs, + uuid=None, + ) + if _cfs.get(f"{_pf['network']}/{_pf['mask_bits']}"): + new_pf.custom_fields = _cfs[f"{_pf['network']}/{_pf['mask_bits']}"] + self.add(new_pf) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=f"Subnet {_pf['network']} {_pf['mask_bits']} {_pf['vrf']} {err}") + continue + else: + if self.job.kwargs.get("debug"): + self.job.log_warning( + message=f"Unable to import Subnet with a 0 mask bits. {_pf['network']} {_pf['name']}." + ) + continue + + def load_ip_addresses(self): + """Load Device42 IP Addresses.""" + self.job.log_info(message="Loading IP Addresses from Device42.") + _cfs = self.device42.get_ipaddr_custom_fields() + for _ip in self.device42.get_ip_addrs(): + _ipaddr = f"{_ip['ip_address']}/{str(_ip['netmask'])}" + try: + _device_name, _port_name = "", "" + if _ip.get("netport_pk") and _ip["netport_pk"] in self.d42_port_map: + port_pk = _ip["netport_pk"] + if self.d42_port_map[port_pk].get("second_device_fk"): + secondary_device_fk = self.d42_port_map[port_pk]["second_device_fk"] + _device_name = self.d42_device_map[secondary_device_fk]["name"] + self.job.log_info( + message=f"Primary: {self.d42_port_map[port_pk]['device']}/Secondary: {_device_name} found for {_ipaddr}." + ) + else: + _device_name = self.d42_port_map[port_pk]["device"] + if self.d42_port_map[port_pk].get("port"): + _port_name = self.d42_port_map[port_pk]["port"] + else: + _port_name = self.d42_port_map[port_pk]["hwaddress"] + try: + self.get(self.device, _device_name) + except ObjectNotFound: + # if the Device isn't being imported there's no reason to have the Device name and interface for it to try and match + _device_name, _port_name = "", "" + _tags = sorted(_ip["tags"].split(",")) if _ip.get("tags") != "" else [] + new_ip = self.ipaddr( + address=_ipaddr, + available=_ip["available"], + label=_ip["label"] if _ip.get("label") else "", + device=_device_name, + interface=_port_name, + primary=False, + vrf=_ip["vrf"], + tags=_tags, + custom_fields=self.d42_ipaddr_default_cfs, + uuid=None, + ) + if _cfs.get(_ipaddr): + new_ip.custom_fields = _cfs[_ipaddr] + self.add(new_ip) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=f"IP Address {_ipaddr} already exists.{err}") + continue + + def load_vlans(self): + """Load Device42 VLANs.""" + _vlans = self.device42.get_vlans_with_location() + for _info in _vlans: + _vlan_name = _info["vlan_name"].strip() + building = None + if _info["vlan_pk"] in self.d42_vlan_map and self.d42_vlan_map[_info["vlan_pk"]].get("custom_fields"): + _cfs = get_custom_field_dict(self.d42_vlan_map[_info["vlan_pk"]]["custom_fields"]) + else: + _cfs = {} + tags = _info["tags"].split(",").sort() if _info.get("tags") else [] + if is_truthy(PLUGIN_CFG.get("customer_is_facility")) and _info.get("customer"): + building = self.d42_building_sitecode_map[_info["customer"].upper()] + elif _info.get("building"): + building = _info["building"] + load_vlan( + diffsync=self, + vlan_id=int(_info["vid"]), + site_name=slugify(building) if building else "Unknown", + vlan_name=_vlan_name, + description=_info["description"] if _info.get("description") else "", + custom_fields=_cfs, + tags=tags, + ) + + def load_connections(self): + """Load Device42 connections.""" + _port_conns = self.device42.get_port_connections() + devices = self.dict()["device"] + for _conn in _port_conns: + if _conn.get("second_src_device"): + if self.d42_device_map[_conn["second_src_device"]]["name"] not in devices: + continue + if self.d42_device_map[_conn["src_device"]]["name"] not in devices: + continue + try: + new_conn = self.conn( + src_device=self.d42_device_map[_conn["second_src_device"]]["name"] + if _conn.get("second_src_device") + else self.d42_device_map[_conn["src_device"]]["name"], + src_port=self.d42_port_map[_conn["src_port"]]["port"], + src_port_mac=self.d42_port_map[_conn["src_port"]]["hwaddress"], + src_type="interface", + dst_device=self.d42_port_map[_conn["dst_port"]]["device"], + dst_port=self.d42_port_map[_conn["dst_port"]]["port"], + dst_port_mac=self.d42_port_map[_conn["dst_port"]]["hwaddress"], + dst_type="interface", + tags=None, + uuid=None, + ) + self.add(new_conn) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=err) + continue + + def load_provider(self, provider_info: dict): + """Load Device42 Providers.""" + _prov = self.d42_vendor_map[provider_info["vendor_fk"]] + try: + self.get(self.provider, _prov.get("name")) + except ObjectNotFound: + new_provider = self.provider( + name=_prov["name"], + notes=_prov["notes"], + vendor_url=_prov["home_page"], + vendor_acct=_prov["account_no"][:30], + vendor_contact1=_prov["escalation_1"], + vendor_contact2=_prov["escalation_2"], + tags=None, + uuid=None, + ) + self.add(new_provider) + + def load_providers_and_circuits(self): + """Load Device42 Providrs and Telco Circuits.""" + _circuits = self.device42.get_telcocircuits() + origin_int, origin_dev, endpoint_int, endpoint_dev = False, False, False, False + ppanel_ports = self.device42.get_patch_panel_port_pks() + for _tc in _circuits: + self.load_provider(_tc) + if _tc["origin_type"] == "Device Port" and _tc["origin_netport_fk"] is not None: + origin_int = self.d42_port_map[_tc["origin_netport_fk"]]["port"] + origin_dev = self.d42_port_map[_tc["origin_netport_fk"]]["device"] + if _tc["end_point_type"] == "Device Port" and _tc["end_point_netport_fk"] is not None: + endpoint_int = self.d42_port_map[_tc["end_point_netport_fk"]]["port"] + endpoint_dev = self.d42_port_map[_tc["end_point_netport_fk"]]["device"] + if _tc["origin_type"] == "Patch panel port" and _tc["origin_patchpanelport_fk"] is not None: + origin_int = ppanel_ports[_tc["origin_patchpanelport_fk"]]["number"] + origin_dev = ppanel_ports[_tc["origin_patchpanelport_fk"]]["name"] + if _tc["end_point_type"] == "Patch panel port" and _tc["end_point_patchpanelport_fk"] is not None: + origin_int = ppanel_ports[_tc["end_point_patchpanelport_fk"]]["number"] + origin_dev = ppanel_ports[_tc["end_point_patchpanelport_fk"]]["name"] + new_circuit = self.circuit( + circuit_id=_tc["circuit_id"], + provider=self.d42_vendor_map[_tc["vendor_fk"]]["name"], + notes=_tc["notes"], + type=_tc["type_name"], + status=get_circuit_status(_tc["status"]), + install_date=_tc["turn_on_date"] if _tc.get("turn_on_date") else _tc["provision_date"], + origin_int=origin_int if origin_int else None, + origin_dev=origin_dev if origin_dev else None, + endpoint_int=endpoint_int if endpoint_int else None, + endpoint_dev=endpoint_dev if endpoint_dev else None, + bandwidth=name_to_bits(f"{_tc['bandwidth']}{_tc['unit'].capitalize()}") / 1000, + tags=_tc["tags"].split(",") if _tc.get("tags") else [], + uuid=None, + ) + self.add(new_circuit) + # Add Connection from A side connection Device to Circuit + if origin_dev and origin_int: + a_side_conn = self.conn( + src_device=origin_dev, + src_port=origin_int, + src_port_mac=self.d42_port_map[_tc["origin_netport_fk"]]["hwaddress"] + if _tc["origin_type"] == "Device" + else None, + src_type="interface" if _tc["origin_type"] == "Device Port" else "patch panel", + dst_device=_tc["circuit_id"], + dst_port=_tc["circuit_id"], + dst_type="circuit", + dst_port_mac=None, + tags=None, + uuid=None, + ) + self.add(a_side_conn) + # Add Connection from Z side connection Circuit to Device + if endpoint_dev and endpoint_int: + z_side_conn = self.conn( + src_device=_tc["circuit_id"], + src_port=_tc["circuit_id"], + src_type="circuit", + dst_device=endpoint_dev, + dst_port=endpoint_int, + dst_port_mac=self.d42_port_map[_tc["end_point_netport_fk"]]["hwaddress"] + if _tc["end_point_type"] == "Device" + else None, + dst_type="interface" if _tc["end_point_type"] == "Device Port" else "patch panel", + src_port_mac=None, + tags=None, + uuid=None, + ) + self.add(z_side_conn) + + def check_dns(self): + """Method to check if a Device has a DNS record and assign as primary if so.""" + for _device in self.store.get_all(model=dcim.Device): + if not re.search(r"\s-\s\w+\s?\d+", _device.name) and not re.search( + r"AP[A-F0-9]{4}\.[A-F0-9]{4}.[A-F0-9]{4}", _device.name + ): + self.set_primary_from_dns(dev_name=_device.name) + else: + self.job.log_warning(message=f"Skipping {_device.name} due to invalid Device name.") + continue + + def get_management_intf(self, dev_name: str): + """Method to find a Device's management interface or create one if one doesn't exist. + + Args: + dev_name (str): Name of Device to find Management interface. + + Returns: + Port: DiffSyncModel Port object that's assumed to be Management interface if found. False if not found. + """ + try: + _intf = self.get(self.port, {"device": dev_name, "name": "mgmt0"}) + except ObjectNotFound: + try: + _intf = self.get(self.port, {"device": dev_name, "name": "management"}) + except ObjectNotFound: + try: + _intf = self.get(self.port, {"device": dev_name, "name": "management0"}) + except ObjectNotFound: + try: + _intf = self.get(self.port, {"device": dev_name, "name": "Management"}) + except ObjectNotFound: + return False + return _intf + + def add_management_interface(self, dev_name: str): + """Method to add a Management interface DiffSyncModel object. + + Args: + dev_name (str): Name of Device to find Management interface. + """ + _intf = self.port( + name="Management", + device=dev_name, + type="other", + enabled=True, + description="Interface added by script for Management of device using DNS A record.", + mode="access", + mtu=1500, + mac_addr="", + custom_fields=self.device42.get_port_default_custom_fields(), + tags=[], + status="active", + uuid=None, + ) + try: + self.add(_intf) + _device = self.get(self.device, dev_name) + _device.add_child(_intf) + return _intf + except ObjectAlreadyExists as err: + self.job.log_warning(message=f"Management interface for {dev_name} already exists. {err}") + + def set_primary_from_dns(self, dev_name: str): + """Method to resolve Device FQDNs A records into an IP and set primary IP for that Device to it if found. + + Checks if `use_dns` setting variable is `True`. + + Args: + dev_name (str): Name of Device to perform DNS query on. + """ + _devname = re.search(r"[a-zA-Z0-9\.\/\?\:\-_=#]+\.[a-zA-Z]{2,6}", dev_name) + if _devname: + _devname = _devname.group() + else: + return "" + _a_record = get_dns_a_record(dev_name=_devname) + if _a_record: + self.job.log_info(message=f"A record found for {_devname} {_a_record}.") + _ip = self.find_ipaddr(address=_a_record) + mgmt_intf = self.get_management_intf(dev_name=dev_name) + if not mgmt_intf: + mgmt_intf = self.add_management_interface(dev_name=dev_name) + if not _ip: + _ip = self.add_ipaddr(address=f"{_a_record}/32", dev_name=dev_name, interface=mgmt_intf.name) + if mgmt_intf and _ip.device != dev_name: + _ip.device = dev_name + _ip.interface = mgmt_intf.name + _ip.primary = True + else: + _ip.primary = True + else: + self.job.log_warning(message=f"A record not found for {_devname}.") + + def find_ipaddr(self, address: str): + """Method to find IPAddress DiffSyncModel object.""" + if ":" in address: + bits = 128 + else: + bits = 32 + + while bits > 0: + _addr = f"{address}/{bits}" + for _vrf in self.get_all("vrf"): + try: + return self.get(self.ipaddr, {"address": _addr, "vrf": _vrf.name}) + except ObjectNotFound: + pass + try: + return self.get(self.ipaddr, {"address": _addr, "vrf": None}) + except ObjectNotFound: + bits = bits - 1 + return False + + def add_ipaddr(self, address: str, dev_name: str, interface: str): + """Method to add IPAddress DiffSyncModel object if one isn't found. + + Used in conjunction with the `use_dns` feature. + """ + _ip = self.ipaddr( + address=address, + available=False, + device=dev_name, + interface=interface, + primary=True, + label="", + vrf=None, + tags=[], + custom_fields=self.d42_ipaddr_default_cfs, + uuid=None, + ) + self.add(_ip) + return _ip + + def load_patch_panels_and_ports(self): + """Load Device42 Patch Panels and Patch Panel Ports.""" + panels = self.device42.get_patch_panels() + for panel in panels: + _building, _room, _rack = None, None, None + if PLUGIN_CFG.get("hostname_mapping") and len(PLUGIN_CFG["hostname_mapping"]) > 0: + _building = get_site_from_mapping(device_name=panel["name"]) + if not _building and PLUGIN_CFG.get("customer_is_facility") and panel["customer_fk"] is not None: + _building = slugify(self.d42_customer_map[panel["customer_fk"]]["name"]) + if not _building and panel["building_fk"] is not None: + _building = slugify(self.d42_building_map[panel["building_fk"]]["name"]) + if not _building and panel["calculated_building_fk"] is not None: + _building = slugify(self.d42_building_map[panel["calculated_building_fk"]]["name"]) + if panel["room_fk"] is not None: + _room = self.d42_room_map[panel["room_fk"]]["name"] + if not _room and panel["calculated_room_fk"] is not None: + _room = self.d42_room_map[panel["calculated_room_fk"]]["name"] + if panel["rack_fk"] is not None: + _rack = self.d42_rack_map[panel["rack_fk"]]["name"] + if not _rack and panel["calculated_rack_fk"] is not None: + _rack = self.d42_rack_map[panel["calculated_rack_fk"]]["name"] + if _building is None and _room is None and _rack is None: + if self.job.kwargs.get("debug"): + self.job.log_debug( + message=f"Unable to determine Site, Room, or Rack for patch panel {panel['name']} so it will NOT be imported." + ) + continue + try: + new_hw = self.get(self.hardware, panel["model_name"]) + except ObjectNotFound: + new_hw = self.hardware( + name=panel["model_name"], + manufacturer=panel["vendor"], + size=panel["size"] if panel.get("size") else 1.0, + depth="Full Depth" if panel["depth"] == 1 else "Half Depth", + part_number="", + custom_fields={}, + uuid=None, + ) + self.add(new_hw) + try: + new_pp = self.get(self.patchpanel, panel["name"]) + except ObjectNotFound: + new_pp = self.patchpanel( + name=panel["name"], + in_service=panel["in_service"], + vendor=panel["vendor"], + model=panel["model_name"], + size=panel["size"] if panel.get("size") else 1.0, + position=panel["position"], + orientation="front" if panel.get("orientation") == "Front" else "rear", + num_ports=panel["number_of_ports"], + rack=_rack, + serial_no=panel["serial_no"], + building=_building, + room=_room, + uuid=None, + ) + self.add(new_pp) + ind = 1 + while ind <= panel["number_of_ports"]: + if "LC" in panel["port_type"]: + port_type = "lc" + elif "FC" in panel["port_type"]: + port_type = "fc" + else: + port_type = "8p8c" + front_intf = self.patchpanelfrontport( + name=f"{ind}", + patchpanel=panel["name"], + port_type=port_type, + uuid=None, + ) + rear_intf = self.patchpanelrearport( + name=f"{ind}", + patchpanel=panel["name"], + port_type=port_type, + uuid=None, + ) + try: + self.add(front_intf) + self.add(rear_intf) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning( + message=f"Patch panel port {ind} for {panel['name']} is already loaded. {err}" + ) + ind = ind + 1 + + def load(self): + """Load data from Device42.""" + self.load_buildings() + self.load_rooms() + self.load_racks() + self.load_vendors() + self.load_hardware_models() + self.load_vrfgroups() + self.load_vlans() + self.load_subnets() + self.load_devices_and_clusters() + self.assign_version_to_master_devices() + self.load_ports() + self.load_ip_addresses() + if is_truthy(PLUGIN_CFG.get("use_dns")): + self.job.log_info(message="Checking DNS entries for all loaded Devices.") + self.check_dns() + self.load_providers_and_circuits() + self.load_patch_panels_and_ports() + # self.load_connections() diff --git a/nautobot_ssot/integrations/device42/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/device42/diffsync/adapters/nautobot.py new file mode 100644 index 000000000..17461a8f6 --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/adapters/nautobot.py @@ -0,0 +1,1004 @@ +"""DiffSync adapter class for Nautobot as source-of-truth.""" + +import ipaddress +from collections import defaultdict + +from diffsync import DiffSync +from diffsync.exceptions import ObjectAlreadyExists, ObjectNotFound +from django.core.exceptions import ValidationError +from django.db.models import ProtectedError +from nautobot.circuits.models import Circuit, CircuitTermination, Provider +from nautobot.dcim.models import ( + Cable, + Device, + DeviceRole, + DeviceType, + FrontPort, + Interface, + Manufacturer, + Platform, + Rack, + RackGroup, + RearPort, + Site, + VirtualChassis, +) +from nautobot.extras.jobs import Job +from nautobot.extras.models import Relationship, RelationshipAssociation, Status +from nautobot.ipam.models import VLAN, VRF, IPAddress, Prefix +from netutils.lib_mapper import ANSIBLE_LIB_MAPPER + +from nautobot_ssot.integrations.device42.constant import PLUGIN_CFG +from nautobot_ssot.integrations.device42.diffsync.models.nautobot import assets, circuits, dcim, ipam +from nautobot_ssot.integrations.device42.utils import nautobot + +try: + from nautobot_device_lifecycle_mgmt.models import SoftwareLCM + + LIFECYCLE_MGMT = True +except ImportError: + print("Device Lifecycle plugin isn't installed so will revert to CustomField for OS version.") + LIFECYCLE_MGMT = False + + +class NautobotAdapter(DiffSync): + """Nautobot adapter for DiffSync.""" + + building = dcim.NautobotBuilding + room = dcim.NautobotRoom + rack = dcim.NautobotRack + vendor = dcim.NautobotVendor + hardware = dcim.NautobotHardware + cluster = dcim.NautobotCluster + device = dcim.NautobotDevice + port = dcim.NautobotPort + vrf = ipam.NautobotVRFGroup + subnet = ipam.NautobotSubnet + ipaddr = ipam.NautobotIPAddress + vlan = ipam.NautobotVLAN + conn = dcim.NautobotConnection + provider = circuits.NautobotProvider + circuit = circuits.NautobotCircuit + patchpanel = assets.NautobotPatchPanel + patchpanelrearport = assets.NautobotPatchPanelRearPort + patchpanelfrontport = assets.NautobotPatchPanelFrontPort + + top_level = [ + "vrf", + "subnet", + "vendor", + "hardware", + "building", + "vlan", + "cluster", + "device", + "patchpanel", + "patchpanelrearport", + "patchpanelfrontport", + "ipaddr", + "provider", + "circuit", + "conn", + ] + + status_map = {} + platform_map = {} + site_map = {} + room_map = {} + rack_map = {} + vendor_map = {} + devicerole_map = {} + devicetype_map = {} + cluster_map = {} + device_map = {} + port_map = {} + vrf_map = {} + prefix_map = {} + ipaddr_map = {} + vlan_map = {} + circuit_map = {} + cable_map = {} + provider_map = {} + rp_map = {} + fp_map = {} + softwarelcm_map = {} + relationship_map = {} + + def __init__(self, *args, job: Job, sync=None, **kwargs): + """Initialize the Nautobot DiffSync adapter. + + Args: + job (Job): Nautobot Job. + sync (object, optional): Nautobot DiffSync. Defaults to None. + """ + super().__init__(*args, **kwargs) + self.job = job + self.sync = sync + self.objects_to_delete = defaultdict(list) + self.objects_to_create = defaultdict(list) + + def sync_complete(self, source: DiffSync, *args, **kwargs): + """Clean up function for DiffSync sync. + + Once the sync is complete, this function runs deleting any objects + from Nautobot that need to be deleted in a specific order. + + Args: + source (DiffSync): DiffSync + """ + if PLUGIN_CFG.get("delete_on_sync"): + for grouping in ( + "ipaddr", + "subnet", + "vrf", + "vlan", + "circuit", + "provider", + "cluster", + "port", + "device", + "patchpanel", + "device_type", + "manufacturer", + "rack", + "site", + ): + for nautobot_object in self.objects_to_delete[grouping]: + try: + if self.job.kwargs.get("debug"): + self.job.log_info(message=f"Deleting {nautobot_object}.") + nautobot_object.delete() + except ProtectedError: + self.job.log(f"Deletion failed protected object: {nautobot_object}") + self.objects_to_delete[grouping] = [] + + if len(self.objects_to_create["sites"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Sites in Nautobot") + Site.objects.bulk_create(self.objects_to_create["sites"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Sites in Nautobot.") + try: + for site in self.objects_to_create["sites"]: + site.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating site. {err}") + if len(self.objects_to_create["rooms"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Sites in Nautobot") + RackGroup.objects.bulk_create(self.objects_to_create["rooms"], batch_size=50) + else: + self.job.log_info(message="Performing creation of RackGroups in Nautobot") + try: + for room in self.objects_to_create["rooms"]: + room.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating room. {err}") + if len(self.objects_to_create["racks"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Racks in Nautobot") + Rack.objects.bulk_create(self.objects_to_create["racks"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Racks in Nautobot") + try: + for rack in self.objects_to_create["racks"]: + rack.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating rack. {err}") + if len(self.objects_to_create["vendors"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Manufacturers in Nautobot") + Manufacturer.objects.bulk_create(self.objects_to_create["vendors"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Manufacturers in Nautobot") + try: + for manu in self.objects_to_create["vendors"]: + manu.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating manufacturer. {err}") + if len(self.objects_to_create["devicetypes"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of DeviceTypes in Nautobot") + DeviceType.objects.bulk_create(self.objects_to_create["devicetypes"], batch_size=50) + else: + self.job.log_info(message="Performing creation of DeviceTypes in Nautobot") + try: + for _dt in self.objects_to_create["devicetypes"]: + _dt.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating device type. {err}") + if len(self.objects_to_create["deviceroles"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of DeviceRoles in Nautobot") + DeviceRole.objects.bulk_create(self.objects_to_create["deviceroles"], batch_size=50) + else: + self.job.log_info(message="Performing creation of DeviceRoles in Nautobot") + try: + for role in self.objects_to_create["deviceroles"]: + role.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating device role. {err}") + if len(self.objects_to_create["platforms"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Platforms in Nautobot") + Platform.objects.bulk_create(self.objects_to_create["platforms"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Platforms in Nautobot") + try: + for platform in self.objects_to_create["platforms"]: + platform.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating platform. {err}") + if len(self.objects_to_create["vrfs"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of VRFs in Nautobot") + VRF.objects.bulk_create(self.objects_to_create["vrfs"], batch_size=50) + else: + self.job.log_info(message="Performing creation of VRFs in Nautobot") + try: + for vrf in self.objects_to_create["vrfs"]: + vrf.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating VRF. {err}") + if len(self.objects_to_create["vlans"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of VLANs in Nautobot") + VLAN.objects.bulk_create(self.objects_to_create["vlans"], batch_size=50) + else: + self.job.log_info(message="Performing creation of VLANs in Nautobot") + try: + for vlan in self.objects_to_create["vlans"]: + vlan.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating VLAN. {err}") + if len(self.objects_to_create["prefixes"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Prefixes in Nautobot") + Prefix.objects.bulk_create(self.objects_to_create["prefixes"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Prefixes in Nautobot") + try: + for prefix in self.objects_to_create["prefixes"]: + prefix.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating VRF. {err}") + if len(self.objects_to_create["clusters"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Virtual Chassis in Nautobot") + VirtualChassis.objects.bulk_create(self.objects_to_create["clusters"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Virtual Chassis in Nautobot") + try: + for cluster in self.objects_to_create["clusters"]: + cluster.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating virtual chassis. {err}") + if len(self.objects_to_create["devices"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Devices in Nautobot") + Device.objects.bulk_create(self.objects_to_create["devices"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Devices in Nautobot") + try: + for dev in self.objects_to_create["devices"]: + dev.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with saving device {dev.name}. {err}") + except VirtualChassis.DoesNotExist as err: + self.job.log_warning(message=f"Error with creating device as VirtualChassis doesn't exist. {err}") + if len(self.objects_to_create["ports"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Interfaces in Nautobot") + Interface.objects.bulk_create(self.objects_to_create["ports"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Interfaces in Nautobot") + try: + for port in self.objects_to_create["ports"]: + port.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating interface. {err}") + except Device.DoesNotExist as err: + self.job.log_warning(message=f"Error with creating interface as Device doesn't exist. {err}") + if len(self.objects_to_create["rear_ports"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Rear Ports in Nautobot") + RearPort.objects.bulk_create(self.objects_to_create["rear_ports"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Rear Ports in Nautobot") + try: + for port in self.objects_to_create["rear_ports"]: + port.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating rear port. {err}") + if len(self.objects_to_create["front_ports"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Front Ports in Nautobot") + FrontPort.objects.bulk_create(self.objects_to_create["front_ports"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Front Ports in Nautobot") + try: + for port in self.objects_to_create["front_ports"]: + port.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating front port. {err}") + if len(self.objects_to_create["ipaddrs"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of IP Addresses in Nautobot") + IPAddress.objects.bulk_create(self.objects_to_create["ipaddrs"], batch_size=50) + else: + self.job.log_info(message="Performing creation of IP Addresses in Nautobot") + try: + for ipaddr in self.objects_to_create["ipaddrs"]: + ipaddr.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating IP address. {err}") + if len(self.objects_to_create["providers"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Providers in Nautobot") + Provider.objects.bulk_create(self.objects_to_create["providers"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Providers in Nautobot") + try: + for provider in self.objects_to_create["providers"]: + provider.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating provider. {err}") + if len(self.objects_to_create["circuits"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk create of Circuits in Nautobot") + Circuit.objects.bulk_create(self.objects_to_create["circuits"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Circuits in Nautobot") + try: + for circuit in self.objects_to_create["circuits"]: + circuit.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating circuit. {err}") + + # if len(self.objects_to_create["cables"]) > 0: + # self.job.log_info(message="Performing bulk create of Cables in Nautobot") + # Cable.objects.bulk_create(self.objects_to_create["cables"], batch_size=50) + + if len(self.objects_to_create["device_primary_ip"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Performing bulk update of device management IP addresses in Nautobot.") + device_primary_ip4_objs = [] + device_primary_ip6_objs = [] + for d in self.objects_to_create["device_primary_ip"]: + dev = Device.objects.get(id=d[0]) + ipaddr = IPAddress.objects.get(id=d[1]) + if ipaddr.family == 4: + dev.primary_ip4_id = d[1] + device_primary_ip4_objs.append(dev) + else: + dev.primary_ip6_id = d[1] + device_primary_ip6_objs.append(dev) + Device.objects.bulk_update(device_primary_ip4_objs, ["primary_ip4_id"], batch_size=50) + Device.objects.bulk_update(device_primary_ip6_objs, ["primary_ip6_id"], batch_size=50) + else: + self.job.log_info(message="Performing assignment of device management IP addresses in Nautobot") + for dev_ip in self.objects_to_create["device_primary_ip"]: + dev, ipaddr = None, None + try: + dev = Device.objects.get(id=dev_ip[0]) + except Device.DoesNotExist as err: + self.job.log_warning( + message=f"Unable to find Device {dev_ip[0].name} to assign primary IP. {err}" + ) + try: + ipaddr = IPAddress.objects.get(id=dev_ip[1]) + ipaddr.validated_save() + try: + if dev and ipaddr: + if ipaddr.assigned_object.device == dev: + if ipaddr.family == 4: + dev.primary_ip4_id = dev_ip[1] + else: + dev.primary_ip6_id = dev_ip[1] + dev.validated_save() + else: + self.job.log_warning( + message=f"IP Address doesn't show assigned to {dev} so can't mark primary." + ) + except ValidationError as err: + self.job.log_warning(message=f"Unable to assign primary IP to {dev}. {err}") + except IPAddress.DoesNotExist as err: + self.job.log_warning( + message=f"Unable to find IP Address {dev_ip[1].address} to assign primary IP. {err}" + ) + except ValidationError as err: + self.job.log_warning(message=f"Unable to save IP Address {dev_ip[1]} for {dev}. {err}") + + if len(self.objects_to_create["master_devices"]) > 0: + master_devices = [] + self.job.log_info(message="Performing assignment of master devices to Virtual Chassis in Nautobot") + for item in self.objects_to_create["master_devices"]: + new_vc = VirtualChassis.objects.get(id=item[0]) + new_vc.master = Device.objects.get(id=item[1]) + if self.job.kwargs["bulk_import"]: + master_devices.append(new_vc) + else: + new_vc.validated_save() + if self.job.kwargs["bulk_import"]: + VirtualChassis.objects.bulk_update(master_devices, ["master"], batch_size=50) + + if len(self.objects_to_create["tagged_vlans"]) > 0: + self.job.log_info(message="Assigning tagged VLANs to Ports in Nautobot.") + for item in self.objects_to_create["tagged_vlans"]: + port, tagged_vlans = item + port.tagged_vlans.set(tagged_vlans) + port.validated_save() + + if LIFECYCLE_MGMT: + if len(self.objects_to_create["softwarelcms"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info( + message="Performing bulk creation of Software Versions in Device Lifecycle plugin." + ) + SoftwareLCM.objects.bulk_create(self.objects_to_create["softwarelcms"], batch_size=50) + else: + self.job.log_info(message="Performing creation of Software Versions in Device Lifecycle plugin.") + try: + for softwarelcm in self.objects_to_create["softwarelcms"]: + softwarelcm.validated_save() + except ValidationError as err: + self.job.log_warning(message=f"Error with creating software version. {err}") + if len(self.objects_to_create["relationshipasscs"]) > 0: + if self.job.kwargs["bulk_import"]: + self.job.log_info(message="Creating Relationships between Devices and Software Versions.") + RelationshipAssociation.objects.bulk_create( + self.objects_to_create["relationshipasscs"], batch_size=50 + ) + else: + self.job.log_info( + message="Performing creation of Relationships between Devices and Software Versions." + ) + try: + for assc in self.objects_to_create["relationshipasscs"]: + assc.validated_save() + except ValidationError as err: + self.job.log_warning( + message=f"Error with creating relationships between device and software version. {err}" + ) + return super().sync_complete(source, *args, **kwargs) + + def load_sites(self): + """Add Nautobot Site objects as DiffSync Building models.""" + for site in Site.objects.all(): + self.site_map[site.slug] = site.id + try: + building = self.building( + name=site.name, + address=site.physical_address, + latitude=site.latitude, + longitude=site.longitude, + contact_name=site.contact_name, + contact_phone=site.contact_phone, + tags=nautobot.get_tag_strings(site.tags), + custom_fields=nautobot.get_custom_field_dict(site.get_custom_fields()), + uuid=site.id, + ) + self.add(building) + except AttributeError: + continue + + def load_rackgroups(self): + """Add Nautobot RackGroup objects as DiffSync Room models.""" + for _rg in RackGroup.objects.select_related("site").all(): + if _rg.site.slug not in self.room_map: + self.room_map[_rg.site.slug] = {} + if _rg.slug not in self.room_map[_rg.site.slug]: + self.room_map[_rg.site.slug][_rg.slug] = {} + self.room_map[_rg.site.slug][_rg.slug] = _rg.id + room = self.room( + name=_rg.name, + building=Site.objects.get(name=_rg.site).name, + notes=_rg.description, + custom_fields=nautobot.get_custom_field_dict(_rg.get_custom_fields()), + uuid=_rg.id, + ) + self.add(room) + _site = self.get(self.building, Site.objects.get(name=_rg.site).name) + _site.add_child(child=room) + + def load_racks(self): + """Add Nautobot Rack objects as DiffSync Rack models.""" + for rack in Rack.objects.select_related("site", "group").all(): + if rack.site.slug not in self.rack_map: + self.rack_map[rack.site.slug] = {} + if rack.group.slug not in self.rack_map[rack.site.slug]: + self.rack_map[rack.site.slug][rack.group.slug] = {} + if rack.name not in self.rack_map[rack.site.slug][rack.group.slug]: + self.rack_map[rack.site.slug][rack.group.slug][rack.name] = {} + self.rack_map[rack.site.slug][rack.group.slug][rack.name] = rack.id + try: + new_rack = self.rack( + name=rack.name, + building=rack.site.name, + room=rack.group.name, + height=rack.u_height, + numbering_start_from_bottom="no" if rack.desc_units else "yes", + tags=nautobot.get_tag_strings(rack.tags), + custom_fields=nautobot.get_custom_field_dict(rack.get_custom_fields()), + uuid=rack.id, + ) + self.add(new_rack) + _room = self.get(self.room, {"name": rack.group, "building": rack.site.name}) + _room.add_child(child=new_rack) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=err) + + def load_manufacturers(self): + """Add Nautobot Manufacturer objects as DiffSync Vendor models.""" + for manu in Manufacturer.objects.all(): + self.vendor_map[manu.slug] = manu.id + new_manu = self.vendor( + name=manu.name, + custom_fields=nautobot.get_custom_field_dict(manu.get_custom_fields()), + uuid=manu.id, + ) + self.add(new_manu) + + def load_device_types(self): + """Add Nautobot DeviceType objects as DiffSync Hardware models.""" + for _dt in DeviceType.objects.select_related("manufacturer").all(): + self.devicetype_map[_dt.slug] = _dt.id + dtype = self.hardware( + name=_dt.model, + manufacturer=_dt.manufacturer.name, + size=_dt.u_height, + depth="Full Depth" if _dt.is_full_depth else "Half Depth", + part_number=_dt.part_number, + custom_fields=nautobot.get_custom_field_dict(_dt.get_custom_fields()), + uuid=_dt.id, + ) + self.add(dtype) + + def load_virtual_chassis(self): + """Add Nautobot Virtual Chassis objects as DiffSync.""" + # We import the master node as a VC + for _vc in VirtualChassis.objects.all(): + self.cluster_map[_vc.name] = _vc.id + _members = [x.name for x in _vc.members.all() if x.name != _vc.name] + if len(_members) > 1: + _members.sort() + new_vc = self.cluster( + name=_vc.name, + members=_members, + tags=nautobot.get_tag_strings(_vc.tags), + custom_fields=nautobot.get_custom_field_dict(_vc.get_custom_fields()), + uuid=_vc.id, + ) + self.add(new_vc) + + def load_devices(self): + """Add Nautobot Device objects as DiffSync Device models.""" + for dev in Device.objects.select_related( + "status", "device_type", "device_role", "site", "rack", "platform", "vc_master_for", "virtual_chassis" + ).all(): + self.device_map[dev.name] = dev.id + # As patch panels are added as Devices, we need to filter them out for their own load method. + if DeviceRole.objects.get(id=dev.device_role.id).name == "patch panel": + patch_panel = self.patchpanel( + name=dev.name, + in_service=bool(str(dev.status.name) == "Active"), + vendor=dev.device_type.manufacturer.name, + model=dev.device_type.model, + size=dev.device_type.u_height, + position=dev.position, + orientation=dev.face if dev.face else "rear", + num_ports=len(FrontPort.objects.filter(device__name=dev.name)), + building=dev.site.slug, + room=dev.rack.group.name if dev.rack else None, + rack=dev.rack.name if dev.rack else None, + serial_no=dev.serial if dev.serial else "", + uuid=dev.id, + ) + self.add(patch_panel) + continue + if dev.platform and dev.platform.name in ANSIBLE_LIB_MAPPER: + _platform = ANSIBLE_LIB_MAPPER[dev.platform.name] + elif dev.platform: + _platform = dev.platform.name + else: + _platform = "" + if LIFECYCLE_MGMT: + _version = nautobot.get_software_version_from_lcm(relations=dev.get_relationships()) + else: + _version = nautobot.get_version_from_custom_field(fields=dev.get_custom_fields()) + _dev = self.device( + name=dev.name, + building=dev.site.slug, + room=dev.rack.group.name if dev.rack else "", + rack=dev.rack.name if dev.rack else "", + rack_position=dev.position, + rack_orientation=dev.face if dev.face else "rear", + hardware=dev.device_type.model, + os=_platform, + os_version=_version, + in_service=bool(str(dev.status) == "Active"), + serial_no=dev.serial if dev.serial else "", + tags=nautobot.get_tag_strings(dev.tags), + master_device=False, + custom_fields=nautobot.get_custom_field_dict(dev.get_custom_fields()), + uuid=dev.id, + cluster_host=None, + vc_position=dev.vc_position, + ) + if dev.virtual_chassis: + _dev.cluster_host = str(dev.virtual_chassis) + if hasattr(dev, "vc_master_for"): + if str(dev.vc_master_for) == _dev.cluster_host: + _dev.master_device = True + self.add(_dev) + + def load_interfaces(self): + """Add Nautobot Interface objects as DiffSync Port models.""" + for port in Interface.objects.select_related("device", "status").all(): + if port.device.name not in self.port_map: + self.port_map[port.device.name] = {} + if port.name not in self.port_map[port.device.name]: + self.port_map[port.device.name][port.name] = {} + self.port_map[port.device.name][port.name] = port.id + if port.mac_address: + _mac_addr = str(port.mac_address).replace(":", "").lower() + self.port_map[_mac_addr[:12]] = port.id + else: + _mac_addr = "" + try: + _port = self.get(self.port, {"device": port.device.name, "name": port.name}) + except ObjectNotFound: + if self.job.kwargs.get("debug"): + self.job.log_debug(message=f"Loading Interface: {port.name} for {port.device}.") + _port = self.port( + name=port.name, + device=port.device.name, + enabled=port.enabled, + mtu=port.mtu, + description=port.description, + mac_addr=_mac_addr[:13], + type=port.type, + tags=nautobot.get_tag_strings(port.tags), + mode=port.mode if port.mode else "access", + status=port.status.slug if hasattr(port, "status") else "active", + vlans=[], + custom_fields=nautobot.get_custom_field_dict(port.get_custom_fields()), + uuid=port.id, + ) + if port.mode == "access" and port.untagged_vlan: + _port.vlans = [port.untagged_vlan.vid] + else: + _vlans = [] + for _vlan in port.tagged_vlans.values(): + _vlans.append(_vlan["vid"]) + _port.vlans = sorted(set(_vlans)) + self.add(_port) + _dev = self.get(self.device, port.device.name) + _dev.add_child(_port) + + def load_vrfs(self): + """Add Nautobot VRF objects as DiffSync VRFGroup models.""" + for vrf in VRF.objects.all(): + self.vrf_map[vrf.name] = vrf.id + if self.job.kwargs.get("debug"): + self.job.log_debug(message=f"Loading VRF: {vrf.name}.") + _vrf = self.vrf( + name=vrf.name, + description=vrf.description, + tags=nautobot.get_tag_strings(vrf.tags), + custom_fields=nautobot.get_custom_field_dict(vrf.get_custom_fields()), + uuid=vrf.id, + ) + self.add(_vrf) + + def load_prefixes(self): + """Add Nautobot Prefix objects as DiffSync Subnet models.""" + for _pf in Prefix.objects.select_related("vrf").all(): + if _pf.vrf: + vrf_name = _pf.vrf.name + else: + vrf_name = "unknown" + if vrf_name not in self.prefix_map: + self.prefix_map[vrf_name] = {} + self.prefix_map[vrf_name][str(_pf.prefix)] = _pf.id + if self.job.kwargs.get("debug"): + self.job.log_debug(message=f"Loading Prefix: {_pf.prefix}.") + ip_net = ipaddress.ip_network(_pf.prefix) + new_pf = self.subnet( + network=str(ip_net.network_address), + mask_bits=ip_net.prefixlen, + description=_pf.description, + vrf=_pf.vrf.name, + tags=nautobot.get_tag_strings(_pf.tags), + custom_fields=nautobot.get_custom_field_dict(_pf.get_custom_fields()), + uuid=_pf.id, + ) + self.add(new_pf) + + def load_ip_addresses(self): + """Add Nautobot IPAddress objects as DiffSync IPAddress models.""" + for _ip in IPAddress.objects.select_related("status", "vrf").all(): + if _ip.vrf: + vrf_name = _ip.vrf.name + else: + vrf_name = "global" + if vrf_name not in self.ipaddr_map: + self.ipaddr_map[vrf_name] = {} + if str(_ip.address) not in self.ipaddr_map[vrf_name]: + self.ipaddr_map[vrf_name][str(_ip.address)] = {} + self.ipaddr_map[vrf_name][str(_ip.address)] = _ip.id + if self.job.kwargs.get("debug"): + self.job.log_debug(message=f"Loading IPAddress: {_ip.address}.") + new_ip = self.ipaddr( + address=str(_ip.address), + available=bool(_ip.status.name != "Active"), + label=_ip.description, + vrf=_ip.vrf.name if _ip.vrf else None, + tags=nautobot.get_tag_strings(_ip.tags), + interface="", + device="", + custom_fields=nautobot.get_custom_field_dict(_ip.get_custom_fields()), + uuid=_ip.id, + primary=None, + ) + if _ip.assigned_object_id: + try: + _intf = Interface.objects.get(id=_ip.assigned_object_id) + new_ip.interface = _intf.name + new_ip.device = _intf.device.name + except Interface.DoesNotExist: + _ip.assigned_object_id = None + _ip.assigned_object_type = None + if self.job.kwargs.get("debug"): + self.job.log_warning( + message=f"Can't find assigned Interface {_ip.assigned_object_id} for {_ip.address}. Removing assignment." + ) + if hasattr(_ip, "primary_ip4_for") or hasattr(_ip, "primary_ip6_for"): + new_ip.primary = True + else: + new_ip.primary = False + try: + self.add(new_ip) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_debug( + message=f"Duplicate IP Address {_ip.address} found and won't be imported. Validate the duplicate address is accurate. {err}" + ) + + def load_vlans(self): + """Add Nautobot VLAN objects as DiffSync VLAN models.""" + for vlan in VLAN.objects.select_related("site").all(): + if vlan.site: + site_slug = vlan.site.slug + else: + site_slug = "global" + if site_slug not in self.vlan_map: + self.vlan_map[site_slug] = {} + if str(vlan.vid) not in self.vlan_map[site_slug]: + self.vlan_map[site_slug][vlan.vid] = {} + self.vlan_map[site_slug][vlan.vid] = vlan.id + if self.job.kwargs.get("debug"): + self.job.log_debug(message=f"Loading VLAN: {vlan.name}.") + try: + _vlan = self.vlan( + name=vlan.name, + vlan_id=vlan.vid, + description=vlan.description if vlan.description else "", + building=vlan.site.slug if vlan.site else "Unknown", + custom_fields=nautobot.get_custom_field_dict(vlan.get_custom_fields()), + tags=nautobot.get_tag_strings(vlan.tags), + uuid=vlan.id, + ) + self.add(_vlan) + except ObjectAlreadyExists as err: + if self.job.kwargs.get("debug"): + self.job.log_warning(message=err) + + def load_cables(self): + """Add Nautobot Cable objects as DiffSync Connection models.""" + for _cable in Cable.objects.all(): + if _cable.termination_a.device.name not in self.cable_map: + self.cable_map[_cable.termination_a.device.name] = {} + if _cable.termination_a.name not in self.cable_map[_cable.termination_a.device.name]: + self.cable_map[_cable.termination_a.device.name][_cable.termination_a.name] = {} + self.cable_map[_cable.termination_a.device.name][_cable.termination_a.name] = _cable.id + if _cable.termination_b.device.name not in self.cable_map: + self.cable_map[_cable.termination_b.device.name] = {} + if _cable.termination_b.name not in self.cable_map[_cable.termination_b.device.name]: + self.cable_map[_cable.termination_b.device.name][_cable.termination_b.name] = {} + self.cable_map[_cable.termination_b.device.name][_cable.termination_b.name] = _cable.id + new_conn = self.conn( + src_device="", + src_port="", + src_type="interface", + dst_device="", + dst_port="", + dst_type="interface", + tags=nautobot.get_tag_strings(_cable.tags), + uuid=_cable.id, + src_port_mac=None, + dst_port_mac=None, + ) + new_conn = self.add_src_connection( + cable_term_type=_cable.termination_a_type, cable_term_id=_cable.termination_a_id, connection=new_conn + ) + new_conn = self.add_dst_connection( + cable_term_type=_cable.termination_b_type, cable_term_id=_cable.termination_b_id, connection=new_conn + ) + self.add(new_conn) + # # Now to ensure that diff matches, add a connection from reverse side. + # new_conn = self.add_src_connection( + # cable_term_type=_cable.termination_b_type, cable_term_id=_cable.termination_b_id, connection=new_conn + # ) + # new_conn = self.add_dst_connection( + # cable_term_type=_cable.termination_a_type, cable_term_id=_cable.termination_a_id, connection=new_conn + # ) + # self.add(new_conn) + + def add_src_connection( + self, cable_term_type: Cable, cable_term_id: Cable, connection: dcim.Connection + ) -> dcim.Connection: + """Method to fill in source portion of a Connection object. + + Works in conjunction with the `load_cables` and `add_dst_connection` methods. + + Args: + cable_term_type (Cable): The `termination_a_type` or `termination_b_type` attribute from a Cable object. + cable_term_id (Cable): The `termination_a_id` or `termination_b_id` attribute from a Cable object. + connection (dcim.Connection): Connection object being created. Expected to be empty with tags and types default `interface` type set for src side. + + Returns: + dcim.Connection: Updated Connection object with source attributes populated. + """ + if "interface" in str(cable_term_type): + src_port = Interface.objects.get(id=cable_term_id) + if src_port.mac_address: + mac_addr = str(src_port.mac_address).replace(":", "").lower() + else: + mac_addr = None + connection.src_port = src_port.name + connection.src_device = src_port.device.name + connection.src_port_mac = mac_addr + elif "circuit" in str(cable_term_type): + connection.src_type = "circuit" + connection.src_port = CircuitTermination.objects.get(id=cable_term_id).circuit.cid + connection.src_device = CircuitTermination.objects.get(id=cable_term_id).circuit.cid + return connection + + def add_dst_connection( + self, cable_term_type: Cable, cable_term_id: Cable, connection: dcim.Connection + ) -> dcim.Connection: + """Method to fill in destination portion of a Connection object. + + Works in conjunction with the `load_cables` and `add_src_connection` methods. + + Args: + cable_term_type (Cable): The `termination_a_type` or `termination_b_type` attribute from a Cable object. + cable_term_id (Cable): The `termination_a_id` or `termination_b_id` attribute from a Cable object. + connection (dcim.Connection): Connection object being created. Expected to be empty with tags and types default `interface` type set for dst side. + + Returns: + dcim.Connection: Updated Connection object with destination attributes populated. + """ + if "interface" in str(cable_term_type): + dst_port = Interface.objects.get(id=cable_term_id) + if dst_port.mac_address: + mac_addr = str(dst_port.mac_address).replace(":", "").lower() + else: + mac_addr = None + connection.dst_port = dst_port.name + connection.dst_device = dst_port.device.name + connection.dst_port_mac = mac_addr + elif "circuit" in str(cable_term_type): + connection.dst_type = "circuit" + connection.dst_port = CircuitTermination.objects.get(id=cable_term_id).circuit.cid + connection.dst_device = CircuitTermination.objects.get(id=cable_term_id).circuit.cid + return connection + + def load_providers(self): + """Add Nautobot Provider objects as DiffSync Provider models.""" + for _prov in Provider.objects.all(): + self.provider_map[_prov.name] = _prov.id + new_prov = self.provider( + name=_prov.name, + notes=_prov.comments, + vendor_url=_prov.portal_url, + vendor_acct=_prov.account, + vendor_contact1=_prov.noc_contact, + vendor_contact2=_prov.admin_contact, + tags=nautobot.get_tag_strings(_prov.tags), + uuid=_prov.id, + ) + self.add(new_prov) + + def load_circuits(self): + """Add Nautobot Circuit objects as DiffSync Circuit models.""" + for _circuit in Circuit.objects.select_related("provider", "type", "status").all(): + self.circuit_map[_circuit.cid] = _circuit.id + new_circuit = self.circuit( + circuit_id=_circuit.cid, + provider=_circuit.provider.name, + notes=_circuit.comments, + type=_circuit.type.name, + status=_circuit.status.name, + install_date=_circuit.install_date, + bandwidth=_circuit.commit_rate, + tags=nautobot.get_tag_strings(_circuit.tags), + uuid=_circuit.id, + origin_int=None, + origin_dev=None, + endpoint_int=None, + endpoint_dev=None, + ) + if hasattr(_circuit.termination_a, "connected_endpoint") and hasattr( + _circuit.termination_a.connected_endpoint, "name" + ): + new_circuit.origin_int = _circuit.termination_a.connected_endpoint.name + new_circuit.origin_dev = _circuit.termination_a.connected_endpoint.device.name + if hasattr(_circuit.termination_z, "connected_endpoint") and hasattr( + _circuit.termination_z.connected_endpoint, "name" + ): + new_circuit.endpoint_int = _circuit.termination_z.connected_endpoint.name + new_circuit.endpoint_dev = _circuit.termination_z.connected_endpoint.device.name + self.add(new_circuit) + + def load_front_ports(self): + """Add Nautobot FrontPort objects as DiffSync PatchPanelFrontPort models.""" + for port in FrontPort.objects.select_related("device").all(): + if port.device.device_role.name == "patch panel": + if port.device.name not in self.fp_map: + self.fp_map[port.device.name] = {} + self.fp_map[port.device.name][port.name] = port.id + front_port = self.patchpanelfrontport( + name=port.name, + patchpanel=port.device.name, + port_type=port.type, + uuid=port.id, + ) + self.add(front_port) + + def load_rear_ports(self): + """Add Nautobot RearPort objects as DiffSync PatchPanelRearPort models.""" + for port in RearPort.objects.select_related("device").all(): + if port.device.device_role.name == "patch panel": + if port.device.name not in self.rp_map: + self.rp_map[port.device.name] = {} + self.rp_map[port.device.name][port.name] = port.id + rear_port = self.patchpanelrearport( + name=port.name, + patchpanel=port.device.name, + port_type=port.type, + uuid=port.id, + ) + self.add(rear_port) + + def load(self): + """Load data from Nautobot.""" + self.status_map = {s.slug: s.id for s in Status.objects.only("id", "slug")} + self.platform_map = {p.slug: p.id for p in Platform.objects.only("id", "slug")} + self.devicerole_map = {dr.slug: dr.id for dr in DeviceRole.objects.only("id", "slug")} + self.relationship_map = {r.name: r.id for r in Relationship.objects.only("id", "name")} + if LIFECYCLE_MGMT: + self.softwarelcm_map = nautobot.get_dlc_version_map() + else: + self.softwarelcm_map = nautobot.get_cf_version_map() + + # Import all Nautobot Site records as Buildings + self.load_sites() + self.load_rackgroups() + self.load_racks() + self.load_manufacturers() + self.load_device_types() + self.load_vrfs() + self.load_vlans() + self.load_prefixes() + self.load_virtual_chassis() + self.load_devices() + self.load_interfaces() + self.load_ip_addresses() + self.load_providers() + self.load_circuits() + self.load_front_ports() + self.load_rear_ports() + # self.load_cables() diff --git a/nautobot_ssot/integrations/device42/diffsync/models/__init__.py b/nautobot_ssot/integrations/device42/diffsync/models/__init__.py new file mode 100644 index 000000000..132b83161 --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/__init__.py @@ -0,0 +1,72 @@ +"""DiffSync model class definitions for nautobot-ssot-device42.""" + +from nautobot_ssot.integrations.device42.diffsync.models.base.assets import ( + PatchPanel, + PatchPanelFrontPort, + PatchPanelRearPort, +) +from nautobot_ssot.integrations.device42.diffsync.models.base.circuits import Circuit, Provider +from nautobot_ssot.integrations.device42.diffsync.models.base.dcim import ( + Building, + Cluster, + Connection, + Device, + Hardware, + Port, + Rack, + Room, + Vendor, +) +from nautobot_ssot.integrations.device42.diffsync.models.base.ipam import VLAN, IPAddress, Subnet, VRFGroup +from nautobot_ssot.integrations.device42.diffsync.models.nautobot.assets import ( + NautobotPatchPanel, + NautobotPatchPanelFrontPort, + NautobotPatchPanelRearPort, +) +from nautobot_ssot.integrations.device42.diffsync.models.nautobot.circuits import NautobotCircuit, NautobotProvider +from nautobot_ssot.integrations.device42.diffsync.models.nautobot.dcim import ( + NautobotBuilding, + NautobotCluster, + NautobotConnection, + NautobotDevice, + NautobotHardware, + NautobotPort, + NautobotRack, + NautobotRoom, + NautobotVendor, +) + +__all__ = ( + "PatchPanel", + "PatchPanelFrontPort", + "PatchPanelRearPort", + "Provider", + "Circuit", + "Building", + "Room", + "Rack", + "Vendor", + "Hardware", + "Cluster", + "Device", + "Port", + "Connection", + "VRFGroup", + "Subnet", + "IPAddress", + "VLAN", + "NautobotCircuit", + "NautobotProvider", + "NautobotPatchPanel", + "NautobotPatchPanelFrontPort", + "NautobotPatchPanelRearPort", + "NautobotBuilding", + "NautobotCluster", + "NautobotConnection", + "NautobotDevice", + "NautobotHardware", + "NautobotPort", + "NautobotRack", + "NautobotRoom", + "NautobotVendor", +) diff --git a/nautobot_ssot/integrations/device42/diffsync/models/base/__init__.py b/nautobot_ssot/integrations/device42/diffsync/models/base/__init__.py new file mode 100644 index 000000000..eb2d5a9c4 --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/base/__init__.py @@ -0,0 +1 @@ +"""Base model classes for nautobot_ssot_device42 data sync.""" diff --git a/nautobot_ssot/integrations/device42/diffsync/models/base/assets.py b/nautobot_ssot/integrations/device42/diffsync/models/base/assets.py new file mode 100644 index 000000000..3d2164699 --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/base/assets.py @@ -0,0 +1,67 @@ +"""Base asset subclasses DiffSyncModel for nautobot_ssot_device42 data sync.""" + +from typing import Optional +from uuid import UUID + +from diffsync import DiffSyncModel + + +class PatchPanel(DiffSyncModel): + """Base Patch Panel model.""" + + _modelname = "patchpanel" + _identifiers = ("name",) + _attributes = ( + "in_service", + "vendor", + "model", + "orientation", + "position", + "num_ports", + "building", + "room", + "rack", + "serial_no", + ) + _children = {} + + name: str + in_service: bool + vendor: str + model: str + orientation: str + position: Optional[float] + num_ports: int + building: Optional[str] + room: Optional[str] + rack: Optional[str] + serial_no: Optional[str] + uuid: Optional[UUID] + + +class PatchPanelRearPort(DiffSyncModel): + """Base Patch Panel RearPort model.""" + + _modelname = "patchpanelrearport" + _identifiers = ("name", "patchpanel") + _attributes = ("port_type",) + _children = {} + + name: str + patchpanel: str + port_type: str + uuid: Optional[UUID] + + +class PatchPanelFrontPort(DiffSyncModel): + """Base Patch Panel FrontPort model.""" + + _modelname = "patchpanelfrontport" + _identifiers = ("name", "patchpanel") + _attributes = ("port_type",) + _children = {} + + name: str + patchpanel: str + port_type: str + uuid: Optional[UUID] diff --git a/nautobot_ssot/integrations/device42/diffsync/models/base/circuits.py b/nautobot_ssot/integrations/device42/diffsync/models/base/circuits.py new file mode 100644 index 000000000..b049cb88d --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/base/circuits.py @@ -0,0 +1,60 @@ +"""Base Circuit subclasses DiffSyncModel for nautobot_ssot_device42 data sync.""" + +from typing import List, Optional +from uuid import UUID + +from diffsync import DiffSyncModel + + +class Provider(DiffSyncModel): + """Base Provider model.""" + + _modelname = "provider" + _identifiers = ("name",) + _attributes = ("notes", "vendor_url", "vendor_acct", "vendor_contact1", "vendor_contact2") + _children = {} + + name: str + notes: Optional[str] + vendor_url: Optional[str] + vendor_acct: Optional[str] + vendor_contact1: Optional[str] + vendor_contact2: Optional[str] + tags: Optional[List[str]] + uuid: Optional[UUID] + + +class Circuit(DiffSyncModel): + """Base TelcoCircuit model.""" + + _modelname = "circuit" + _identifiers = ( + "circuit_id", + "provider", + ) + _attributes = ( + "notes", + "type", + "status", + "install_date", + "origin_int", + "origin_dev", + "endpoint_int", + "endpoint_dev", + "bandwidth", + "tags", + ) + _children = {} + circuit_id: str + provider: str + notes: Optional[str] + type: str + status: str + install_date: Optional[str] + origin_int: Optional[str] + origin_dev: Optional[str] + endpoint_int: Optional[str] + endpoint_dev: Optional[str] + bandwidth: Optional[int] + tags: Optional[List[str]] + uuid: Optional[UUID] diff --git a/nautobot_ssot/integrations/device42/diffsync/models/base/dcim.py b/nautobot_ssot/integrations/device42/diffsync/models/base/dcim.py new file mode 100644 index 000000000..ee3b31ef6 --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/base/dcim.py @@ -0,0 +1,200 @@ +"""Base DCIM subclasses DiffSyncModel for nautobot_ssot_device42 data sync.""" + +from typing import List, Optional +from uuid import UUID + +from diffsync import DiffSyncModel + + +class Building(DiffSyncModel): + """Base Building model.""" + + _modelname = "building" + _identifiers = ("name",) + _attributes = ("address", "latitude", "longitude", "contact_name", "contact_phone", "tags", "custom_fields") + _children = {"room": "rooms"} + name: str + address: Optional[str] + latitude: Optional[float] + longitude: Optional[float] + contact_name: Optional[str] + contact_phone: Optional[str] + rooms: List["Room"] = list() + tags: Optional[List[str]] + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class Room(DiffSyncModel): + """Base Room model.""" + + _modelname = "room" + _identifiers = ("name", "building") + _attributes = ("notes", "custom_fields") + _children = {"rack": "racks"} + name: str + building: str + notes: Optional[str] + racks: List["Rack"] = list() + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class Rack(DiffSyncModel): + """Base Rack model.""" + + _modelname = "rack" + _identifiers = ("name", "building", "room") + _attributes = ("height", "numbering_start_from_bottom", "tags", "custom_fields") + _children = {} + name: str + building: str + room: str + height: int + numbering_start_from_bottom: str + tags: Optional[List[str]] + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class Vendor(DiffSyncModel): + """Base Vendor model.""" + + _modelname = "vendor" + _identifiers = ("name",) + _attributes = ("custom_fields",) + _children = {} + name: str + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class Hardware(DiffSyncModel): + """Base Hardware model.""" + + _modelname = "hardware" + _identifiers = ("name",) + _attributes = ("manufacturer", "size", "depth", "part_number", "custom_fields") + _children = {} + name: str + manufacturer: str + size: float + depth: Optional[str] + part_number: Optional[str] + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class Cluster(DiffSyncModel): + """Base Cluster model.""" + + _modelname = "cluster" + _identifiers = ("name",) + _attributes = ("tags", "custom_fields") + _children = {} + name: str + members: Optional[List[str]] + tags: Optional[List[str]] + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class Device(DiffSyncModel): + """Device42 Device model.""" + + _modelname = "device" + _identifiers = ("name",) + _attributes = ( + "building", + "room", + "rack", + "rack_position", + "rack_orientation", + "hardware", + "os", + "os_version", + "in_service", + "serial_no", + "tags", + "cluster_host", + "master_device", + "custom_fields", + "vc_position", + ) + _children = {"port": "interfaces"} + name: str + building: str + room: Optional[str] + rack: Optional[str] + rack_position: Optional[float] + rack_orientation: Optional[str] + hardware: str + os: Optional[str] + os_version: Optional[str] + in_service: Optional[bool] + interfaces: Optional[List["Port"]] = [] + serial_no: Optional[str] + tags: Optional[List[str]] + cluster_host: Optional[str] + master_device: bool + vc_position: Optional[int] + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class Port(DiffSyncModel): + """Base Port model.""" + + _modelname = "port" + _identifiers = ("device", "name") + _attributes = ( + "enabled", + "mtu", + "description", + "mac_addr", + "type", + "mode", + "status", + "tags", + "vlans", + "custom_fields", + ) + _children = {} + name: str + device: str + enabled: Optional[bool] + mtu: Optional[int] + description: Optional[str] + mac_addr: Optional[str] + type: str + tags: Optional[List[str]] + mode: str + status: str + vlans: Optional[List[int]] = [] + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class Connection(DiffSyncModel): + """Base Connection model.""" + + _modelname = "conn" + _identifiers = ("src_device", "src_port", "src_port_mac", "dst_device", "dst_port", "dst_port_mac") + _attributes = ("src_type", "dst_type") + _children = {} + + src_device: str + src_port: str + src_type: str + src_port_mac: Optional[str] + dst_device: str + dst_port: str + dst_type: str + dst_port_mac: Optional[str] + tags: Optional[List[str]] + uuid: Optional[UUID] + + +Building.update_forward_refs() +Room.update_forward_refs() +Device.update_forward_refs() diff --git a/nautobot_ssot/integrations/device42/diffsync/models/base/ipam.py b/nautobot_ssot/integrations/device42/diffsync/models/base/ipam.py new file mode 100644 index 000000000..b61925dad --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/base/ipam.py @@ -0,0 +1,80 @@ +"""Base IPAM subclasses DiffSyncModel for nautobot_ssot_device42 data sync.""" + +from typing import List, Optional +from uuid import UUID + +from diffsync import DiffSyncModel + + +class VRFGroup(DiffSyncModel): + """Base VRFGroup model.""" + + _modelname = "vrf" + _identifiers = ("name",) + _attributes = ("description", "tags", "custom_fields") + _children = {} + name: str + description: Optional[str] + tags: Optional[List[str]] + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class Subnet(DiffSyncModel): + """Base Subnet model.""" + + _modelname = "subnet" + _identifiers = ( + "network", + "mask_bits", + "vrf", + ) + _attributes = ("description", "tags", "custom_fields") + _children = {} + network: str + mask_bits: int + description: Optional[str] + vrf: Optional[str] + tags: Optional[List[str]] + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class IPAddress(DiffSyncModel): + """Base IP Address model.""" + + _modelname = "ipaddr" + _identifiers = ("address", "vrf") + _attributes = ("available", "label", "device", "interface", "primary", "tags", "custom_fields") + _children = {} + + address: str + available: bool + label: Optional[str] + device: Optional[str] + interface: Optional[str] + primary: Optional[bool] + vrf: Optional[str] + tags: Optional[List[str]] + custom_fields: Optional[dict] + uuid: Optional[UUID] + + +class VLAN(DiffSyncModel): + """Base VLAN model.""" + + _modelname = "vlan" + _identifiers = ( + "vlan_id", + "building", + ) + _attributes = ("name", "description", "custom_fields", "tags") + _children = {} + + name: str + vlan_id: int + description: Optional[str] + building: Optional[str] + custom_fields: Optional[dict] + tags: Optional[List[str]] + uuid: Optional[UUID] diff --git a/nautobot_ssot/integrations/device42/diffsync/models/nautobot/__init__.py b/nautobot_ssot/integrations/device42/diffsync/models/nautobot/__init__.py new file mode 100644 index 000000000..24435fc6a --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/nautobot/__init__.py @@ -0,0 +1 @@ +"""Base Nautobot classes for nautobot_ssot_device42 data sync.""" diff --git a/nautobot_ssot/integrations/device42/diffsync/models/nautobot/assets.py b/nautobot_ssot/integrations/device42/diffsync/models/nautobot/assets.py new file mode 100644 index 000000000..2a941e8d9 --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/nautobot/assets.py @@ -0,0 +1,244 @@ +"""DiffSyncModel Asset subclasses for Nautobot Device42 data sync.""" + +from django.core.exceptions import ValidationError +from django.utils.text import slugify +from nautobot.dcim.models import Device, DeviceType, FrontPort, RearPort +from nautobot_ssot.integrations.device42.constant import PLUGIN_CFG +from nautobot_ssot.integrations.device42.diffsync.models.base.assets import ( + PatchPanel, + PatchPanelRearPort, + PatchPanelFrontPort, +) +from nautobot_ssot.integrations.device42.diffsync.models.nautobot.dcim import NautobotRack +from nautobot_ssot.integrations.device42.utils import nautobot + + +def find_site(diffsync, attrs): + """Method to determine Site for Patch Panel based upon object attributes.""" + pp_site = False + try: + if attrs.get("building"): + pp_site = diffsync.site_map[slugify(attrs["building"])] + elif attrs.get("room") and attrs.get("rack"): + rack = diffsync.get(NautobotRack, {"name": attrs["rack"], "group": attrs["room"]}) + site_name = rack.building + pp_site = diffsync.site_map[slugify(site_name)] + except KeyError: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning(message=f"Unable to find Site {attrs.get('building')}.") + return pp_site + + +class NautobotPatchPanel(PatchPanel): + """Nautobot Patch Panel model.""" + + def find_rack(self, diffsync, building: str, room: str, rack: str): + """Method to determine Site for Patch Panel based upon object attributes.""" + pp_rack = False + try: + if building is not None and room is not None and rack is not None: + pp_rack = diffsync.rack_map[slugify(building)][slugify(room)][rack] + elif rack is not None: + for new_rack in diffsync.objects_to_create["racks"]: + if new_rack.name is rack: + pp_rack = new_rack.id + except KeyError: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning(message=f"Unable to find Rack using Room {room} & Rack {rack}.") + return pp_rack + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Patch Panel Device in Nautobot.""" + diffsync.job.log_info(message=f"Creating Patch Panel {ids['name']} Device.") + try: + diffsync.device_map[ids["name"]] + except KeyError: + pp_site = find_site(diffsync=diffsync, attrs=attrs) + pp_rack = cls.find_rack( + cls, diffsync=diffsync, building=attrs.get("building"), room=attrs.get("room"), rack=attrs.get("rack") + ) + pp_role = nautobot.verify_device_role(diffsync=diffsync, role_name="patch panel") + if attrs.get("in_service"): + pp_status = diffsync.status_map["active"] + else: + pp_status = diffsync.status_map["offline"] + patch_panel = Device( + name=ids["name"], + status_id=pp_status, + site_id=pp_site, + device_type_id=diffsync.devicetype_map[slugify(attrs["model"])], + device_role_id=pp_role, + serial=attrs["serial_no"], + ) + if pp_rack is not False and attrs.get("position") and attrs.get("orientation"): + patch_panel.rack_id = pp_rack + patch_panel.position = int(attrs["position"]) + patch_panel.face = attrs["orientation"] + try: + diffsync.objects_to_create["devices"].append(patch_panel) + diffsync.device_map[ids["name"]] = patch_panel.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + except ValidationError as err: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning(message=f"Unable to create {ids['name']} patch panel. {err}") + return None + + def update(self, attrs): + """Update Patch Panel object in Nautobot.""" + ppanel = Device.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating Patch Panel {ppanel.name} Device.") + if "in_service" in attrs: + if attrs["in_service"] is True: + ppanel.status_id = self.diffsync.status_map["active"] + else: + ppanel.status_id = self.diffsync.status_map["offline"] + if "vendor" in attrs and "model" in attrs: + ppanel.device_type = DeviceType.objects.get(model=attrs["model"]) + if "orientation" in attrs: + ppanel.face = attrs["orientation"] + if "position" in attrs: + ppanel.position = attrs["position"] + if "building" in attrs: + pp_site = find_site(diffsync=self.diffsync, attrs=attrs) + if pp_site: + ppanel.site_id = pp_site + if "room" in attrs or "rack" in attrs: + if attrs.get("building"): + _building = attrs["building"] + else: + _building = self.building + if "room" in attrs: + _room = attrs["room"] + else: + _room = self.room + if "rack" in attrs: + _rack = attrs["rack"] + else: + _rack = self.rack + pp_rack = self.find_rack(diffsync=self.diffsync, building=_building, room=_room, rack=_rack) + if pp_rack: + ppanel.rack_id = pp_rack + ppanel.face = attrs["orientation"] if attrs.get("orientation") else self.orientation + if "serial_no" in attrs: + ppanel.serial = attrs["serial_no"] + try: + ppanel.validated_save() + return super().update(attrs) + except ValidationError as err: + self.diffsync.job.log_warning(message=f"Unable to update {self.name} patch panel. {err}") + return None + + def delete(self): + """Delete Patch Panel Device object from Nautobot. + + Because a Patch Panel Device has a direct relationship with Ports it can't be deleted before they are. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"Patch panel {self.name} will be deleted.") + _pp = Device.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["patchpanel"].append(_pp) # pylint: disable=protected-access + return self + + +class NautobotPatchPanelRearPort(PatchPanelRearPort): + """Nautobot Patch Panel RearPort model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Patch Panel Port in Nautobot.""" + diffsync.job.log_info(message=f"Creating patch panel port {ids['name']} for {ids['patchpanel']}.") + try: + diffsync.rp_map[ids["patchpanel"]][ids["name"]] + except KeyError: + rear_port = RearPort( + name=ids["name"], + device_id=diffsync.device_map[ids["patchpanel"]], + type=attrs["port_type"], + positions=ids["name"], + ) + try: + diffsync.objects_to_create["rear_ports"].append(rear_port) + if ids["patchpanel"] not in diffsync.rp_map: + diffsync.rp_map[ids["patchpanel"]] = {} + diffsync.rp_map[ids["patchpanel"]][ids["name"]] = rear_port.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + except ValidationError as err: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_debug(message=f"Unable to create patch panel {ids['name']}. {err}") + return None + + def update(self, attrs): + """Update RearPort object in Nautobot.""" + port = RearPort.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating patch panel port {port.name} for {self.patchpanel}.") + if "type" in attrs: + port.type = attrs["type"] + try: + port.validated_save() + return super().update(attrs) + except ValidationError as err: + self.diffsync.job.log_warning(message=f"Unable to update {self.name} RearPort. {err}") + return None + + def delete(self): + """Delete RearPort object from Nautobot.""" + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"RearPort {self.name} for {self.patchpanel} will be deleted.") + port = RearPort.objects.get(id=self.uuid) + port.delete() + return self + + +class NautobotPatchPanelFrontPort(PatchPanelFrontPort): + """Nautobot Patch Panel FrontPort model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Patch Panel FrontPort in Nautobot.""" + diffsync.job.log_info(message=f"Creating patch panel front port {ids['name']} for {ids['patchpanel']}.") + try: + diffsync.fp_map[ids["patchpanel"]][ids["name"]] + except KeyError: + front_port = FrontPort( + name=ids["name"], + device_id=diffsync.device_map[ids["patchpanel"]], + type=attrs["port_type"], + rear_port_id=diffsync.rp_map[ids["patchpanel"]][ids["name"]], + rear_port_position=ids["name"], + ) + try: + diffsync.objects_to_create["front_ports"].append(front_port) + if ids["patchpanel"] not in diffsync.fp_map: + diffsync.fp_map[ids["patchpanel"]] = {} + diffsync.fp_map[ids["patchpanel"]][ids["name"]] = front_port.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + except ValidationError as err: + diffsync.job.log_debug(message=f"Unable to create patch panel front port {ids['name']}. {err}") + return None + + def update(self, attrs): + """Update FrontPort object in Nautobot.""" + port = FrontPort.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating patch panel front port {self.name} for {self.patchpanel}.") + if "type" in attrs: + port.type = attrs["type"] + try: + port.validated_save() + return super().update(attrs) + except ValidationError as err: + self.diffsync.job.log_warning(message=f"Unable to update {self.name} FrontPort. {err}") + return None + + def delete(self): + """Delete FrontPort object from Nautobot.""" + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"FrontPort {self.name} for {self.patchpanel} will be deleted.") + port = FrontPort.objects.get(id=self.uuid) + port.delete() + return self diff --git a/nautobot_ssot/integrations/device42/diffsync/models/nautobot/circuits.py b/nautobot_ssot/integrations/device42/diffsync/models/nautobot/circuits.py new file mode 100644 index 000000000..86e8b085a --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/nautobot/circuits.py @@ -0,0 +1,217 @@ +"""DiffSyncModel Circuit subclasses for Nautobot Device42 data sync.""" + +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError +from django.utils.text import slugify +from nautobot.circuits.models import Circuit as OrmCircuit +from nautobot.circuits.models import CircuitTermination as OrmCT +from nautobot.circuits.models import Provider as OrmProvider +from nautobot.dcim.models import Cable as OrmCable +from nautobot.extras.models import Status as OrmStatus +from nautobot_ssot.integrations.device42.constant import INTF_SPEED_MAP, PLUGIN_CFG +from nautobot_ssot.integrations.device42.diffsync.models.base.circuits import Circuit, Provider +from nautobot_ssot.integrations.device42.diffsync.models.nautobot.dcim import NautobotDevice +from nautobot_ssot.integrations.device42.utils import nautobot + + +class NautobotProvider(Provider): + """Nautobot Provider model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Provider object in Nautobot.""" + diffsync.job.log_info(message=f"Creating Provider {ids['name']}.") + try: + _provider = diffsync.provider_map[slugify(ids["name"])] + except KeyError: + _provider = OrmProvider( + name=ids["name"], + slug=slugify(ids["name"]), + account=attrs["vendor_acct"] if attrs.get("vendor_acct") else "", + portal_url=attrs["vendor_url"] if attrs.get("vendor_url") else "", + noc_contact=attrs["vendor_contact1"] if attrs.get("vendor_contact1") else "", + admin_contact=attrs["vendor_contact2"] if attrs.get("vendor_contact2") else "", + comments=attrs["notes"] if attrs.get("notes") else "", + ) + if attrs.get("tags"): + for _tag in nautobot.get_tags(attrs["tags"]): + _provider.tags.add(_tag) + try: + diffsync.objects_to_create["providers"].append(_provider) + diffsync.provider_map[slugify(ids["name"])] = _provider.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + except ValidationError as err: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning(message=f"Unable to create {ids['name']} provider. {err}") + return None + + def update(self, attrs): + """Update Provider object in Nautobot.""" + _prov = OrmProvider.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating Provider {_prov.name}.") + if "notes" in attrs: + _prov.comments = attrs["notes"] + if "vendor_url" in attrs: + _prov.portal_url = attrs["vendor_url"] + if "vendor_acct" in attrs: + _prov.account = attrs["vendor_acct"] + if "vendor_contact1" in attrs: + _prov.noc_contact = attrs["vendor_contact1"] + if "vendor_contact2" in attrs: + _prov.admin_contact = attrs["vendor_contact2"] + if "tags" in attrs: + nautobot.update_tags(tagged_obj=_prov, new_tags=attrs["tags"]) + _prov.validated_save() + return super().update(attrs) + + def delete(self): + """Delete Provider object from Nautobot. + + Because Provider has a direct relationship with Circuits it can't be deleted before them. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + self.diffsync.job.log_info(message=f"Provider {self.name} will be deleted.") + super().delete() + provider = OrmProvider.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["provider"].append(provider) # pylint: disable=protected-access + return self + + +class NautobotCircuit(Circuit): + """Nautobot TelcoCircuit model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Circuit object in Nautobot.""" + diffsync.job.log_info(message=f"Creating Circuit {ids['circuit_id']}.") + try: + diffsync.circuit_map[ids["circuit_id"]] + except KeyError: + _circuit = OrmCircuit( + cid=ids["circuit_id"], + provider_id=diffsync.provider_map[slugify(ids["provider"])], + type=nautobot.verify_circuit_type(attrs["type"]), + status_id=diffsync.status_map[slugify(attrs["status"])], + install_date=attrs["install_date"] if attrs.get("install_date") else None, + commit_rate=attrs["bandwidth"] if attrs.get("bandwidth") else None, + comments=attrs["notes"] if attrs.get("notes") else "", + ) + if attrs.get("tags"): + for _tag in nautobot.get_tags(attrs["tags"]): + _circuit.tags.add(_tag) + diffsync.objects_to_create["circuits"].append(_circuit) + if attrs.get("origin_int") and attrs.get("origin_dev"): + if attrs["origin_dev"] not in diffsync.circuit_map: + diffsync.circuit_map[attrs["origin_dev"]] = {} + diffsync.circuit_map[attrs["origin_dev"]][attrs["origin_int"]] = _circuit.id + cls.connect_circuit_to_device( + diffsync=diffsync, + intf=attrs["origin_int"], + dev=attrs["origin_dev"], + term_side="A", + circuit=_circuit, + ) + if attrs.get("endpoint_int") and attrs.get("endpoint_dev"): + if attrs["endpoint_dev"] not in diffsync.circuit_map: + diffsync.circuit_map[attrs["endpoint_dev"]] = {} + diffsync.circuit_map[attrs["endpoint_dev"]][attrs["endpoint_int"]] = _circuit.id + cls.connect_circuit_to_device( + diffsync=diffsync, + intf=attrs["endpoint_int"], + dev=attrs["endpoint_dev"], + term_side="Z", + circuit=_circuit, + ) + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update Circuit object in Nautobot.""" + _circuit = OrmCircuit.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating Circuit {_circuit.cid}.") + if "notes" in attrs: + _circuit.comments = attrs["notes"] + if "type" in attrs: + _circuit.type = nautobot.verify_circuit_type(attrs["type"]) + if "status" in attrs: + _circuit.status = OrmStatus.objects.get(name=attrs["status"]) + if "install_date" in attrs: + _circuit.install_date = attrs["install_date"] + if "bandwidth" in attrs: + _circuit.commit_rate = attrs["bandwidth"] + if "origin_int" in attrs and "origin_dev" in attrs: + self.connect_circuit_to_device( + diffsync=self.diffsync, + intf=attrs["origin_int"], + dev=attrs["origin_dev"], + term_side="A", + circuit=_circuit, + ) + if "endpoint_int" in attrs and "endpoint_dev" in attrs: + self.connect_circuit_to_device( + diffsync=self.diffsync, + intf=attrs["endpoint_int"], + dev=attrs["endpoint_dev"], + term_side="Z", + circuit=_circuit, + ) + if "tags" in attrs: + nautobot.update_tags(tagged_obj=_circuit, new_tags=attrs["tags"]) + _circuit.validated_save() + return super().update(attrs) + + @staticmethod + def connect_circuit_to_device(diffsync, intf: str, dev: str, term_side: str, circuit: OrmCircuit): + """Method to handle Circuit Termination to a Device. + + Args: + diffsync (obj): DiffSync Job for maps. + intf (str): Interface of Device to connect Circuit Termination. + dev (str): Device with respective interface to connect Circuit to. + term_side (str): Which side of the CircuitTermination this connection is on, A or Z. + circuit (OrmCircuit): The actual Circuit object that the CircuitTermination is connecting to. + """ + try: + _intf = diffsync.port_map[dev][intf] + try: + _term = diffsync.circuit_map[dev][intf] + except KeyError: + _site = diffsync.get(NautobotDevice, dev) + _site = diffsync.site_map[slugify(_site.name)] + _term = OrmCT( + circuit_id=circuit, + term_side=term_side, + site_id=_site, + port_speed=INTF_SPEED_MAP[_intf.type], + ) + diffsync.objects_to_create["circuits"].append(_term) + if _intf and _term: + new_cable = OrmCable( + termination_a_type=ContentType.objects.get(app_label="dcim", model="interface"), + termination_a_id=_intf, + termination_b_type=ContentType.objects.get(app_label="circuits", model="circuittermination"), + termination_b_id=_term, + status_id=diffsync.status_map["connected"], + color=nautobot.get_random_color(), + ) + diffsync.objects_to_create["cables"].append(new_cable) + if dev not in diffsync.cable_map: + diffsync.cable_map[dev] = {} + diffsync.cable_map[dev][intf] = new_cable.id + except KeyError: + print(f"Unable to find {dev}") + + def delete(self): + """Delete Provider object from Nautobot. + + Because Provider has a direct relationship with Circuits it can't be deleted before them. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + self.diffsync.job.log_info(message=f"Circuit {self.circuit_id} will be deleted.") + super().delete() + circuit = OrmCircuit.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["circuit"].append(circuit) # pylint: disable=protected-access + return self diff --git a/nautobot_ssot/integrations/device42/diffsync/models/nautobot/dcim.py b/nautobot_ssot/integrations/device42/diffsync/models/nautobot/dcim.py new file mode 100644 index 000000000..e5cf6bb08 --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/nautobot/dcim.py @@ -0,0 +1,970 @@ +"""DiffSyncModel DCIM subclasses for Nautobot Device42 data sync.""" + +from decimal import Decimal +from typing import Optional +from uuid import UUID + +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError +from django.utils.text import slugify +from nautobot.circuits.models import CircuitTermination as OrmCT +from nautobot.core.settings_funcs import is_truthy +from nautobot.dcim.models import Cable as OrmCable +from nautobot.dcim.models import Device as OrmDevice +from nautobot.dcim.models import DeviceType as OrmDeviceType +from nautobot.dcim.models import FrontPort as OrmFrontPort +from nautobot.dcim.models import Interface as OrmInterface +from nautobot.dcim.models import Manufacturer as OrmManufacturer +from nautobot.dcim.models import Rack as OrmRack +from nautobot.dcim.models import RackGroup as OrmRackGroup +from nautobot.dcim.models import Site as OrmSite +from nautobot.dcim.models import VirtualChassis as OrmVC +from nautobot.extras.models import RelationshipAssociation +from nautobot.extras.models import Status as OrmStatus +from nautobot_ssot.integrations.device42.constant import DEFAULTS, INTF_SPEED_MAP, PLUGIN_CFG +from nautobot_ssot.integrations.device42.diffsync.models.base.dcim import ( + Building, + Cluster, + Connection, + Device, + Hardware, + Port, + Rack, + Room, + Vendor, +) +from nautobot_ssot.integrations.device42.utils import device42, nautobot + +try: + from nautobot_device_lifecycle_mgmt.models import SoftwareLCM + + LIFECYCLE_MGMT = True +except ImportError: + print("Device Lifecycle plugin isn't installed so will revert to CustomField for OS version.") + LIFECYCLE_MGMT = False + + +class NautobotBuilding(Building): + """Nautobot Building model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Site object in Nautobot.""" + diffsync.job.log_info(message=f"Creating Site {ids['name']}.") + def_site_status = diffsync.status_map[slugify(DEFAULTS.get("site_status"))] + new_site = OrmSite( + name=ids["name"], + slug=slugify(ids["name"]), + status_id=def_site_status, + physical_address=attrs["address"] if attrs.get("address") else "", + latitude=round(Decimal(attrs["latitude"] if attrs["latitude"] else 0.0), 6), + longitude=round(Decimal(attrs["longitude"] if attrs["longitude"] else 0.0), 6), + contact_name=attrs["contact_name"] if attrs.get("contact_name") else "", + contact_phone=attrs["contact_phone"] if attrs.get("contact_phone") else "", + ) + if attrs.get("tags"): + for _tag in nautobot.get_tags(attrs["tags"]): + new_site.tags.add(_tag) + _facility = device42.get_facility(tags=attrs["tags"], diffsync=diffsync) + if _facility: + new_site.facility = _facility.upper() + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=new_site) + diffsync.objects_to_create["sites"].append(new_site) + diffsync.site_map[slugify(ids["name"])] = new_site.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update Site object in Nautobot.""" + _site = OrmSite.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating building {_site.name}.") + if "address" in attrs: + _site.physical_address = attrs["address"] + if "latitude" in attrs: + _site.latitude = round(Decimal(attrs["latitude"]), 6) + if "longitude" in attrs: + _site.longitude = round(Decimal(attrs["longitude"]), 6) + if "contact_name" in attrs: + _site.contact_name = attrs["contact_name"] + if "contact_phone" in attrs: + _site.contact_phone = attrs["contact_phone"] + if "tags" in attrs: + if attrs.get("tags"): + nautobot.update_tags(tagged_obj=_site, new_tags=attrs["tags"]) + _facility = device42.get_facility(tags=attrs["tags"], diffsync=self.diffsync) + if _facility: + _site.facility = _facility.upper() + else: + _site.tags.clear() + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_site) + _site.validated_save() + return super().update(attrs) + + def delete(self): + """Delete Site object from Nautobot. + + Because Site has a direct relationship with many other objects it can't be deleted before anything else. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"Site {self.name} will be deleted.") + site = OrmSite.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["site"].append(site) # pylint: disable=protected-access + return self + + +class NautobotRoom(Room): + """Nautobot Room model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create RackGroup object in Nautobot.""" + diffsync.job.log_info(message=f"Creating RackGroup {ids['name']}.") + new_rg = OrmRackGroup( + name=ids["name"], + slug=slugify(ids["name"]), + site_id=diffsync.site_map[slugify(ids["building"])], + description=attrs["notes"] if attrs.get("notes") else "", + ) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=new_rg) + diffsync.objects_to_create["rooms"].append(new_rg) + if slugify(ids["building"]) not in diffsync.room_map: + diffsync.room_map[slugify(ids["building"])] = {} + diffsync.room_map[slugify(ids["building"])][slugify(ids["name"])] = new_rg.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update RackGroup object in Nautobot.""" + _rg = OrmRackGroup.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating RackGroup {_rg.name}.") + if "notes" in attrs: + _rg.description = attrs["notes"] + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_rg) + _rg.validated_save() + return super().update(attrs) + + def delete(self): + """Delete RackGroup object from Nautobot.""" + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"RackGroup {self.name} will be deleted.") + rackgroup = OrmRackGroup.objects.get(id=self.uuid) + rackgroup.delete() + return self + + +class NautobotRack(Rack): + """Nautobot Rack model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Rack object in Nautobot.""" + diffsync.job.log_info(message=f"Creating Rack {ids['name']}.") + _site = diffsync.site_map[slugify(ids["building"])] + _rg = diffsync.room_map[slugify(ids["building"])][slugify(ids["room"])] + new_rack = OrmRack( + name=ids["name"], + site_id=_site, + group_id=_rg, + status_id=diffsync.status_map[slugify(DEFAULTS.get("rack_status"))], + u_height=attrs["height"] if attrs.get("height") else 1, + desc_units=not (is_truthy(attrs["numbering_start_from_bottom"])), + ) + if attrs.get("tags"): + for _tag in nautobot.get_tags(attrs["tags"]): + new_rack.tags.add(_tag) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=new_rack) + diffsync.objects_to_create["racks"].append(new_rack) + if slugify(ids["building"]) not in diffsync.rack_map: + diffsync.rack_map[slugify(ids["building"])] = {} + if slugify(ids["room"]) not in diffsync.rack_map[slugify(ids["building"])]: + diffsync.rack_map[slugify(ids["building"])][slugify(ids["room"])] = {} + diffsync.rack_map[slugify(ids["building"])][slugify(ids["room"])][ids["name"]] = new_rack.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update Rack object in Nautobot.""" + _rack = OrmRack.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating Rack {_rack.name}.") + if "height" in attrs: + _rack.u_height = attrs["height"] + if "numbering_start_from_bottom" in attrs: + _rack.desc_units = not (is_truthy(attrs["numbering_start_from_bottom"])) + if "tags" in attrs: + if attrs.get("tags"): + nautobot.update_tags(tagged_obj=_rack, new_tags=attrs["tags"]) + else: + _rack.tags.clear() + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_rack) + _rack.validated_save() + return super().update(attrs) + + def delete(self): + """Delete Rack object from Nautobot. + + Because Rack has a direct relationship with Devices it can't be deleted before they are. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"Rack {self.name} will be deleted.") + rack = OrmRack.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["rack"].append(rack) # pylint: disable=protected-access + return self + + +class NautobotVendor(Vendor): + """Nautobot Vendor model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Manufacturer object in Nautobot.""" + diffsync.job.log_info(message=f"Creating Manufacturer {ids['name']}.") + try: + diffsync.vendor_map[slugify(ids["name"])] + except KeyError: + new_manu = OrmManufacturer( + name=ids["name"], + slug=slugify(ids["name"]), + ) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=new_manu) + diffsync.objects_to_create["vendors"].append(new_manu) + diffsync.vendor_map[slugify(ids["name"])] = new_manu.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update Manufacturer object in Nautobot.""" + _manu = OrmManufacturer.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating Manufacturer {_manu.name}.") + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_manu) + _manu.validated_save() + return super().update(attrs) + + def delete(self): + """Delete Manufacturer object from Nautobot. + + Because Manufacturer has a direct relationship with DeviceTypes and other objects it can't be deleted before them. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"Manufacturer {self.name} will be deleted.") + _manu = OrmManufacturer.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["manufacturer"].append(_manu) # pylint: disable=protected-access + return self + + +class NautobotHardware(Hardware): + """Nautobot Hardware model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create DeviceType object in Nautobot.""" + diffsync.job.log_info(message=f"Creating DeviceType {ids['name']}.") + try: + diffsync.devicetype_map[slugify(ids["name"])] + except KeyError: + new_dt = OrmDeviceType( + model=ids["name"], + slug=slugify(ids["name"]), + manufacturer_id=diffsync.vendor_map[slugify(attrs["manufacturer"])], + part_number=attrs["part_number"] if attrs.get("part_number") else "", + u_height=int(attrs["size"]) if attrs.get("size") else 1, + is_full_depth=bool(attrs.get("depth") == "Full Depth"), + ) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=new_dt) + diffsync.objects_to_create["devicetypes"].append(new_dt) + diffsync.devicetype_map[slugify(ids["name"])] = new_dt.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update DeviceType object in Nautobot.""" + _dt = OrmDeviceType.objects.get(id=self.uuid) + self.diffsync.job.log_debug(message=f"Updating DeviceType {_dt.model}.") + if "manufacturer" in attrs: + _dt.manufacturer = OrmManufacturer.objects.get(slug=slugify(attrs["manufacturer"])) + if "part_number" in attrs: + if attrs["part_number"] is not None: + _dt.part_number = attrs["part_number"] + else: + _dt.part_number = "" + if "size" in attrs: + _dt.u_height = int(attrs["size"]) + if "depth" in attrs: + _dt.is_full_depth = bool(attrs["depth"] == "Full Depth") + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_dt) + _dt.validated_save() + return super().update(attrs) + + def delete(self): + """Delete DeviceType object from Nautobot. + + Because DeviceType has a direct relationship with Devices it can't be deleted before all Devices are. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"DeviceType {self.name} will be deleted.") + _dt = OrmDeviceType.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["device_type"].append(_dt) # pylint: disable=protected-access + return self + + +class NautobotCluster(Cluster): + """Nautobot Cluster model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Virtual Chassis object in Nautobot. + + As the master node of the VC needs to be a regular Device, we'll create that and then the VC. + Member devices will be added to VC at Device creation. + """ + diffsync.job.log_debug(message=f"Creating VirtualChassis {ids['name']}.") + new_vc = OrmVC( + name=ids["name"], + ) + if attrs.get("tags"): + for _tag in nautobot.get_tags(attrs["tags"]): + new_vc.tags.add(_tag) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=new_vc) + diffsync.objects_to_create["clusters"].append(new_vc) + diffsync.cluster_map[ids["name"]] = new_vc.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update Virtual Chassis object in Nautobot.""" + _vc = OrmVC.objects.get(id=self.uuid) + self.diffsync.job.log_debug(message=f"Updating VirtualChassis {_vc.name}.") + if "tags" in attrs: + if attrs.get("tags"): + nautobot.update_tags(tagged_obj=_vc, new_tags=attrs["tags"]) + else: + _vc.tags.clear() + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_vc) + _vc.validated_save() + return super().update(attrs) + + def delete(self): + """Delete Virtual Chassis object from Nautobot. + + Because Virtual Chassis has a direct relationship with Devices it can't be deleted before they are. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"Virtual Chassis {self.name} will be deleted.") + _cluster = OrmVC.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["cluster"].append(_cluster) # pylint: disable=protected-access + return self + + +class NautobotDevice(Device): + """Nautobot Device model.""" + + @staticmethod + def _get_site(diffsync, building: str): + """Get Site ID from Building name.""" + try: + _site = diffsync.site_map[slugify(building)] + return _site + except KeyError: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_debug(message=f"Unable to find Site {building}.") + return None + + @classmethod + def create(cls, diffsync, ids, attrs): # pylint: disable=inconsistent-return-statements + """Create Device object in Nautobot.""" + diffsync.job.log_info(message=f"Creating Device {ids['name']}.") + if attrs["in_service"]: + _status = diffsync.status_map["active"] + else: + _status = diffsync.status_map["offline"] + if attrs.get("tags") and len(attrs["tags"]) > 0: + _role = nautobot.verify_device_role( + diffsync=diffsync, role_name=device42.find_device_role_from_tags(tag_list=attrs["tags"]) + ) + else: + _role = nautobot.verify_device_role(diffsync=diffsync, role_name=DEFAULTS.get("device_role")) + try: + _dt = diffsync.devicetype_map[slugify(attrs["hardware"])] + except KeyError: + diffsync.job.log_warning(message=f"Unable to find DeviceType {attrs['hardware']}.") + return None + _site = cls._get_site(diffsync, building=attrs["building"]) + if not _site: + diffsync.job.log_warning(message=f"Can't create {ids['name']} as unable to determine Site.") + return None + new_device = OrmDevice( + name=ids["name"][:64], + status_id=_status, + site_id=_site, + device_type_id=_dt, + device_role_id=_role, + serial=attrs["serial_no"] if attrs.get("serial_no") else "", + ) + if attrs.get("rack"): + new_device.rack_id = diffsync.rack_map[slugify(attrs["building"])][slugify(attrs["room"])][attrs["rack"]] + new_device.position = int(attrs["rack_position"]) if attrs["rack_position"] else None + new_device.face = attrs["rack_orientation"] if attrs["rack_orientation"] else "front" + if attrs.get("os"): + devicetype = diffsync.get(NautobotHardware, attrs["hardware"]) + new_device.platform_id = nautobot.verify_platform( + diffsync=diffsync, + platform_name=attrs["os"], + manu=diffsync.vendor_map[slugify(devicetype.manufacturer)], + ) + if attrs.get("os_version"): + if LIFECYCLE_MGMT and attrs.get("os"): + manu_id = None + for dt in diffsync.objects_to_create["devicetypes"]: + if dt.model == attrs["hardware"]: + manu_id = dt.manufacturer_id + if manu_id: + soft_lcm = cls._add_software_lcm( + diffsync=diffsync, os=attrs["os"], version=attrs["os_version"], manufacturer=manu_id + ) + cls._assign_version_to_device(diffsync=diffsync, device=new_device.id, software_lcm=soft_lcm) + else: + attrs["custom_fields"].append({"key": "OS Version", "value": attrs["os_version"]}) + if attrs.get("cluster_host"): + try: + _vc = diffsync.cluster_map[attrs["cluster_host"]] + new_device.virtual_chassis_id = _vc + if attrs.get("master_device") and attrs["master_device"]: + for vc in diffsync.objects_to_create["clusters"]: + if vc.name == attrs["cluster_host"]: + diffsync.objects_to_create["master_devices"].append((_vc, new_device.id)) + # vc.master = new_device + except KeyError: + diffsync.job.log_warning(message=f"Unable to find Virtual Chassis {attrs['cluster_host']}") + if attrs.get("vc_position"): + new_device.vc_position = attrs["vc_position"] + if attrs.get("tags"): + for _tag in nautobot.get_tags(attrs["tags"]): + new_device.tags.add(_tag) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=new_device) + diffsync.objects_to_create["devices"].append(new_device) + diffsync.device_map[ids["name"]] = new_device.id + return super().create(diffsync=diffsync, ids=ids, attrs=attrs) + + def update(self, attrs): + """Update Device object in Nautobot.""" + _dev = OrmDevice.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating Device {self.name} in {_dev.site} with {attrs}") + if "building" in attrs: + site_id = None + try: + site_id = OrmSite.objects.get(name=attrs["building"]) + except OrmSite.DoesNotExist: + for site in self.diffsync.objects_to_create["sites"]: + if site.slug == attrs["building"]: + site.validated_save() + self.diffsync.objects_to_create["sites"].remove(site) + site_id = self._get_site(diffsync=self.diffsync, building=attrs["building"]) + if site_id: + _dev.site_id = site_id + if "rack_position" in attrs: + _dev.position = int(attrs["rack_position"]) if attrs["rack_position"] else None + if "rack_orientation" in attrs: + _dev.face = attrs["rack_orientation"] + if "rack" in attrs: + try: + _dev.rack = OrmRack.objects.get(name=attrs["rack"], group__name=self.room) + except OrmRack.DoesNotExist as err: + self.diffsync.job.log_warning(message=f"Unable to find rack {attrs['rack']} in {self.room} {err}") + if "rack" in attrs and "room" in attrs: + try: + _dev.rack = OrmRack.objects.get(name=attrs["rack"], group__name=attrs["room"]) + _dev.site = _dev.rack.site + except OrmRack.DoesNotExist as err: + if self.diffsync.job.kwargs.get("debug"): + self.diffsync.job.log_warning( + message=f"Unable to find rack {attrs['rack']} in {attrs['room']} {err}" + ) + if "hardware" in attrs: + for new_dt in self.diffsync.objects_to_create["devicetypes"]: + if new_dt.model == attrs["hardware"]: + new_dt.validated_save() + self.diffsync.objects_to_create["devicetypes"].remove(new_dt) + _dev.device_type_id = self.diffsync.devicetype_map[slugify(attrs["hardware"])] + if "os" in attrs: + if attrs.get("hardware"): + _hardware = self.diffsync.get(NautobotHardware, attrs["hardware"]) + else: + _hardware = self.diffsync.get(NautobotHardware, self.hardware) + _dev.platform_id = nautobot.verify_platform( + diffsync=self.diffsync, + platform_name=attrs["os"], + manu=self.diffsync.vendor_map[slugify(_hardware.manufacturer)], + ) + if "os_version" in attrs: + if attrs.get("os"): + _os = attrs["os"] + else: + _os = self.os + if attrs.get("os_version"): + if LIFECYCLE_MGMT: + soft_lcm = self._add_software_lcm( + diffsync=self.diffsync, + os=_os, + version=attrs["os_version"], + manufacturer=_dev.device_type.manufacturer.id, + ) + self._assign_version_to_device(diffsync=self.diffsync, device=_dev.id, software_lcm=soft_lcm) + else: + attrs["custom_fields"].append( + { + "key": "OS Version", + "value": attrs["os_version"] if attrs.get("os_version") else self.os_version, + } + ) + if "in_service" in attrs: + if attrs["in_service"]: + _status = self.diffsync.status_map["active"] + else: + _status = self.diffsync.status_map["offline"] + _dev.status_id = _status + if "serial_no" in attrs: + _dev.serial = attrs["serial_no"] + if _dev.device_role.name == "Unknown" and self.tags: + _dev.device_role_id = nautobot.verify_device_role( + diffsync=self.diffsync, role_name=device42.find_device_role_from_tags(tag_list=self.tags) + ) + if "tags" in attrs: + if attrs.get("tags"): + _dev.device_role_id = nautobot.verify_device_role( + diffsync=self.diffsync, role_name=device42.find_device_role_from_tags(tag_list=attrs["tags"]) + ) + else: + _dev.device_role_id = nautobot.verify_device_role( + diffsync=self.diffsync, role_name=DEFAULTS.get("device_role") + ) + nautobot.update_tags(tagged_obj=_dev, new_tags=attrs["tags"]) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_dev) + # ensure that VC Master Device is set to that + if "cluster_host" in attrs or "master_device" in attrs: + if attrs.get("cluster_host"): + _clus_host = attrs["cluster_host"] + else: + _clus_host = self.cluster_host + try: + _vc = self.diffsync.cluster_map[_clus_host] + _dev.virtual_chassis_id = _vc + _dev.vc_position = self.vc_position + if attrs.get("master_device"): + try: + vc = OrmVC.objects.get(id=_vc) + vc.master = _dev + vc.validated_save() + except OrmVC.DoesNotExist: + for vc in self.diffsync.objects_to_create["clusters"]: + if vc.name == _clus_host: + vc = self.diffsync.objects_to_create["clusters"].pop(vc) + vc.master = _dev + vc.validated_save() + except KeyError: + self.diffsync.job.log_warning(message=f"Unable to find Virtual Chassis {_clus_host}") + if "vc_position" in attrs: + # need to ensure the new position isn't already taken + try: + if attrs.get("cluster_host"): + vc = OrmVC.objects.get(name=attrs["cluster_host"]) + else: + vc = OrmVC.objects.get(name=self.cluster_host) + try: + dev = OrmDevice.objects.get(virtual_chassis=vc, vc_position=attrs["vc_position"]) + dev.vc_position = None + dev.virtual_chassis = None + dev.validated_save() + except OrmDevice.DoesNotExist: + self.diffsync.job.log_info(message=f"Didn't find Device in VC position: {attrs['vc_position']}.") + except OrmVC.DoesNotExist as err: + self.diffsync.job.log_warning( + message=f"Unable to find Virtual Chassis {attrs['cluster_host'] if attrs.get('cluster_host') else self.cluster_host}. {err}" + ) + _dev.vc_position = attrs["vc_position"] + try: + _dev.validated_save() + return super().update(attrs) + except ValidationError as err: + self.diffsync.job.log_warning(message=f"Error updating Device {self.name} with {attrs}. {err}") + return None + + def delete(self): + """Delete Device object from Nautobot. + + Because Device has a direct relationship with Ports and IP Addresses it can't be deleted before they are. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"Device {self.name} will be deleted.") + _dev = OrmDevice.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["device"].append(_dev) # pylint: disable=protected-access + return self + + @staticmethod + def _add_software_lcm(diffsync: object, os: str, version: str, manufacturer: UUID): + """Add OS Version as SoftwareLCM if Device Lifecycle Plugin found.""" + _platform = nautobot.verify_platform(diffsync=diffsync, platform_name=os, manu=manufacturer) + try: + os_ver = diffsync.softwarelcm_map[os][version] + except KeyError: + os_ver = SoftwareLCM( + device_platform_id=_platform, + version=version, + ) + diffsync.objects_to_create["softwarelcms"].append(os_ver) + if os not in diffsync.softwarelcm_map: + diffsync.softwarelcm_map[os] = {} + diffsync.softwarelcm_map[os][version] = os_ver.id + os_ver = os_ver.id + return os_ver + + @staticmethod + def _assign_version_to_device(diffsync, device: UUID, software_lcm: UUID): + """Add Relationship between Device and SoftwareLCM.""" + try: + dev = OrmDevice.objects.get(id=device) + relations = dev.get_relationships() + software_relation_id = diffsync.relationship_map["Software on Device"] + for _, relationships in relations.items(): + for relationship, queryset in relationships.items(): + if relationship.id == software_relation_id: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning( + message=f"Deleting Software Version Relationships for {dev.name} to assign a new version." + ) + queryset.delete() + except OrmDevice.DoesNotExist: + diffsync.job.log_warning(message=f"Unable to find Device {device} to assign software to.") + new_assoc = RelationshipAssociation( + relationship_id=diffsync.relationship_map["Software on Device"], + source_type=ContentType.objects.get_for_model(SoftwareLCM), + source_id=software_lcm, + destination_type=ContentType.objects.get_for_model(OrmDevice), + destination_id=device, + ) + diffsync.objects_to_create["relationshipasscs"].append(new_assoc) + + +class NautobotPort(Port): + """Nautobot Port model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): # pylint: disable=inconsistent-return-statements + """Create Interface object in Nautobot.""" + try: + _dev = diffsync.device_map[ids["device"]] + except KeyError: + diffsync.job.log_warning( + message=f"Attempting to create Interface {ids['name']} for {ids['device']} failed as {ids['device']} doesn't exist." + ) + return None + new_intf = OrmInterface( + name=ids["name"], + device_id=_dev, + enabled=is_truthy(attrs["enabled"]), + mtu=attrs["mtu"] if attrs.get("mtu") else 1500, + description=attrs["description"], + type=attrs["type"], + mac_address=attrs["mac_addr"][:12] if attrs.get("mac_addr") else None, + mode=attrs["mode"], + status_id=diffsync.status_map[attrs["status"]], + ) + if attrs.get("tags"): + for _tag in nautobot.get_tags(attrs["tags"]): + new_intf.tags.add(_tag) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=new_intf) + if attrs.get("vlans"): + nautobot.apply_vlans_to_port( + diffsync=diffsync, device_name=ids["device"], mode=attrs["mode"], vlans=attrs["vlans"], port=new_intf + ) + diffsync.objects_to_create["ports"].append(new_intf) + if ids["device"] not in diffsync.port_map: + diffsync.port_map[ids["device"]] = {} + diffsync.port_map[ids["device"]][ids["name"]] = new_intf.id + if attrs.get("mac_addr"): + diffsync.port_map[attrs["mac_addr"][:12]] = new_intf.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update Interface object in Nautobot.""" + _port = OrmInterface.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating Port {_port.name} on {_port.device.name} with {attrs}") + if "enabled" in attrs: + _port.enabled = is_truthy(attrs["enabled"]) + if "mtu" in attrs: + _port.mtu = attrs["mtu"] + if "description" in attrs: + _port.description = attrs["description"] + if "mac_addr" in attrs: + _port.mac_address = attrs["mac_addr"][:12] if attrs.get("mac_addr") else None + if "type" in attrs: + _port.type = attrs["type"] + if "mode" in attrs: + _port.mode = attrs["mode"] + if attrs.get("status"): + _port.status_id = self.diffsync.status_map[attrs["status"]] + if "tags" in attrs: + if attrs.get("tags"): + nautobot.update_tags(tagged_obj=_port, new_tags=attrs["tags"]) + else: + _port.tags.clear() + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_port) + if "vlans" in attrs: + if attrs.get("mode"): + _mode = attrs["mode"] + else: + _mode = self.mode + if attrs.get("device"): + _device = attrs["device"] + else: + _device = self.device + # must ensure any new VLANs that are created + for update_vlan in attrs["vlans"]: + for vlan in self.diffsync.objects_to_create["vlans"]: + if vlan.vid == update_vlan and vlan.site == _port.device.site: + new_vlan = self.diffsync.objects_to_create["vlans"].pop( + self.diffsync.objects_to_create["vlans"].index(vlan) + ) + new_vlan.validated_save() + nautobot.apply_vlans_to_port( + diffsync=self.diffsync, device_name=_device, mode=_mode, vlans=attrs["vlans"], port=_port + ) + try: + _port.validated_save() + return super().update(attrs) + except ValidationError as err: + self.diffsync.job.log_warning( + message=f"Validation error for updating Port {_port.name} for {_port.device.name}: {err}" + ) + return None + + def delete(self): + """Delete Interface object from Nautobot.""" + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"Interface {self.name} for {self.device} will be deleted.") + _intf = OrmInterface.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["port"].append(_intf) # pylint: disable=protected-access + return self + + +class NautobotConnection(Connection): + """Nautobot Connection model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): # pylint: disable=inconsistent-return-statements + """Create Cable object in Nautobot.""" + diffsync.job.log_info( + message=f"Creating Cable between {ids['src_device']}'s {ids['src_port']} port to {ids['dst_device']} {ids['dst_port']} port." + ) + new_cable = None + if attrs["src_type"] == "circuit" or attrs["dst_type"] == "circuit": + new_cable = cls.get_circuit_connections(cls, diffsync=diffsync, ids=ids, attrs=attrs) + elif attrs["src_type"] == "interface" and attrs["dst_type"] == "interface": + new_cable = cls.get_device_connections(cls, diffsync=diffsync, ids=ids) + if new_cable: + diffsync.objects_to_create["cables"].append(new_cable) + if ids["src_device"] not in diffsync.cable_map: + diffsync.cable_map[ids["src_device"]] = {} + if ids["dst_device"] not in diffsync.cable_map: + diffsync.cable_map[ids["dst_device"]] = {} + diffsync.cable_map[ids["src_device"]][ids["src_port"]] = new_cable.id + diffsync.cable_map[ids["dst_device"]][ids["dst_port"]] = new_cable.id + return super().create(diffsync=diffsync, ids=ids, attrs=attrs) + else: + return None + + def get_circuit_connections(self, diffsync, ids, attrs) -> Optional[OrmCable]: + """Method to create a Cable between a Circuit and a Device. + + Args: + diffsync (obj): DiffSync job used for logging. + ids (dict): Identifying attributes for the object. + attrs (dict): Non-identifying attributes for the object. + + Returns: + Optional[OrmCable]: If the Interfaces are found and a cable is created, returns Cable else None. + """ + _intf, circuit = None, None + if attrs["src_type"] == "interface": + try: + circuit = diffsync.circuit_map[ids["dst_device"]] + except KeyError: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning( + message=f"Unable to find Circuit for {ids['src_device']}: {ids['src_port']} to connect to Circuit {ids['dst_device']}" + ) + return None + try: + _intf = diffsync.port_map[ids["src_port_mac"]] + except KeyError: + try: + _intf = diffsync.port_map[ids["src_device"]][ids["src_port"]] + except KeyError: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning( + message=f"Unable to find source port for {ids['src_device']}: {ids['src_port']} to connect to Circuit {ids['dst_device']}" + ) + return None + elif attrs["src_type"] == "patch panel": + try: + _intf = OrmFrontPort.objects.get(device__name=ids["src_device"], name=ids["src_port"]) + circuit = diffsync.circuit_map[ids["dst_device"]] + except KeyError: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning( + message=f"Unable to find patch panel port for {ids['src_device']}: {ids['src_port']} to connect to Circuit {ids['dst_device']}" + ) + return None + if attrs["dst_type"] == "interface": + try: + circuit = diffsync.circuit_map[ids["src_device"]] + except KeyError: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning( + message=f"Unable to find Circuit for {ids['src_device']}: {ids['src_port']} to connect to Circuit {ids['dst_device']}" + ) + return None + try: + _intf = diffsync.port_map[ids["dst_port_mac"]] + except KeyError: + try: + _intf = diffsync.port_map[ids["dst_device"]][ids["dst_port"]] + except KeyError: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning( + message=f"Unable to find destination port for {ids['dst_device']}: {ids['dst_port']} to connect to Circuit {ids['src_device']}" + ) + return None + elif attrs["dst_type"] == "patch panel": + try: + circuit = diffsync.circuit_map[ids["src_device"]] + except KeyError as err: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning(message=f"Unable to find Circuit {ids['dst_device']} {err}") + return None + try: + _intf = OrmFrontPort.objects.get(device__name=ids["dst_device"], name=ids["dst_port"]) + except OrmFrontPort.DoesNotExist as err: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning( + message=f"Unable to find destination port for {ids['dst_device']}: {ids['dst_port']} to connect to Circuit {ids['src_device']} {err}" + ) + return None + if circuit and _intf: + _ct = { + "circuit_id": circuit, + "site": _intf.device.site, + } + else: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning(message=f"Unable to find Circuit and Interface {ids}") + return None + if attrs["src_type"] == "circuit": + _ct["term_side"] = "Z" + if attrs["dst_type"] == "circuit": + _ct["term_side"] = "A" + try: + circuit_term = OrmCT.objects.get(**_ct) + except OrmCT.DoesNotExist: + circuit_term = OrmCT(**_ct) + circuit_term.port_speed = INTF_SPEED_MAP[_intf.type] if isinstance(_intf, OrmInterface) else None + circuit_term.validated_save() + if _intf and not _intf.cable and not circuit_term.cable: + new_cable = OrmCable( + termination_a_type=ContentType.objects.get(app_label="dcim", model="interface") + if attrs["src_type"] == "interface" + else ContentType.objects.get(app_label="dcim", model="frontport"), + termination_a_id=_intf, + termination_b_type=ContentType.objects.get(app_label="circuits", model="circuittermination"), + termination_b_id=circuit_term.id, + status=OrmStatus.objects.get(name="Connected"), + color=nautobot.get_random_color(), + ) + return new_cable + else: + return None + + def get_device_connections(self, diffsync, ids) -> Optional[OrmCable]: + """Method to create a Cable between two Devices. + + Args: + diffsync (obj): DiffSync job used for logging. + ids (dict): Identifying attributes for the object. + + Returns: + Optional[OrmCable]: If the Interfaces are found and a cable is created, returns Cable else None. + """ + _src_port, _dst_port = None, None + try: + _src_port = diffsync.port_map[ids["src_port_mac"]] + except KeyError: + try: + _src_port = diffsync.port_map[ids["src_device"]][ids["src_port"]] + except KeyError: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning( + message=f"Unable to find source port for {ids['src_device']}: {ids['src_port']} {ids['src_port_mac']}" + ) + return None + try: + _dst_port = diffsync.port_map[ids["dst_port_mac"]] + except KeyError: + try: + _dst_port = diffsync.port_map[ids["dst_device"]][ids["dst_port"]] + except KeyError: + if diffsync.job.kwargs.get("debug"): + diffsync.job.log_warning( + message=f"Unable to find destination port for {ids['dst_device']}: {ids['dst_port']} {ids['dst_port_mac']}" + ) + return None + if _src_port and _dst_port: + new_cable = OrmCable( + termination_a_type=ContentType.objects.get(app_label="dcim", model="interface"), + termination_a_id=_src_port, + termination_b_type=ContentType.objects.get(app_label="dcim", model="interface"), + termination_b_id=_dst_port, + status_id=diffsync.status_map["connected"], + color=nautobot.get_random_color(), + ) + return new_cable + else: + return None + + def delete(self): + """Delete Cable object from Nautobot.""" + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info( + message=f"Deleting Cable between {self.src_device}'s {self.src_port} port to {self.dst_device} {self.dst_port} port." + ) + _conn = OrmCable.objects.get(id=self.uuid) + _conn.delete() + return self diff --git a/nautobot_ssot/integrations/device42/diffsync/models/nautobot/ipam.py b/nautobot_ssot/integrations/device42/diffsync/models/nautobot/ipam.py new file mode 100644 index 000000000..8be3d3158 --- /dev/null +++ b/nautobot_ssot/integrations/device42/diffsync/models/nautobot/ipam.py @@ -0,0 +1,363 @@ +"""DiffSyncModel IPAM subclasses for Nautobot Device42 data sync.""" + +import re +from django.contrib.contenttypes.models import ContentType +from django.forms import ValidationError +from django.utils.text import slugify +from nautobot.dcim.models import Interface as OrmInterface +from nautobot.extras.models import Status as OrmStatus +from nautobot.ipam.models import VLAN as OrmVLAN +from nautobot.ipam.models import VRF as OrmVRF +from nautobot.ipam.models import IPAddress as OrmIPAddress +from nautobot.ipam.models import Prefix as OrmPrefix +from nautobot_ssot.integrations.device42.constant import PLUGIN_CFG +from nautobot_ssot.integrations.device42.diffsync.models.base.ipam import VLAN, IPAddress, Subnet, VRFGroup +from nautobot_ssot.integrations.device42.utils import nautobot + + +class NautobotVRFGroup(VRFGroup): + """Nautobot VRFGroup model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create VRF object in Nautobot.""" + _vrf = OrmVRF(name=ids["name"], description=attrs["description"]) + diffsync.job.log_info(message=f"Creating VRF {_vrf.name}.") + if attrs.get("tags"): + for _tag in nautobot.get_tags(attrs["tags"]): + _vrf.tags.add(_tag) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_vrf) + diffsync.objects_to_create["vrfs"].append(_vrf) + diffsync.vrf_map[ids["name"]] = _vrf.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update VRF object in Nautobot.""" + _vrf = OrmVRF.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating VRF {_vrf.name}.") + if "description" in attrs: + _vrf.description = attrs["description"] + if "tags" in attrs: + if attrs.get("tags"): + nautobot.update_tags(tagged_obj=_vrf, new_tags=attrs["tags"]) + else: + _vrf.tags.clear() + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_vrf) + _vrf.validated_save() + return super().update(attrs) + + def delete(self): + """Delete VRF object from Nautobot. + + Because VRF has a direct relationship with many other objects it can't be deleted before anything else. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"VRF {self.name} will be deleted.") + vrf = OrmVRF.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["vrf"].append(vrf) # pylint: disable=protected-access + return self + + +class NautobotSubnet(Subnet): + """Nautobot Subnet model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Prefix object in Nautobot.""" + if ids.get("vrf"): + vrf_name = ids["vrf"] + else: + vrf_name = "unknown" + prefix = f"{ids['network']}/{ids['mask_bits']}" + diffsync.job.log_info(message=f"Creating Prefix {prefix} in VRF {vrf_name}.") + _pf = OrmPrefix( + prefix=prefix, + vrf_id=diffsync.vrf_map[vrf_name], + description=attrs["description"], + status_id=diffsync.status_map["active"], + ) + if attrs.get("tags"): + for _tag in nautobot.get_tags(attrs["tags"]): + _pf.tags.add(_tag) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_pf) + diffsync.objects_to_create["prefixes"].append(_pf) + if vrf_name not in diffsync.prefix_map: + diffsync.prefix_map[vrf_name] = {} + diffsync.prefix_map[vrf_name][f"{ids['network']}/{ids['mask_bits']}"] = _pf.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update Prefix object in Nautobot.""" + _pf = OrmPrefix.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Updating Prefix {_pf.prefix}.") + if "description" in attrs: + _pf.description = attrs["description"] + if "tags" in attrs: + if attrs.get("tags"): + nautobot.update_tags(tagged_obj=_pf, new_tags=attrs["tags"]) + else: + _pf.tags.clear() + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_pf) + _pf.validated_save() + return super().update(attrs) + + def delete(self): + """Delete Subnet object from Nautobot. + + Because Subnet has a direct relationship with many other objects it can't be deleted before anything else. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + subnet = OrmPrefix.objects.get(id=self.uuid) + self.diffsync.job.log_info(message=f"Prefix {subnet.prefix} will be deleted.") + self.diffsync.objects_to_delete["subnet"].append(subnet) # pylint: disable=protected-access + return self + + +class NautobotIPAddress(IPAddress): + """Nautobot IP Address model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create IP Address object in Nautobot.""" + # if "/32" in ids["address"] and attrs.get("primary"): + # _pf = OrmPrefix.objects.net_contains(ids["address"]) + # # the last Prefix is the most specific and is assumed the one the IP address resides in + # if len(_pf) > 1: + # _range = _pf[len(_pf) - 1] + # _netmask = _range.prefix_length + # else: + # # for the edge case where the DNS answer doesn't reside in a pre-existing Prefix + # _netmask = "32" + # _address = re.sub(r"\/32", f"/{_netmask}", ids["address"]) + # else: + + # Define regex match for Management interface (ex Management/Mgmt/mgmt/management) + # mgmt = r"^[mM]anagement|^[mM]gmt" + + _address = ids["address"] + _ip = OrmIPAddress( + address=_address, + vrf_id=diffsync.vrf_map[ids["vrf"]] if ids.get("vrf") else None, + status_id=diffsync.status_map["active"] if not attrs.get("available") else diffsync.status_map["reserved"], + description=attrs["label"] if attrs.get("label") else "", + ) + if attrs.get("device") and attrs.get("interface"): + try: + diffsync.job.log_info(message=f"Creating IPAddress {_address}.") + intf = diffsync.port_map[attrs["device"]][attrs["interface"]] + _ip.assigned_object_type = ContentType.objects.get(app_label="dcim", model="interface") + _ip.assigned_object_id = intf + + if attrs.get("primary"): + diffsync.objects_to_create["device_primary_ip"].append( + (diffsync.device_map[attrs["device"]], _ip.id) + ) + except KeyError: + diffsync.job.log_debug( + message=f"Unable to find Interface {attrs['interface']} for {attrs['device']}.", + ) + if attrs.get("interface"): + if re.search(r"[Ll]oopback", attrs["interface"]): + _ip.role = "loopback" + if attrs.get("tags"): + nautobot.update_tags(tagged_obj=_ip, new_tags=attrs["tags"]) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_ip) + diffsync.objects_to_create["ipaddrs"].append(_ip) + if ids.get("vrf"): + vrf_name = ids["vrf"] + else: + vrf_name = "global" + if vrf_name not in diffsync.ipaddr_map: + diffsync.ipaddr_map[vrf_name] = {} + diffsync.ipaddr_map[vrf_name][_address] = _ip.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update IPAddress object in Nautobot.""" + try: + _ipaddr = OrmIPAddress.objects.get(id=self.uuid) + except OrmIPAddress.DoesNotExist: + if self.diffsync.job.kwargs.get("debug"): + self.diffsync.job.log_debug( + message="IP Address passed to update but can't be found. This shouldn't happen. Why is this happening?!?!" + ) + return + self.diffsync.job.log_info( + message=f"Updating IPAddress {_ipaddr.address} for {_ipaddr.vrf.name if _ipaddr.vrf else ''}" + ) + if "available" in attrs: + _ipaddr.status = ( + OrmStatus.objects.get(name="Active") + if not attrs["available"] + else OrmStatus.objects.get(name="Reserved") + ) + if "label" in attrs: + _ipaddr.description = attrs["label"] if attrs.get("label") else "" + if attrs.get("device") and attrs.get("interface"): + _device = attrs["device"] + if self.primary: + nautobot.unassign_primary(_ipaddr) + try: + intf = OrmInterface.objects.get(device__name=_device, name=attrs["interface"]) + _ipaddr.assigned_object_type = ContentType.objects.get(app_label="dcim", model="interface") + _ipaddr.assigned_object_id = intf.id + try: + _ipaddr.validated_save() + except ValidationError as err: + self.diffsync.job.log_warning( + message=f"Failure updating Device & Interface for {_ipaddr.address}. {err}" + ) + except OrmInterface.DoesNotExist as err: + self.diffsync.job.log_warning( + message=f"Unable to find Interface {attrs['interface']} for {attrs['device']}. {err}" + ) + elif attrs.get("device"): + try: + intf = OrmInterface.objects.get(device__name=attrs["device"], name=self.interface) + _ipaddr.assigned_object_type = ContentType.objects.get(app_label="dcim", model="interface") + _ipaddr.assigned_object_id = intf.id + nautobot.unassign_primary(_ipaddr) + except OrmInterface.DoesNotExist as err: + self.diffsync.job.log_debug( + message=f"Unable to find Interface {attrs['interface'] if attrs.get('interface') else self.interface} for {attrs['device']} {err}" + ) + elif attrs.get("interface"): + try: + OrmInterface.objects.get(name=attrs["interface"], device__name=self.device) + except OrmInterface.DoesNotExist: + for port in self.diffsync.objects_to_create["ports"]: + if port.name == attrs["interface"] and port.device_id == self.diffsync.device_map[self.device]: + try: + port.validated_save() + except ValidationError as err: + self.diffsync.job.log_warning( + message=f"Failure saving port {port.name} for IPAddress {_ipaddr.address}. {err}" + ) + try: + if attrs.get("device") and attrs["device"] in self.diffsync.port_map: + intf = self.diffsync.port_map[attrs["device"]][attrs["interface"]] + else: + intf = self.diffsync.port_map[self.device][attrs["interface"]] + _ipaddr.assigned_object_type = ContentType.objects.get(app_label="dcim", model="interface") + _ipaddr.assigned_object_id = intf + try: + _ipaddr.validated_save() + except ValidationError as err: + self.diffsync.job.log_warning(message=f"Failure updating Interface for {_ipaddr.address}. {err}") + except KeyError as err: + self.diffsync.job.log_debug( + message=f"Unable to find Interface {attrs['interface']} for {attrs['device'] if attrs.get('device') else self.device}. {err}" + ) + if attrs.get("primary") or self.primary is True: + if getattr(_ipaddr, "assigned_object"): + if _ipaddr.family == 4: + _ipaddr.assigned_object.device.primary_ip4 = _ipaddr + else: + _ipaddr.assigned_object.device.primary_ip6 = _ipaddr + _ipaddr.assigned_object.device.validated_save() + else: + self.diffsync.job.log_warning( + message=f"IPAddress {_ipaddr.address} is showing unassigned from an Interface so can't be marked primary." + ) + if "tags" in attrs: + if attrs.get("tags"): + nautobot.update_tags(tagged_obj=_ipaddr, new_tags=attrs["tags"]) + else: + _ipaddr.tags.clear() + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_ipaddr) + try: + _ipaddr.validated_save() + return super().update(attrs) + except ValidationError as err: + self.diffsync.job.log_warning(message=f"Unable to update IP Address {self.address} with {attrs}. {err}") + return None + + def delete(self): + """Delete IPAddress object from Nautobot. + + Because IPAddress has a direct relationship with many other objects it can't be deleted before anything else. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"IP Address {self.address} will be deleted.") + ipaddr = OrmIPAddress.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["ipaddr"].append(ipaddr) # pylint: disable=protected-access + return self + + +class NautobotVLAN(VLAN): + """Nautobot VLAN model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create VLAN object in Nautobot.""" + _site_name = None, None + if ids["building"] != "Unknown": + _site_name = slugify(ids["building"]) + else: + _site_name = "global" + diffsync.job.log_info(message=f"Creating VLAN {ids['vlan_id']} {attrs['name']} for {_site_name}") + new_vlan = OrmVLAN( + name=attrs["name"], + vid=ids["vlan_id"], + site_id=diffsync.site_map[_site_name] if _site_name != "global" else None, + status_id=diffsync.status_map["active"], + description=attrs["description"], + ) + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=new_vlan) + if attrs.get("tags"): + nautobot.update_tags(tagged_obj=new_vlan, new_tags=attrs["tags"]) + diffsync.objects_to_create["vlans"].append(new_vlan) + if _site_name not in diffsync.vlan_map: + diffsync.vlan_map[_site_name] = {} + diffsync.vlan_map[_site_name][ids["vlan_id"]] = new_vlan.id + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update VLAN object in Nautobot.""" + _vlan = OrmVLAN.objects.get(id=self.uuid) + self.diffsync.job.log_info( + message=f"Updating VLAN {_vlan.name} {_vlan.vid} for {_vlan.site.name if _vlan.site else 'global'}." + ) + if "name" in attrs: + _vlan.name = attrs["name"] + if "description" in attrs: + _vlan.description = attrs["description"] if attrs.get("description") else "" + if attrs.get("custom_fields"): + nautobot.update_custom_fields(new_cfields=attrs["custom_fields"], update_obj=_vlan) + if "tags" in attrs: + if attrs.get("tags"): + nautobot.update_tags(tagged_obj=_vlan, new_tags=attrs["tags"]) + else: + _vlan.tags.clear() + _vlan.validated_save() + return super().update(attrs) + + def delete(self): + """Delete VLAN object from Nautobot. + + Because VLAN has a direct relationship with many other objects it can't be deleted before anything else. + The self.diffsync.objects_to_delete dictionary stores all objects for deletion and removes them from Nautobot + in the correct order. This is used in the Nautobot adapter sync_complete function. + """ + if PLUGIN_CFG.get("delete_on_sync"): + super().delete() + self.diffsync.job.log_info(message=f"VLAN {self.name} {self.vlan_id} {self.building} will be deleted.") + vlan = OrmVLAN.objects.get(id=self.uuid) + self.diffsync.objects_to_delete["vlan"].append(vlan) # pylint: disable=protected-access + return self diff --git a/nautobot_ssot/integrations/device42/jobs.py b/nautobot_ssot/integrations/device42/jobs.py new file mode 100644 index 000000000..8b8d6772d --- /dev/null +++ b/nautobot_ssot/integrations/device42/jobs.py @@ -0,0 +1,149 @@ +# pylint: disable=too-few-public-methods +"""Jobs for Device42 integration with SSoT plugin.""" + +from django.templatetags.static import static +from django.urls import reverse +from nautobot.extras.jobs import BooleanVar, Job +from nautobot_ssot.jobs.base import DataMapping, DataSource + +from nautobot_ssot.integrations.device42.constant import PLUGIN_CFG +from nautobot_ssot.integrations.device42.diffsync.adapters.device42 import Device42Adapter +from nautobot_ssot.integrations.device42.diffsync.adapters.nautobot import NautobotAdapter +from nautobot_ssot.integrations.device42.utils.device42 import Device42API + + +name = "Device42 SSoT" # pylint: disable=invalid-name + + +class Device42DataSource(DataSource, Job): + """Device42 SSoT Data Source.""" + + debug = BooleanVar(description="Enable for more verbose debug logging", default=False) + bulk_import = BooleanVar(description="Enable using bulk create option for object creation.", default=False) + + class Meta: + """Meta data for Device42.""" + + name = "Device42" + data_source = "Device42" + data_source_icon = static("nautobot_ssot_device42/d42_logo.png") + description = "Sync information from Device42 to Nautobot" + + @classmethod + def config_information(cls): + """Dictionary describing the configuration of this DataSource.""" + return { + "Device42 Host": PLUGIN_CFG.get("device42_host"), + "Username": PLUGIN_CFG.get("device42_username"), + "Verify SSL": str(PLUGIN_CFG.get("device42_verify_ssl")), + } + + @classmethod + def data_mappings(cls): + """List describing the data mappings involved in this DataSource.""" + return ( + DataMapping( + "Buildings", f"{PLUGIN_CFG['device42_host']}admin/rackraj/building/", "Sites", reverse("dcim:site_list") + ), + DataMapping( + "Rooms", + f"{PLUGIN_CFG['device42_host']}admin/rackraj/room/", + "Rack Groups", + reverse("dcim:rackgroup_list"), + ), + DataMapping( + "Racks", f"{PLUGIN_CFG['device42_host']}admin/rackraj/rack/", "Racks", reverse("dcim:rack_list") + ), + DataMapping( + "Vendors", + f"{PLUGIN_CFG['device42_host']}admin/rackraj/organisation/", + "Manufacturers", + reverse("dcim:manufacturer_list"), + ), + DataMapping( + "Hardware Models", + f"{PLUGIN_CFG['device42_host']}admin/rackraj/hardware/", + "Device Types", + reverse("dcim:devicetype_list"), + ), + DataMapping( + "Devices", f"{PLUGIN_CFG['device42_host']}admin/rackraj/device/", "Devices", reverse("dcim:device_list") + ), + DataMapping( + "Ports", + f"{PLUGIN_CFG['device42_host']}admin/rackraj/netport/", + "Interfaces", + reverse("dcim:interface_list"), + ), + DataMapping( + "Cables", f"{PLUGIN_CFG['device42_host']}admin/rackraj/cable/", "Cables", reverse("dcim:cable_list") + ), + DataMapping( + "VPC (VRF Groups)", + f"{PLUGIN_CFG['device42_host']}admin/rackraj/vrfgroup/", + "VRFs", + reverse("ipam:vrf_list"), + ), + DataMapping( + "Subnets", f"{PLUGIN_CFG['device42_host']}admin/rackraj/vlan/", "Prefixes", reverse("ipam:prefix_list") + ), + DataMapping( + "IP Addresses", + f"{PLUGIN_CFG['device42_host']}admin/rackraj/ip_address/", + "IP Addresses", + reverse("ipam:ipaddress_list"), + ), + DataMapping( + "VLANs", f"{PLUGIN_CFG['device42_host']}admin/rackraj/switch_vlan/", "VLANs", reverse("ipam:vlan_list") + ), + DataMapping( + "Vendors", + f"{PLUGIN_CFG['device42_host']}admin/rackraj/organisation/", + "Providers", + reverse("circuits:provider_list"), + ), + DataMapping( + "Telco Circuits", + f"{PLUGIN_CFG['device42_host']}admin/rackraj/circuit/", + "Circuits", + reverse("circuits:circuit_list"), + ), + ) + + def load_source_adapter(self): + """Load data from Device42 into DiffSync models.""" + if self.kwargs["debug"]: + self.log_info(message="Connecting to Device42...") + client = Device42API( + base_url=PLUGIN_CFG["device42_host"], + username=PLUGIN_CFG["device42_username"], + password=PLUGIN_CFG["device42_password"], + verify=PLUGIN_CFG["device42_verify_ssl"], + ) + self.source_adapter = Device42Adapter(job=self, sync=self.sync, client=client) + if self.kwargs["debug"]: + self.log_info(message="Loading data from Device42...") + self.source_adapter.load() + + def load_target_adapter(self): + """Load data from Nautobot into DiffSync models.""" + self.target_adapter = NautobotAdapter(job=self, sync=self.sync) + if self.kwargs["debug"]: + self.log_info(message="Loading data from Nautobot...") + self.target_adapter.load() + + def execute_sync(self): + """Execute the synchronization of data from Device42 to Nautobot.""" + + def post_run(self): + """Execute sync after Job is complete so the transactions are not atomic.""" + if not self.kwargs["dry_run"]: + self.log_info(message="Beginning synchronization of data from Device42 into Nautobot.") + if self.source_adapter is not None and self.target_adapter is not None: + self.source_adapter.sync_to(self.target_adapter, flags=self.diffsync_flags) + else: + self.log_warning(message="Not both adapters were properly initialized prior to synchronization.") + self.log_info(message="Synchronization from Device42 into Nautobot is complete.") + + +jobs = [Device42DataSource] diff --git a/nautobot_ssot/integrations/device42/migrations/__init__.py b/nautobot_ssot/integrations/device42/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot_ssot/integrations/device42/static/nautobot_ssot_device42/d42_logo.png b/nautobot_ssot/integrations/device42/static/nautobot_ssot_device42/d42_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ef0611965496114e21d8c985d66eeadda547f516 GIT binary patch literal 8304 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 TPtC;rG2YzN`a;QhknsNiY~b;Y literal 0 HcmV?d00001 diff --git a/nautobot_ssot/integrations/device42/utils/__init__.py b/nautobot_ssot/integrations/device42/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot_ssot/integrations/device42/utils/device42.py b/nautobot_ssot/integrations/device42/utils/device42.py new file mode 100644 index 000000000..d3ce5061b --- /dev/null +++ b/nautobot_ssot/integrations/device42/utils/device42.py @@ -0,0 +1,704 @@ +"""Utility functions for Device42 API.""" + +import re +from typing import List + +import requests +import urllib3 +from diffsync.exceptions import ObjectNotFound +from nautobot.core.settings_funcs import is_truthy +from netutils.lib_mapper import PYATS_LIB_MAPPER + +from nautobot_ssot.integrations.device42.constant import DEFAULTS, FC_INTF_MAP, INTF_NAME_MAP, PHY_INTF_MAP, PLUGIN_CFG +from nautobot_ssot.integrations.device42.diffsync.models.base.ipam import VLAN + + +class MissingConfigSetting(Exception): + """Exception raised for missing configuration settings. + + Attributes: + message (str): Returned explanation of Error. + """ + + def __init__(self, setting): + """Initialize Exception with Setting that is missing and message.""" + self.setting = setting + self.message = f"Missing configuration setting - {setting}!" + super().__init__(self.message) + + +def merge_offset_dicts(orig_dict: dict, offset_dict: dict) -> dict: + """Method to merge two dicts and merge a list if found. + + Args: + orig_dict (dict): Dict to have data merged from. + offset_dict (dict): Dict to be merged into with offset data. Expects this to be like orig_dict but with offset data. + + Returns: + dict: Dict with merged data from both dicts. + """ + out = {} + for key, value in offset_dict.items(): + if key in orig_dict and key in offset_dict: + if isinstance(value, list): + out[key] = orig_dict[key] + value + else: + out[key] = value + return out + + +def get_intf_type(intf_record: dict) -> str: # pylint: disable=too-many-branches + """Method to determine an Interface type based on a few factors. + + Those factors include: + - Port type + - Port Speed Note: `port_speed` was used instead of `speedcapable` as `speedcapable` reported nothing. + - Discovered type for port + + Anything explicitly not matched will go to `other`. + + Args: + intf_record (dict): Interface record from Device42 with details about the Port. + + Returns: + _port_type (str): The Nautobot type appropriate for the interface based upon criteria explained above. + """ + _port_name = re.search(r"^[a-zA-Z]+-?[a-zA-Z]+", intf_record["port_name"].strip()) + + if _port_name: + _port_name = _port_name.group() + + _port_type = "other" + # if switch is physical and name is from PHY_INTF_MAP dict + if intf_record["port_type"] == "physical" and intf_record.get("discovered_type"): + if ( + "ethernet" in intf_record["discovered_type"] + and intf_record.get("port_speed") + and intf_record["port_speed"] in PHY_INTF_MAP + ): + _port_type = PHY_INTF_MAP[intf_record["port_speed"]] + elif ( + "fibreChannel" in intf_record["discovered_type"] + and intf_record.get("port_speed") + and intf_record["port_speed"] in FC_INTF_MAP + ): + _port_type = FC_INTF_MAP[intf_record["port_speed"]] + elif intf_record["port_speed"] in PHY_INTF_MAP: + _port_type = PHY_INTF_MAP[intf_record["port_speed"]] + elif _port_name and _port_name in INTF_NAME_MAP: + _port_type = INTF_NAME_MAP[_port_name]["itype"] + elif "gigabitEthernet" in intf_record["discovered_type"]: + _port_type = "1000base-t" + elif "dot11" in intf_record["discovered_type"]: + _port_type = "ieee802.11a" + if intf_record["port_type"] == "logical" and intf_record.get("discovered_type"): + if intf_record["discovered_type"] == "ieee8023adLag" or intf_record["discovered_type"] == "lacp": + _port_type = "lag" + elif ( + intf_record["discovered_type"] == "softwareLoopback" + or intf_record["discovered_type"] == "l2vlan" + or intf_record["discovered_type"] == "propVirtual" + ): + if _port_name and re.search(r"[pP]ort-?[cC]hannel", _port_name): + _port_type = "lag" + else: + _port_type = "virtual" + return _port_type + + +def get_intf_status(port: dict): + """Method to determine Interface Status. + + Args: + port (dict): Dictionary containing port `up` and `up_admin` keys. + """ + _status = "planned" + if "up" in port and "up_admin" in port: + if not is_truthy(port["up"]) and not is_truthy(port["up_admin"]): + _status = "decommissioning" + elif not is_truthy(port["up"]) and is_truthy(port["up_admin"]): + _status = "failed" + elif is_truthy(port["up"]) and is_truthy(port["up_admin"]): + _status = "active" + elif port.get("up_admin"): + _status = "active" + return _status + + +def get_netmiko_platform(network_os: str) -> str: + """Method to return the netmiko platform if a pyATS platform is provided. + + Args: + network_os (str): Name of platform to map if match found. + + Returns: + str: Netmiko platform name or original if no match. + """ + if network_os: + if network_os == "f5": + network_os = "bigip" + net_os = network_os.replace("-", "") + if net_os in PYATS_LIB_MAPPER: + return PYATS_LIB_MAPPER[net_os] + return network_os + + +def find_device_role_from_tags(tag_list: List[str]) -> str: + """Determine a Device role based upon a Tag matching the `role_prepend` setting. + + Args: + tag_list (List[str]): List of Tags as strings to search. + + Returns: + str: The Default device role defined in plugin settings. + """ + _prepend = PLUGIN_CFG.get("device42_role_prepend") + if _prepend: + for _tag in tag_list: + if re.search(_prepend, _tag): + return re.sub(_prepend, "", _tag) + return DEFAULTS.get("device_role") + + +def get_facility(tags: List[str], diffsync=None): # pylint: disable=inconsistent-return-statements + """Determine Site facility from a specified Tag.""" + if not PLUGIN_CFG.get("device42_facility_prepend"): + diffsync.log_failure(message="The `facility_prepend` setting is missing or invalid.") + raise MissingConfigSetting("device42_facility_prepend") + for _tag in tags: + if re.search(PLUGIN_CFG.get("device42_facility_prepend"), _tag): + return re.sub(PLUGIN_CFG.get("device42_facility_prepend"), "", _tag) + + +def get_custom_field_dict(cfields: List[dict]) -> dict: + """Creates dictionary of CustomField with CF key, value, and description. + + Args: + cfields (List[dict]): List of Custom Fields with their value and notes. + + Returns: + cf_dict (dict): Return a dict of CustomField with key, value, and note (description). + """ + cf_dict = {} + for cfield in cfields: + cf_dict[cfield["key"]] = cfield + return cf_dict + + +def load_vlan( # pylint: disable=dangerous-default-value, too-many-arguments + diffsync, + vlan_id: int, + site_name: str, + vlan_name: str = "", + description: str = "", + custom_fields: dict = {}, + tags: list = [], +): + """Find or create specified Site VLAN. + + Args: + diffsync (obj): DiffSync adapter with logger and get method. + vlan_id (int): VLAN ID for site. + site_name (str): Site name for associated VLAN. + vlan_name (str): Name of VLAN to be created. + description (str): Description for VLAN. + custom_fields (dict): Dictionary of CustomFields for VLAN. + tags (list): List of Tags to be applied to VLAN. + """ + try: + diffsync.get(VLAN, {"vlan_id": vlan_id, "building": site_name}) + diffsync.job.log_warning(message=f"Duplicate VLAN attempted to be loaded: {vlan_id} {site_name}") + except ObjectNotFound: + diffsync.job.log_info(message=f"Loading VLAN {vlan_id} {vlan_name} for {site_name}") + new_vlan = VLAN( + name=f"VLAN{vlan_id:04d}" if not vlan_name else vlan_name, + vlan_id=vlan_id, + description=description, + building=site_name, + custom_fields=custom_fields, + tags=tags, + uuid=None, + ) + diffsync.add(new_vlan) + + +class Device42API: # pylint: disable=too-many-public-methods + """Device42 API class.""" + + def __init__(self, base_url: str, username: str, password: str, verify: bool = True): + """Create Device42 API connection.""" + self.base_url = base_url + self.verify = verify + self.username = username + self.password = password + self.headers = {"Content-Type": "application/x-www-form-urlencoded"} + + if verify is False: + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + def validate_url(self, path): + """Validate URL formatting is correct.""" + if not self.base_url.endswith("/") and not path.startswith("/"): + full_path = f"{self.base_url}/{path}" + else: + full_path = f"{self.base_url}{path}" + if not full_path.endswith("/"): + return full_path + return full_path + + def api_call(self, path: str, method: str = "GET", params: dict = None, payload: dict = None): + """Method to send Request to Device42 of type `method`. Defaults to GET request. + + Args: + path (str): API path to send request to. + method (str, optional): API request method. Defaults to "GET". + params (dict, optional): Additional parameters to send to API. Defaults to None. + payload (dict, optional): Message payload to be sent as part of API call. + + Raises: + Exception: Error thrown if request errors. + + Returns: + dict: JSON payload of API response. + """ + url = self.validate_url(path) + return_data = {} + + if params is None: + params = {} + + params.update( + { + "_paging": "1", + "_return_as_object": "1", + "_max_results": "1000", + } + ) + + resp = requests.request( + method=method, + headers=self.headers, + auth=(self.username, self.password), + url=url, + params=params, + verify=self.verify, + data=payload, + timeout=60, + ) + try: + resp.raise_for_status() + except requests.exceptions.HTTPError as err: + print(f"Error in communicating to Device42 API: {err}") + return False + + return_data = resp.json() + # print(f"Total count for {url}: {return_data.get('total_count')}") + # Handle Device42 pagination + counter = 0 + pagination = False + if isinstance(return_data, dict) and return_data.get("total_count"): + while (return_data.get("offset") + return_data.get("limit")) < return_data.get("total_count"): + pagination = True + # print("Handling paginated response from Device42.") + new_offset = return_data["offset"] + return_data["limit"] + params.update({"offset": new_offset}) + counter += 1 + response = requests.request( + method="GET", + headers=self.headers, + auth=(self.username, self.password), + url=url, + params=params, + timeout=60, + verify=self.verify, + ) + response.raise_for_status() + return_data = merge_offset_dicts(return_data, response.json()) + # print( + # f"Number of devices: {len(return_data['Devices'])}.\noffset: {return_data.get('offset')}.\nlimit: {return_data.get('limit')}." + # ) + + # Handle possible infinite loop. + if counter > 10000: + print("Too many pagination loops in Device42 request. Possible infinite loop.") + print(url) + break + + # print(f"Exiting API request loop after {counter} loops.") + + if pagination: + return_data.pop("offset", None) + + return return_data + + def doql_query(self, query: str) -> dict: + """Method to perform a DOQL query against Device42. + + Args: + query (str): DOQL query to be sent to Device42. + + Returns: + dict: Returned data from Device42 for DOQL query. + """ + params = { + "query": query, + "output_type": "json", + } + url = "services/data/v1.0/query/" + return self.api_call(path=url, params=params) + + def get_buildings(self) -> List: + """Method to get all Buildings from Device42.""" + return self.api_call(path="api/1.0/buildings")["buildings"] + + def get_building_pks(self) -> dict: + """Method to obtain all Buildings from Device42 mapped to their PK. + + Returns: + dict: Dictionary of Buildings with their PK as key. + """ + query = "SELECT * FROM view_building_v1" + results = self.doql_query(query=query) + return {x["building_pk"]: x for x in results} + + def get_rooms(self) -> List: + """Method to get all Rooms from Device42.""" + return self.api_call(path="api/1.0/rooms")["rooms"] + + def get_room_pks(self) -> dict: + """Method to obtain all Rooms from Device42 mapped to their PK. + + Returns: + dict: Dictionary of Rooms with their PK as key. + """ + query = "SELECT * FROM view_room_v1" + results = self.doql_query(query=query) + return {x["room_pk"]: x for x in results} + + def get_racks(self) -> List: + """Method to get all Racks from Device42.""" + return self.api_call(path="api/1.0/racks")["racks"] + + def get_rack_pks(self) -> dict: + """Method to obtain all Racks from Device42 mapped to their PK. + + Returns: + dict: Dictionary of Racks with their PK as key. + """ + query = "SELECT * FROM view_rack_v1" + results = self.doql_query(query=query) + return {x["rack_pk"]: x for x in results} + + def get_vendors(self) -> List: + """Method to get all Vendors from Device42.""" + return self.api_call(path="api/1.0/vendors")["vendors"] + + def get_hardware_models(self) -> List: + """Method to get all Hardware Models from Device42.""" + return self.api_call(path="api/1.0/hardwares")["models"] + + def get_devices(self) -> List[dict]: + """Method to get all Network Devices from Device42.""" + return self.api_call(path="api/1.0/devices/all/?is_it_switch=yes")["Devices"] + + def get_cluster_members(self) -> dict: + """Method to get all member devices of a cluster from Device42. + + Returns: + dict: Dictionary of all clusters with associated members. + """ + query = "SELECT m.name as cluster, string_agg(d.name, '%3B ') as members, h.name as hardware, d.network_device, d.os_name as os, b.name as customer, d.tags FROM view_device_v1 m JOIN view_devices_in_cluster_v1 c ON c.parent_device_fk = m.device_pk JOIN view_device_v1 d ON d.device_pk = c.child_device_fk JOIN view_hardware_v1 h ON h.hardware_pk = d.hardware_fk JOIN view_customer_v1 b ON b.customer_pk = d.customer_fk WHERE m.type like '%cluster%' GROUP BY m.name, h.name, d.network_device, d.os_name, b.name, d.tags" + _results = self.doql_query(query=query) + + return { + _i["cluster"]: { + "members": sorted(list(_i["members"].split("%3B "))), + "is_network": _i["network_device"], + "hardware": _i["hardware"], + "os": _i["os"], + "customer": _i["customer"], + "tags": _i["tags"].split(",") if _i.get("tags") else [], + } + for _i in _results + } + + def get_ports_with_vlans(self) -> List[dict]: + """Method to get all Ports with attached VLANs from Device42. + + This retrieves only the information we care about via DOQL in one giant json blob instead of multiple API calls. + + Returns: + List[dict]: Dict of interface information from DOQL query. + """ + query = "SELECT array_agg( distinct concat (v.vlan_pk)) AS vlan_pks, n.netport_pk, n.port AS port_name, n.description, n.up, n.up_admin, n.discovered_type, n.hwaddress, n.port_type, n.port_speed, n.mtu, n.tags, n.second_device_fk, d.name AS device_name FROM view_vlan_v1 v LEFT JOIN view_vlan_on_netport_v1 vn ON vn.vlan_fk = v.vlan_pk LEFT JOIN view_netport_v1 n ON n.netport_pk = vn.netport_fk LEFT JOIN view_device_v1 d ON d.device_pk = n.device_fk WHERE n.port is not null GROUP BY n.netport_pk, n.port, n.description, n.up, n.up_admin, n.discovered_type, n.hwaddress, n.port_type, n.port_speed, n.mtu, n.tags, n.second_device_fk, d.name" + return self.doql_query(query=query) + + def get_ports_wo_vlans(self) -> List[dict]: + """Method to get all Ports from Device42. + + Returns: + List[dict]: Dict of Interface information from DOQL query. + """ + query = "SELECT m.netport_pk, m.port as port_name, m.description, m.up_admin, m.discovered_type, m.hwaddress, m.port_type, m.port_speed, m.mtu, m.tags, m.second_device_fk, d.name as device_name FROM view_netport_v1 m JOIN view_device_v1 d on d.device_pk = m.device_fk WHERE m.port is not null GROUP BY m.netport_pk, m.port, m.description, m.up_admin, m.discovered_type, m.hwaddress, m.port_type, m.port_speed, m.mtu, m.tags, m.second_device_fk, d.name" + return self.doql_query(query=query) + + def get_port_default_custom_fields(self) -> List[dict]: + """Method to retrieve the default CustomFields for Ports from Device42. + + This is needed to ensure all Posts have same CustomFields to match Nautobot. + + Returns: + List[dict]: List of dictionaries of CustomFields matching D42 format from the API without values. + """ + query = "SELECT cf.key, cf.value, cf.notes FROM view_netport_custom_fields_v1 cf" + results = self.doql_query(query=query) + return self.get_all_custom_fields(results) + + def get_port_custom_fields(self) -> dict: + """Method to retrieve custom fields for Ports from Device42. + + Returns: + dict: Dictionary of CustomFields matching D42 format from the API. + """ + query = "SELECT cf.key, cf.value, cf.notes, np.port as port_name, d.name as device_name FROM view_netport_custom_fields_v1 cf LEFT JOIN view_netport_v1 np ON np.netport_pk = cf.netport_fk LEFT JOIN view_device_v1 d ON d.device_pk = np.device_fk" + results = self.doql_query(query=query) + _fields = {} + for _cf in results: + _fields[_cf["device_name"]] = {} + for _cf in results: + _fields[_cf["device_name"]][_cf["port_name"]] = {} + for _cf in results: + _fields[_cf["device_name"]][_cf["port_name"]][_cf["key"]] = { + "key": _cf["key"], + "value": _cf["value"], + "notes": _cf["notes"], + } + return _fields + + def get_vrfgroups(self) -> dict: + """Method to retrieve VRF Groups from Device42. + + Returns: + dict: Response from Device42 containing VRFGroups. + """ + return self.api_call(path="api/1.0/vrfgroup/")["vrfgroup"] + + def get_subnets(self) -> List[dict]: + """Method to get all subnets and associated data from Device42. + + Returns: + dict: Dict of subnets from Device42. + """ + query = "SELECT s.name, s.network, s.mask_bits, s.tags, v.name as vrf FROM view_subnet_v1 s JOIN view_vrfgroup_v1 v ON s.vrfgroup_fk = v.vrfgroup_pk" + return self.doql_query(query=query) + + def get_subnet_default_custom_fields(self) -> dict: + """Method to retrieve the default CustomFields for Subnets from Device42. + + This is needed to ensure all Subnets have same CustomFields to match Nautobot. + + Returns: + dict: Dictionary of CustomFields matching D42 format from the API without values. + """ + query = "SELECT cf.key, cf.value, cf.notes FROM view_subnet_custom_fields_v1 cf" + results = self.doql_query(query=query) + return self.get_all_custom_fields(results) + + def get_subnet_custom_fields(self) -> dict: + """Method to retrieve custom fields for Subnets from Device42. + + Returns: + dict: Dictionary of dictionaries of CustomFields matching D42 format from the API. + """ + query = "SELECT cf.key, cf.value, cf.notes, s.name AS subnet_name, s.network, s.mask_bits FROM view_subnet_custom_fields_v1 cf LEFT JOIN view_subnet_v1 s ON s.subnet_pk = cf.subnet_fk" + results = self.doql_query(query=query) + + default_cfs = self.get_subnet_default_custom_fields() + + _fields = {} + for _cf in results: + _fields[f"{_cf['network']}/{_cf['mask_bits']}"] = default_cfs + + for _cf in results: + _fields[f"{_cf['network']}/{_cf['mask_bits']}"][_cf["key"]] = { + "key": _cf["key"], + "value": _cf["value"], + "notes": _cf["notes"], + } + return _fields + + def get_ip_addrs(self) -> List[dict]: + """Method to get all IP addresses and relevant data from Device42 via DOQL. + + Returns: + List[dict]: List of dicts with info about each IP address. + """ + query = "SELECT i.ip_address, i.available, i.label, i.tags, np.netport_pk, s.network as subnet, s.mask_bits as netmask, v.name as vrf FROM view_ipaddress_v1 i LEFT JOIN view_subnet_v1 s ON s.subnet_pk = i.subnet_fk LEFT JOIN view_netport_v1 np ON np.netport_pk = i.netport_fk LEFT JOIN view_vrfgroup_v1 v ON v.vrfgroup_pk = s.vrfgroup_fk WHERE s.mask_bits <> 0" + return self.doql_query(query=query) + + def get_ipaddr_default_custom_fields(self) -> dict: + """Method to retrieve the default CustomFields for IP Addresses from Device42. + + This is needed to ensure all IPAddresses have same CustomFields to match Nautobot. + + Returns: + dict: Dictionary of CustomFields with label as key and remaining info as value. + """ + query = "SELECT cf.key, cf.value, cf.notes FROM view_ipaddress_custom_fields_v1 cf" + results = self.doql_query(query=query) + return self.get_all_custom_fields(results) + + def get_ipaddr_custom_fields(self) -> dict: + """Method to retrieve the CustomFields for IP Addresses from Device42. + + Returns: + dict: Dictionary of CustomFields from D42 matched to IP Addressmatching D42 format from the API with values. + """ + query = "SELECT cf.key, cf.value, cf.notes, i.ip_address, s.mask_bits FROM view_ipaddress_custom_fields_v1 cf LEFT JOIN view_ipaddress_v1 i ON i.ipaddress_pk = cf.ipaddress_fk LEFT JOIN view_subnet_v1 s ON s.subnet_pk = i.subnet_fk" + results = self.doql_query(query=query) + + default_cfs = self.get_all_custom_fields(results) + + _fields = {} + for _cf in results: + addr = f"{_cf['ip_address']}/{_cf['mask_bits']}" + if addr not in _fields: + _fields[addr] = default_cfs + _fields[addr][_cf["key"]] = { + "key": _cf["key"], + "value": _cf["value"], + "notes": _cf["notes"], + } + return _fields + + @staticmethod + def get_all_custom_fields(custom_fields: List[dict]) -> dict: + """Get all Custom Fields for object. + + As Device42 only returns CustomFields with values in them when using DOQL, we need to compile a list of all Custom Fields on an object to match Nautobot method. + + Args: + custom_fields (List[dict]): List of Custom Fields for an object. + + Returns: + dict: List of all Custom Fields nulled. + """ + _cfs = {} + for _cf in custom_fields: + _cfs[_cf["key"]] = { + "key": _cf["key"], + "value": None, + "notes": None, + } + return _cfs + + def get_vlans_with_location(self) -> List[dict]: + """Method to get all VLANs with Building and Customer info to attach to find Site. + + Returns: + List[dict]: List of dicts of VLANs and location information. + """ + query = "SELECT v.vlan_pk, v.number AS vid, v.description, v.tags, vn.vlan_name, b.name as building, c.name as customer FROM view_vlan_v1 v LEFT JOIN view_vlan_on_netport_v1 vn ON vn.vlan_fk = v.vlan_pk LEFT JOIN view_netport_v1 n on n.netport_pk = vn.netport_fk LEFT JOIN view_device_v2 d on d.device_pk = n.device_fk LEFT JOIN view_building_v1 b ON b.building_pk = d.building_fk LEFT JOIN view_customer_v1 c ON c.customer_pk = d.customer_fk WHERE vn.vlan_name is not null and v.number <> 0 GROUP BY v.vlan_pk, v.number, v.description, v.tags, vn.vlan_name, b.name, c.name" + return self.doql_query(query=query) + + def get_vlan_info(self) -> dict: + """Method to obtain the VLAN name and ID paired to primary key. + + Returns: + dict: Mapping of VLAN primary key to VLAN name and ID. + """ + vinfo_query = "SELECT v.vlan_pk, v.name, v.number as vid FROM view_vlan_v1 v" + cfields_query = "SELECT cf.key, cf.value, cf.notes, v.vlan_pk FROM view_vlan_custom_fields_v1 cf LEFT JOIN view_vlan_v1 v ON v.vlan_pk = cf.vlan_fk" + doql_vlans = self.doql_query(query=vinfo_query) + vlans_cfs = self.doql_query(query=cfields_query) + vlan_dict = {str(x["vlan_pk"]): {"name": x["name"], "vid": x["vid"]} for x in doql_vlans} + for _cf in vlans_cfs: + if str(_cf["vlan_pk"]) in vlan_dict: + vlan_dict[str(_cf["vlan_pk"])]["custom_fields"] = {} + for _cf in vlans_cfs: + vlan_dict[str(_cf["vlan_pk"])]["custom_fields"][_cf["key"]] = { + "key": _cf["key"], + "value": _cf["value"], + "notes": _cf["notes"], + } + return vlan_dict + + def get_device_pks(self) -> dict: + """Get all Devices with their primary keys for reference in other functions. + + Returns: + dict: Dict of Devices where the key is the primary key of the Device. + """ + query = "SELECT name, device_pk FROM view_device_v1 WHERE name <> ''" + _devs = self.doql_query(query=query) + return {x["device_pk"]: x for x in _devs} + + def get_port_pks(self) -> dict: + """Get all ports with their associated primary keys for reference in other functions. + + Returns: + dict: Dict of ports where key is the primary key of the Port with the port name. + """ + query = "SELECT np.port, np.netport_pk, np.hwaddress, np.second_device_fk, d.name as device FROM view_netport_v1 np JOIN view_device_v1 d ON d.device_pk = np.device_fk" + _ports = self.doql_query(query=query) + for _port in _ports: + if not _port["port"] and _port.get("hwaddress"): + _port["port"] = _port["hwaddress"] + return {x["netport_pk"]: x for x in _ports} + + def get_port_connections(self) -> dict: + """Gather all Ports with connections to determine connections between interfaces for Cables. + + Returns: + dict: Information about each port and it's connection information. + """ + query = "SELECT netport_pk as src_port, device_fk as src_device, second_device_fk as second_src_device, remote_netport_fk as dst_port FROM view_netport_v1 WHERE device_fk is not null AND remote_netport_fk is not null" + return self.doql_query(query=query) + + def get_telcocircuits(self) -> List[dict]: + """Method to retrieve all information about TelcoCircuits from Device42. + + Returns: + List[dict]: List of dictionaries containing information about each circuit in Device42. + """ + query = "SELECT * FROM view_telcocircuit_v1" + return self.doql_query(query=query) + + def get_vendor_pks(self) -> dict: + """Method to obtain all Vendors from Device42 mapped to their PK. + + Returns: + dict: Dictionary of Vendors with their PK as key. + """ + query = "SELECT * FROM view_vendor_v1" + results = self.doql_query(query=query) + return {x["vendor_pk"]: x for x in results} + + def get_patch_panels(self) -> List[dict]: + """Method to obtain all patch panels from Device42. + + Returns: + dict: Dictionary of Patch Panels in Device42. + """ + query = "SELECT a.name, a.in_service, a.serial_no, a.customer_fk, a.building_fk, a.calculated_building_fk, a.room_fk, a.calculated_room_fk, a.calculated_rack_fk, a.size, a.depth, m.number_of_ports, m.name as model_name, m.port_type_name as port_type, v.name as vendor, a.rack_fk, a.start_at as position, a.orientation FROM view_asset_v1 a LEFT JOIN view_patchpanelmodel_v1 m ON m.patchpanelmodel_pk = a.patchpanelmodel_fk JOIN view_vendor_v1 v ON v.vendor_pk = m.vendor_fk WHERE a.patchpanelmodel_fk is not null AND a.name is not null" + return self.doql_query(query=query) + + def get_patch_panel_port_pks(self) -> dict: + """Method to obtain all Patch Panel Ports from Device42 mapped to their PK. + + Returns: + dict: Dictionary of Patch Panel Ports with their PK as key. + """ + query = "SELECT p.*, a.name FROM view_patchpanelport_v1 p JOIN view_asset_v1 a ON a.asset_pk = p.patchpanel_asset_fk" + results = self.doql_query(query=query) + return {x["patchpanelport_pk"]: x for x in results} + + def get_customer_pks(self) -> dict: + """Method to obtain all Customers from Device42 mapped to their PK. + + Returns: + dict: Dictionary of Customers with their PK as key. + """ + query = "SELECT * FROM view_customer_v1" + results = self.doql_query(query=query) + return {x["customer_pk"]: x for x in results} diff --git a/nautobot_ssot/integrations/device42/utils/nautobot.py b/nautobot_ssot/integrations/device42/utils/nautobot.py new file mode 100644 index 000000000..1bd396e70 --- /dev/null +++ b/nautobot_ssot/integrations/device42/utils/nautobot.py @@ -0,0 +1,371 @@ +# pylint: disable=duplicate-code +"""Utility functions for Nautobot ORM.""" +import random +from typing import List, OrderedDict +from uuid import UUID + +from diffsync.exceptions import ObjectNotFound +from django.contrib.contenttypes.models import ContentType +from django.utils.text import slugify +from nautobot.circuits.models import CircuitType +from nautobot.dcim.models import Device, DeviceRole, Interface, Platform +from nautobot.extras.choices import CustomFieldTypeChoices +from nautobot.extras.models import CustomField, Relationship, Tag +from nautobot.ipam.models import IPAddress +from nautobot.utilities.utils import slugify_dashes_to_underscores +from netutils.lib_mapper import ANSIBLE_LIB_MAPPER_REVERSE, NAPALM_LIB_MAPPER_REVERSE +from taggit.managers import TaggableManager +from nautobot_ssot.integrations.device42.diffsync.models.base.dcim import Device as NautobotDevice + +try: + from nautobot_device_lifecycle_mgmt.models import SoftwareLCM + + LIFECYCLE_MGMT = True +except ImportError: + print("Device Lifecycle plugin isn't installed so will revert to CustomField for OS version.") + LIFECYCLE_MGMT = False + + +def get_random_color() -> str: + """Get random hex code color string. + + Returns: + str: Hex code value for a color with hash stripped. + """ + return f"{random.randint(0, 0xFFFFFF):06x}" # nosec: B311 + + +def verify_device_role(diffsync, role_name: str, role_color: str = "") -> UUID: + """Verifies DeviceRole object exists in Nautobot. If not, creates it. + + Args: + diffsync (obj): DiffSync Job object. + role_name (str): Name of role to verify. + role_color (str): Color of role to verify. Must be hex code format. + + Returns: + UUID: ID of found or created DeviceRole object. + """ + if not role_color: + role_color = get_random_color() + try: + role_obj = diffsync.devicerole_map[slugify(role_name)] + except KeyError: + role_obj = DeviceRole(name=role_name, slug=slugify(role_name), color=role_color) + diffsync.objects_to_create["deviceroles"].append(role_obj) + diffsync.devicerole_map[slugify(role_name)] = role_obj.id + role_obj = role_obj.id + return role_obj + + +def verify_platform(diffsync, platform_name: str, manu: UUID) -> UUID: + """Verifies Platform object exists in Nautobot. If not, creates it. + + Args: + diffsync (obj): DiffSync Job with maps. + platform_name (str): Name of platform to verify. + manu (UUID): The ID (primary key) of platform manufacturer. + + Returns: + UUID: UUID for found or created Platform object. + """ + if ANSIBLE_LIB_MAPPER_REVERSE.get(platform_name): + _name = ANSIBLE_LIB_MAPPER_REVERSE[platform_name] + else: + _name = platform_name + if NAPALM_LIB_MAPPER_REVERSE.get(platform_name): + napalm_driver = NAPALM_LIB_MAPPER_REVERSE[platform_name] + else: + napalm_driver = platform_name + try: + platform_obj = diffsync.platform_map[slugify(platform_name)] + except KeyError: + platform_obj = Platform( + name=_name, + slug=slugify(platform_name), + manufacturer_id=manu, + napalm_driver=napalm_driver[:50], + ) + diffsync.objects_to_create["platforms"].append(platform_obj) + diffsync.platform_map[slugify(platform_name)] = platform_obj.id + platform_obj = platform_obj.id + return platform_obj + + +def get_or_create_mgmt_intf(intf_name: str, dev: Device) -> Interface: + """Creates a Management interface with specified name. + + This is expected to be used when assigning a management IP to a device that doesn't + have a Management interface and we can't determine which one to assign the IP to. + + Args: + intf_name (str): Name of Interface to be created. + dev (Device): Device object for Interface to be assigned to. + + Returns: + Interface: Management Interface object that was created. + """ + # check if Interface already exists, returns it or creates it + try: + mgmt_intf = Interface.objects.get(name=intf_name.strip(), device__name=dev.name) + except Interface.DoesNotExist: + print(f"Mgmt Intf Not Found! Creating {intf_name} {dev.name}") + mgmt_intf = Interface( + name=intf_name.strip(), + device=dev, + type="other", + enabled=True, + mgmt_only=True, + ) + mgmt_intf.validated_save() + return mgmt_intf + + +def get_or_create_tag(tag_name: str) -> Tag: + """Finds or creates a Tag that matches `tag_name`. + + Args: + tag_name (str): Name of Tag to be created. + + Returns: + Tag: Tag object that was found or created. + """ + try: + _tag = Tag.objects.get(slug=slugify(tag_name)) + except Tag.DoesNotExist: + new_tag = Tag( + name=tag_name, + slug=slugify(tag_name), + color=get_random_color(), + ) + new_tag.validated_save() + _tag = new_tag + return _tag + + +def get_tags(tag_list: List[str]) -> List[Tag]: + """Gets list of Tags from list of strings. + + This is the opposite of the `get_tag_strings` function. + + Args: + tag_list (List[str]): List of Tags as strings to find. + + Returns: + (List[Tag]): List of Tag object primary keys matching list of strings passed in. + """ + return [get_or_create_tag(x) for x in tag_list if x != ""] + + +def update_tags(tagged_obj: object, new_tags: List[str]): + """Update tags on Nautobot object to match what is provided in new tags. + + Args: + tagged_obj (object): Nautobot object with Tags attached. + new_tags (List[str]): List of updated Tags. + """ + current_tags = tagged_obj.tags.names() + for tag in new_tags: + if tag not in current_tags: + tagged_obj.tags.add(tag) + for tag in current_tags: + if tag not in new_tags: + tagged_obj.tags.remove(tag) + + +def get_tag_strings(list_tags: TaggableManager) -> List[str]: + """Gets string values of all Tags in a list. + + This is the opposite of the `get_tags` function. + + Args: + list_tags (TaggableManager): List of Tag objects to convert to strings. + + Returns: + List[str]: List of string values matching the Tags passed in. + """ + _strings = list(list_tags.names()) + if len(_strings) > 1: + _strings.sort() + return _strings + + +def get_custom_field_dict(cfields: OrderedDict) -> dict: + """Creates dictionary of CustomField with CF key, value, and description. + + Args: + cfields (OrderedDict): List of CustomFields with their value. + + Returns: + cf_dict (dict): Return a dict of CustomField with key, value, and note (description). + """ + cf_dict = {} + for _cf, _cf_value in cfields.items(): + cf_dict[_cf.label] = { + "key": _cf.label, + "value": _cf_value, + "notes": _cf.description if _cf.description != "" else None, + } + return cf_dict + + +def update_custom_fields(new_cfields: dict, update_obj: object): + """Update passed object's CustomFields. + + Args: + new_cfields (OrderedDict): Dictionary of CustomFields on object to be updated to match. + update_obj (object): Object to be updated with CustomFields. + """ + current_cf = get_custom_field_dict(update_obj.get_custom_fields()) + for old_cf, old_cf_dict in current_cf.items(): + if old_cf not in new_cfields: + removed_cf = CustomField.objects.get( + label=old_cf_dict["key"], content_types=ContentType.objects.get_for_model(type(update_obj)) + ) + removed_cf.delete() + for new_cf, new_cf_dict in new_cfields.items(): + if new_cf not in current_cf: + _cf_dict = { + "name": slugify_dashes_to_underscores(new_cf_dict["key"]), + "slug": slugify_dashes_to_underscores(new_cf_dict["key"]), + "type": CustomFieldTypeChoices.TYPE_TEXT, + "label": new_cf_dict["key"], + } + field, _ = CustomField.objects.get_or_create(name=_cf_dict["name"], defaults=_cf_dict) + field.content_types.add(ContentType.objects.get_for_model(type(update_obj)).id) + update_obj.custom_field_data.update({slugify_dashes_to_underscores(new_cf_dict["key"]): new_cf_dict["value"]}) + + +def verify_circuit_type(circuit_type: str) -> CircuitType: + """Method to find or create a CircuitType in Nautobot. + + Args: + circuit_type (str): Name of CircuitType to be found or created. + + Returns: + CircuitType: CircuitType object found or created. + """ + try: + _ct = CircuitType.objects.get(slug=slugify(circuit_type)) + except CircuitType.DoesNotExist: + _ct = CircuitType( + name=circuit_type, + slug=slugify(circuit_type), + ) + _ct.validated_save() + return _ct + + +def get_software_version_from_lcm(relations: dict): + """Method to obtain Software version for a Device from Relationship. + + Args: + relations (dict): Results of a `get_relationships()` on a Device. + + Returns: + str: String of SoftwareLCM version. + """ + version = "" + if LIFECYCLE_MGMT: + _softwarelcm = Relationship.objects.get(name="Software on Device") + if _softwarelcm in relations["destination"]: + if len(relations["destination"][_softwarelcm]) > 0: + if hasattr(relations["destination"][_softwarelcm][0].source, "version"): + version = relations["destination"][_softwarelcm][0].source.version + return version + + +def get_version_from_custom_field(fields: OrderedDict): + """Method to obtain a software version for a Device from its custom fields.""" + for field, value in fields.items(): + if field.label == "OS Version": + return value + return "" + + +def determine_vc_position(vc_map: dict, virtual_chassis: str, device_name: str) -> int: + """Determine position of Member Device in Virtual Chassis based on name and other factors. + + Args: + vc_map (dict): Dictionary of virtual chassis positions mapped to devices. + virtual_chassis (str): Name of the virtual chassis that device is being added to. + device_name (str): Name of member device to be added in virtual chassis. + + Returns: + int: Position for member device in Virtual Chassis. Will always be position 2 or higher as 1 is master device. + """ + return sorted(vc_map[virtual_chassis]["members"]).index(device_name) + 2 + + +def get_dlc_version_map(): + """Method to create nested dictionary of Software versions mapped to their ID along with Platform. + + This should only be used if the Device Lifecycle plugin is found to be installed. + + Returns: + dict: Nested dictionary of versions mapped to their ID and to their Platform. + """ + version_map = {} + for ver in SoftwareLCM.objects.only("id", "device_platform", "version"): + if ver.device_platform.slug not in version_map: + version_map[ver.device_platform.slug] = {} + version_map[ver.device_platform.slug][ver.version] = ver.id + return version_map + + +def get_cf_version_map(): + """Method to create nested dictionary of Software versions mapped to their ID along with Platform. + + This should only be used if the Device Lifecycle plugin is not found. It will instead use custom field "OS Version". + + Returns: + dict: Nested dictionary of versions mapped to their ID and to their Platform. + """ + version_map = {} + for dev in Device.objects.only("id", "platform", "_custom_field_data"): + if dev.platform.slug not in version_map: + version_map[dev.platform.slug] = {} + if "os-version" in dev.custom_field_data: + version_map[dev.platform.slug][dev.custom_field_data["os-version"]] = dev.id + return version_map + + +def apply_vlans_to_port(diffsync, device_name: str, mode: str, vlans: list, port: Interface): + """Determine appropriate VLANs to add to a Port link. + + Args: + diffsync (DiffSyncAdapter): DiffSync Adapter with get and vlan_map. + device_name (str): Name of Device associated to Port. + mode (str): Port mode, access or trunk. + vlans (list): List of VLANs to be attached to Port. + port (Interface): Port to have VLANs applied to. + """ + try: + dev = diffsync.get(NautobotDevice, device_name) + site_name = slugify(dev.building) + except ObjectNotFound: + site_name = "global" + if mode == "access" and len(vlans) == 1: + _vlan = vlans[0] + port.untagged_vlan_id = diffsync.vlan_map[site_name][_vlan] + else: + tagged_vlans = [] + for _vlan in vlans: + tagged_vlan = diffsync.vlan_map[site_name][_vlan] + if tagged_vlan: + tagged_vlans.append(tagged_vlan) + diffsync.objects_to_create["tagged_vlans"].append((port, tagged_vlans)) + + +def unassign_primary(ipaddr: IPAddress): + """Handle unassigning primary IP address from a Device. + + Args: + ipaddr (IPAddress): IP Address that's set to primary and needs to be unset. + """ + _dev = ipaddr.assigned_object.device + if hasattr(ipaddr, "primary_ip4_for"): + _dev.primary_ip4 = None + elif hasattr(ipaddr, "primary_ip6_for"): + _dev.primary_ip6 = None + _dev.validated_save() diff --git a/nautobot_ssot/tests/device42/__init__.py b/nautobot_ssot/tests/device42/__init__.py new file mode 100644 index 000000000..ab116d98c --- /dev/null +++ b/nautobot_ssot/tests/device42/__init__.py @@ -0,0 +1 @@ +"""Unit tests for Device42 SSoT App.""" diff --git a/nautobot_ssot/tests/device42/fixtures/__init__.py b/nautobot_ssot/tests/device42/fixtures/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot_ssot/tests/device42/fixtures/get_all_custom_fields_recv.json b/nautobot_ssot/tests/device42/fixtures/get_all_custom_fields_recv.json new file mode 100644 index 000000000..28890c768 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_all_custom_fields_recv.json @@ -0,0 +1,17 @@ +{ + "Test Custom Field": { + "key": "Test Custom Field", + "value": null, + "notes": null + }, + "Environment": { + "key": "Environment", + "value": null, + "notes": null + }, + "Department": { + "key": "Department", + "value": null, + "notes": null + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_all_custom_fields_sent.json b/nautobot_ssot/tests/device42/fixtures/get_all_custom_fields_sent.json new file mode 100644 index 000000000..f5e1d98e3 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_all_custom_fields_sent.json @@ -0,0 +1,17 @@ +[ + { + "key": "Test Custom Field", + "value": "Success", + "notes": null + }, + { + "key": "Environment", + "value": "test", + "notes": null + }, + { + "key": "Department", + "value": "HR", + "notes": null + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_building_pks_recv.json b/nautobot_ssot/tests/device42/fixtures/get_building_pks_recv.json new file mode 100644 index 000000000..1ddeda96d --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_building_pks_recv.json @@ -0,0 +1,68 @@ +{ + "1": { + "name": "Dell - Round Rock 2", + "tags": [ + "AUS", + "sitecode-aus" + ], + "notes": "", + "longitude": null, + "address": "501 Dell Way, Round Rock, TX 78682", + "latitude": null, + "contact_name": "John Smith", + "building_pk": 1, + "contact_phone": "1-800-GET-HELP", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "RFID Cards" + } + ], + "last_changed": "2000-01-01T01:12:00:00.12345+00:00" + }, + "2": { + "name": "Apple Park", + "tags": [ + "SFO", + "sitecode-sfo" + ], + "notes": "", + "longitude": null, + "address": "One Apple Park Way, Cupertino, CA 95014", + "latitude": null, + "contact_name": "Jon Gotti", + "building_pk": 2, + "contact_phone": "1-555-867-5309", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "Eye Scanner" + } + ], + "last_changed": "2000-01-01T01:12:00:00.12345+00:00" + }, + "3": { + "name": "Microsoft HQ", + "tags": [ + "SEA", + "sitecode-sea" + ], + "notes": "", + "longitude": null, + "address": "One Microsoft Way, Redmond, WA 98052", + "latitude": null, + "contact_name": "John Doe", + "building_pk": 3, + "contact_phone": "use the batsignal!", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "RFID Card" + } + ], + "last_changed": "2000-01-01T01:12:00:00.12345+00:00" + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_building_pks_sent.json b/nautobot_ssot/tests/device42/fixtures/get_building_pks_sent.json new file mode 100644 index 000000000..b224cfc4d --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_building_pks_sent.json @@ -0,0 +1,68 @@ +[ + { + "name": "Dell - Round Rock 2", + "tags": [ + "AUS", + "sitecode-aus" + ], + "notes": "", + "longitude": null, + "address": "501 Dell Way, Round Rock, TX 78682", + "latitude": null, + "contact_name": "John Smith", + "building_pk": 1, + "contact_phone": "1-800-GET-HELP", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "RFID Cards" + } + ], + "last_changed": "2000-01-01T01:12:00:00.12345+00:00" + }, + { + "name": "Apple Park", + "tags": [ + "SFO", + "sitecode-sfo" + ], + "notes": "", + "longitude": null, + "address": "One Apple Park Way, Cupertino, CA 95014", + "latitude": null, + "contact_name": "Jon Gotti", + "building_pk": 2, + "contact_phone": "1-555-867-5309", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "Eye Scanner" + } + ], + "last_changed": "2000-01-01T01:12:00:00.12345+00:00" + }, + { + "name": "Microsoft HQ", + "tags": [ + "SEA", + "sitecode-sea" + ], + "notes": "", + "longitude": null, + "address": "One Microsoft Way, Redmond, WA 98052", + "latitude": null, + "contact_name": "John Doe", + "building_pk": 3, + "contact_phone": "use the batsignal!", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "RFID Card" + } + ], + "last_changed": "2000-01-01T01:12:00:00.12345+00:00" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_buildings.json b/nautobot_ssot/tests/device42/fixtures/get_buildings.json new file mode 100644 index 000000000..a158912ba --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_buildings.json @@ -0,0 +1,70 @@ +{ + "total_count": 3, + "buildings": [ + { + "name": "Dell - Round Rock 2", + "tags": [ + "AUS", + "sitecode-aus" + ], + "notes": "", + "longitude": null, + "address": "501 Dell Way, Round Rock, TX 78682", + "latitude": null, + "contact_name": "John Smith", + "building_pk": 1, + "contact_phone": "1-800-GET-HELP", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "RFID Cards" + } + ] + }, + { + "name": "Apple Park", + "tags": [ + "SFO", + "sitecode-sfo" + ], + "notes": "", + "longitude": null, + "address": "One Apple Park Way, Cupertino, CA 95014", + "latitude": null, + "contact_name": "Jon Gotti", + "building_pk": 2, + "contact_phone": "1-555-867-5309", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "Eye Scanner" + } + ] + }, + { + "name": "Microsoft HQ", + "tags": [ + "SEA", + "sitecode-sea" + ], + "notes": "", + "longitude": null, + "address": "One Microsoft Way, Redmond, WA 98052", + "latitude": null, + "contact_name": "John Doe", + "building_pk": 3, + "contact_phone": "use the batsignal!", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "RFID Card" + } + ] + } + ], + "limit": 1000, + "offset": 0 +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_buildings_recv.json b/nautobot_ssot/tests/device42/fixtures/get_buildings_recv.json new file mode 100644 index 000000000..550399494 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_buildings_recv.json @@ -0,0 +1,65 @@ +[ + { + "name": "Dell - Round Rock 2", + "tags": [ + "AUS", + "sitecode-aus" + ], + "notes": "", + "longitude": null, + "address": "501 Dell Way, Round Rock, TX 78682", + "latitude": null, + "contact_name": "John Smith", + "building_pk": 1, + "contact_phone": "1-800-GET-HELP", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "RFID Cards" + } + ] + }, + { + "name": "Apple Park", + "tags": [ + "SFO", + "sitecode-sfo" + ], + "notes": "", + "longitude": null, + "address": "One Apple Park Way, Cupertino, CA 95014", + "latitude": null, + "contact_name": "Jon Gotti", + "building_pk": 2, + "contact_phone": "1-555-867-5309", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "Eye Scanner" + } + ] + }, + { + "name": "Microsoft HQ", + "tags": [ + "SEA", + "sitecode-sea" + ], + "notes": "", + "longitude": null, + "address": "One Microsoft Way, Redmond, WA 98052", + "latitude": null, + "contact_name": "John Doe", + "building_pk": 3, + "contact_phone": "use the batsignal!", + "custom_fields": [ + { + "notes": null, + "key": "Security System", + "value": "RFID Card" + } + ] + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_cluster_members_recv.json b/nautobot_ssot/tests/device42/fixtures/get_cluster_members_recv.json new file mode 100644 index 000000000..0b4439385 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_cluster_members_recv.json @@ -0,0 +1,23 @@ +{ + "corea.testcluster.com": { + "members": [ + "corea.testcluster.com - Switch 1", + "corea.testcluster.com - Switch 2" + ], + "is_network": true, + "hardware": "Nexus 9000V", + "os": "nxos", + "customer": "DFW", + "tags": [] + }, + "stack01.testexample.com": { + "members": [ + "stack01.testexample.com - Switch 1" + ], + "is_network": true, + "hardware": "WS-C2960S-48TS-L", + "os": "ios", + "customer": "AUS", + "tags": [] + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_cluster_members_sent.json b/nautobot_ssot/tests/device42/fixtures/get_cluster_members_sent.json new file mode 100644 index 000000000..721cd0e9c --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_cluster_members_sent.json @@ -0,0 +1,20 @@ +[ + { + "cluster": "corea.testcluster.com", + "members": "corea.testcluster.com - Switch 2%3B corea.testcluster.com - Switch 1", + "hardware": "Nexus 9000V", + "network_device": true, + "os": "nxos", + "customer": "DFW", + "tags": "" + }, + { + "cluster": "stack01.testexample.com", + "members": "stack01.testexample.com - Switch 1", + "hardware": "WS-C2960S-48TS-L", + "network_device": true, + "os": "ios", + "customer": "AUS", + "tags": "" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_customer_pks_recv.json b/nautobot_ssot/tests/device42/fixtures/get_customer_pks_recv.json new file mode 100644 index 000000000..3b5224db6 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_customer_pks_recv.json @@ -0,0 +1,32 @@ +{ + "1": { + "customer_pk": 1, + "name": "SFO", + "contact_info": "", + "notes": "", + "tags": "", + "type_id": 2, + "type": "Customer", + "manager": "" + }, + "2": { + "customer_pk": 2, + "name": "AUS", + "contact_info": "", + "notes": "", + "tags": "", + "type_id": 2, + "type": "Customer", + "manager": "" + }, + "3": { + "customer_pk": 3, + "name": "SEA", + "contact_info": "", + "notes": "", + "tags": "", + "type_id": 2, + "type": "Customer", + "manager": "" + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_customer_pks_sent.json b/nautobot_ssot/tests/device42/fixtures/get_customer_pks_sent.json new file mode 100644 index 000000000..74ba68125 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_customer_pks_sent.json @@ -0,0 +1,32 @@ +[ + { + "customer_pk": 1, + "name": "SFO", + "contact_info": "", + "notes": "", + "tags": "", + "type_id": 2, + "type": "Customer", + "manager": "" + }, + { + "customer_pk": 2, + "name": "AUS", + "contact_info": "", + "notes": "", + "tags": "", + "type_id": 2, + "type": "Customer", + "manager": "" + }, + { + "customer_pk": 3, + "name": "SEA", + "contact_info": "", + "notes": "", + "tags": "", + "type_id": 2, + "type": "Customer", + "manager": "" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_device_pks_recv.json b/nautobot_ssot/tests/device42/fixtures/get_device_pks_recv.json new file mode 100644 index 000000000..ea9b287dd --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_device_pks_recv.json @@ -0,0 +1,14 @@ +{ + "1": { + "name": "core-router.testexample.com", + "device_pk": 1 + }, + "2": { + "name": "aggr-switch.testexample.com", + "device_pk": 2 + }, + "3": { + "name": "distro-switch.testexample.com", + "device_pk": 3 + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_device_pks_sent.json b/nautobot_ssot/tests/device42/fixtures/get_device_pks_sent.json new file mode 100644 index 000000000..cd59d8d16 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_device_pks_sent.json @@ -0,0 +1,14 @@ +[ + { + "name": "core-router.testexample.com", + "device_pk": 1 + }, + { + "name": "aggr-switch.testexample.com", + "device_pk": 2 + }, + { + "name": "distro-switch.testexample.com", + "device_pk": 3 + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_devices_recv.json b/nautobot_ssot/tests/device42/fixtures/get_devices_recv.json new file mode 100644 index 000000000..6a589e6c0 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_devices_recv.json @@ -0,0 +1,366 @@ +[ + { + "last_updated": "2020-12-03T06:02:40.357Z", + "orientation": 1, + "ip_addresses": [], + "serial_no": "FDA2833B2Q4", + "nonauthoritativealiases": [], + "service_level": "Production", + "is_it_blade_host": "no", + "hw_size": 1.0, + "id": 1, + "custom_fields": [ + { + "notes": null, + "key": "Last Backup", + "value": null + }, + { + "notes": null, + "key": "Business Unit", + "value": null + } + ], + "aliases": [], + "category": "", + "hdd_details": [], + "uuid": "", + "hw_depth": 2, + "os": "ios", + "cpuspeed": null, + "hw_model": "Cisco WS-C3650-48PD", + "hddraid_type": null, + "type": "physical", + "hddcount": null, + "building": "Microsoft HQ", + "xpos": 0, + "device_external_links": [], + "start_at": 5.0, + "tags": [ + "Lab", + "SEA" + ], + "hw_model_id": 15, + "in_service": true, + "hddsize": null, + "mac_addresses": [], + "corethread": null, + "cpucount": null, + "virtual_host_name": null, + "hddraid": null, + "is_it_virtual_host": "no", + "is_it_switch": "yes", + "customer": "SEA", + "customer_id": 1, + "ucs_manager": null, + "name": "", + "room": "IDF", + "rack_id": 1, + "notes": "", + "ram": null, + "asset_no": "", + "rack": "IDF-RACK-01", + "device_sub_type": "Rackable", + "manufacturer": "Cisco Systems Inc", + "osver": "Fuji 16.9.5", + "device_purchase_line_items": [], + "cpucore": null, + "where": 3, + "device_id": 1 + }, + { + "last_updated": "2021-123-03T06:06:38.836Z", + "orientation": 1, + "ip_addresses": [ + { + "subnet": "Management-10.10.25.0/24", + "macaddress": "a6:23:18:bc:3d:12", + "subnet_id": 4077, + "ip": "10.10.25.79", + "label": "mgmt0", + "type": null + } + ], + "serial_no": "FDA2732Q36Q", + "nonauthoritativealiases": [], + "device_id": 2, + "service_level": "Production", + "is_it_blade_host": "no", + "hw_size": 1.0, + "id": 2, + "custom_fields": [ + { + "notes": null, + "key": "Last Backup", + "value": null + }, + { + "notes": null, + "key": "Business Unit", + "value": null + } + ], + "aliases": [], + "category": "", + "hdd_details": [], + "uuid": "", + "hw_depth": 2, + "os": "nx-os", + "cpuspeed": "", + "hw_model": "Cisco N3K-C3172TQ-10GT", + "hddraid_type": null, + "type": "physical", + "hddcount": "", + "building": "Apple Park", + "xpos": 0, + "device_external_links": [], + "start_at": 16.0, + "tags": [ + "nautobot-dc-core", + "SFO" + ], + "hw_model_id": 14, + "in_service": true, + "hddsize": "", + "mac_addresses": [ + { + "mac": "ab:cd:ef:12:34:56", + "vlan": "VLAN 104", + "port_name": "Ethernet1/1", + "port": null + }, + { + "mac": "ab:cd:ef:12:34:57", + "vlan": "VLAN 104", + "port_name": "Ethernet1/2", + "port": null + }, + { + "mac": "ab:cd:ef:12:34:58", + "vlan": "VLAN 104", + "port_name": "Ethernet1/3", + "port": null + }, + { + "mac": "ab:cd:ef:12:34:59", + "vlan": "VLAN 104", + "port_name": "Ethernet1/4", + "port": null + } + ], + "corethread": "", + "cpucount": "", + "virtual_host_name": null, + "hddraid": null, + "is_it_virtual_host": "yes", + "is_it_switch": "yes", + "customer": "SFO", + "customer_id": 2, + "ucs_manager": null, + "name": "core-router.testexample.com", + "room": "Server Room", + "row": "A", + "rack_id": 17, + "notes": "", + "ram": "", + "asset_no": "", + "device_sub_type": "Rackable", + "manufacturer": "Cisco Systems Inc", + "osver": "6.0(2)U3(1)", + "device_purchase_line_items": [], + "cpucore": "", + "where": 2, + "rack": "Rack A1" + }, + { + "last_updated": "2022-12-03T06:02:43.811Z", + "orientation": 1, + "ip_addresses": [], + "serial_no": "FDA2732Q36R", + "nonauthoritativealiases": [], + "service_level": "Production", + "is_it_blade_host": "no", + "hw_size": 1.0, + "id": 3, + "custom_fields": [ + { + "notes": null, + "key": "Last Backup", + "value": null + }, + { + "notes": null, + "key": "Business Unit", + "value": null + } + ], + "aliases": [], + "category": "", + "hdd_details": [], + "uuid": "", + "hw_depth": 2, + "os": "ios", + "cpuspeed": null, + "hw_model": "Cisco WS-C3650-48PD", + "hddraid_type": null, + "type": "physical", + "hddcount": null, + "building": "Apple Park", + "xpos": 0, + "device_external_links": [], + "start_at": 20.0, + "tags": [ + "Lab", + "SFO" + ], + "hw_model_id": 15, + "in_service": true, + "hddsize": null, + "mac_addresses": [], + "corethread": null, + "cpucount": null, + "virtual_host_name": null, + "hddraid": null, + "is_it_virtual_host": "no", + "is_it_switch": "yes", + "customer": "SFO", + "customer_id": 2, + "ucs_manager": null, + "name": "labswitch.testexample.com", + "room": "IDF 02", + "rack_id": 42, + "notes": "", + "ram": null, + "asset_no": "", + "rack": "Rack B2", + "device_sub_type": "Rackable", + "manufacturer": "Cisco Systems Inc", + "osver": "Fuji 16.9.5", + "device_purchase_line_items": [], + "cpucore": null, + "where": 2, + "device_id": 3 + }, + { + "last_updated": "2020-12-16T06:03:04.995Z", + "ip_addresses": [], + "serial_no": "", + "hw_depth": null, + "service_level": "Production", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "hw_size": null, + "id": 4, + "custom_fields": [ + { + "notes": null, + "key": "Last Backup", + "value": null + }, + { + "notes": null, + "key": "Business Unit", + "value": null + } + ], + "aliases": [], + "category": "", + "is_it_virtual_host": "no", + "hdd_details": null, + "uuid": "", + "cpuspeed": null, + "hw_model": null, + "customer_id": 1, + "type": "cluster", + "hddcount": null, + "device_external_links": [], + "tags": [ + "AUS", + "TEST" + ], + "hw_model_id": null, + "in_service": true, + "hddsize": null, + "mac_addresses": [], + "hddraid": null, + "nonauthoritativealiases": [], + "cpucount": null, + "virtual_host_name": null, + "corethread": null, + "manufacturer": null, + "customer": "AUS", + "hddraid_type": null, + "ucs_manager": null, + "name": "stack01.testexample.com", + "notes": "", + "ram": null, + "asset_no": "", + "osver": "15.2(2)E9", + "device_purchase_line_items": [], + "cpucore": null, + "os": "ios", + "device_id": 4 + }, + { + "last_updated": "2022-11-21T05:02:04.127Z", + "ip_addresses": [], + "serial_no": "FBC3832R36S", + "hw_depth": 2, + "service_level": "Production", + "is_it_blade_host": "no", + "hw_size": 1.0, + "id": 5, + "custom_fields": [ + { + "notes": null, + "key": "Last Backup", + "value": null + }, + { + "notes": null, + "key": "Business Unit", + "value": null + } + ], + "aliases": [], + "category": "", + "is_it_virtual_host": "no", + "hdd_details": [], + "uuid": "", + "cpuspeed": null, + "hw_model": "Cisco WS-C2960X-48LPD-L", + "hddraid_type": null, + "type": "physical", + "building": "Dell - Round Rock 2", + "hddcount": null, + "manufacturer": "Cisco Systems Inc", + "device_external_links": [], + "tags": [ + "AUS", + "TEST" + ], + "hw_model_id": 103, + "in_service": true, + "hddsize": null, + "mac_addresses": [], + "hddraid": null, + "nonauthoritativealiases": [], + "cpucount": null, + "virtual_host_name": null, + "corethread": null, + "is_it_switch": "yes", + "customer": "AUS", + "customer_id": 1, + "ucs_manager": null, + "name": "stack01.testexample.com - Switch 1", + "notes": "", + "ram": null, + "asset_no": "", + "device_sub_type": "Rackable", + "osver": "15.2(2)E9", + "device_purchase_line_items": [], + "cpucore": null, + "os": "ios", + "device_id": 5 + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_devices_sent.json b/nautobot_ssot/tests/device42/fixtures/get_devices_sent.json new file mode 100644 index 000000000..464785ffe --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_devices_sent.json @@ -0,0 +1,371 @@ +{ + "total_count": 5, + "limit": 1000, + "Devices": [ + { + "last_updated": "2020-12-03T06:02:40.357Z", + "orientation": 1, + "ip_addresses": [], + "serial_no": "FDA2833B2Q4", + "nonauthoritativealiases": [], + "service_level": "Production", + "is_it_blade_host": "no", + "hw_size": 1.0, + "id": 1, + "custom_fields": [ + { + "notes": null, + "key": "Last Backup", + "value": null + }, + { + "notes": null, + "key": "Business Unit", + "value": null + } + ], + "aliases": [], + "category": "", + "hdd_details": [], + "uuid": "", + "hw_depth": 2, + "os": "ios", + "cpuspeed": null, + "hw_model": "Cisco WS-C3650-48PD", + "hddraid_type": null, + "type": "physical", + "hddcount": null, + "building": "Microsoft HQ", + "xpos": 0, + "device_external_links": [], + "start_at": 5.0, + "tags": [ + "Lab", + "SEA" + ], + "hw_model_id": 15, + "in_service": true, + "hddsize": null, + "mac_addresses": [], + "corethread": null, + "cpucount": null, + "virtual_host_name": null, + "hddraid": null, + "is_it_virtual_host": "no", + "is_it_switch": "yes", + "customer": "SEA", + "customer_id": 1, + "ucs_manager": null, + "name": "", + "room": "IDF", + "rack_id": 1, + "notes": "", + "ram": null, + "asset_no": "", + "rack": "IDF-RACK-01", + "device_sub_type": "Rackable", + "manufacturer": "Cisco Systems Inc", + "osver": "Fuji 16.9.5", + "device_purchase_line_items": [], + "cpucore": null, + "where": 3, + "device_id": 1 + }, + { + "last_updated": "2021-123-03T06:06:38.836Z", + "orientation": 1, + "ip_addresses": [ + { + "subnet": "Management-10.10.25.0/24", + "macaddress": "a6:23:18:bc:3d:12", + "subnet_id": 4077, + "ip": "10.10.25.79", + "label": "mgmt0", + "type": null + } + ], + "serial_no": "FDA2732Q36Q", + "nonauthoritativealiases": [], + "device_id": 2, + "service_level": "Production", + "is_it_blade_host": "no", + "hw_size": 1.0, + "id": 2, + "custom_fields": [ + { + "notes": null, + "key": "Last Backup", + "value": null + }, + { + "notes": null, + "key": "Business Unit", + "value": null + } + ], + "aliases": [], + "category": "", + "hdd_details": [], + "uuid": "", + "hw_depth": 2, + "os": "nx-os", + "cpuspeed": "", + "hw_model": "Cisco N3K-C3172TQ-10GT", + "hddraid_type": null, + "type": "physical", + "hddcount": "", + "building": "Apple Park", + "xpos": 0, + "device_external_links": [], + "start_at": 16.0, + "tags": [ + "nautobot-dc-core", + "SFO" + ], + "hw_model_id": 14, + "in_service": true, + "hddsize": "", + "mac_addresses": [ + { + "mac": "ab:cd:ef:12:34:56", + "vlan": "VLAN 104", + "port_name": "Ethernet1/1", + "port": null + }, + { + "mac": "ab:cd:ef:12:34:57", + "vlan": "VLAN 104", + "port_name": "Ethernet1/2", + "port": null + }, + { + "mac": "ab:cd:ef:12:34:58", + "vlan": "VLAN 104", + "port_name": "Ethernet1/3", + "port": null + }, + { + "mac": "ab:cd:ef:12:34:59", + "vlan": "VLAN 104", + "port_name": "Ethernet1/4", + "port": null + } + ], + "corethread": "", + "cpucount": "", + "virtual_host_name": null, + "hddraid": null, + "is_it_virtual_host": "yes", + "is_it_switch": "yes", + "customer": "SFO", + "customer_id": 2, + "ucs_manager": null, + "name": "core-router.testexample.com", + "room": "Server Room", + "row": "A", + "rack_id": 17, + "notes": "", + "ram": "", + "asset_no": "", + "device_sub_type": "Rackable", + "manufacturer": "Cisco Systems Inc", + "osver": "6.0(2)U3(1)", + "device_purchase_line_items": [], + "cpucore": "", + "where": 2, + "rack": "Rack A1" + }, + { + "last_updated": "2022-12-03T06:02:43.811Z", + "orientation": 1, + "ip_addresses": [], + "serial_no": "FDA2732Q36R", + "nonauthoritativealiases": [], + "service_level": "Production", + "is_it_blade_host": "no", + "hw_size": 1.0, + "id": 3, + "custom_fields": [ + { + "notes": null, + "key": "Last Backup", + "value": null + }, + { + "notes": null, + "key": "Business Unit", + "value": null + } + ], + "aliases": [], + "category": "", + "hdd_details": [], + "uuid": "", + "hw_depth": 2, + "os": "ios", + "cpuspeed": null, + "hw_model": "Cisco WS-C3650-48PD", + "hddraid_type": null, + "type": "physical", + "hddcount": null, + "building": "Apple Park", + "xpos": 0, + "device_external_links": [], + "start_at": 20.0, + "tags": [ + "Lab", + "SFO" + ], + "hw_model_id": 15, + "in_service": true, + "hddsize": null, + "mac_addresses": [], + "corethread": null, + "cpucount": null, + "virtual_host_name": null, + "hddraid": null, + "is_it_virtual_host": "no", + "is_it_switch": "yes", + "customer": "SFO", + "customer_id": 2, + "ucs_manager": null, + "name": "labswitch.testexample.com", + "room": "IDF 02", + "rack_id": 42, + "notes": "", + "ram": null, + "asset_no": "", + "rack": "Rack B2", + "device_sub_type": "Rackable", + "manufacturer": "Cisco Systems Inc", + "osver": "Fuji 16.9.5", + "device_purchase_line_items": [], + "cpucore": null, + "where": 2, + "device_id": 3 + }, + { + "last_updated": "2020-12-16T06:03:04.995Z", + "ip_addresses": [], + "serial_no": "", + "hw_depth": null, + "service_level": "Production", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "hw_size": null, + "id": 4, + "custom_fields": [ + { + "notes": null, + "key": "Last Backup", + "value": null + }, + { + "notes": null, + "key": "Business Unit", + "value": null + } + ], + "aliases": [], + "category": "", + "is_it_virtual_host": "no", + "hdd_details": null, + "uuid": "", + "cpuspeed": null, + "hw_model": null, + "customer_id": 1, + "type": "cluster", + "hddcount": null, + "device_external_links": [], + "tags": [ + "AUS", + "TEST" + ], + "hw_model_id": null, + "in_service": true, + "hddsize": null, + "mac_addresses": [], + "hddraid": null, + "nonauthoritativealiases": [], + "cpucount": null, + "virtual_host_name": null, + "corethread": null, + "manufacturer": null, + "customer": "AUS", + "hddraid_type": null, + "ucs_manager": null, + "name": "stack01.testexample.com", + "notes": "", + "ram": null, + "asset_no": "", + "osver": "15.2(2)E9", + "device_purchase_line_items": [], + "cpucore": null, + "os": "ios", + "device_id": 4 + }, + { + "last_updated": "2022-11-21T05:02:04.127Z", + "ip_addresses": [], + "serial_no": "FBC3832R36S", + "hw_depth": 2, + "service_level": "Production", + "is_it_blade_host": "no", + "hw_size": 1.0, + "id": 5, + "custom_fields": [ + { + "notes": null, + "key": "Last Backup", + "value": null + }, + { + "notes": null, + "key": "Business Unit", + "value": null + } + ], + "aliases": [], + "category": "", + "is_it_virtual_host": "no", + "hdd_details": [], + "uuid": "", + "cpuspeed": null, + "hw_model": "Cisco WS-C2960X-48LPD-L", + "hddraid_type": null, + "type": "physical", + "building": "Dell - Round Rock 2", + "hddcount": null, + "manufacturer": "Cisco Systems Inc", + "device_external_links": [], + "tags": [ + "AUS", + "TEST" + ], + "hw_model_id": 103, + "in_service": true, + "hddsize": null, + "mac_addresses": [], + "hddraid": null, + "nonauthoritativealiases": [], + "cpucount": null, + "virtual_host_name": null, + "corethread": null, + "is_it_switch": "yes", + "customer": "AUS", + "customer_id": 1, + "ucs_manager": null, + "name": "stack01.testexample.com - Switch 1", + "notes": "", + "ram": null, + "asset_no": "", + "device_sub_type": "Rackable", + "osver": "15.2(2)E9", + "device_purchase_line_items": [], + "cpucore": null, + "os": "ios", + "device_id": 5 + } + ], + "offset": 0 +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_hardware_models_recv.json b/nautobot_ssot/tests/device42/fixtures/get_hardware_models_recv.json new file mode 100644 index 000000000..de8e5d3c3 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_hardware_models_recv.json @@ -0,0 +1,394 @@ +[ + { + "end_of_support": null, + "part_no": "N9K-C9SS2PQ", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 6, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "http://www.cisco.com/c/en/us/support/switches/nexus-9332pq-switch/model.html", + "power": null, + "front_image_id": 740, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco N9K-C9SS2PQ", + "notes": "", + "depth": "Full Depth", + "front_image_url": "/mt/01010101010101010101010101010101/images/N9K-C9332PQ.jpg", + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "2348UPQ", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 7, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "http://www.cisco.com/c/en/us/support/switches/nexus-2348upq-10ge-fabric-extender/model.html", + "power": null, + "front_image_id": 742, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco NEXUS 2348UPQ", + "notes": "", + "depth": "Full Depth", + "front_image_url": "/mt/01010101010101010101010101010101/images/Nexus_2348UPQ.jpg", + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "2348TQ", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 8, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "http://www.cisco.com/c/en/us/support/switches/nexus-2348tq-10gt-fabric-extender/model.html", + "power": null, + "front_image_id": 743, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco NEXUS 2348TQ", + "notes": "", + "depth": "Full Depth", + "front_image_url": "/mt/01010101010101010101010101010101/images/Nexus_2348TQ.jpg", + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "WS-C3650-48PD", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 11, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco Catalyst 3650 48PD", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 14, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco N3K-C3172TQ-10GT", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "WS-C3650-48PD", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 15, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco WS-C3650-48PD", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "WS-C2960X-48LPD-L", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 16, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco WS-C2960X-48LPD-L", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 18, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 700, + "name": "Cisco WS-C3750X-48T-S", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 19, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 700, + "name": "Cisco WS-C3750X-48T-L", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 21, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco WS-C2960CX-8PC-L", + "notes": "", + "depth": "Full Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 22, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco WS-C3650-48PD-S", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 35, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco WS-C3850-48T", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 448, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": null, + "type": "", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Meraki", + "watts": null, + "name": "MX64W-HW", + "notes": "", + "depth": null, + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 449, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": null, + "name": "FPR-2130", + "notes": "", + "depth": "Full Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_hardware_models_sent.json b/nautobot_ssot/tests/device42/fixtures/get_hardware_models_sent.json new file mode 100644 index 000000000..1c5dc38d8 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_hardware_models_sent.json @@ -0,0 +1,399 @@ +{ + "models": [ + { + "end_of_support": null, + "part_no": "N9K-C9SS2PQ", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 6, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "http://www.cisco.com/c/en/us/support/switches/nexus-9332pq-switch/model.html", + "power": null, + "front_image_id": 740, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco N9K-C9SS2PQ", + "notes": "", + "depth": "Full Depth", + "front_image_url": "/mt/01010101010101010101010101010101/images/N9K-C9332PQ.jpg", + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "2348UPQ", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 7, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "http://www.cisco.com/c/en/us/support/switches/nexus-2348upq-10ge-fabric-extender/model.html", + "power": null, + "front_image_id": 742, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco NEXUS 2348UPQ", + "notes": "", + "depth": "Full Depth", + "front_image_url": "/mt/01010101010101010101010101010101/images/Nexus_2348UPQ.jpg", + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "2348TQ", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 8, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "http://www.cisco.com/c/en/us/support/switches/nexus-2348tq-10gt-fabric-extender/model.html", + "power": null, + "front_image_id": 743, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco NEXUS 2348TQ", + "notes": "", + "depth": "Full Depth", + "front_image_url": "/mt/01010101010101010101010101010101/images/Nexus_2348TQ.jpg", + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "WS-C3650-48PD", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 11, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco Catalyst 3650 48PD", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 14, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco N3K-C3172TQ-10GT", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "WS-C3650-48PD", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 15, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco WS-C3650-48PD", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "WS-C2960X-48LPD-L", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 16, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco WS-C2960X-48LPD-L", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 18, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 700, + "name": "Cisco WS-C3750X-48T-S", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "yes", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 19, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 700, + "name": "Cisco WS-C3750X-48T-L", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 21, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco WS-C2960CX-8PC-L", + "notes": "", + "depth": "Full Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 22, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco WS-C3650-48PD-S", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 35, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": 0, + "name": "Cisco WS-C3850-48T", + "notes": "", + "depth": "Half Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 448, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": null, + "type": "", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Meraki", + "watts": null, + "name": "MX64W-HW", + "notes": "", + "depth": null, + "front_image_url": null, + "width_ratio_raw": 2520 + }, + { + "end_of_support": null, + "part_no": "", + "width_ratio": "1", + "is_it_blade_host": "no", + "is_it_switch": "no", + "back_image_url": null, + "custom_fields": [], + "aliases": [], + "hardware_id": 449, + "end_of_life": null, + "power_max": null, + "blade_size": null, + "size": 1.0, + "type": "physical", + "spec_url": "", + "power": null, + "front_image_id": null, + "back_image_id": null, + "auto_add_ports": "no", + "manufacturer": "Cisco Systems Inc", + "watts": null, + "name": "FPR-2130", + "notes": "", + "depth": "Full Depth", + "front_image_url": null, + "width_ratio_raw": 2520 + } + ], + "total_count": 14, + "limit": 1000, + "offset": 0 +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_ip_addrs.json b/nautobot_ssot/tests/device42/fixtures/get_ip_addrs.json new file mode 100644 index 000000000..2e7cd39de --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_ip_addrs.json @@ -0,0 +1,26 @@ +[ + { + "ip_address": "10.0.10.1", + "available": false, + "label": "gateway", + "tags": "", + "port_name": "Ethernet1", + "hwaddress": "aabbccddeeff", + "subnet": "10.0.10.0", + "netmask": 24, + "vrf": "default", + "device": "core-router.testexample.com" + }, + { + "ip_address": "1.1.1.1", + "available": false, + "label": "internet", + "tags": "", + "port_name": "1/0/1", + "hwaddress": "aabbccddeef1", + "subnet": "1.1.1.0", + "netmask": 28, + "vrf": "public", + "device": "core-router.testexample.com" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_recv.json b/nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_recv.json new file mode 100644 index 000000000..ca088c3f8 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_recv.json @@ -0,0 +1,14 @@ +{ + "1.1.1.1/32": { + "Owner": { + "key": "Owner", + "value": "AT&T", + "notes": null + }, + "Visibility": { + "key": "Visibility", + "value": "Public", + "notes": null + } + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_sent.json b/nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_sent.json new file mode 100644 index 000000000..3c0db9fe5 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_sent.json @@ -0,0 +1,16 @@ +[ + { + "key": "Visibility", + "value": "Public", + "notes": null, + "ip_address": "1.1.1.1", + "mask_bits": 32 + }, + { + "key": "Owner", + "value": "AT&T", + "notes": null, + "ip_address": "1.1.1.1", + "mask_bits": 32 + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_ipaddr_default_custom_fields_recv.json b/nautobot_ssot/tests/device42/fixtures/get_ipaddr_default_custom_fields_recv.json new file mode 100644 index 000000000..af06473c1 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_ipaddr_default_custom_fields_recv.json @@ -0,0 +1,12 @@ +{ + "Owner": { + "key": "Owner", + "value": null, + "notes": null + }, + "Visibility": { + "key": "Visibility", + "value": null, + "notes": null + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_ipaddr_default_custom_fields_sent.json b/nautobot_ssot/tests/device42/fixtures/get_ipaddr_default_custom_fields_sent.json new file mode 100644 index 000000000..c68815c76 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_ipaddr_default_custom_fields_sent.json @@ -0,0 +1,12 @@ +[ + { + "key": "Visibility", + "value": "Public", + "notes": null + }, + { + "key": "Owner", + "value": "AT&T", + "notes": null + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_patch_panel_port_pks_recv.json b/nautobot_ssot/tests/device42/fixtures/get_patch_panel_port_pks_recv.json new file mode 100644 index 000000000..bce9b6f60 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_patch_panel_port_pks_recv.json @@ -0,0 +1,54 @@ +{ + "1": { + "patchpanelport_pk": 1, + "number": 1, + "pair": false, + "label": "", + "patchpanel_asset_fk": 3, + "module_asset_fk": null, + "connection_type": "Device Port", + "connection_patchpanelport_fk": null, + "connection_netport_fk": null, + "connection_tapport_fk": null, + "connection_telcocircuit_fk": null, + "label_1": "", + "label_2": "", + "port_type_id": 1, + "port_type_name": "RJ45", + "cable_color_id": null, + "cable_color_name": null, + "back_connection_type": "panel", + "back_connection_patchpanelport_fk": null, + "switch_back_connection_netport_fk": null, + "back_connection_telcocircuit_fk": null, + "back_cable_id": null, + "back_cable_name": null, + "name": "DFW Patch 1" + }, + "2": { + "patchpanelport_pk": 2, + "number": 1, + "pair": false, + "label": "", + "patchpanel_asset_fk": 2, + "module_asset_fk": null, + "connection_type": "Telco Circuit", + "connection_patchpanelport_fk": null, + "connection_netport_fk": null, + "connection_tapport_fk": null, + "connection_telcocircuit_fk": null, + "label_1": null, + "label_2": null, + "port_type_id": 3, + "port_type_name": "Fiber LC", + "cable_color_id": null, + "cable_color_name": null, + "back_connection_type": "cable", + "back_connection_patchpanelport_fk": null, + "switch_back_connection_netport_fk": null, + "back_connection_telcocircuit_fk": null, + "back_cable_id": null, + "back_cable_name": null, + "name": "AUS Patch 2" + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_patch_panel_port_pks_sent.json b/nautobot_ssot/tests/device42/fixtures/get_patch_panel_port_pks_sent.json new file mode 100644 index 000000000..d76401bb8 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_patch_panel_port_pks_sent.json @@ -0,0 +1,54 @@ +[ + { + "patchpanelport_pk": 1, + "number": 1, + "pair": false, + "label": "", + "patchpanel_asset_fk": 3, + "module_asset_fk": null, + "connection_type": "Device Port", + "connection_patchpanelport_fk": null, + "connection_netport_fk": null, + "connection_tapport_fk": null, + "connection_telcocircuit_fk": null, + "label_1": "", + "label_2": "", + "port_type_id": 1, + "port_type_name": "RJ45", + "cable_color_id": null, + "cable_color_name": null, + "back_connection_type": "panel", + "back_connection_patchpanelport_fk": null, + "switch_back_connection_netport_fk": null, + "back_connection_telcocircuit_fk": null, + "back_cable_id": null, + "back_cable_name": null, + "name": "DFW Patch 1" + }, + { + "patchpanelport_pk": 2, + "number": 1, + "pair": false, + "label": "", + "patchpanel_asset_fk": 2, + "module_asset_fk": null, + "connection_type": "Telco Circuit", + "connection_patchpanelport_fk": null, + "connection_netport_fk": null, + "connection_tapport_fk": null, + "connection_telcocircuit_fk": null, + "label_1": null, + "label_2": null, + "port_type_id": 3, + "port_type_name": "Fiber LC", + "cable_color_id": null, + "cable_color_name": null, + "back_connection_type": "cable", + "back_connection_patchpanelport_fk": null, + "switch_back_connection_netport_fk": null, + "back_connection_telcocircuit_fk": null, + "back_cable_id": null, + "back_cable_name": null, + "name": "AUS Patch 2" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_patch_panels.json b/nautobot_ssot/tests/device42/fixtures/get_patch_panels.json new file mode 100644 index 000000000..835bb2cac --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_patch_panels.json @@ -0,0 +1,80 @@ +[ + { + "name": "AUS Patch 1", + "in_service": true, + "serial_no": "", + "customer_fk": null, + "building_fk": null, + "calculated_building_fk": 2, + "room_fk": null, + "calculated_room_fk": 1, + "calculated_rack_fk": 1, + "size": 2.0, + "depth": 2, + "number_of_ports": 72, + "model_name": "Ortronics OR-PHAHJU72", + "port_type": "RJ45", + "vendor": "Ortronics", + "rack_fk": 1, + "position": 40.0, + "orientation": "Front" + }, + { + "name": "AUS Patch 2", + "in_service": true, + "serial_no": "", + "customer_fk": null, + "building_fk": null, + "calculated_building_fk": 2, + "room_fk": null, + "calculated_room_fk": 2, + "calculated_rack_fk": 2, + "size": 2.0, + "depth": 2, + "number_of_ports": 72, + "model_name": "Corning CCH-03U", + "vendor": "Corning", + "port_type": "Fiber LC", + "rack_fk": 2, + "position": 34.0, + "orientation": "Front" + }, + { + "name": "DFW Patch 1", + "serial_no": "", + "customer_fk": null, + "building_fk": null, + "calculated_building_fk": 3, + "room_fk": null, + "calculated_room_fk": 3, + "calculated_rack_fk": 3, + "size": 2.0, + "depth": 2, + "number_of_ports": 72, + "model_name": "Ortronics OR-PHAHJU72", + "vendor": "Ortronics", + "port_type": "RJ45", + "rack_fk": 3, + "position": 1.0, + "orientation": "Front" + }, + { + "name": "DFW Patch 2", + "serial_no": "", + "customer_fk": null, + "building_fk": null, + "calculated_building_fk": 3, + "room_fk": null, + "calculated_room_fk": 4, + "calculated_rack_fk": 4, + "size": 2.0, + "depth": 2, + "number_of_ports": 72, + "model_name": "Corning CCH-03U", + "vendor": "Corning", + "port_type": "Fiber LC", + "rack_fk": 4, + "position": 34.0, + "orientation": "Front" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_port_connections.json b/nautobot_ssot/tests/device42/fixtures/get_port_connections.json new file mode 100644 index 000000000..ca8a242f0 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_port_connections.json @@ -0,0 +1,14 @@ +[ + { + "src_port": 1, + "src_device": 1, + "dst_device": 2, + "dst_port": 2 + }, + { + "src_port": 3, + "src_device": 1, + "dst_device": 2, + "dst_port": 4 + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_recv.json b/nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_recv.json new file mode 100644 index 000000000..5ffbd81b1 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_recv.json @@ -0,0 +1,16 @@ +{ + "core-router.testexample.com": { + "Ethernet1/1": { + "Software Version": { + "key": "Software Version", + "value": "10R.2D.2", + "notes": null + }, + "EOL Date": { + "key": "EOL Date", + "value": "12/31/2999", + "notes": null + } + } + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_sent.json b/nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_sent.json new file mode 100644 index 000000000..50fdde944 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_sent.json @@ -0,0 +1,16 @@ +[ + { + "key": "Software Version", + "value": "10R.2D.2", + "notes": null, + "port_name": "Ethernet1/1", + "device_name": "core-router.testexample.com" + }, + { + "key": "EOL Date", + "value": "12/31/2999", + "notes": null, + "port_name": "Ethernet1/1", + "device_name": "core-router.testexample.com" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_port_pks_recv.json b/nautobot_ssot/tests/device42/fixtures/get_port_pks_recv.json new file mode 100644 index 000000000..884573b11 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_port_pks_recv.json @@ -0,0 +1,14 @@ +{ + "1": { + "port": "Management0", + "netport_pk": 1, + "hwaddress": "1234567890ab", + "device": "core-router.testexample.com" + }, + "2": { + "port": "Ethernet1/1", + "netport_pk": 2, + "hwaddress": "abcdef012345", + "device": "distro-switch.testexample.com" + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_port_pks_sent.json b/nautobot_ssot/tests/device42/fixtures/get_port_pks_sent.json new file mode 100644 index 000000000..d39b5d84b --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_port_pks_sent.json @@ -0,0 +1,14 @@ +[ + { + "port": "Management0", + "netport_pk": 1, + "hwaddress": "1234567890ab", + "device": "core-router.testexample.com" + }, + { + "port": "Ethernet1/1", + "netport_pk": 2, + "hwaddress": "abcdef012345", + "device": "distro-switch.testexample.com" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_recv.json b/nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_recv.json new file mode 100644 index 000000000..196766f03 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_recv.json @@ -0,0 +1,19 @@ +[ + { + "vlan_ids": [ + "1" + ], + "netport_pk": 4, + "port_name": "GigabitEthernet1/0/1", + "description": "GigabitEthernet1/0/1", + "up": true, + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "abcdef012345", + "port_type": "physical", + "port_speed": "1.0 Gbps", + "mtu": 1518, + "tags": "first-tag,second-tag", + "device_name": "core-router.testexample.com" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_sent.json b/nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_sent.json new file mode 100644 index 000000000..196766f03 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_sent.json @@ -0,0 +1,19 @@ +[ + { + "vlan_ids": [ + "1" + ], + "netport_pk": 4, + "port_name": "GigabitEthernet1/0/1", + "description": "GigabitEthernet1/0/1", + "up": true, + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "abcdef012345", + "port_type": "physical", + "port_speed": "1.0 Gbps", + "mtu": 1518, + "tags": "first-tag,second-tag", + "device_name": "core-router.testexample.com" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_recv.json b/nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_recv.json new file mode 100644 index 000000000..de1b97f23 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_recv.json @@ -0,0 +1,58 @@ +[ + { + "port_name": "Ethernet1/1.100", + "netport_pk": 1, + "description": "Ethernet1/0/1", + "up_admin": true, + "discovered_type": "l2vlan", + "hwaddress": "", + "port_type": "logical", + "port_speed": "40 Gbps", + "mtu": 9150, + "tags": "", + "second_device_fk": null, + "device_name": "core-router.testexample.com" + }, + { + "port_name": "Management0", + "netport_pk": 2, + "description": "", + "up_admin": true, + "discovered_type": "static", + "hwaddress": "1234567890ab", + "port_type": "physical", + "port_speed": null, + "mtu": null, + "tags": "", + "second_device_fk": null, + "device_name": "core-router.testexample.com" + }, + { + "port_name": "Ethernet1/1", + "netport_pk": 3, + "description": "", + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "abcdef012345", + "port_type": "physical", + "port_speed": "10 Gbps", + "mtu": 1500, + "tags": "", + "second_device_fk": null, + "device_name": "core-router.testexample.com" + }, + { + "port_name": "GigabitEthernet1/0/1", + "netport_pk": 4, + "description": "GigabitEthernet1/0/1", + "up": true, + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "abcdef012345", + "port_type": "physical", + "port_speed": "1.0 Gbps", + "mtu": 1518, + "tags": "first-tag,second-tag", + "device_name": "core-router.testexample.com" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_sent.json b/nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_sent.json new file mode 100644 index 000000000..de1b97f23 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_sent.json @@ -0,0 +1,58 @@ +[ + { + "port_name": "Ethernet1/1.100", + "netport_pk": 1, + "description": "Ethernet1/0/1", + "up_admin": true, + "discovered_type": "l2vlan", + "hwaddress": "", + "port_type": "logical", + "port_speed": "40 Gbps", + "mtu": 9150, + "tags": "", + "second_device_fk": null, + "device_name": "core-router.testexample.com" + }, + { + "port_name": "Management0", + "netport_pk": 2, + "description": "", + "up_admin": true, + "discovered_type": "static", + "hwaddress": "1234567890ab", + "port_type": "physical", + "port_speed": null, + "mtu": null, + "tags": "", + "second_device_fk": null, + "device_name": "core-router.testexample.com" + }, + { + "port_name": "Ethernet1/1", + "netport_pk": 3, + "description": "", + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "abcdef012345", + "port_type": "physical", + "port_speed": "10 Gbps", + "mtu": 1500, + "tags": "", + "second_device_fk": null, + "device_name": "core-router.testexample.com" + }, + { + "port_name": "GigabitEthernet1/0/1", + "netport_pk": 4, + "description": "GigabitEthernet1/0/1", + "up": true, + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "abcdef012345", + "port_type": "physical", + "port_speed": "1.0 Gbps", + "mtu": 1518, + "tags": "first-tag,second-tag", + "device_name": "core-router.testexample.com" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_rack_pks_recv.json b/nautobot_ssot/tests/device42/fixtures/get_rack_pks_recv.json new file mode 100644 index 000000000..28fdda54c --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_rack_pks_recv.json @@ -0,0 +1,74 @@ +{ + "1": { + "rack_pk": 1, + "name": "Rack 1", + "size": 42, + "number_between_us": true, + "numbering_start_from_bottom": true, + "first_number": 1, + "row": "", + "room_fk": 1, + "vendor_fk": 1, + "notes": "", + "orientation": "left", + "asset_no": "", + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00", + "start_row": 3.0, + "start_col": 3.0 + }, + "2": { + "rack_pk": 2, + "name": "Server Rack", + "size": 48, + "number_between_us": true, + "numbering_start_from_bottom": true, + "first_number": 1, + "row": "", + "room_fk": 2, + "vendor_fk": 1, + "notes": "", + "orientation": "left", + "asset_no": "", + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00", + "start_row": 4.0, + "start_col": 3.0 + }, + "3": { + "rack_pk": 3, + "name": "Rack A", + "size": 48, + "number_between_us": true, + "numbering_start_from_bottom": true, + "first_number": 1, + "row": "", + "room_fk": 2, + "vendor_fk": 2, + "notes": "", + "orientation": "left", + "asset_no": "", + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00", + "start_row": 5.0, + "start_col": 3.0 + }, + "4": { + "rack_pk": 4, + "name": "Secondary IDF", + "size": 42, + "number_between_us": true, + "numbering_start_from_bottom": true, + "first_number": 1, + "row": "", + "room_fk": 2, + "vendor_fk": 2, + "notes": "", + "orientation": "left", + "asset_no": "", + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00", + "start_row": 6.0, + "start_col": 3.0 + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_rack_pks_sent.json b/nautobot_ssot/tests/device42/fixtures/get_rack_pks_sent.json new file mode 100644 index 000000000..fd43b7cdc --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_rack_pks_sent.json @@ -0,0 +1,74 @@ +[ + { + "rack_pk": 1, + "name": "Rack 1", + "size": 42, + "number_between_us": true, + "numbering_start_from_bottom": true, + "first_number": 1, + "row": "", + "room_fk": 1, + "vendor_fk": 1, + "notes": "", + "orientation": "left", + "asset_no": "", + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00", + "start_row": 3.0, + "start_col": 3.0 + }, + { + "rack_pk": 2, + "name": "Server Rack", + "size": 48, + "number_between_us": true, + "numbering_start_from_bottom": true, + "first_number": 1, + "row": "", + "room_fk": 2, + "vendor_fk": 1, + "notes": "", + "orientation": "left", + "asset_no": "", + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00", + "start_row": 4.0, + "start_col": 3.0 + }, + { + "rack_pk": 3, + "name": "Rack A", + "size": 48, + "number_between_us": true, + "numbering_start_from_bottom": true, + "first_number": 1, + "row": "", + "room_fk": 2, + "vendor_fk": 2, + "notes": "", + "orientation": "left", + "asset_no": "", + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00", + "start_row": 5.0, + "start_col": 3.0 + }, + { + "rack_pk": 4, + "name": "Secondary IDF", + "size": 42, + "number_between_us": true, + "numbering_start_from_bottom": true, + "first_number": 1, + "row": "", + "room_fk": 2, + "vendor_fk": 2, + "notes": "", + "orientation": "left", + "asset_no": "", + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00", + "start_row": 6.0, + "start_col": 3.0 + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_racks.json b/nautobot_ssot/tests/device42/fixtures/get_racks.json new file mode 100644 index 000000000..58a24e1e1 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_racks.json @@ -0,0 +1,101 @@ +{ + "total_count": 4, + "limit": 1000, + "racks": [ + { + "building": "Dell - Round Rock 2", + "number_between_us": true, + "orientation": "left", + "tags": ["sitecode-aus", "AUS"], + "numbering_start_from_bottom": "yes", + "col_size": 1.0, + "rack_middle_option": "Start at", + "asset_no": "", + "name": "Rack 1", + "available_u": 48.0, + "rack_url": "/api/1.0/racks/1/", + "room": "Network Closet", + "start_col": 3.0, + "row_size": 1.0, + "start_row": 1.0, + "first_number": 1, + "size": 48, + "rack_id": 1, + "notes": "", + "custom_fields": [], + "row": "1" + }, + { + "building": "Apple Park", + "number_between_us": true, + "orientation": "left", + "tags": ["sitecode-sfo", "SFO"], + "numbering_start_from_bottom": "yes", + "col_size": 1.0, + "rack_middle_option": "Start at", + "asset_no": "", + "name": "Server Rack", + "available_u": 48.0, + "rack_url": "/api/1.0/racks/2/", + "room": "Server Room", + "start_col": 3.0, + "row_size": 1.0, + "start_row": 2.0, + "first_number": 1, + "size": 48, + "rack_id": 2, + "notes": "", + "custom_fields": [], + "row": "" + }, + { + "orientation": "left", + "numbering_start_from_bottom": "yes", + "custom_fields": [], + "size": 48, + "available_u": 48.0, + "row": "", + "rack_id": 3, + "rack_middle_option": "Start at", + "number_between_us": true, + "tags": ["sitecode-sea", "SEA"], + "col_size": 1.0, + "start_col": 3.0, + "row_size": 1.0, + "first_number": 1, + "manufacturer": "Legrand", + "building": "Microsoft HQ", + "start_row": 5.0, + "name": "Rack 1", + "room": "Secondary IDF", + "notes": "", + "asset_no": "", + "rack_url": "/api/1.0/racks/3/" + }, + { + "orientation": "left", + "numbering_start_from_bottom": "yes", + "custom_fields": [], + "size": 48, + "available_u": 48.0, + "row": "", + "rack_id": 4, + "rack_middle_option": "Start at", + "number_between_us": true, + "tags": ["sitecode-sea", "SEA"], + "col_size": 1.0, + "start_col": 3.0, + "row_size": 1.0, + "first_number": 1, + "manufacturer": "Legrand", + "building": "Microsoft HQ", + "start_row": 6.0, + "name": "Rack A", + "room": "Main IDF", + "notes": "", + "asset_no": "", + "rack_url": "/api/1.0/racks/4/" + } + ], + "offset": 0 +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_racks_recv.json b/nautobot_ssot/tests/device42/fixtures/get_racks_recv.json new file mode 100644 index 000000000..744a42428 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_racks_recv.json @@ -0,0 +1,96 @@ +[ + { + "building": "Dell - Round Rock 2", + "number_between_us": true, + "orientation": "left", + "tags": ["sitecode-aus", "AUS"], + "numbering_start_from_bottom": "yes", + "col_size": 1.0, + "rack_middle_option": "Start at", + "asset_no": "", + "name": "Rack 1", + "available_u": 48.0, + "rack_url": "/api/1.0/racks/1/", + "room": "Network Closet", + "start_col": 3.0, + "row_size": 1.0, + "start_row": 1.0, + "first_number": 1, + "size": 48, + "rack_id": 1, + "notes": "", + "custom_fields": [], + "row": "1" + }, + { + "building": "Apple Park", + "number_between_us": true, + "orientation": "left", + "tags": ["sitecode-sfo", "SFO"], + "numbering_start_from_bottom": "yes", + "col_size": 1.0, + "rack_middle_option": "Start at", + "asset_no": "", + "name": "Server Rack", + "available_u": 48.0, + "rack_url": "/api/1.0/racks/2/", + "room": "Server Room", + "start_col": 3.0, + "row_size": 1.0, + "start_row": 2.0, + "first_number": 1, + "size": 48, + "rack_id": 2, + "notes": "", + "custom_fields": [], + "row": "" + }, + { + "orientation": "left", + "numbering_start_from_bottom": "yes", + "custom_fields": [], + "size": 48, + "available_u": 48.0, + "row": "", + "rack_id": 3, + "rack_middle_option": "Start at", + "number_between_us": true, + "tags": ["sitecode-sea", "SEA"], + "col_size": 1.0, + "start_col": 3.0, + "row_size": 1.0, + "first_number": 1, + "manufacturer": "Legrand", + "building": "Microsoft HQ", + "start_row": 5.0, + "name": "Rack 1", + "room": "Secondary IDF", + "notes": "", + "asset_no": "", + "rack_url": "/api/1.0/racks/3/" + }, + { + "orientation": "left", + "numbering_start_from_bottom": "yes", + "custom_fields": [], + "size": 48, + "available_u": 48.0, + "row": "", + "rack_id": 4, + "rack_middle_option": "Start at", + "number_between_us": true, + "tags": ["sitecode-sea", "SEA"], + "col_size": 1.0, + "start_col": 3.0, + "row_size": 1.0, + "first_number": 1, + "manufacturer": "Legrand", + "building": "Microsoft HQ", + "start_row": 6.0, + "name": "Rack A", + "room": "Main IDF", + "notes": "", + "asset_no": "", + "rack_url": "/api/1.0/racks/4/" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_room_pks_recv.json b/nautobot_ssot/tests/device42/fixtures/get_room_pks_recv.json new file mode 100644 index 000000000..a5a9c6dec --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_room_pks_recv.json @@ -0,0 +1,47 @@ +{ + "1": { + "room_pk": 1, + "name": "AUS Primary IDF", + "building_fk": 1, + "notes": "", + "grid_cols": 5, + "grid_rows": 11, + "height": null, + "height_units": "m", + "grid_size": null, + "raised_floor": true, + "raised_floor_height": null, + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00" + }, + "2": { + "room_pk": 2, + "name": "SFO Primary IDF", + "building_fk": 2, + "notes": "", + "grid_cols": 5, + "grid_rows": 11, + "height": null, + "height_units": "m", + "grid_size": null, + "raised_floor": true, + "raised_floor_height": null, + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00" + }, + "3": { + "room_pk": 3, + "name": "SEA Primary IDF", + "building_fk": 3, + "notes": "", + "grid_cols": 0, + "grid_rows": 0, + "height": null, + "height_units": "m", + "grid_size": null, + "raised_floor": true, + "raised_floor_height": null, + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00" + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_room_pks_sent.json b/nautobot_ssot/tests/device42/fixtures/get_room_pks_sent.json new file mode 100644 index 000000000..4866822df --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_room_pks_sent.json @@ -0,0 +1,47 @@ +[ + { + "room_pk": 1, + "name": "AUS Primary IDF", + "building_fk": 1, + "notes": "", + "grid_cols": 5, + "grid_rows": 11, + "height": null, + "height_units": "m", + "grid_size": null, + "raised_floor": true, + "raised_floor_height": null, + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00" + }, + { + "room_pk": 2, + "name": "SFO Primary IDF", + "building_fk": 2, + "notes": "", + "grid_cols": 5, + "grid_rows": 11, + "height": null, + "height_units": "m", + "grid_size": null, + "raised_floor": true, + "raised_floor_height": null, + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00" + }, + { + "room_pk": 3, + "name": "SEA Primary IDF", + "building_fk": 3, + "notes": "", + "grid_cols": 0, + "grid_rows": 0, + "height": null, + "height_units": "m", + "grid_size": null, + "raised_floor": true, + "raised_floor_height": null, + "tags": "", + "last_changed": "2000-12-12T00:12:00.123456+00:00" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_rooms.json b/nautobot_ssot/tests/device42/fixtures/get_rooms.json new file mode 100644 index 000000000..ed2334b05 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_rooms.json @@ -0,0 +1,83 @@ +{ + "total_count": 3, + "limit": 3, + "rooms": [ + { + "building": "Dell - Round Rock 2", + "reverse_yaxis": "no", + "name": "Network Closet", + "notes": "", + "room_id": 1, + "reverse_xaxis": "no", + "building_id": 1, + "custom_fields": [ + { + "notes": null, + "key": "Monitoring ID", + "value": null + } + ], + "tags": [ + "sitecode-aus", "AUS" + ] + }, + { + "building": "Apple Park", + "reverse_yaxis": "no", + "name": "Server Room", + "notes": "", + "room_id": 2, + "reverse_xaxis": "no", + "building_id": 2, + "custom_fields": [ + { + "notes": null, + "key": "Monitoring ID", + "value": null + } + ], + "tags": [ + "sitecode-sfo", "SFO" + ] + }, + { + "building": "Microsoft HQ", + "reverse_yaxis": "no", + "name": "Main IDF", + "notes": "", + "room_id": 3, + "reverse_xaxis": "no", + "building_id": 2, + "custom_fields": [ + { + "notes": null, + "key": "Monitoring ID", + "value": null + } + ], + "tags": [ + "sitecode-sea", "SEA" + ] + }, + { + "building": "Microsoft HQ", + "reverse_yaxis": "no", + "name": "Secondary IDF", + "notes": "", + "room_id": 4, + "reverse_xaxis": "no", + "building_id": 1, + "custom_fields": [ + { + "notes": null, + "key": "Monitoring ID", + "value": null + } + ], + "tags": [ + "sitecode-sea", "SEA" + ] + } + ], + "offset": 0 +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_rooms_recv.json b/nautobot_ssot/tests/device42/fixtures/get_rooms_recv.json new file mode 100644 index 000000000..f73ce8243 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_rooms_recv.json @@ -0,0 +1,78 @@ +[ + { + "building": "Dell - Round Rock 2", + "reverse_yaxis": "no", + "name": "Network Closet", + "notes": "", + "room_id": 1, + "reverse_xaxis": "no", + "building_id": 1, + "custom_fields": [ + { + "notes": null, + "key": "Monitoring ID", + "value": null + } + ], + "tags": [ + "sitecode-aus", "AUS" + ] + }, + { + "building": "Apple Park", + "reverse_yaxis": "no", + "name": "Server Room", + "notes": "", + "room_id": 2, + "reverse_xaxis": "no", + "building_id": 2, + "custom_fields": [ + { + "notes": null, + "key": "Monitoring ID", + "value": null + } + ], + "tags": [ + "sitecode-sfo", "SFO" + ] + }, + { + "building": "Microsoft HQ", + "reverse_yaxis": "no", + "name": "Main IDF", + "notes": "", + "room_id": 3, + "reverse_xaxis": "no", + "building_id": 2, + "custom_fields": [ + { + "notes": null, + "key": "Monitoring ID", + "value": null + } + ], + "tags": [ + "sitecode-sea", "SEA" + ] + }, + { + "building": "Microsoft HQ", + "reverse_yaxis": "no", + "name": "Secondary IDF", + "notes": "", + "room_id": 4, + "reverse_xaxis": "no", + "building_id": 1, + "custom_fields": [ + { + "notes": null, + "key": "Monitoring ID", + "value": null + } + ], + "tags": [ + "sitecode-sea", "SEA" + ] + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_recv.json b/nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_recv.json new file mode 100644 index 000000000..478e75c9c --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_recv.json @@ -0,0 +1,26 @@ +{ + "172.16.0.0/12": { + "Department": { + "key": "Department", + "value": "Billing", + "notes": null + }, + "Public Subnet": { + "key": "Public Subnet", + "value": "Yes", + "notes": null + } + }, + "172.16.0.0/24": { + "Department": { + "key": "Department", + "value": "Billing", + "notes": null + }, + "Public Subnet": { + "key": "Public Subnet", + "value": "Yes", + "notes": null + } + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_sent.json b/nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_sent.json new file mode 100644 index 000000000..c7f26bc4b --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_sent.json @@ -0,0 +1,18 @@ +[ + { + "key": "Public Subnet", + "value": "Yes", + "notes": null, + "subnet_name": "Public", + "network": "172.16.0.0", + "mask_bits": 12 + }, + { + "key": "Department", + "value": "Billing", + "notes": null, + "subnet_name": "Billing", + "network": "172.16.0.0", + "mask_bits": 24 + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_recv.json b/nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_recv.json new file mode 100644 index 000000000..4509e1ec7 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_recv.json @@ -0,0 +1,12 @@ +{ + "Department": { + "key": "Department", + "value": null, + "notes": null + }, + "Public Subnet": { + "key": "Public Subnet", + "value": null, + "notes": null + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_sent.json b/nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_sent.json new file mode 100644 index 000000000..c7f26bc4b --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_sent.json @@ -0,0 +1,18 @@ +[ + { + "key": "Public Subnet", + "value": "Yes", + "notes": null, + "subnet_name": "Public", + "network": "172.16.0.0", + "mask_bits": 12 + }, + { + "key": "Department", + "value": "Billing", + "notes": null, + "subnet_name": "Billing", + "network": "172.16.0.0", + "mask_bits": 24 + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_subnets.json b/nautobot_ssot/tests/device42/fixtures/get_subnets.json new file mode 100644 index 000000000..ccd2b1a66 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_subnets.json @@ -0,0 +1,23 @@ +[ + { + "name": "Management", + "network": "192.168.0.0", + "mask_bits": 16, + "tags": "", + "vrf": "Management" + }, + { + "name": "Webservers", + "network": "172.16.0.0", + "mask_bits": 12, + "tags": "", + "vrf": "DMZ" + }, + { + "name": "AppServers", + "network": "10.0.0.0", + "mask_bits": 8, + "tags": "", + "vrf": "App" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_telcocircuits.json b/nautobot_ssot/tests/device42/fixtures/get_telcocircuits.json new file mode 100644 index 000000000..6beca1638 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_telcocircuits.json @@ -0,0 +1,64 @@ +[ + { + "telcocircuit_pk": 1, + "circuit_id": "AUS1.2.3A.B.4.PR1", + "type_id": 1, + "status_id": 1, + "type_name": "Fiber", + "status": "Production", + "bandwidth": 10, + "unit": "gbps", + "vendor_fk": 1, + "customer_fk": null, + "origin_type": "Device Port", + "origin_patchpanelport_fk": null, + "origin_netport_fk": 1, + "origin_telcocircuit_fk": null, + "origin_vendor_fk": null, + "origin_back_patch_panel": false, + "end_point_type": "Vendor", + "end_point_patchpanelport_fk": null, + "end_point_netport_fk": null, + "end_point_telcocircuit_fk": null, + "end_point_vendor_fk": 1, + "end_back_patch_panel": false, + "dmark_asset_fk": 1, + "order_date": null, + "provision_date": null, + "turn_on_date": null, + "notes": "", + "tags": "", + "last_changed": "2001-12-31T12:00:00.123456+00:00" + }, + { + "telcocircuit_pk": 2, + "circuit_id": "123/T3/AUSICO10123/DFWRCOOU12", + "type_id": 2, + "status_id": 1, + "type_name": "MPLS", + "status": "Production", + "bandwidth": 1, + "unit": "gbps", + "vendor_fk": 2, + "customer_fk": 2, + "origin_type": "Patch panel port", + "origin_patchpanelport_fk": 1, + "origin_netport_fk": null, + "origin_telcocircuit_fk": null, + "origin_vendor_fk": null, + "origin_back_patch_panel": true, + "end_point_type": "Device Port", + "end_point_patchpanelport_fk": null, + "end_point_netport_fk": 3, + "end_point_telcocircuit_fk": null, + "end_point_vendor_fk": null, + "end_back_patch_panel": false, + "dmark_asset_fk": 1, + "order_date": null, + "provision_date": null, + "turn_on_date": null, + "notes": "", + "tags": "", + "last_changed": "2001-12-31T12:00:00.123456+00:00" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_vendor_pks_recv.json b/nautobot_ssot/tests/device42/fixtures/get_vendor_pks_recv.json new file mode 100644 index 000000000..e6a1de64c --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_vendor_pks_recv.json @@ -0,0 +1,50 @@ +{ + "1": { + "vendor_pk": 1, + "name": "Cisco Systems Inc", + "account_no": "12345", + "home_page": "https://www.cisco.com/", + "contact_info": "", + "escalation_1": "", + "escalation_2": "", + "notes": "", + "tags": "", + "last_changed": "2001-12-31T12:00:00.123456+00:00" + }, + "2": { + "vendor_pk": 2, + "name": "Juniper", + "account_no": "AB123.5-67", + "home_page": "https://junipernetworks.com/", + "contact_info": "", + "escalation_1": "", + "escalation_2": "", + "notes": "", + "tags": "", + "last_changed": "2001-12-31T12:00:00.123456+00:00" + }, + "3": { + "vendor_pk": 3, + "name": "Palo Alto", + "account_no": "PA1234", + "home_page": "https://www.paloaltonetworks.com/", + "contact_info": "", + "escalation_1": "", + "escalation_2": "", + "notes": "", + "tags": "", + "last_changed": "2001-12-31T12:00:00.123456+00:00" + }, + "4": { + "vendor_pk": 4, + "name": "Arista", + "account_no": "", + "home_page": "", + "contact_info": "", + "escalation_1": "", + "escalation_2": "", + "notes": "", + "tags": "", + "last_changed": "2001-12-31T12:00:00.123456+00:00" + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_vendor_pks_sent.json b/nautobot_ssot/tests/device42/fixtures/get_vendor_pks_sent.json new file mode 100644 index 000000000..08ce8b260 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_vendor_pks_sent.json @@ -0,0 +1,50 @@ +[ + { + "vendor_pk": 1, + "name": "Cisco Systems Inc", + "account_no": "12345", + "home_page": "https://www.cisco.com/", + "contact_info": "", + "escalation_1": "", + "escalation_2": "", + "notes": "", + "tags": "", + "last_changed": "2001-12-31T12:00:00.123456+00:00" + }, + { + "vendor_pk": 2, + "name": "Juniper", + "account_no": "AB123.5-67", + "home_page": "https://junipernetworks.com/", + "contact_info": "", + "escalation_1": "", + "escalation_2": "", + "notes": "", + "tags": "", + "last_changed": "2001-12-31T12:00:00.123456+00:00" + }, + { + "vendor_pk": 3, + "name": "Palo Alto", + "account_no": "PA1234", + "home_page": "https://www.paloaltonetworks.com/", + "contact_info": "", + "escalation_1": "", + "escalation_2": "", + "notes": "", + "tags": "", + "last_changed": "2001-12-31T12:00:00.123456+00:00" + }, + { + "vendor_pk": 4, + "name": "Arista", + "account_no": "", + "home_page": "", + "contact_info": "", + "escalation_1": "", + "escalation_2": "", + "notes": "", + "tags": "", + "last_changed": "2001-12-31T12:00:00.123456+00:00" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_vendors_recv.json b/nautobot_ssot/tests/device42/fixtures/get_vendors_recv.json new file mode 100644 index 000000000..faad019c3 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_vendors_recv.json @@ -0,0 +1,41 @@ +[ + { + "escalation_1": "", + "name": "Cisco Systems Inc", + "contact_info": "", + "escalation_2": "", + "notes": "", + "vendor_id": 1, + "home_page": "", + "tags": [], + "account_no": "", + "custom_fields": [], + "aliases": "" + }, + { + "escalation_1": "", + "name": "Palo Alto", + "contact_info": "", + "escalation_2": "", + "notes": "", + "vendor_id": 2, + "home_page": "", + "tags": [], + "account_no": "", + "custom_fields": [], + "aliases": "" + }, + { + "escalation_1": "", + "name": "Juniper", + "contact_info": "", + "escalation_2": "", + "notes": "", + "vendor_id": 3, + "home_page": "", + "tags": [], + "account_no": "", + "custom_fields": [], + "aliases": "" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_vendors_sent.json b/nautobot_ssot/tests/device42/fixtures/get_vendors_sent.json new file mode 100644 index 000000000..79696792d --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_vendors_sent.json @@ -0,0 +1,46 @@ +{ + "total_count": 3, + "limit": 4, + "vendors": [ + { + "escalation_1": "", + "name": "Cisco Systems Inc", + "contact_info": "", + "escalation_2": "", + "notes": "", + "vendor_id": 1, + "home_page": "", + "tags": [], + "account_no": "", + "custom_fields": [], + "aliases": "" + }, + { + "escalation_1": "", + "name": "Palo Alto", + "contact_info": "", + "escalation_2": "", + "notes": "", + "vendor_id": 2, + "home_page": "", + "tags": [], + "account_no": "", + "custom_fields": [], + "aliases": "" + }, + { + "escalation_1": "", + "name": "Juniper", + "contact_info": "", + "escalation_2": "", + "notes": "", + "vendor_id": 3, + "home_page": "", + "tags": [], + "account_no": "", + "custom_fields": [], + "aliases": "" + } + ], + "offset": 0 +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_vlan_info_cfields.json b/nautobot_ssot/tests/device42/fixtures/get_vlan_info_cfields.json new file mode 100644 index 000000000..3c4e4bdc9 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_vlan_info_cfields.json @@ -0,0 +1,14 @@ +[ + { + "key": "Owner", + "value": "IT", + "notes": null, + "vlan_pk": 1 + }, + { + "key": "Purpose", + "value": "Servers", + "notes": null, + "vlan_pk": 2 + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_vlan_info_recv.json b/nautobot_ssot/tests/device42/fixtures/get_vlan_info_recv.json new file mode 100644 index 000000000..343ae1ff2 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_vlan_info_recv.json @@ -0,0 +1,28 @@ +{ + "1": { + "name": "Public", + "vid": 100, + "custom_fields": { + "Owner": { + "key": "Owner", + "value": "IT", + "notes": null + } + } + }, + "2": { + "name": "DMZ", + "vid": 200, + "custom_fields": { + "Purpose": { + "key": "Purpose", + "value": "Servers", + "notes": null + } + } + }, + "3": { + "name": "App", + "vid": 300 + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_vlan_info_vlaninfo.json b/nautobot_ssot/tests/device42/fixtures/get_vlan_info_vlaninfo.json new file mode 100644 index 000000000..907b4531c --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_vlan_info_vlaninfo.json @@ -0,0 +1,17 @@ +[ + { + "vlan_pk": 1, + "name": "Public", + "vid": 100 + }, + { + "vlan_pk": 2, + "name": "DMZ", + "vid": 200 + }, + { + "vlan_pk": 3, + "name": "App", + "vid": 300 + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_vlans_with_location.json b/nautobot_ssot/tests/device42/fixtures/get_vlans_with_location.json new file mode 100644 index 000000000..5b1352952 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_vlans_with_location.json @@ -0,0 +1,20 @@ +[ + { + "vlan_pk": 1, + "vid": 100, + "description": "Public", + "tags": "", + "vlan_name": "Public", + "building": "Dell - Round Rock 2", + "customer": "AUS" + }, + { + "vlan_pk": 2, + "vid": 200, + "description": "DMZ", + "tags": "", + "vlan_name": "DMZ", + "building": "Microsoft HQ", + "customer": "SEA" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_vrfgroups_recv.json b/nautobot_ssot/tests/device42/fixtures/get_vrfgroups_recv.json new file mode 100644 index 000000000..33fd9d58f --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_vrfgroups_recv.json @@ -0,0 +1,36 @@ +[ + { + "buildings": [], + "description": "", + "tags": [ + "tag-one", + "tag-two" + ], + "groups": "", + "id": 1, + "custom_fields": [], + "name": "Default" + }, + { + "buildings": [], + "description": "Production environment", + "tags": [ + "test" + ], + "groups": "", + "id": 2, + "custom_fields": [], + "name": "Production" + }, + { + "buildings": [], + "description": "Development environment", + "tags": [ + "dev" + ], + "groups": "", + "id": 2, + "custom_fields": [], + "name": "Development" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/get_vrfgroups_sent.json b/nautobot_ssot/tests/device42/fixtures/get_vrfgroups_sent.json new file mode 100644 index 000000000..0d6a6d238 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/get_vrfgroups_sent.json @@ -0,0 +1,41 @@ +{ + "total_count": 3, + "offset": 0, + "limit": 1000, + "vrfgroup": [ + { + "buildings": [], + "description": "", + "tags": [ + "tag-one", + "tag-two" + ], + "groups": "", + "id": 1, + "custom_fields": [], + "name": "Default" + }, + { + "buildings": [], + "description": "Production environment", + "tags": [ + "test" + ], + "groups": "", + "id": 2, + "custom_fields": [], + "name": "Production" + }, + { + "buildings": [], + "description": "Development environment", + "tags": [ + "dev" + ], + "groups": "", + "id": 2, + "custom_fields": [], + "name": "Development" + } + ] +} \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/merged_ports.json b/nautobot_ssot/tests/device42/fixtures/merged_ports.json new file mode 100644 index 000000000..df606e8af --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/merged_ports.json @@ -0,0 +1,36 @@ +[ + { + "netport_pk": 1, + "vlan_pks": [ + "1" + ], + "port_name": "FortyGigabitEthernet1/1/1", + "description": "Fo1/1/1", + "up": false, + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "012345678abc", + "port_type": "physical", + "port_speed": "40 Gbps", + "mtu": 1500, + "tags": "first-tag,second-tag", + "device_name": "core-router.testexample.com" + }, + { + "netport_pk": 2, + "vlan_pks": [ + "1" + ], + "port_name": "FortyGigabitEthernet1/1/2", + "description": "Fo1/1/2", + "up": false, + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "012345678abd", + "port_type": "physical", + "port_speed": "40 Gbps", + "mtu": 1500, + "tags": "", + "device_name": "core-router.testexample.com" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/ports_with_vlans.json b/nautobot_ssot/tests/device42/fixtures/ports_with_vlans.json new file mode 100644 index 000000000..df606e8af --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/ports_with_vlans.json @@ -0,0 +1,36 @@ +[ + { + "netport_pk": 1, + "vlan_pks": [ + "1" + ], + "port_name": "FortyGigabitEthernet1/1/1", + "description": "Fo1/1/1", + "up": false, + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "012345678abc", + "port_type": "physical", + "port_speed": "40 Gbps", + "mtu": 1500, + "tags": "first-tag,second-tag", + "device_name": "core-router.testexample.com" + }, + { + "netport_pk": 2, + "vlan_pks": [ + "1" + ], + "port_name": "FortyGigabitEthernet1/1/2", + "description": "Fo1/1/2", + "up": false, + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "012345678abd", + "port_type": "physical", + "port_speed": "40 Gbps", + "mtu": 1500, + "tags": "", + "device_name": "core-router.testexample.com" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/fixtures/ports_wo_vlans.json b/nautobot_ssot/tests/device42/fixtures/ports_wo_vlans.json new file mode 100644 index 000000000..6f4e9c596 --- /dev/null +++ b/nautobot_ssot/tests/device42/fixtures/ports_wo_vlans.json @@ -0,0 +1,28 @@ +[ + { + "netport_pk": 1, + "port_name": "FortyGigabitEthernet1/1/1", + "description": "Fo1/1/1", + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "012345678abc", + "port_type": "physical", + "port_speed": "40 Gbps", + "mtu": 1500, + "tags": "first-tag,second-tag", + "device_name": "core-router.testexample.com" + }, + { + "netport_pk": 2, + "port_name": "FortyGigabitEthernet1/1/2", + "description": "Fo1/1/2", + "up_admin": true, + "discovered_type": "ethernetCsmacd", + "hwaddress": "012345678abd", + "port_type": "physical", + "port_speed": "40 Gbps", + "mtu": 1500, + "tags": "", + "device_name": "core-router.testexample.com" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/device42/unit/__init__.py b/nautobot_ssot/tests/device42/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot_ssot/tests/device42/unit/test_device42_adapter.py b/nautobot_ssot/tests/device42/unit/test_device42_adapter.py new file mode 100644 index 000000000..4f259c959 --- /dev/null +++ b/nautobot_ssot/tests/device42/unit/test_device42_adapter.py @@ -0,0 +1,422 @@ +"""Unit tests for the Device42 DiffSync adapter class.""" +import json +import uuid +from unittest.mock import MagicMock, patch +from diffsync.exceptions import ObjectAlreadyExists, ObjectNotFound +from django.contrib.contenttypes.models import ContentType +from django.utils.text import slugify +from nautobot.utilities.testing import TransactionTestCase +from nautobot.extras.models import Job, JobResult +from parameterized import parameterized +from nautobot_ssot.integrations.device42.diffsync.adapters.device42 import ( + Device42Adapter, + get_dns_a_record, + get_circuit_status, + get_site_from_mapping, +) +from nautobot_ssot.integrations.device42.jobs import Device42DataSource + + +def load_json(path): + """Load a json file.""" + with open(path, encoding="utf-8") as file: + return json.loads(file.read()) + + +BUILDING_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_buildings_recv.json") +ROOM_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_rooms_recv.json") +RACK_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_racks_recv.json") +VENDOR_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_vendors_recv.json") +HARDWARE_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_hardware_models_recv.json") +VRFGROUP_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_vrfgroups_recv.json") +VLAN_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_vlans_with_location.json") +SUBNET_DEFAULT_CFS_FIXTURE = load_json( + "./nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_recv.json" +) +SUBNET_CFS_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_recv.json") +SUBNET_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_subnets.json") +DEVICE_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_devices_recv.json") +CLUSTER_MEMBER_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_cluster_members_recv.json") +PORTS_W_VLANS_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_recv.json") +PORTS_WO_VLANS_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_recv.json") +PORT_CUSTOM_FIELDS = load_json("./nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_recv.json") +IPADDRESS_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_ip_addrs.json") +IPADDRESS_CF_FIXTURE = load_json("./nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_recv.json") + + +class Device42AdapterTestCase(TransactionTestCase): # pylint: disable=too-many-public-methods + """Test the Device42Adapter class.""" + + databases = ("default", "job_logs") + + def setUp(self): + """Method to initialize test case.""" + # Create a mock client + self.d42_client = MagicMock() + self.d42_client.get_buildings.return_value = BUILDING_FIXTURE + self.d42_client.get_rooms.return_value = ROOM_FIXTURE + self.d42_client.get_racks.return_value = RACK_FIXTURE + self.d42_client.get_vendors.return_value = VENDOR_FIXTURE + self.d42_client.get_hardware_models.return_value = HARDWARE_FIXTURE + self.d42_client.get_vrfgroups.return_value = VRFGROUP_FIXTURE + self.d42_client.get_vlans_with_location.return_value = VLAN_FIXTURE + self.d42_client.get_subnet_default_custom_fields.return_value = SUBNET_DEFAULT_CFS_FIXTURE + self.d42_client.get_subnet_custom_fields.return_value = SUBNET_CFS_FIXTURE + self.d42_client.get_subnets.return_value = SUBNET_FIXTURE + self.d42_client.get_devices.return_value = DEVICE_FIXTURE + self.d42_client.get_cluster_members.return_value = CLUSTER_MEMBER_FIXTURE + self.d42_client.get_ports_with_vlans.return_value = PORTS_W_VLANS_FIXTURE + self.d42_client.get_ports_wo_vlans.return_value = PORTS_WO_VLANS_FIXTURE + self.d42_client.get_port_custom_fields.return_value = PORT_CUSTOM_FIELDS + self.d42_client.get_ip_addrs.return_value = IPADDRESS_FIXTURE + self.d42_client.get_ipaddr_custom_fields.return_value = IPADDRESS_CF_FIXTURE + + self.job = Device42DataSource() + self.job.log_info = MagicMock() + self.job.log_warning = MagicMock() + self.job.kwargs["debug"] = True + self.job.job_result = JobResult.objects.create( + name=self.job.class_path, obj_type=ContentType.objects.get_for_model(Job), user=None, job_id=uuid.uuid4() + ) + self.device42 = Device42Adapter(job=self.job, sync=None, client=self.d42_client) + self.mock_device = MagicMock() + self.mock_device.name = "cluster1 - Switch 1" + self.mock_device.os_version = "1.0" + self.cluster_dev = MagicMock() + self.cluster_dev.name = "cluster1" + self.master_dev = MagicMock() + self.master_dev.name = "cluster1" + self.master_dev.os_version = "" + + @patch( + "nautobot_ssot.integrations.device42.diffsync.adapters.device42.PLUGIN_CFG", + {"customer_is_facility": True}, + ) + def test_data_loading(self): + """Test the load() function.""" + self.device42.load_buildings() + self.assertEqual( + {site["name"] for site in BUILDING_FIXTURE}, + {site.get_unique_id() for site in self.device42.get_all("building")}, + ) + self.device42.load_rooms() + self.assertEqual( + {f"{room['name']}__{room['building']}" for room in ROOM_FIXTURE}, + {room.get_unique_id() for room in self.device42.get_all("room")}, + ) + self.device42.load_racks() + self.assertEqual( + {f"{rack['name']}__{rack['building']}__{rack['room']}" for rack in RACK_FIXTURE}, + {rack.get_unique_id() for rack in self.device42.get_all("rack")}, + ) + self.device42.load_vendors() + self.assertEqual( + {vendor["name"] for vendor in VENDOR_FIXTURE}, + {vendor.get_unique_id() for vendor in self.device42.get_all("vendor")}, + ) + self.device42.load_hardware_models() + self.assertEqual( + {model["name"] for model in HARDWARE_FIXTURE}, + {model.get_unique_id() for model in self.device42.get_all("hardware")}, + ) + self.device42.load_vrfgroups() + self.assertEqual( + {vrf["name"] for vrf in VRFGROUP_FIXTURE}, + {vrf.get_unique_id() for vrf in self.device42.get_all("vrf")}, + ) + self.device42.load_vlans() + self.assertEqual( + { + f"{vlan['vid']}__{slugify(self.device42.d42_building_sitecode_map[vlan['customer']])}" + for vlan in VLAN_FIXTURE + }, + {vlan.get_unique_id() for vlan in self.device42.get_all("vlan")}, + ) + self.device42.load_subnets() + self.assertEqual( + {f"{net['network']}__{net['mask_bits']}__{net['vrf']}" for net in SUBNET_FIXTURE}, + {net.get_unique_id() for net in self.device42.get_all("subnet")}, + ) + self.device42.load_devices_and_clusters() + self.assertEqual( + {dev["name"] for dev in DEVICE_FIXTURE}, + {dev.get_unique_id() for dev in self.device42.get_all("device")}, + ) + self.device42.load_ports() + self.assertEqual( + {f"{port['device_name']}__{port['port_name']}" for port in PORTS_WO_VLANS_FIXTURE}, + {port.get_unique_id() for port in self.device42.get_all("port")}, + ) + self.device42.load_ip_addresses() + self.assertEqual( + {f"{ipaddr['ip_address']}/{ipaddr['netmask']}__{ipaddr['vrf']}" for ipaddr in IPADDRESS_FIXTURE}, + {ipaddr.get_unique_id() for ipaddr in self.device42.get_all("ipaddr")}, + ) + + def test_load_buildings_duplicate_site(self): + """Validate functionality of the load_buildings() function when duplicate site is loaded.""" + self.device42.load_buildings() + self.device42.load_buildings() + self.job.log_warning.assert_called_with( + message="Microsoft HQ is already loaded. ('Object Microsoft HQ already present', building \"Microsoft HQ\")" + ) + + def test_load_rooms_duplicate_room(self): + """Validate functionality of the load_rooms() function when duplicate room is loaded.""" + self.device42.load_buildings() + self.device42.load_rooms() + self.device42.load_rooms() + self.job.log_warning.assert_called_with( + message="Secondary IDF is already loaded. ('Object Secondary IDF__Microsoft HQ already present', room \"Secondary IDF__Microsoft HQ\")" + ) + + def test_load_rooms_missing_building(self): + """Validate functionality of the load_rooms() function when room loaded with missing building.""" + ROOM_FIXTURE[0]["building"] = "" + self.device42.load_buildings() + self.device42.load_rooms() + self.job.log_warning.assert_called_with(message="Network Closet is missing Building and won't be imported.") + + def test_load_racks_duplicate_rack(self): + """Validate the functionality of the load_racks() function when duplicate rack is loaded.""" + self.device42.load_buildings() + self.device42.load_rooms() + self.device42.load_racks() + self.device42.load_racks() + self.job.log_warning.assert_called_with( + message="Rack Rack A already exists. ('Object Rack A__Microsoft HQ__Main IDF already present', rack \"Rack A__Microsoft HQ__Main IDF\")" + ) + + def test_load_racks_missing_building_and_room(self): + """Validate functionality of the load_racks() function when rack loaded with missing building and room.""" + RACK_FIXTURE[0]["building"] = "" + RACK_FIXTURE[0]["room"] = "" + self.device42.load_buildings() + self.device42.load_rooms() + self.device42.load_racks() + self.job.log_warning.assert_called_with(message="Rack 1 is missing Building and Room and won't be imported.") + + def test_load_hardware_models_duplicate_model(self): + """Validate functionality of the load_hardware_models() function with duplicate model.""" + self.device42.load_hardware_models() + self.device42.load_hardware_models() + self.job.log_warning.assert_called_with( + message="Hardware model already exists. ('Object FPR-2130 already present', hardware \"FPR-2130\")" + ) + + def test_load_cluster_duplicate_cluster(self): + """Validate functionality of the load_cluster() function when cluster loaded with duplicate cluster.""" + self.device42.get = MagicMock() + self.device42.get.side_effect = ObjectAlreadyExists(message="Duplicate object found.", existing_object=None) + self.device42.load_cluster(cluster_info=DEVICE_FIXTURE[3]) + self.job.log_warning.assert_called_with( + message="Cluster stack01.testexample.com already has been added. ('Duplicate object found.', None)" + ) + + @patch( + "nautobot_ssot.integrations.device42.diffsync.adapters.device42.PLUGIN_CFG", + {"ignore_tag": "TEST"}, + ) + def test_load_cluster_ignore_tag(self): + """Validate functionality of the load_cluster() function when cluster has ignore tag.""" + self.device42.load_cluster(cluster_info=DEVICE_FIXTURE[3]) + self.job.log_info.assert_called_once_with(message="Cluster stack01.testexample.com being loaded from Device42.") + self.job.log_warning.assert_called_once_with( + message="Cluster stack01.testexample.com has ignore tag so skipping." + ) + + def test_load_devices_with_blank_building(self): + """Validate functionality of the load_devices_and_clusters() function when device has a blank building.""" + self.device42.load_hardware_models() + self.device42.load_devices_and_clusters() + self.job.log_warning.assert_called_with( + message="Device stack01.testexample.com can't be loaded as we're unable to find associated Building." + ) + + def test_assign_version_to_master_devices_with_valid_os_version(self): + """Validate functionality of the assign_version_to_master_devices() function with valid os_version.""" + self.device42.device42_clusters = {"cluster1": {"members": [self.mock_device]}} + self.device42.get_all = MagicMock() + self.device42.get_all.return_value = [self.cluster_dev] + + self.device42.get = MagicMock() + self.device42.get.side_effect = [self.mock_device, self.master_dev] + + self.device42.assign_version_to_master_devices() + + self.assertEqual(self.master_dev.os_version, "1.0") + self.job.log_info.assert_called_once_with(message="Assigning 1.0 version to cluster1.") + + def test_assign_version_to_master_devices_with_blank_os_version(self): + """Validate functionality of the assign_version_to_master_devices() function with blank os_version.""" + self.mock_device.os_version = "" + self.device42.device42_clusters = {"cluster1": {"members": [self.mock_device]}} + + self.device42.get_all = MagicMock() + self.device42.get_all.return_value = [self.cluster_dev] + + self.device42.get = MagicMock() + self.device42.get.side_effect = [self.mock_device, self.master_dev] + + self.device42.assign_version_to_master_devices() + + self.assertEqual(self.master_dev.os_version, "") + self.job.log_info.assert_called_once_with( + message="Software version for cluster1 - Switch 1 is blank so will not assign version to cluster1." + ) + + def test_assign_version_to_master_devices_with_missing_cluster_host(self): + """Validate functionality of the assign_version_to_master_devices() function with missing cluster host in device42_clusters.""" + self.device42.get_all = MagicMock() + self.device42.get_all.return_value = [self.cluster_dev] + + self.device42.get = MagicMock() + self.device42.get.return_value = KeyError + + self.device42.assign_version_to_master_devices() + self.job.log_warning.assert_called_once_with( + message="Unable to find cluster host in device42_clusters dictionary. 'cluster1'" + ) + + def test_assign_version_to_master_devices_with_missing_master_device(self): + """Validate functionality of the assign_version_to_master_devices() function with missing master device.""" + self.device42.device42_clusters = {"cluster1": {"members": [self.mock_device]}} + self.device42.get_all = MagicMock() + self.device42.get_all.return_value = [self.cluster_dev] + + self.device42.get = MagicMock() + self.device42.get.side_effect = [self.mock_device, ObjectNotFound] + + self.device42.assign_version_to_master_devices() + self.job.log_warning.assert_called_once_with( + message="Unable to find VC Master Device cluster1 to assign version." + ) + + statuses = [ + ("Production", "Production", "Active"), + ("Provisioning", "Provisioning", "Provisioning"), + ("Canceled", "Canceled", "Deprovisioning"), + ("Decommissioned", "Decommissioned", "Decommissioned"), + ("Ordered", "Ordered", "Offline"), + ] + + @parameterized.expand(statuses, skip_on_empty=True) + def test_get_circuit_status(self, name, sent, received): # pylint: disable=unused-argument + """Test get_circuit_status success.""" + self.assertEqual(get_circuit_status(sent), received) + + @patch( + "nautobot_ssot.integrations.device42.diffsync.adapters.device42.PLUGIN_CFG", + {"hostname_mapping": [{"^aus.+|AUS.+": "austin"}]}, + ) + def test_get_site_from_mapping(self): + """Test the get_site_from_mapping method.""" + expected = "austin" + self.assertEqual(get_site_from_mapping(device_name="aus.test.com"), expected) + + @patch( + "nautobot_ssot.integrations.device42.diffsync.adapters.device42.PLUGIN_CFG", + {"hostname_mapping": [{"^aus.+|AUS.+": "austin"}]}, + ) + def test_get_site_from_mapping_missing_site(self): + """Test the get_site_from_mapping method with missing site.""" + expected = "" + self.assertEqual(get_site_from_mapping(device_name="dfw.test.com"), expected) + + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.is_fqdn_resolvable", return_value=True) + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.fqdn_to_ip", return_value="192.168.0.1") + def test_get_dns_a_record_success(self, mock_fqdn_to_ip, mock_is_fqdn_resolvable): + """Test the get_dns_a_record method success.""" + result = get_dns_a_record("example.com") + mock_is_fqdn_resolvable.assert_called_once_with("example.com") + mock_fqdn_to_ip.assert_called_once_with("example.com") + self.assertEqual(result, "192.168.0.1") + + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.is_fqdn_resolvable", return_value=False) + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.fqdn_to_ip") + def test_get_dns_a_record_failure(self, mock_fqdn_to_ip, mock_is_fqdn_resolvable): + """Test the get_dns_a_record method failure.""" + result = get_dns_a_record("invalid-hostname") + mock_is_fqdn_resolvable.assert_called_once_with("invalid-hostname") + mock_fqdn_to_ip.assert_not_called() + self.assertFalse(result) + + @patch( + "nautobot_ssot.integrations.device42.diffsync.adapters.device42.PLUGIN_CFG", + {"hostname_mapping": [{"^nyc.+|NYC.+": "new-york-city"}]}, + ) + def test_get_building_for_device_from_mapping(self): + """Test the get_building_for_device method using site_mapping.""" + mock_dev_record = {"name": "nyc.test.com"} + expected = "new-york-city" + self.assertEqual(self.device42.get_building_for_device(dev_record=mock_dev_record), expected) + + def test_get_building_for_device_from_device_record(self): + """Test the get_building_for_device method from device record.""" + mock_dev_record = {"name": "la.test.com", "building": "los-angeles"} + expected = "los-angeles" + self.assertEqual(self.device42.get_building_for_device(dev_record=mock_dev_record), expected) + + def test_get_building_for_device_missing_building(self): + """Test the get_building_for_device method with missing building.""" + mock_dev_record = {"name": "la.test.com", "building": None} + expected = "" + self.assertEqual(self.device42.get_building_for_device(dev_record=mock_dev_record), expected) + + def test_filter_ports(self): + """Method to test filter_ports success.""" + vlan_ports = load_json("./nautobot_ssot/tests/device42/fixtures/ports_with_vlans.json") + no_vlan_ports = load_json("./nautobot_ssot/tests/device42/fixtures/ports_wo_vlans.json") + merged_ports = load_json("./nautobot_ssot/tests/device42/fixtures/merged_ports.json") + result = self.device42.filter_ports(vlan_ports, no_vlan_ports) + self.assertEqual(merged_ports, result) + + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.get_dns_a_record") + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.Device42Adapter.find_ipaddr") + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.Device42Adapter.get_management_intf") + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.Device42Adapter.add_management_interface") + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.Device42Adapter.add_ipaddr") + def test_set_primary_from_dns_with_valid_fqdn( # pylint: disable=too-many-arguments + self, mock_add_ipaddr, mock_add_mgmt_intf, mock_get_mgmt_intf, mock_find_ipaddr, mock_dns_a_record + ): + """Method to test the set_primary_from_dns functionality with valid FQDN.""" + mock_dns_a_record.return_value = "10.0.0.1" + mock_find_ipaddr.return_value = False + mock_mgmt_interface = MagicMock(name="mgmt_intf") + mock_mgmt_interface.name = "eth0" + mock_get_mgmt_intf.return_value = mock_mgmt_interface + mock_add_mgmt_intf.return_value = mock_mgmt_interface + mock_ip = MagicMock() + mock_add_ipaddr.return_value = mock_ip + dev_name = "router.test-example.com" + self.device42.set_primary_from_dns(dev_name) + + mock_dns_a_record.assert_called_once_with(dev_name=dev_name) + mock_find_ipaddr.assert_called_once_with(address="10.0.0.1") + mock_get_mgmt_intf.assert_called_once_with(dev_name=dev_name) + mock_add_mgmt_intf.assert_not_called() + mock_add_ipaddr.assert_called_once_with(address="10.0.0.1/32", dev_name=dev_name, interface="eth0") + self.assertEqual(mock_ip.device, "router.test-example.com") + self.assertEqual(mock_ip.interface, "eth0") + self.assertTrue(mock_ip.primary) + + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.get_dns_a_record") + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.Device42Adapter.find_ipaddr") + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.Device42Adapter.get_management_intf") + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.Device42Adapter.add_management_interface") + @patch("nautobot_ssot.integrations.device42.diffsync.adapters.device42.Device42Adapter.add_ipaddr") + def test_set_primary_from_dns_with_invalid_fqdn( # pylint: disable=too-many-arguments + self, mock_add_ipaddr, mock_add_mgmt_intf, mock_get_mgmt_intf, mock_find_ipaddr, mock_dns_a_record + ): + """Method to test the set_primary_from_dns functionality with invalid FQDN.""" + mock_dns_a_record.return_value = "" + dev_name = "router.test-example.com" + self.job.log_warning = MagicMock() + self.device42.set_primary_from_dns(dev_name=dev_name) + + mock_dns_a_record.assert_called_once_with(dev_name=dev_name) + mock_find_ipaddr.assert_not_called() + mock_get_mgmt_intf.assert_not_called() + mock_add_mgmt_intf.assert_not_called() + mock_add_ipaddr.assert_not_called() + self.job.log_warning.assert_called_once() diff --git a/nautobot_ssot/tests/device42/unit/test_utils_device42.py b/nautobot_ssot/tests/device42/unit/test_utils_device42.py new file mode 100644 index 000000000..a83f5a2c4 --- /dev/null +++ b/nautobot_ssot/tests/device42/unit/test_utils_device42.py @@ -0,0 +1,772 @@ +"""Tests of Device42 utility methods.""" + +import json +from unittest.mock import MagicMock, patch + +import responses +from django.conf import settings +from nautobot.utilities.testing import TestCase +from parameterized import parameterized +from nautobot_ssot.integrations.device42.jobs import Device42DataSource +from nautobot_ssot.integrations.device42.utils import device42 + + +def load_json(path): + """Load a json file.""" + with open(path, encoding="utf-8") as file: + return json.loads(file.read()) + + +class TestMissingConfigSetting(TestCase): + """Test MissingConfigSetting Exception.""" + + def setUp(self): + """Setup MissingConfigSetting instance.""" + self.setting = "D42_URL" + self.missing_setting = device42.MissingConfigSetting(setting=self.setting) + + def test_missingconfigsetting(self): + self.assertTrue(self.missing_setting.setting == "D42_URL") + self.assertLogs("Missing configuration setting - D42_URL!") + + +class TestUtilsDevice42(TestCase): + """Test Device42 util methods.""" + + databases = ("default", "job_logs") + + def test_merge_offset_dicts(self): + first_dict = {"total_count": 10, "limit": 2, "offset": 2, "Objects": ["a", "b"]} + second_dict = {"total_count": 10, "limit": 2, "offset": 4, "Objects": ["c", "d"]} + result_dict = {"total_count": 10, "limit": 2, "offset": 4, "Objects": ["a", "b", "c", "d"]} + self.assertEqual(device42.merge_offset_dicts(orig_dict=first_dict, offset_dict=second_dict), result_dict) + + def test_get_intf_type_eth_intf(self): + # test physical Ethernet interfaces + eth_intf = { + "port_name": "GigabitEthernet0/1", + "port_type": "physical", + "discovered_type": "ethernetCsmacd", + "port_speed": "1.0 Gbps", + } + self.assertEqual(device42.get_intf_type(intf_record=eth_intf), "1000base-t") + + def test_get_intf_type_fc_intf(self): + # test physical FiberChannel interfaces + fc_intf = { + "port_name": "FC0/1", + "port_type": "physical", + "discovered_type": "fibreChannel", + "port_speed": "1.0 Gbps", + "device_name": "core-router.testexample.com", + } + self.assertEqual(device42.get_intf_type(intf_record=fc_intf), "1gfc-sfp") + + def test_get_intf_type_unknown_phy_intf(self): + # test physical interfaces that don't have a discovered_type of Ethernet or FiberChannel + unknown_phy_intf_speed = { + "port_name": "Ethernet0/1", + "port_type": "physical", + "discovered_type": "Unknown", + "port_speed": "1.0 Gbps", + } + self.assertEqual(device42.get_intf_type(intf_record=unknown_phy_intf_speed), "1000base-t") + + @patch.object(Device42DataSource, "debug", True) + def test_get_intf_name_mapping(self): + # test name of Interface matching INTF_NAME_MAP + ethernet_interface = { + "port_name": "FastEthernet1/1", + "port_type": "physical", + "discovered_type": "ethernetCsmacd", + "port_speed": "10 Mbps", + } + self.assertEqual(device42.get_intf_type(intf_record=ethernet_interface), "100base-tx") + + def test_get_intf_type_gigabit_ethernet_intf(self): + # test physical interface that's discovered as gigabitEthernet + gigabit_ethernet_intf = { + "port_name": "Vethernet100", + "port_type": "physical", + "discovered_type": "gigabitEthernet", + "port_speed": "0", + } + self.assertEqual(device42.get_intf_type(intf_record=gigabit_ethernet_intf), "1000base-t") + + def test_get_intf_type_dot11_intf(self): + # test physical interface discoverd as dot11a/b + dot11_intf = { + "port_name": "01:23:45:67:89:AB.0", + "port_type": "physical", + "discovered_type": "dot11b", + "port_speed": None, + } + self.assertEqual(device42.get_intf_type(intf_record=dot11_intf), "ieee802.11a") + + def test_get_intf_type_ad_lag_intf(self): + # test 802.3ad lag logical interface + ad_lag_intf = { + "port_name": "port-channel100", + "port_type": "logical", + "discovered_type": "ieee8023adLag", + "port_speed": "100 Mbps", + "device_name": "core-router.testexample.com", + } + self.assertEqual(device42.get_intf_type(intf_record=ad_lag_intf), "lag") + + def test_get_intf_type_lacp_intf(self): + # test lacp logical interface + lacp_intf = { + "port_name": "Internal_Trunk", + "port_type": "logical", + "discovered_type": "lacp", + "port_speed": "40 Gbps", + "device_name": "core-router.testexample.com", + } + self.assertEqual(device42.get_intf_type(intf_record=lacp_intf), "lag") + + def test_get_intf_type_virtual_intf(self): + # test "virtual" logical interface + virtual_intf = { + "port_name": "Vlan100", + "port_type": "logical", + "discovered_type": "propVirtual", + "port_speed": "1.0 Gbps", + "device_name": "distro-switch.testexample.com", + } + self.assertEqual(device42.get_intf_type(intf_record=virtual_intf), "virtual") + + def test_get_intf_type_port_channel_intf(self): + # test Port-Channel logical interface + port_channel_intf = { + "port_name": "port-channel100", + "port_type": "logical", + "discovered_type": "propVirtual", + "port_speed": "20 Gbps", + "device_name": "distro-switch.testexample.com", + } + self.assertEqual(device42.get_intf_type(intf_record=port_channel_intf), "lag") + + port_statuses = [ + ("active", {"up": True, "up_admin": True}, "active"), + ("decommissioning", {"up": False, "up_admin": False}, "decommissioning"), + ("failed", {"up": False, "up_admin": True}, "failed"), + ("planned", {}, "planned"), + ("up_admin", {"up_admin": True}, "active"), + ] + + @parameterized.expand(port_statuses, skip_on_empty=True) + def test_get_intf_status(self, name, sent, received): # pylint: disable=unused-argument + self.assertEqual(device42.get_intf_status(sent), received) + + netmiko_platforms = [ + ("asa", "asa", "cisco_asa"), + ("ios", "ios", "cisco_ios"), + ("iosxe", "iosxe", "cisco_ios"), + ("iosxr", "iosxr", "cisco_xr"), + ("nxos", "nxos", "cisco_nxos"), + ("bigip", "f5", "f5_tmsh"), + ("junos", "junos", "juniper_junos"), + ("dell", "dell", "dell"), + ] + + @parameterized.expand(netmiko_platforms, skip_on_empty=True) + def test_get_netmiko_platform(self, name, sent, received): # pylint: disable=unused-argument + self.assertEqual(device42.get_netmiko_platform(sent), received) + + def test_find_device_role_from_tags(self): + tags_w_role = [ + "core-router", + "nautobot-core-router", + ] + self.assertEqual(device42.find_device_role_from_tags(tag_list=tags_w_role), "core-router") + tags_missing_role = [ + "802.1x", + ] + self.assertEqual(device42.find_device_role_from_tags(tag_list=tags_missing_role), "Unknown") + + def test_get_facility(self): + tags = ["core-router", "nautobot-core-router", "sitecode-DFW"] + self.assertEqual(device42.get_facility(tags=tags), "DFW") + + def test_get_facility_exception(self): + """Test that get_facility throws Exception if setting is missing.""" + tags = ["core-router", "nautobot-core-router", "sitecode-DFW"] + configs = settings.PLUGINS_CONFIG.get("nautobot_ssot", {}) + original = configs["device42_facility_prepend"] + configs.pop("device42_facility_prepend") + dsync = MagicMock() + dsync.log_failure = MagicMock() + with self.assertRaises(device42.MissingConfigSetting): + device42.get_facility(tags=tags, diffsync=dsync) + dsync.log_failure.assert_called_once_with(message="The `facility_prepend` setting is missing or invalid.") + # restore setting to what it was before + configs["device42_facility_prepend"] = original + + def test_get_custom_field_dict(self): + """Test the get_custom_field_dict method.""" + expected = { + "Test": { + "key": "Test", + "value": None, + "notes": None, + } + } + mock_custom_fields = [{"key": "Test", "value": None, "notes": None}] + actual = device42.get_custom_field_dict(cfields=mock_custom_fields) + self.assertEqual(actual, expected) + + +class TestDevice42Api(TestCase): # pylint: disable=too-many-public-methods + """Test Base Device42 API Client and Calls.""" + + databases = ("default", "job_logs") + + def setUp(self): + """Setup Device42API instance.""" + self.uri = "https://device42.testexample.com" + self.username = "testuser" + self.password = "testpassword" # nosec B105 + self.verify = False + self.dev42 = device42.Device42API(self.uri, self.username, self.password, self.verify) + + def test_validate_url(self): + """Test validate_url success.""" + validate_url = self.dev42.validate_url("api_endpoint") + self.assertEqual(validate_url, "https://device42.testexample.com/api_endpoint") + + def test_validate_url_missing_extra_slash(self): + """Test validate_url success with missing '/'.""" + # Instantiate a new object, to test additional logic for missing'/': + self.uri = "https://device42.testexample.com" + self.dev42 = device42.Device42API(self.uri, self.username, self.password, self.verify) + validate_url = self.dev42.validate_url("api_endpoint") + self.assertEqual(validate_url, "https://device42.testexample.com/api_endpoint") + + def test_validate_url_path_has_slash(self): + """Test validate_url success when path has '/'.""" + # Instantiate a new object, to test additional logic for missing'/': + self.uri = "https://device42.testexample.com" + self.dev42 = device42.Device42API(self.uri, self.username, self.password, self.verify) + validate_url = self.dev42.validate_url("/api_endpoint") + self.assertEqual(validate_url, "https://device42.testexample.com/api_endpoint") + + def test_validate_url_verify_true(self): + """Test validate_url success with verify true.""" + # Instantiate a new object, to test additional logic for verify True + self.dev42 = device42.Device42API(self.uri, self.username, self.password, verify=True) + validate_url = self.dev42.validate_url("api_endpoint") + self.assertEqual(validate_url, "https://device42.testexample.com/api_endpoint") + + @responses.activate + def test_get_buildings(self): + """Test get_buildings success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_buildings.json") + responses.add( + responses.GET, + "https://device42.testexample.com/api/1.0/buildings", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_buildings_recv.json") + response = self.dev42.get_buildings() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_building_pks(self): + """Test get_building_pks success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_building_pks_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT * FROM view_building_v1&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + with open("./nautobot_ssot/tests/device42/fixtures/get_building_pks_recv.json", "r", encoding="utf-8") as file: + json_data = file.read() + expected = json.loads(json_data, object_hook=lambda d: {int(k) if k.isdigit() else k: v for k, v in d.items()}) + response = self.dev42.get_building_pks() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_rooms(self): + """Test get_rooms success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_rooms.json") + responses.add( + responses.GET, + "https://device42.testexample.com/api/1.0/rooms", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_rooms_recv.json") + response = self.dev42.get_rooms() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_room_pks(self): + """Test get_room_pks success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_room_pks_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT * FROM view_room_v1&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + with open("./nautobot_ssot/tests/device42/fixtures/get_room_pks_recv.json", "r", encoding="utf-8") as file: + json_data = file.read() + expected = json.loads(json_data, object_hook=lambda d: {int(k) if k.isdigit() else k: v for k, v in d.items()}) + response = self.dev42.get_room_pks() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_racks(self): + """Test get_racks success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_racks.json") + responses.add( + responses.GET, + "https://device42.testexample.com/api/1.0/racks", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_racks_recv.json") + response = self.dev42.get_racks() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_rack_pks(self): + """Test get_room_pks success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_rack_pks_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT * FROM view_rack_v1&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + with open("./nautobot_ssot/tests/device42/fixtures/get_rack_pks_recv.json", "r", encoding="utf-8") as file: + json_data = file.read() + expected = json.loads(json_data, object_hook=lambda d: {int(k) if k.isdigit() else k: v for k, v in d.items()}) + response = self.dev42.get_rack_pks() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_vendors(self): + """Test get_vendors success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_vendors_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/api/1.0/vendors", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_vendors_recv.json") + response = self.dev42.get_vendors() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_hardware_models(self): + """Test get_hardware_models success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_hardware_models_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/api/1.0/hardwares", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_hardware_models_recv.json") + response = self.dev42.get_hardware_models() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_devices(self): + """Test get_devices success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_devices_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/api/1.0/devices/all/?is_it_switch=yes&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_devices_recv.json") + response = self.dev42.get_devices() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_cluster_members(self): + """Test get_cluster_members success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_cluster_members_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT+m.name+as+cluster%2C+string_agg%28d.name%2C+%27%253B+%27%29+as+members%2C+h.name+as+hardware%2C+d.network_device%2C+d.os_name+as+os%2C+b.name+as+customer%2C+d.tags+FROM+view_device_v1+m+JOIN+view_devices_in_cluster_v1+c+ON+c.parent_device_fk+%3D+m.device_pk+JOIN+view_device_v1+d+ON+d.device_pk+%3D+c.child_device_fk+JOIN+view_hardware_v1+h+ON+h.hardware_pk+%3D+d.hardware_fk+JOIN+view_customer_v1+b+ON+b.customer_pk+%3D+d.customer_fk+WHERE+m.type+like+%27%25cluster%25%27+GROUP+BY+m.name%2C+h.name%2C+d.network_device%2C+d.os_name%2C+b.name%2C+d.tags&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_cluster_members_recv.json") + response = self.dev42.get_cluster_members() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + self.assertTrue( + responses.calls[0].request.url + == "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT+m.name+as+cluster%2C+string_agg%28d.name%2C+%27%253B+%27%29+as+members%2C+h.name+as+hardware%2C+d.network_device%2C+d.os_name+as+os%2C+b.name+as+customer%2C+d.tags+FROM+view_device_v1+m+JOIN+view_devices_in_cluster_v1+c+ON+c.parent_device_fk+%3D+m.device_pk+JOIN+view_device_v1+d+ON+d.device_pk+%3D+c.child_device_fk+JOIN+view_hardware_v1+h+ON+h.hardware_pk+%3D+d.hardware_fk+JOIN+view_customer_v1+b+ON+b.customer_pk+%3D+d.customer_fk+WHERE+m.type+like+%27%25cluster%25%27+GROUP+BY+m.name%2C+h.name%2C+d.network_device%2C+d.os_name%2C+b.name%2C+d.tags&output_type=json&_paging=1&_return_as_object=1&_max_results=1000" + ) + + @responses.activate + def test_get_ports_with_vlans(self): + """Test get_ports_with_vlans success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT array_agg( distinct concat (v.vlan_pk)) AS vlan_pks, n.netport_pk, n.port AS port_name, n.description, n.up, n.up_admin, n.discovered_type, n.hwaddress, n.port_type, n.port_speed, n.mtu, n.tags, n.second_device_fk, d.name AS device_name FROM view_vlan_v1 v LEFT JOIN view_vlan_on_netport_v1 vn ON vn.vlan_fk = v.vlan_pk LEFT JOIN view_netport_v1 n ON n.netport_pk = vn.netport_fk LEFT JOIN view_device_v1 d ON d.device_pk = n.device_fk WHERE n.port is not null GROUP BY n.netport_pk, n.port, n.description, n.up, n.up_admin, n.discovered_type, n.hwaddress, n.port_type, n.port_speed, n.mtu, n.tags, n.second_device_fk, d.name&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_ports_with_vlans_recv.json") + response = self.dev42.get_ports_with_vlans() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_ports_wo_vlans(self): + """Test get_ports_wo_vlans success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT m.netport_pk, m.port as port_name, m.description, m.up_admin, m.discovered_type, m.hwaddress, m.port_type, m.port_speed, m.mtu, m.tags, m.second_device_fk, d.name as device_name FROM view_netport_v1 m JOIN view_device_v1 d on d.device_pk = m.device_fk WHERE m.port is not null GROUP BY m.netport_pk, m.port, m.description, m.up_admin, m.discovered_type, m.hwaddress, m.port_type, m.port_speed, m.mtu, m.tags, m.second_device_fk, d.name&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_ports_wo_vlans_recv.json") + response = self.dev42.get_ports_wo_vlans() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_port_default_custom_fields(self): + """Test get_port_default_custom_fields success.""" + test_query = [ + {"key": "Software Version", "value": "10R.2D.2", "notes": None}, + {"key": "EOL Date", "value": "12/31/2999", "notes": None}, + ] + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT cf.key, cf.value, cf.notes FROM view_netport_custom_fields_v1 cf&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = { + "EOL Date": {"key": "EOL Date", "value": None, "notes": None}, + "Software Version": {"key": "Software Version", "value": None, "notes": None}, + } + response = self.dev42.get_port_default_custom_fields() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_port_custom_fields(self): + """Test get_port_custom_fields success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT cf.key, cf.value, cf.notes, np.port as port_name, d.name as device_name FROM view_netport_custom_fields_v1 cf LEFT JOIN view_netport_v1 np ON np.netport_pk = cf.netport_fk LEFT JOIN view_device_v1 d ON d.device_pk = np.device_fk&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_port_custom_fields_recv.json") + response = self.dev42.get_port_custom_fields() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_vrfgroups(self): + """Test get_vrfgroups success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_vrfgroups_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/api/1.0/vrfgroup/?_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_vrfgroups_recv.json") + response = self.dev42.get_vrfgroups() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_subnets(self): + """Test get_subnets success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_subnets.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT s.name, s.network, s.mask_bits, s.tags, v.name as vrf FROM view_subnet_v1 s JOIN view_vrfgroup_v1 v ON s.vrfgroup_fk = v.vrfgroup_pk&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_subnets.json") + response = self.dev42.get_subnets() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_subnet_default_custom_fields(self): + """Test get_subnet_default_custom_fields success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT cf.key, cf.value, cf.notes FROM view_subnet_custom_fields_v1 cf&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_subnet_default_custom_fields_recv.json") + response = self.dev42.get_subnet_default_custom_fields() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_subnet_custom_fields(self): + """Test get_subnet_custom_fields success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT cf.key, cf.value, cf.notes, s.name AS subnet_name, s.network, s.mask_bits FROM view_subnet_custom_fields_v1 cf LEFT JOIN view_subnet_v1 s ON s.subnet_pk = cf.subnet_fk&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT cf.key, cf.value, cf.notes FROM view_subnet_custom_fields_v1 cf&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_subnet_custom_fields_recv.json") + response = self.dev42.get_subnet_custom_fields() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 2) + + @responses.activate + def test_get_ip_addrs(self): + """Test get_ip_addrs success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_ip_addrs.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT i.ip_address, i.available, i.label, i.tags, np.netport_pk, s.network as subnet, s.mask_bits as netmask, v.name as vrf FROM view_ipaddress_v1 i LEFT JOIN view_subnet_v1 s ON s.subnet_pk = i.subnet_fk LEFT JOIN view_netport_v1 np ON np.netport_pk = i.netport_fk LEFT JOIN view_vrfgroup_v1 v ON v.vrfgroup_pk = s.vrfgroup_fk WHERE s.mask_bits <> 0&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_ip_addrs.json") + response = self.dev42.get_ip_addrs() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_ipaddr_default_custom_fields(self): + """Test get_ipaddr_default_custom_fields success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_ipaddr_default_custom_fields_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT cf.key, cf.value, cf.notes FROM view_ipaddress_custom_fields_v1 cf&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_ipaddr_default_custom_fields_recv.json") + response = self.dev42.get_ipaddr_default_custom_fields() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_ipaddr_custom_fields(self): + """Test get_ipaddr_custom_fields success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT cf.key, cf.value, cf.notes, i.ip_address, s.mask_bits FROM view_ipaddress_custom_fields_v1 cf LEFT JOIN view_ipaddress_v1 i ON i.ipaddress_pk = cf.ipaddress_fk LEFT JOIN view_subnet_v1 s ON s.subnet_pk = i.subnet_fk&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_ipaddr_custom_fields_recv.json") + response = self.dev42.get_ipaddr_custom_fields() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + def test_get_all_custom_fields(self): + """Test get_all_custom_fields success.""" + test_sample = load_json("./nautobot_ssot/tests/device42/fixtures/get_all_custom_fields_sent.json") + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_all_custom_fields_recv.json") + response = self.dev42.get_all_custom_fields(test_sample) + self.assertEqual(response, expected) + + @responses.activate + def test_get_vlans_with_location(self): + """Test get_vlans_with_location success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_vlans_with_location.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT v.vlan_pk, v.number AS vid, v.description, v.tags, vn.vlan_name, b.name as building, c.name as customer FROM view_vlan_v1 v LEFT JOIN view_vlan_on_netport_v1 vn ON vn.vlan_fk = v.vlan_pk LEFT JOIN view_netport_v1 n on n.netport_pk = vn.netport_fk LEFT JOIN view_device_v2 d on d.device_pk = n.device_fk LEFT JOIN view_building_v1 b ON b.building_pk = d.building_fk LEFT JOIN view_customer_v1 c ON c.customer_pk = d.customer_fk WHERE vn.vlan_name is not null and v.number <> 0 GROUP BY v.vlan_pk, v.number, v.description, v.tags, vn.vlan_name, b.name, c.name&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_vlans_with_location.json") + response = self.dev42.get_vlans_with_location() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_vlan_info(self): + """Test get_vlan_info success.""" + vinfo_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_vlan_info_vlaninfo.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT v.vlan_pk, v.name, v.number as vid FROM view_vlan_v1 v&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=vinfo_query, + status=200, + ) + cfields_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_vlan_info_cfields.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT cf.key, cf.value, cf.notes, v.vlan_pk FROM view_vlan_custom_fields_v1 cf LEFT JOIN view_vlan_v1 v ON v.vlan_pk = cf.vlan_fk&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=cfields_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_vlan_info_recv.json") + response = self.dev42.get_vlan_info() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 2) + + @responses.activate + def test_get_device_pks(self): + """Test get_device_pks success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_device_pks_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT name, device_pk FROM view_device_v1 WHERE name <> ''&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + with open("./nautobot_ssot/tests/device42/fixtures/get_device_pks_recv.json", "r", encoding="utf-8") as file: + json_data = file.read() + expected = json.loads(json_data, object_hook=lambda d: {int(k) if k.isdigit() else k: v for k, v in d.items()}) + response = self.dev42.get_device_pks() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_port_pks(self): + """Test get_port_pks success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_port_pks_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT np.port, np.netport_pk, np.hwaddress, np.second_device_fk, d.name as device FROM view_netport_v1 np JOIN view_device_v1 d ON d.device_pk = np.device_fk&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + with open("./nautobot_ssot/tests/device42/fixtures/get_port_pks_recv.json", "r", encoding="utf-8") as file: + json_data = file.read() + expected = json.loads(json_data, object_hook=lambda d: {int(k) if k.isdigit() else k: v for k, v in d.items()}) + response = self.dev42.get_port_pks() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_port_connections(self): + """Test get_port_connections success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_port_connections.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT netport_pk as src_port, device_fk as src_device, second_device_fk as second_src_device, remote_netport_fk as dst_port FROM view_netport_v1 WHERE device_fk is not null AND remote_netport_fk is not null&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_port_connections.json") + response = self.dev42.get_port_connections() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_telcocircuits(self): + """Test get_telcocircuits success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_telcocircuits.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT * FROM view_telcocircuit_v1&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_telcocircuits.json") + response = self.dev42.get_telcocircuits() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_vendor_pks(self): + """Test get_vendor_pks success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_vendor_pks_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT * FROM view_vendor_v1&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + with open("./nautobot_ssot/tests/device42/fixtures/get_vendor_pks_recv.json", "r", encoding="utf-8") as file: + json_data = file.read() + expected = json.loads(json_data, object_hook=lambda d: {int(k) if k.isdigit() else k: v for k, v in d.items()}) + response = self.dev42.get_vendor_pks() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_patch_panels(self): + """Test get_patch_panels success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_patch_panels.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT+a.name%2C+a.in_service%2C+a.serial_no%2C+a.customer_fk%2C+a.building_fk%2C+a.calculated_building_fk%2C+a.room_fk%2C+a.calculated_room_fk%2C+a.calculated_rack_fk%2C+a.size%2C+a.depth%2C+m.number_of_ports%2C+m.name+as+model_name%2C+m.port_type_name+as+port_type%2C+v.name+as+vendor%2C+a.rack_fk%2C+a.start_at+as+position%2C+a.orientation+FROM+view_asset_v1+a+LEFT+JOIN+view_patchpanelmodel_v1+m+ON+m.patchpanelmodel_pk+%3D+a.patchpanelmodel_fk+JOIN+view_vendor_v1+v+ON+v.vendor_pk+%3D+m.vendor_fk+WHERE+a.patchpanelmodel_fk+is+not+null+AND+a.name+is+not+null&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + expected = load_json("./nautobot_ssot/tests/device42/fixtures/get_patch_panels.json") + response = self.dev42.get_patch_panels() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_patch_panel_port_pks(self): + """Test get_patch_panel_port_pks success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_patch_panel_port_pks_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT p.*, a.name FROM view_patchpanelport_v1 p JOIN view_asset_v1 a ON a.asset_pk = p.patchpanel_asset_fk&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + with open( + "./nautobot_ssot/tests/device42/fixtures/get_patch_panel_port_pks_recv.json", "r", encoding="utf-8" + ) as file: + json_data = file.read() + expected = json.loads(json_data, object_hook=lambda d: {int(k) if k.isdigit() else k: v for k, v in d.items()}) + response = self.dev42.get_patch_panel_port_pks() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) + + @responses.activate + def test_get_customer_pks(self): + """Test get_customer_pks success.""" + test_query = load_json("./nautobot_ssot/tests/device42/fixtures/get_customer_pks_sent.json") + responses.add( + responses.GET, + "https://device42.testexample.com/services/data/v1.0/query/?query=SELECT * FROM view_customer_v1&output_type=json&_paging=1&_return_as_object=1&_max_results=1000", + json=test_query, + status=200, + ) + with open("./nautobot_ssot/tests/device42/fixtures/get_customer_pks_recv.json", "r", encoding="utf-8") as file: + json_data = file.read() + expected = json.loads(json_data, object_hook=lambda d: {int(k) if k.isdigit() else k: v for k, v in d.items()}) + response = self.dev42.get_customer_pks() + self.assertEqual(response, expected) + self.assertTrue(len(responses.calls) == 1) diff --git a/nautobot_ssot/tests/device42/unit/test_utils_nautobot.py b/nautobot_ssot/tests/device42/unit/test_utils_nautobot.py new file mode 100644 index 000000000..5fae1a4d3 --- /dev/null +++ b/nautobot_ssot/tests/device42/unit/test_utils_nautobot.py @@ -0,0 +1,234 @@ +"""Tests of Nautobot utility methods.""" +from uuid import UUID +from unittest.mock import MagicMock, patch +from diffsync.exceptions import ObjectNotFound +from django.contrib.contenttypes.models import ContentType +from nautobot.utilities.testing import TransactionTestCase +from nautobot.dcim.models import Manufacturer, Site, Region, Device, DeviceRole, DeviceType, Interface +from nautobot.extras.choices import CustomFieldTypeChoices +from nautobot.extras.models import CustomField, Status +from nautobot.ipam.models import VLAN +from nautobot_ssot.integrations.device42.diffsync.models.nautobot.dcim import NautobotDevice +from nautobot_ssot.integrations.device42.utils.nautobot import ( + verify_platform, + determine_vc_position, + update_custom_fields, + apply_vlans_to_port, +) + + +class TestNautobotUtils(TransactionTestCase): # pylint: disable=too-many-instance-attributes + """Test Nautobot utility methods.""" + + databases = ("default", "job_logs") + + def setUp(self): + """Setup shared test objects.""" + super().setUp() + self.status_active = Status.objects.get(name="Active") + self.cisco_manu, _ = Manufacturer.objects.get_or_create(name="Cisco") + self.site = Site.objects.create(name="Test Site", slug="test-site", status=self.status_active) + _dt = DeviceType(model="CSR1000v", manufacturer=self.cisco_manu) + _dr = DeviceRole(name="CORE") + self.dev = Device(name="Test", device_role=_dr, device_type=_dt, site=self.site, status=self.status_active) + self.intf = Interface( + name="Management", type="virtual", mode="access", device=self.dev, status=self.status_active + ) + self.mock_dev = NautobotDevice( + name="Test", + building="Microsoft HQ", + room=None, + rack=None, + rack_position=None, + rack_orientation=None, + hardware="CSR1000v", + os="cisco_ios", + os_version="16.2.3", + in_service=True, + serial_no="12345678", + tags=[], + cluster_host=None, + master_device=False, + vc_position=None, + custom_fields=None, + uuid=None, + ) + self.mock_vlan = VLAN( + vid=1, + name="Test", + site=self.site, + status=self.status_active, + ) + self.dsync = MagicMock() + self.dsync.get = MagicMock() + self.dsync.platform_map = {} + self.dsync.vlan_map = {"microsoft-hq": {}, "global": {}} + self.dsync.vlan_map["microsoft-hq"][1] = self.mock_vlan.id + self.dsync.site_map = {} + self.dsync.status_map = {} + self.dsync.objects_to_create = {"platforms": [], "vlans": [], "tagged_vlans": []} + self.dsync.site_map["test-site"] = self.site.id + self.dsync.status_map["active"] = self.status_active.id + + def test_lifecycle_mgmt_available(self): + """Validate that the DLC App module is available.""" + with patch("nautobot_device_lifecycle_mgmt.models.SoftwareLCM"): + from nautobot_device_lifecycle_mgmt.models import ( # noqa: F401 # pylint: disable=import-outside-toplevel, unused-import + SoftwareLCM, + ) + from nautobot_ssot.integrations.device42.utils.nautobot import ( # noqa: F401 # pylint: disable=import-outside-toplevel, unused-import + LIFECYCLE_MGMT, + ) + + self.assertTrue(LIFECYCLE_MGMT) + + def test_verify_platform_ios(self): + """Test the verify_platform method with IOS.""" + platform = verify_platform(diffsync=self.dsync, platform_name="cisco_ios", manu=self.cisco_manu.id) + self.assertEqual(type(platform), UUID) + self.assertEqual(self.dsync.objects_to_create["platforms"][0].name, "cisco.ios.ios") + self.assertEqual(self.dsync.objects_to_create["platforms"][0].slug, "cisco_ios") + self.assertEqual(self.dsync.objects_to_create["platforms"][0].napalm_driver, "ios") + + def test_verify_platform_iosxr(self): + """Test the verify_platform method with IOS-XR.""" + platform = verify_platform(diffsync=self.dsync, platform_name="cisco_xr", manu=self.cisco_manu.id) + self.assertEqual(type(platform), UUID) + self.assertEqual(self.dsync.objects_to_create["platforms"][0].name, "cisco.iosxr.iosxr") + self.assertEqual(self.dsync.objects_to_create["platforms"][0].slug, "cisco_xr") + self.assertEqual(self.dsync.objects_to_create["platforms"][0].napalm_driver, "iosxr") + + def test_verify_platform_junos(self): + """Test the verify_platform method with JunOS.""" + juniper_manu, _ = Manufacturer.objects.get_or_create(name="Juniper") + platform = verify_platform(diffsync=self.dsync, platform_name="juniper_junos", manu=juniper_manu.id) + self.assertEqual(type(platform), UUID) + self.assertEqual(self.dsync.objects_to_create["platforms"][0].name, "junipernetworks.junos.junos") + self.assertEqual(self.dsync.objects_to_create["platforms"][0].slug, "juniper_junos") + self.assertEqual(self.dsync.objects_to_create["platforms"][0].napalm_driver, "junos") + + def test_verify_platform_f5(self): + """Test the verify_platform method with F5 BIG-IP.""" + f5_manu, _ = Manufacturer.objects.get_or_create(name="F5") + platform = verify_platform(diffsync=self.dsync, platform_name="f5_tmsh", manu=f5_manu.id) + self.assertEqual(type(platform), UUID) + self.assertEqual(self.dsync.objects_to_create["platforms"][0].name, "f5_tmsh") + self.assertEqual(self.dsync.objects_to_create["platforms"][0].slug, "f5_tmsh") + self.assertEqual(self.dsync.objects_to_create["platforms"][0].napalm_driver, "f5_tmsh") + + def test_determine_vc_position(self): + vc_map = { + "switch_vc_example": { + "members": [ + "switch_vc_example - Switch 1", + "switch_vc_example - Switch 2", + ], + }, + "node_vc_example": { + "members": [ + "node_vc_example - node0", + "node_vc_example - node1", + "node_vc_example - node2", + ], + }, + "firewall_pair_example": { + "members": ["firewall - FTX123456AB", "firewall - FTX234567AB"], + }, + } + sw1_pos = determine_vc_position( + vc_map=vc_map, virtual_chassis="switch_vc_example", device_name="switch_vc_example - Switch 1" + ) + self.assertEqual(sw1_pos, 2) + sw2_pos = determine_vc_position( + vc_map=vc_map, virtual_chassis="switch_vc_example", device_name="switch_vc_example - Switch 2" + ) + self.assertEqual(sw2_pos, 3) + node3_pos = determine_vc_position( + vc_map=vc_map, virtual_chassis="node_vc_example", device_name="node_vc_example - node2" + ) + self.assertEqual(node3_pos, 4) + fw_pos = determine_vc_position( + vc_map=vc_map, virtual_chassis="firewall_pair_example", device_name="firewall - FTX123456AB" + ) + self.assertEqual(fw_pos, 2) + + def test_update_custom_fields_add_cf(self): + """Test the update_custom_fields method adds a CustomField.""" + test_site = Site.objects.create(name="Test", slug="test") + self.assertEqual(len(test_site.get_custom_fields()), 3) + mock_cfs = { + "Test Custom Field": {"key": "Test Custom Field", "value": None, "notes": None}, + } + update_custom_fields(new_cfields=mock_cfs, update_obj=test_site) + self.assertEqual(len(test_site.get_custom_fields()), 1) + self.assertEqual(test_site.custom_field_data["test_custom_field"], None) + + def test_update_custom_fields_remove_cf(self): + """Test the update_custom_fields method removes a CustomField.""" + test_region = Region.objects.create(name="Test", slug="test") + _cf_dict = { + "name": "department", + "slug": "department", + "type": CustomFieldTypeChoices.TYPE_TEXT, + "label": "Department", + } + field, _ = CustomField.objects.get_or_create(name=_cf_dict["name"], defaults=_cf_dict) + field.content_types.add(ContentType.objects.get_for_model(Region).id) + test_region.custom_field_data.update({_cf_dict["name"]: "IT"}) + mock_cfs = { + "Test Custom Field": {"key": "Test Custom Field", "value": None, "notes": None}, + } + update_custom_fields(new_cfields=mock_cfs, update_obj=test_region) + test_region.refresh_from_db() + self.assertFalse( + test_region.custom_field_data.get("Department"), "department should not exist in the dictionary" + ) + + def test_update_custom_fields_updates_cf(self): + """Test the update_custom_fields method updates a CustomField.""" + test_region = Region.objects.create(name="Test", slug="test") + _cf_dict = { + "name": "department", + "slug": "department", + "type": CustomFieldTypeChoices.TYPE_TEXT, + "label": "Department", + } + field, _ = CustomField.objects.get_or_create(name=_cf_dict["name"], defaults=_cf_dict) + field.content_types.add(ContentType.objects.get_for_model(Region).id) + mock_cfs = { + "Department": {"key": "Department", "value": "IT", "notes": None}, + } + update_custom_fields(new_cfields=mock_cfs, update_obj=test_region) + self.assertEqual(test_region.custom_field_data["department"], "IT") + + def test_apply_vlans_to_port_access_port(self): + """Test the apply_vlans_to_port() method adds a single VLAN to a port.""" + self.dsync.vlan_map["microsoft-hq"][1] = self.mock_vlan.id + self.dsync.get.return_value = self.mock_dev + apply_vlans_to_port(diffsync=self.dsync, device_name="Test", mode="access", vlans=[1], port=self.intf) + self.assertIsNotNone(self.intf.untagged_vlan_id) + self.assertEqual(self.intf.untagged_vlan_id, self.mock_vlan.id) + + def test_apply_vlans_to_port_tagged_port(self): + """Test the apply_vlans_to_port() method adds multiple VLANs to a port.""" + mock_vlan2 = VLAN( + vid=2, + name="Test2", + site=self.site, + status=self.status_active, + ) + self.dsync.vlan_map["microsoft-hq"][2] = mock_vlan2.id + self.dsync.get.return_value = self.mock_dev + self.intf.mode = "tagged" + apply_vlans_to_port(diffsync=self.dsync, device_name="Test", mode="tagged", vlans=[1, 2], port=self.intf) + port_update = self.dsync.objects_to_create["tagged_vlans"][0] + self.assertEqual(port_update[0], self.intf) + self.assertEqual(port_update[1], [self.mock_vlan.id, mock_vlan2.id]) + + def test_apply_vlans_to_port_w_missing_device(self): + """Test the apply_vlans_to_port() method when Device not found.""" + self.dsync.get.side_effect = ObjectNotFound + self.dsync.vlan_map["global"][1] = self.mock_vlan.id + apply_vlans_to_port(diffsync=self.dsync, device_name="Test", mode="access", vlans=[1], port=self.intf) + self.assertIsNotNone(self.intf.untagged_vlan_id) + self.assertEqual(self.intf.untagged_vlan_id, self.mock_vlan.id) diff --git a/pyproject.toml b/pyproject.toml index 91f018be9..8a1c51f91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,6 +124,9 @@ aristacv = [ "cloudvision", "cvprac", ] +device42 = [ + "requests", +] infoblox = [ "dnspython", ] From 728759d126dea80a5374be997684f7da0d06af4f Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:39:25 -0500 Subject: [PATCH 20/33] style: Changing Job name to match other integrations --- nautobot_ssot/integrations/device42/jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautobot_ssot/integrations/device42/jobs.py b/nautobot_ssot/integrations/device42/jobs.py index 8b8d6772d..b7ed10451 100644 --- a/nautobot_ssot/integrations/device42/jobs.py +++ b/nautobot_ssot/integrations/device42/jobs.py @@ -12,7 +12,7 @@ from nautobot_ssot.integrations.device42.utils.device42 import Device42API -name = "Device42 SSoT" # pylint: disable=invalid-name +name = "SSoT - Device42" # pylint: disable=invalid-name class Device42DataSource(DataSource, Job): From b4d27e91fdbb894542f74f962a3e9ef7ca019fb4 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Tue, 26 Sep 2023 04:36:32 +0000 Subject: [PATCH 21/33] fix: Add link to conflict error message --- nautobot_ssot/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nautobot_ssot/__init__.py b/nautobot_ssot/__init__.py index 055685cac..59e498622 100644 --- a/nautobot_ssot/__init__.py +++ b/nautobot_ssot/__init__.py @@ -30,6 +30,7 @@ def _check_for_conflicting_apps(): if intersection: raise RuntimeError( f"The following apps are installed and conflict with `nautobot-ssot`: {', '.join(intersection)}." + "See: https://docs.nautobot.com/projects/ssot/en/latest/admin/upgrade/#potential-apps-conflicts" ) From 6539aa07e9e48cf98f63079617117f4d81585e35 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:18:18 -0500 Subject: [PATCH 22/33] chore: Add old project name to conflicting App names --- nautobot_ssot/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nautobot_ssot/__init__.py b/nautobot_ssot/__init__.py index 055685cac..8756092f9 100644 --- a/nautobot_ssot/__init__.py +++ b/nautobot_ssot/__init__.py @@ -19,6 +19,7 @@ _CONFLICTING_APP_NAMES = [ "nautobot_ssot_aci", "nautobot_ssot_aristacv", + "nautobot_ssot_device42", "nautobot_ssot_infoblox", "nautobot_ssot_ipfabric", "nautobot_ssot_servicenow", From 581b0612bca47cc9fbc7e8b52470367553a29f8f Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Fri, 29 Sep 2023 07:27:24 +0000 Subject: [PATCH 23/33] feat: Allow to skip conflicts --- development/development.env | 1 + docs/admin/upgrade.md | 9 +++++++++ nautobot_ssot/__init__.py | 6 +++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/development/development.env b/development/development.env index 09468330c..70406dcd8 100644 --- a/development/development.env +++ b/development/development.env @@ -45,6 +45,7 @@ NAUTOBOT_CELERY_TASK_SOFT_TIME_LIMIT=3600 NAUTOBOT_CELERY_TASK_TIME_LIMIT=3600 NAUTOBOT_SSOT_HIDE_EXAMPLE_JOBS="False" +NAUTOBOT_SSOT_ALLOW_CONFLICTING_APPS="False" NAUTOBOT_SSOT_ENABLE_ACI="True" NAUTOBOT_SSOT_ACI_TAG="ACI" diff --git a/docs/admin/upgrade.md b/docs/admin/upgrade.md index b4c62585c..1d182e47e 100644 --- a/docs/admin/upgrade.md +++ b/docs/admin/upgrade.md @@ -25,5 +25,14 @@ To prevent conflicts during `nautobot-ssot` upgrade: These steps will help prevent issues during `nautobot-ssot` upgrades. Always back up your data and thoroughly test your configuration after these changes. +!!! note + It's possible to allow conflicting apps to remain in `PLUGINS` during the upgrade process. You can specify the following environment variable in `development/development.env` to allow conflicting apps: + + ```bash + NAUTOBOT_SSOT_ALLOW_CONFLICTING_APPS=True + ``` + + However, this is not recommended. + !!! warning If conflicting apps remain in `PLUGINS`, the `nautobot-ssot` app will raise an exception during startup to prevent potential conflicts. diff --git a/nautobot_ssot/__init__.py b/nautobot_ssot/__init__.py index 59e498622..1f8ce6e3e 100644 --- a/nautobot_ssot/__init__.py +++ b/nautobot_ssot/__init__.py @@ -1,6 +1,8 @@ """Plugin declaration for nautobot_ssot.""" # Metadata is inherited from Nautobot. If not including Nautobot in the environment, this should be added +from os import getenv + try: from importlib import metadata except ImportError: @@ -9,6 +11,7 @@ from django.conf import settings from nautobot.extras.plugins import PluginConfig +from nautobot.core.settings_funcs import is_truthy from nautobot_ssot.integrations.utils import each_enabled_integration_module from nautobot_ssot.utils import logger @@ -34,7 +37,8 @@ def _check_for_conflicting_apps(): ) -_check_for_conflicting_apps() +if not is_truthy(getenv("NAUTOBOT_SSOT_ALLOW_CONFLICTING_APPS", "False")): + _check_for_conflicting_apps() class NautobotSSOTPluginConfig(PluginConfig): From fe0343cdc5ab613ae949a2efc4edef7517e209c7 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Fri, 29 Sep 2023 12:24:02 +0000 Subject: [PATCH 24/33] fix: PR comments --- docs/admin/upgrade.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/admin/upgrade.md b/docs/admin/upgrade.md index 1d182e47e..ca79d35a7 100644 --- a/docs/admin/upgrade.md +++ b/docs/admin/upgrade.md @@ -26,7 +26,7 @@ To prevent conflicts during `nautobot-ssot` upgrade: These steps will help prevent issues during `nautobot-ssot` upgrades. Always back up your data and thoroughly test your configuration after these changes. !!! note - It's possible to allow conflicting apps to remain in `PLUGINS` during the upgrade process. You can specify the following environment variable in `development/development.env` to allow conflicting apps: + It's possible to allow conflicting apps to remain in `PLUGINS` during the upgrade process. You can specify the following environment variable to allow conflicting apps (see `development/development.env` for an example): ```bash NAUTOBOT_SSOT_ALLOW_CONFLICTING_APPS=True From ed5b28a5f4b279e2aa8838d09d887ead565156f4 Mon Sep 17 00:00:00 2001 From: Christian Adell Date: Fri, 29 Sep 2023 15:48:58 +0200 Subject: [PATCH 25/33] fix lint --- nautobot_ssot/integrations/aci/jobs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nautobot_ssot/integrations/aci/jobs.py b/nautobot_ssot/integrations/aci/jobs.py index fd062ba5b..ee5a3663e 100644 --- a/nautobot_ssot/integrations/aci/jobs.py +++ b/nautobot_ssot/integrations/aci/jobs.py @@ -51,7 +51,9 @@ class Meta: # pylint: disable=too-few-public-methods def __init__(self): """Initialize ExampleYAMLDataSource.""" super().__init__() - self.diffsync_flags = self.diffsync_flags | DiffSyncFlags.SKIP_UNMATCHED_DST # pylint: disable=unsupported-binary-operation + self.diffsync_flags = ( + self.diffsync_flags | DiffSyncFlags.SKIP_UNMATCHED_DST # pylint: disable=unsupported-binary-operation + ) @classmethod def data_mappings(cls): From 98a1b3ad98dffbafbe481e34fac649e1c826acce Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Fri, 29 Sep 2023 08:56:44 -0500 Subject: [PATCH 26/33] =?UTF-8?q?docs:=20=F0=9F=93=9D=20Add=20credit=20to?= =?UTF-8?q?=20@Eric-Jckson=20for=20tag=202.0=20fix=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nautobot_ssot/integrations/aristacv/utils/cloudvision.py | 2 +- nautobot_ssot/tests/aristacv/test_utils_cloudvision.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nautobot_ssot/integrations/aristacv/utils/cloudvision.py b/nautobot_ssot/integrations/aristacv/utils/cloudvision.py index a6ae2d574..1b976d858 100644 --- a/nautobot_ssot/integrations/aristacv/utils/cloudvision.py +++ b/nautobot_ssot/integrations/aristacv/utils/cloudvision.py @@ -304,7 +304,7 @@ def get_tags_by_type(client, creator_type: int = tag_models.CREATOR_TYPE_USER): tags.append(dev_tag) return tags - +# credit to @Eric-Jckson in https://github.com/nautobot/nautobot-plugin-ssot-arista-cloudvision/pull/164 for update to get_device_tags() def get_device_tags(client, device_id: str): """Get tags for specific device.""" tag_stub = tag_services.TagAssignmentServiceStub(client) diff --git a/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py b/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py index 398c8459d..c29418506 100644 --- a/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py +++ b/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py @@ -128,7 +128,9 @@ def test_get_device_tags(self): mock_tag.value.device_id.value = "JPE12345678" tag_stub = MagicMock() - tag_stub.TagAssignmentServiceStub.return_value.GetAll.return_value = [mock_tag] + tag_stub.TagAssignmentServiceStub.return_value.GetAll.return_value = [ + mock_tag + ] # credit to @Eric-Jckson in https://github.com/nautobot/nautobot-plugin-ssot-arista-cloudvision/pull/164 for update to get_device_tags() with patch("nautobot_ssot.integrations.aristacv.utils.cloudvision.tag_services", tag_stub): results = cloudvision.get_device_tags(client=self.client, device_id="JPE12345678") From a682a7b1215b333462a46ca6f7a8643b69d4d1e4 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:17:35 -0500 Subject: [PATCH 27/33] =?UTF-8?q?style:=20=F0=9F=9A=A8=20Fix=20spacing=20f?= =?UTF-8?q?or=20black?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nautobot_ssot/integrations/aristacv/utils/cloudvision.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nautobot_ssot/integrations/aristacv/utils/cloudvision.py b/nautobot_ssot/integrations/aristacv/utils/cloudvision.py index 1b976d858..c4305a4ba 100644 --- a/nautobot_ssot/integrations/aristacv/utils/cloudvision.py +++ b/nautobot_ssot/integrations/aristacv/utils/cloudvision.py @@ -304,6 +304,7 @@ def get_tags_by_type(client, creator_type: int = tag_models.CREATOR_TYPE_USER): tags.append(dev_tag) return tags + # credit to @Eric-Jckson in https://github.com/nautobot/nautobot-plugin-ssot-arista-cloudvision/pull/164 for update to get_device_tags() def get_device_tags(client, device_id: str): """Get tags for specific device.""" From b56898addd44a182db47ca8782d8715991b2c6d5 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:20:43 -0500 Subject: [PATCH 28/33] =?UTF-8?q?docs:=20=F0=9F=93=9D=20Add=20documentatio?= =?UTF-8?q?n=20and=20images=20about=20the=20Device42=20integration.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/admin/integrations/device42_setup.md | 84 +++++++++++++++++++++ docs/images/device42_dashboard.png | Bin 0 -> 178778 bytes docs/images/device42_detail-view.png | Bin 0 -> 168731 bytes docs/images/device42_job-form.png | Bin 0 -> 255977 bytes docs/images/device42_jobresult.png | Bin 0 -> 148897 bytes docs/images/device42_ssot-sync-details.png | Bin 0 -> 321470 bytes docs/user/integrations/device42.md | 47 ++++++++++++ mkdocs.yml | 2 + 8 files changed, 133 insertions(+) create mode 100644 docs/admin/integrations/device42_setup.md create mode 100644 docs/images/device42_dashboard.png create mode 100644 docs/images/device42_detail-view.png create mode 100644 docs/images/device42_job-form.png create mode 100644 docs/images/device42_jobresult.png create mode 100644 docs/images/device42_ssot-sync-details.png create mode 100644 docs/user/integrations/device42.md diff --git a/docs/admin/integrations/device42_setup.md b/docs/admin/integrations/device42_setup.md new file mode 100644 index 000000000..a9a67ec0e --- /dev/null +++ b/docs/admin/integrations/device42_setup.md @@ -0,0 +1,84 @@ +# Device42 Integration Setup + +This guide will walk you through steps to set up Device42 integration with the `nautobot_ssot` app. + +## Prerequisites + +Before configuring the integration, please ensure, that `nautobot-ssot` app was [installed with Device42 integration extra dependencies](../install.md#install-guide). + +```shell +pip install nautobot-ssot[device42] +``` + +## Configuration + +Integration behavior can be controlled with the following settings: + +| Configuration Variable | Type | Usage | +| ---------------------- | ------- | ----------------------------------------------------------------------------------------------------- | +| device42_host | string | This defines the FQDN of the Device42 instance, ie `https://device42.example.com`. | +| device42_username | string | This defines the username of the account used to connect to the Device42 API endpoint. | +| device42_password | string | This defines the password of the account used to connect to the Device42 API endpoint. | +| device42_verify | boolean | This denotes whether SSL validation of the Device42 endpoint should be enabled or not. | + +When creating Sites and Racks in Nautobot it is required to define a Status for each. It is also required to define a Role for your Device when created. You may define the default for each of those objects being imported with the respective values in your `nautobot_config.py` file. + +| Configuration Variable | Type | Usage | Default | +| --------------------------------------------------- | ------ | ---------------------------------------------------------- | -------------------- | +| device42_defaults["site_status"] | string | Default status for Sites synced to Nautobot. | Active | +| device42_defaults["rack_status"] | string | Default status for Racks synced to Nautobot. | Active | +| device42_defaults["device_role"] | string | Default role for Devices synced to Nautobot. | Unknown | + +When syncing from Device42, the integration will create new Devices that do not exist in Nautobot and delete any that are not in Device42. This behavior can be controlled with the `device42_delete_on_sync` setting. This option prevents objects from being deleted from Nautobot during a synchronization. This is handy if your Device42 data fluctuates a lot and you wish to control what is removed from Nautobot. This means objects will only be added, never deleted when set to False. In addition, while syncing your Devices from Device42 you can enable the `device42_use_dns` setting to perform DNS resolution of Device hostname's when assigning management IP addresses. When True, there will be an additional process of performing DNS queries for each Device in the sync and if an A record is found, will be assigned as management IP for the Device. It will attempt to use the interface for the IP based upon data from Device42 but will create a Management interface and assign the IP to it if an interface can't be determined. + +| Configuration Variable | Type | Usage | Default | +| --------------------------------------------------- | ------- | ---------------------------------------------------------------------------- | -------------------- | +| device42_delete_on_sync | boolean | Devices in Nautobot that don't exist in Device42 will be deleted. | False | +| device42_use_dns | boolean | Enables DNS resolution of Device name's for assigning primary IP addresses. | False | +| device42_customer_is_facility | boolean | True when utilizing the Customer field in Device42 to denote the site code. | False | + +> When these variables are not defined in the app settings, the integration will use the default values mentioned. + +Due to particular data points not being available in Device42 it was decided to utilize the tagging system as a secondary method of defining information. The 'device42_facility_prepend' setting defines the string that is expected on a Tag when determining a Building's site code. If a Building has a Tag that starts with `sitecode-` it will assume the remaining Tag is the facility code. Like the `device42_facility_prepend` option, the `device42_role_prepend` setting defines the string on a Tag that defines a Device's role. If a Device has a Tag that starts with `nautobot-` it will assume the remaining string is the name of the Device's role, such as `access-switch` for example. Lastly, there is the `device42_ignore_tag` setting that enables the specification of a Tag that when found on a Device will have it skipped from import. + +| Configuration Variable | Type | Usage | Default | +| --------------------------------------------------- | ------- | ---------------------------------------------------------------------------- | -------------------- | +| device42_facility_prepend | str | Devices in Nautobot that don't exist in Device42 will be deleted. | "" | +| device42_role_prepend | str | Define DeviceRole using a Tag. Will use Tag starting with this string. | "" | +| device42_ignore_tag | str | True when utilizing the Customer field in Device42 to denote the site code. | "" | + +Finally, there is the `device42_hostname_mapping` setting that enables the parsing of Device hostname's for codes that indicate the assigned Site using the site code. This option allows you to define a mapping of a regex pattern that defines a Device's hostname and which Site the Device should be assigned. This is helpful if the location information for Devices in Device42 is inaccurate and your Device's are named with the Site name or code in it. For example, if you have Device's called `DFW-access-switch`, you could map that as `{"^DFW.+": "dallas"}` where `dallas` is the slug form for your Site name. + +| Configuration Variable | Type | Usage | Default | +| -------------------------- | ----------- | ---------------------------------------------------------- | ------- | +| device42_hostname_mapping | List[dict] | Define mapping of a hostname that indicate site. | [{}] | + +> As the Device hostname is used as the identifier for Device objects any change in hostname implies a new Device and thus should trigger a deletion and creation of a new Device in Nautobot. For this reason, the hostname parsing feature is not done during updates and only at initial creation of the Device. If you need to correct the Site or Role for a Device after initial creation you will need to manually correct it or delete it and run the import Job again. + +Below is an example snippet from `nautobot_config.py` that demonstrates how to enable and configure the Device42 integration: + +```python +PLUGINS_CONFIG = { + "nautobot_ssot": { + "enable_device42": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_DEVICE42")), + "device42_host": os.getenv("NAUTOBOT_SSOT_DEVICE42_HOST", ""), + "device42_username": os.getenv("NAUTOBOT_SSOT_DEVICE42_USERNAME", ""), + "device42_password": os.getenv("NAUTOBOT_SSOT_DEVICE42_PASSWORD", ""), + "device42_verify_ssl": False, + "device42_defaults": { + "site_status": "Active", + "rack_status": "Active", + "device_role": "Unknown", + }, + "device42_delete_on_sync": False, + "device42_use_dns": True, + "device42_customer_is_facility": True, + "device42_facility_prepend": "sitecode-", + "device42_role_prepend": "nautobot-", + "device42_ignore_tag": "", + "device42_hostname_mapping": [], + } +``` + +!!! note + All integration settings are defined in the block above as an example. Only some will be needed as described above. diff --git a/docs/images/device42_dashboard.png b/docs/images/device42_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..d7c3f760e4d6ae21d4d267ef0bc19e20f55a1d1a GIT binary patch literal 178778 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�{t2IEK=kB*%Yts8?7lv1=8>wo)jU)UVG?K{4NCN2HU3*0L~o zm%$%Nd)qV6B%_50+kp^AJ>(%X;37V*I8wklfD##@3yKpj+i%z z>l>#HvD|UaVUGN$PX7m#I@4>amUZRovGK2?@Y!|T`GrVC$#Q17ta@MDB+iZ;U3M)r zZ42wA(A3FApX$Jt;V~Ju5_*Y zAa)|6P&bTwWrxkN6g_q4lqroIyE>42Kn^TZ(!0il!%U%GcB#OtHyJ}jRA!hpD8Cgp znc8&d7suqEHtf?7Rho9gIdW!Q$pZ0$zZ^*_r+$Y>lW#H$*VIdP)xTjFn%Tk}7e3<+ z7zV4^txRY@T1B{xv*z4GN@~TohIqdQwv#amCl4ZK}^9^^_weTc=Y!2s`uZpig0fF630fJQ2S6H>sfl1Q0{tP0{ZyEAmYM(@U#c?Y8huRkF+ z%y@rO!$mBZxrWf$y3V2qIMFS!V~t_G)0U}9^U%7Ui9j`To{dzdX$8Z1^i_x0Lzww> z)G8$SzPZO|?b%?$;^v5rkYW^+Mhb7fk#DIcmo2fk$sGVN#H58=af*8B0D|Wsj-*E= z=|NKJD`c6VaQCfavN_T<(L6ec!fB@k9*?`zsYQUS8$cS(Cm5mFMp>OTi339NuikP4PT?3dcD}GERNYP=PYZ2EEob?X`R=4TaQ~} znTCdTZOGOuqHiUHYcD8qj zR+{3v%A#~2Jt`M3d1+nX+N<-KYsFf1q-3=%x26dsi7#`$7lT>fmCrHkW=3gpzOSk& zImzCKdt~%W{=tuK+#Y|TGwxW;PZ}q0dUk@W!LD+?Kp2}!-xnp;JC~>AP!?kW?xjbR zI~Zj-#+S^d@=Ip9EPGiTc#kWmOx-?ctVaT_)5S>iokaexHbru!mWtCBV~YbxF{IVq zOY4l&Tc33Hti|3PMa9{S=5z$GNvvd;n0SX7MM!Kh7>4O=hiV8Aj9EAwR0}#y%8r)I zdC9eQC%?R$ecIff8 zudP4%`Lpk7M6P5d_gZ%QmB)4Wxfztp-H@Vpil>!O_OxKsaQ&Bb3Hw!CU~<<01jK{o ze2clh?sF_#>w|eKVtHGZim9pqO0isN5wgok|jb6f8w32H$6Yrf1O846CMqKg2fY09E=(e?wOtPf&9Y^&n zgLeabjV>|MM!MHuXYvq-;^!!!d|Wi&3;r{u@yjT%uizdgyl4nM(!=Ff15eA3%f*P0 zIB`4JC6}N&3?QqMDSD&0JtR9pCawpnT|1lisaM2`p_@~}+*-QoRJAZIHTk`N+=0^V zIeO{@lDAD(8VgvR>Gng@(7c;K(jq5P5Z`{lpaW}^%YB>C@IGW{X66!=^Ge8g-fQ?d zD$LxFSHQf@oZen;xfa`~pxvg;NT|FbUCNZqSJIbaGHzlp^CmFvPX@vlvRU8!QptL_ zBKFR)j*sCrX6@Llo7^LS=FgpZ_n5UMJYF}W5x4DL<;WfE3)M2x*xQ-<{evANEl(58 z#FLHu=}5f_$J-(gJWLC-5Me0Xcpb&ivu3N>R))Cy=Z$NQjLsZ&dsX@3;ox>@w?^-p z_8bAKkApUh#Hz?>0m^8eRpaL$nHj)X59MKtN-JWy>nay$njKQIaE&SKyOD% zDFkqqfOXu`eE(Oz32-SK&-zfeVg{M~$^=7Oi_ofV>CV%I>yxQQEfM5p0dYFo59Tf9 z&dMv_PxW<0bl%r@4z=Qq zFwnf#xZWuoUEssZUi(F2S+4hk#-M=W+Z@KG^*7Bq+Ane@vsoF?z5bYvW$oS0*N06J zkE+QHy>N)T*emcTimG5XV|nS?m2uQOlJfLK|LN}TpMr|F&(`S z`zUNw_I7=8K3FWQoT=?~{2X&N3>`lq24O{fbwXl_9fw1B?1|<-FTR1}!@c%?7xLkI z_E52#1+ZBbl2vnf_}$Ze;*G4xxfXP_xv|shuzvIl<>{%Ch#v0?g(HTS(ghBJ!%w

YGH>+$#+vO9)^u`Zj-TgwAq8dVFy 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

E8#szzO!XoQ;Ry$Swq)uDaz&M{JksA+j=_0WLq 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®pdQ_ey6oHZ`!N3Y6gTxl*rZH~;)NOijNHaN+Y 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%0AZTa2y&jy-LFpTV=?m|W#$k$t6Z578YyRNy zMhAkwpy#7U#VypKOk$>}^U)UAkSe@JC8vy>QMLAaII{1Kwcvjv9sj1H{_me$gN<-> znld7#)NriVAsh1kqG!kwMkD4_I#UAh(a;Lqv>I67-)_NzE-y8F^0pbop9zqiUqOz@ zDmcCjTB|iQi7~A$TjoMf!=+vV_^6seaS(u-TG7;`ZoF7HNC<<`!ck#viMmbkH>LWj zi%lwo3eVwM{6V2CJeT`8&(PbcGc~BiS-&Y!5j8-ovoCQ2z>>F-%~u}p423zgQ2%JH zZo}L~E_pNPNf5E9{gxYFJ*-$>3|=&b4X|Lq5t+@A`Sr_4W%!B;ogkf@F zaz=822BQ+yf^!xR#Ga>hv!R?13f*j`dG{I^J*Hnb%F|OlzVB;=T(*8nXylTdJd~%1 zyzX3+UyBJgIKq;^Hkyoywy-wQ8=nbpuzFD)%^@Z&(uxw`OsK>C?yyJN4cAmKtcqYV zegPojGjv^CrfvKp=|h*z1?a8_yqO#WJxjlm{4NIKAWFN1#Y;MJkxYvFp}ipDlbT<2 zorFp~i^QK#E|A6F?`wuqFCraW(=i~4cC%K4ZqqIqQ5cjDO<=c_Z?J6{cKdvGnpJ!4 z=iXLJwh$Eb5~RBiL&J!{*){l6Kb*6zG*1I$hV&is-v3 zZe5Rb_M^^Kw38XyE#w4ZBD78#di3orqid_HH%G6!L{oN6eLevNtlJ8Qmy)~vMyP&h zKRo%Zg%J)z;moD3S<)Bcsy0P)EJZi`ItgWgn`?wVauZbZuLSy`sBv?{oBc&%%(|OU zKDYT681?}gk~d~%nf`O&^zX8rxa3sQt4&}}h8GvXDBuRlV^_o7I{(#^I?3`k*ov}~ znkLE%d#!gq{6yf0>Lng}y7=Uy%twNBBUyl?~cwWGoWIjNfi~5 z8nIBMsSFknPpWk(~;FN7ZrjP;(PK`x7&**$LdH_24*Ir(F zwD2!c&-Vm)NAh>x(DguU#PI=<#y${+1I$M`Gtt`Ns!@tyt zESS0%Km!za=d~Wy|3G{7Uk&Ns0^AQ$^80&ps)`e<9@hY*yCUds)%`A`RuLq46%f5a zr4IZHKkAV!=DN{`^6TGwdGM;Atzaou%pvn<^bPUMA1ELnXZqGJWmf%tHNFdK@u$_lF#&~x^E(DNyZC_ahpW9s2lt=m z_YaS^4NHsS2L@Z>E0ucXI%je7@E`AN<0}8-jC!DN_*)KL3VKcL+eUI|KtB` z>-O(+B>0c(Y&k1{RW8MBW@`Kuer|sNScRL?6F)4T1g0ilEbRA}6jR2oaEd*)a(B}o zfDLhGf5x^aw*F}b9M6`*nQeeyc;(kh0++3i6MC?Hhxjj^az6%Kc42N{>7W3xuwNW( z4gVAU*Z=kPM(qG=t91WB>ZRFV8ZUJ+wXw4)CMbfHs%~+cLQ~AQCqg~ESn;QaWL%`k z{A!fPiMx(gIt22`zmsF69^dk@Ep;|{8m+>Y49=ae7k!2g;mO?%IOF54{gV3hT;p9* zP{%dXDR20uU5CSd8Q(>c?18cRq-;+U>HRtq@rd)swAq23={NHn8OU3hU&jX-^0UfU z+n$+4^R+2hOc=b7w^A&m)sHZ`BENGHf9KTUOqH4)1a_v0t!Uj}f3YHt(dgZ1o+)}E zD2`65$t3X-;?aHR1ASTp+nwC=j}6^&^@0BBNlt*pL6%d3fU3f0GYdO@rEXA!Z#H#oGRr!V1%5f{y35OH1uE%Z5aoU;RfxN!)meZy$eyrWN^Oe8rU zgM9)^%f8iXv_$IK`YE$33gw1Br+dt#HzNo7gQuyWA|(e}05@vb8?1eL{n)klmW%9( z(~s_aJ$CLPMgnuc%M0Dv08o+1>`#1`JUL( zI4Q{ovSV6M_V>B&3m)9Ii5Ef0?ScnwGWkUySalV!=#S=-(H0QU4=e_hJUhURKU~7v zpuGhcbaqTOq8Nxb0YQYaVIZcT?c@~pfdjh$DDJ9UwL2~QWq`uY^os-uZAHLMDM<#Z zRwvc`c3TV?h-G&00)ekSy-)R*3!Z8#ny;>T$Faj-m%Ce%uyV76pXwWOd0-9t=O)E? z$tG1fQ2K3rfxFeBV`{BShRu&fXp2swC-FD2rq7pvqE3W3`tbfDE!alE>?SL9c7yfS zV?#;pt4g6)`wY^rJNf`W;Gf?#41D~3d*<*Fuw{PzX1OPrX*fy>p(TOFEJI*?so177 z1Sk@p1|9U7IskUJ$^$@;o(`om5hSwo-1E`r*z%4-pSjbep>q)PfZK(?tHU}+5`i+1 zKSBkBGlzs}Uyb($;~MBa3X^kzJ3yDoi`YC~15}?}_IUd$0ZQ8aCC5iATXgmbhU0iq z&yI*%vZS*f|8+Y<8kjo!eb5HZVcc>L_&mvI7l0=NYOlO+pH2hRyI%AxP!QAFT+mSi zisZ#M9H9<=S<|a_@m0J)y8c8P`^33DYWYTn!48((AtM|XzDIeJ7fk+9|?GA z2b3hq>WV?el$VBGHZmcYQhh6knye_Lu@DMjwth*4nN(s9_AWn&4%cv#q=jH z&Lq;JMe{&=|4~j=xCC1@{phZxZ@^cI8h5&4uev5T)C>C8HsFl(*J2=%Em?B4MV91_&Usl^jA* zN+cz01i#g|FX7+eXfF_zTau8vlICf&Y2`bOZbo1~Pwwcv5q852!v<}n1_)Q7+t-?l zqYo`w$mo>Bkoju+$JaNiHfxsO*`{gVY~K48!j+Jj*iW|dp0mJdvM-q7oa(B^xES-e zJMLjs$!2eYo@w+^m#L7J4TX?z<1zd#@ayM=-i483B>Rc7f+@Msk(0ZYrFIu|-=Fx{ zAdTFNdxSJ#l`eh3!k-yvv%?pKwpfNUw<_-SPu_k1-v8xqo-Dyc#caY71YS|Mc3i^1 zZgq=TyIft^yvh63;qr;Z8^*~;lN4g5n#D`K8`^~Mw`Mon2C5Fv2sIAGrQSZZawY;H zL`;qP{eZK+vwB(&Nr2+^$>FkzMQPr}T9v1#RCuOt@8H!`Kfb$OCASs&a&8)b zh1q9NM*EmP*ziMgftLIP4tje9W%9~}e0yy*p zdxBBq^h1!aSH9L%u`1_+is0_nU~8ew5yDD6Y_;%961mtnGG3%SRg`L4-hpM_6Be&8E^~i(#r8MF-E>COmY%HOD(`>i32~c zogM%x#*+Zsp~c}qfms(8rp^83rvU!LxOdc9Z%1?bhADXskh*X{jM*0fPO!G+bLI$a z9R{G?Bxq;O&WGN=(6NX{6i8&j>w*J|j~dpGeO^D#b3a!Z{ceG3^Ak z_uy+gCJ3nTB!b1hVARvKYbo?t-y4klGl8*5>57`&3#O9msq#^5-M~yf()>nk){^YU zDfJh!8rX%QsI+E2l{fntFr25E)NEL*gnoY?K)rl9&hvoY_K;Lj;k34O)1!9L?aW-b zEKemVaBJp_C&cTjB#|ukUROTNOa)RYxH~?|hFuJoI$}L+VXw8^((V)y*p5dtLTV=9 zS}ZSrn$p|WwJ|+zZq$dVj0pXgt5zemW7Rd7O<Pg5GTrm}K7}#L0|{zF zK36>)w${K{+uQFd?jl+V(_nQitj~n8Vj|y`*3IP>j-?_zQ@znzl>>{cgcFUcgAF{b zRrKX$<9noqk03!wFqdJV1hELl)HUHGQa=4Ib29r=AK zX4fmK@+8oa@NNc@pQ#A9^IL#RgV}J!wLCla8c+$9f)>7}0v<003uJ3cKJFIz6(CBM z+>5kMKC0yixAJPP+|a^QYsZa%U$TsqU@t@_*~#n)C&8$c8KP&E-6lZQ7SGBlzJUQ7 z&-UJ!^M;F{3rh7cwkDnGa|l%*M-J;rDZnky(HDUHobmuN_*r*_io1CTk~-!?^=AOA zCBC-O^$hg^PAx0MoYhgYHl^o0meE0!5>}BlW6iO9)E7UDdsB7jMA+PbcVyr>L%$|5 z$mOj9Zm;*q`);*;_@y!}Jo!&BhZw%fKN#8+> zkH>FqIAUCBVniIUQUw1(w3hYcz~OxL{om0$`IGM(&c;%js) zYox^Loqyb6L>XL@ZokQTrXuTP!arr|6FT)+{~LcI0zUV;^<0X2Zwe4lWvCe>H{J5J zHsXa097oq-#+LM(B40~8BbE=G*;VKwxDqbS4A?wly*Orc4N#%9ebrOJDUzMadPrULC z6xOb~NCRrB@?PK~o1I`kg-hMD(@NpX=O`7HEljU0<))F0)aS|vfWVO)(WGL`^svUG z0he9?$&|ouv1oEho!UT%Uv>2NLran}lXmt+T0nhg_yCyga1X-~ct|+cXL-z_NM@3w zQiTIR*L)myd0ZA<*^e>V; zSTpW%w1tTFR<(j$KcMkC;AE^SmVNiv<^C3&|1Ok-$j-|&RJ(xqH0)x(>>t}IyAu`Y zrR=@RjVIJ4K5C&UJZdq~UEoO|o~PMpnufHHW^jr&McFhtEI7d(ZNrH?Nq+joVl{W1 ze5{5aa%Cy?(I|__&OGrN?+usSUB|lCQPFf6-RPsuytyWF_SmL2Y`T2Co9qz%fc8Q* z4;9~w{@5?jXBd`H_q9ahRJ~X2T^Rp*dEtPYNJjn;9agdO_36ro2M!YNT;Y$h2DMr0gbDQ~RE@vPMs^0S-U^>SB~@FDvKy2;ad&in zMTFkZ11(%Sl{8bTH|gmN#? zfmWXoy0FqZR!He+@h!VOzX|HJxZY5jw97vJD9hwX9Z+Fe1kiP}U#_5F#WmCUzWr*! z2$1UMU*{-8;D9dV^7{nUv8cM%K*j|?_rQZ=bQNgXoIMZJF}P3w{Fh;|u1M2=xe}*5 zbkboC0FlAKhl)D6^U!%w_rePEH?m|P#uY`)mU|Rv63N#{0=m;n7@nh(VW4I~dAZ4# z53w*b;{RY_7tpJ5D}1&DXPzHoVHhMXh)Kwyd-Cp!@|7Ar#5;Ig*~Xh9YvD7$m(bYB ztKU07(C}Rz$gx|NWfsr(so29_o7Qi1hV><6Ve@{qMVeF^7Yy5hC^Ee1!&(xGwqGIr zT>c)T2Q#nXTQrRyGV*shY|eNthdCH#h+oO17l*zNzF(oC`Y4}Q9mO{b*`3?XcC4<* zz3(z@QgAQ)(?=1Uz)Xtl2kk7gt85T`n54WFKSEFmUpC(JRUH2Ha!K!F^kt>S`mLgw zMpD+0K%{B1cgF<$gu0;{g}Y5NTS9n5T-NOPe04Zpb?U2u94m&WSMG6tB=F@dH8JI} zGF9pZNzE_#)Q7#;Ia{f3c58@DM!Bjovo4wVa9?mji|i8wETDL4^wo@-`jzL++64tE zg2t8%6}w<_S#O7RZJR4Zb3C<{N~wLCZ>_!1NMvX))914PtYyBXHPT5z4)`%{HAv-v zqgin7bRU2Z$EuKmj)}v*Jxe|Hl8@~PxD3NV<8fBVZsY1xt3c&?d2dqs4z$%q?F#NdgSvh#ZK@nRAWbyb@0{X0^i>I^J6rysV zj8tBYg$tYDo?9&3Ym$Ajj77E`S3P#!H&_%FE+d~M+e`AF?=agLJC!cZiQU^iea#CP zKEfj?VS3JXGegnHH2=yL<)Pi{FUDtNf1yTTU}>ioV^l{Iamy9c7ispa$4!MJ4g~rf z)~}a(wFF!fH|O<9R?vQqkjlo9AM7ZnQ!_>uKDQG*7U8TO{G@w)^TgcG>iy|5@DQgb)V1P7?69|jj z>4FiY49`ldB!=vvJ3PQ-EjeXHM(+Ew37YAHC~wy zt&hrqqS|c;XXm>YO!>g@7`WGVWm6Xlk0qOrh%Yf*5)->=VR;Rh6)Ywgivk- zt>!>WxR+!VgmDyULMqjpmp3+h3#N@I4VoQOI$^*#P5QhONef%C3SC)rQ40O$qU6^0 z%`xSz!lWI`HX4xb#Y6v;F;_%XNfA-=uW68N{^~nQT?B^8k^WG`jzSsa2m0_GSvLR(vs5jQi$DprhuFUB6Kph zWU5BNT}tlpCyt#UjCkXjY`gqqyCPu+;*(Xli>a-T4LOvqNGv)WIcvYMWxOvKogM<$c3Qx07tV zYj+v>>7r85!20?<|0(F5AoI`l!5PFHC~UJT3#&m=-~PFq@&T|-=fUKKH}|1#fRP*~ z3g29;I8Wl_WjB;G&JtV zvN-aI$$CJ7Xx_mkdV?YarYz`a06j;>?IJLSS>vf)w~|SYx}CmT?OdgeX!!alkb3*0 zx`1Sw+j#N=#z=fczr8xTd?TWDZJGznU)P=RV13fo7NqSHQ*p_{PMH5@ z=1O_T%v@2R%Q%Gi%+AJ#ke|~UwGlz;2*=yJ!717*&IxKT1R2roc%Pl=R23D_ch7vr zl~3*pWD?kJ&F0{hE!Fb{?;Q)NtG5L{Xfe}K%dEeJqgSVxrfdz4O{C8@738niTMg80 zH1IF|Xcoc%hOpJYJO{K>)ftERC5E3~n&!RR+aUql7)g6L)oZBjkVVX)*L65_s|{S< zcCrpTU6zXlE}IVYesBE&fpL)kC^QJ{4xq~bNPuqfR;+6SvvGkdE!RtKll+7#A#*anXUHWi&wKm<}u zU_#}&IrN!`D>w=7FeQoQWxM_-VdpCf50+~!J~ePGTQx6gON$Tt;=n$nqn-KGTfetN zJ@DRvIco)p_6$Wag^Y9Yxdy_`TeimB(F8&comNB>qv&CKbr4C13Y=eBCNY02c(pVZ zL)g?rYn4-?;~ZaCm`6K)WlZouRx^A@lVpUhZTx@Lu3j4yk8mOa6A8tcNYd?vW#6#AnY=<`n@1pUt1 zphDNub2bWsB6kqINf|290&?<)rplO~W{$}wm1B!RFkyr6hY^HSZo7rIH-%Vk7QM+5 zDeBhf^DJ{LxAd9X6t=#Dl~#e$B(`9bhER`H+GYT>o{zOVwv8SzQ%b$QrA`fChdM&G zQCj@lIhC5QGsd)&2{?66w8Hd8Yxq8q?mRbeLtfu8*CSm41Wj>3btMy$5LJFSg?W<^AbLv+|qok58>zZ9Le-DHB>_PcwmlD3W)<)S| z)FN|8rbct)@=DNJ{Z+~NjE)htWRFv?^Fzogmm60{^llIFDQccWuZf3>R@gMD;j zz<|X-OE%V}q&)=-Z4_--?-(2Jo*|k{SbDsglbw~W;-Bj0KJHjFMZ9FNi>R}r2Ev5U z4n~Kty%(k!uCZ+Fis9LEnYowoXvp1NaAXq^M6EYk9^vuptSt&PM8GH!)eKU_whSS?Wb0R-n0(BlRQEd!}Na2fFPj&V^u>)f(PDJv>&x39Z zCak!AExHF%!HIK$Zu2t;ihCr_80yK9`ilc{65nd81{`BlDKAWxD88NQg~K7VdPe1t zeTNz~Q%ED|K8Bf(E{g6ow<)YFGOHvQbTUEt!do<;=cW>%;fBUL=4`%sZWs-e#V3eh zR3y2;(Wm_*dF-Q2g&Y4=aLgR2jl=_jy1G4#k|d;h|5lRhSRNTvD#hR%-H2B}3)j!~ zOKlKD5xco`)$Ef=El{2t2UT-I3Z!Q8l_gLuTc)fe&a z5arRG+rF7y(;C{1f{Wp7&mOA|n;Xn}J?N`%Hf0iq6uLnQ>^AplBk1k37+!Q-fxG*Y zK!eo=<>6pZD0I3I2n{sz>D1d9&hw?PD~ztUJxo>iCin9TOU57h29UhB7`zG_XI=Xr+JpNZgMZpSB|9nEO^ay_|KNVw49L|C%CRW8t zZ|sRvFMS7fi=3?wpDm?KGkjN5w0_izR+k7pMh^WtKhsz#QY?^g_az;EXO!MkITX~Z z8om{`M45(2#HS0nI4t`+FMkqVDbh!DOG?Bh@YHB!WaJWP0WJYWCBh-+sSePoG`N=u zb$XQQkj=WUbtPXotoe2Z6;DZ7()p#&vtK?OdZ(A%(1Xe?=_SnMcSGMUUHE-zcM>hqS|47;$^-bJ9S^o{6UGnIP}Xjx)3iXmW}aXd}}UU#s@a5*Vr+0Xh5 zlNY|Ovq$I#_n>0yMOq3;^l(4PD&MPiE!|*z_;pq0+#Jkha-e#WfMjN!Cx?y}mH0R8 zR=VFJ?K7x=WR9q!--g+nAAnLO@TO_BhMUjF5^b`Je|K=_3Fh4ja@?FBLM@lb%x+A4 zRbni`tadF{VDgUGXRpU!Aw@}N%k2WJ#QU!Y+#1aAwM%rq>iT>eY*5$@ncp!R`37TS z4&F9X%bpE|t`85qmTpS3py#)lnFP#Dhqc;2*G33P-yL0sls@&ymk<5&3$anQw=VZC z)9~{{q|CSMp(LxID&4ZnO0KQts#8l{1Ldq24de^NlN}yB@^mUG?5i3UvQCD zv!=#kArLVib#RovKSJ#RSeB(n33r(I&ulV|6fI8JljpIie33TID|OPyoYNJM%kJ+W zmOpX0%PVdfx_DSRnPn5jG)Y?Y7B_sS7t{QO(l{p@hM9xDv@8UpDvPz8BHNsreY)S* zGuWxsE0C}*N0Om>XxYHM!mh1i$E&Oy&Hk8?s_bPh+ZPI5Z$@fn%_v$g;`yF^nj_S2 z+#X)+{GivO&RVneYmP5zJc#6a$|o1*jfKsq`jYpyHptO<@9@5u%+|DgHSEP;d6k&1 zjVM&ju-V2dA@wWwpA#XZW5S_lphMc)6A%g9cjX8ifBo&*m9Y+WT3az341kLT>a4I6 zF^)paoQQqVh|O=(Fhmt8SkAa=sF8w{mL3I|z#w~#< zc7dO9Ko3nOo>K}8$-k8?n`Yq+aNq4rLm(GIt~kk6zqMW^D7RyB`eM(t7iE=CKy$HF z+L6u7&rihI|J2y?OOssmlR|ehXr{W7 zIiX_1mC$5?<>U=}gqdiu_>r&)3h&s&R)_-ac|5E!eC7(#%pyw01l7bP0TB z=>+_Y{)M)_^~*)x*PIwQ>s&g;FS((;7ySU6 z5@z0k9ZhTX>244S90`uQS8{1wNYe|9@f~|Nxn(}}fGe;2ABhoi3)D{HvFnSi)X^(YKz1&3W=vN9pMXF;x?rs3v1Gp5eD-YE^kq> zp#q^e5rwp3a`vvzh)~Rm&AZhT=*z_3r>w&+`5k zY5n#g3X1hBU7KC-g$Rkpxhk@~R6$n00ZMO(64-p$h5+#0=Z8YAMh8K zs)Uqn$(s(!*V00Hg3UwFVtTxp9?P2p=Y!9{?MMoKO2haO4TRQI_@Hx&?o1!@!hMot z$TfSMsNeM$I{h-KSv!YxQF2Aqj=4Urp;cZZW22E09Voqz3w&#ag+c6o2Y6WdJ*C%O zK1(ccTG{>*g<^xGKdjHnrRhY5N8)41(=}2ElDiiS=CL}mjJk$1T-Pw1?kYRIH(2?P9vit756t$JLx)wZcd{L%83?%#+N@9rD?$vz!x<@0c}I@Q{>1 zKP(K@QawUTv0rQ7S*$%$66RcAcgnRjWsx`&Z1IpvnQNk(zOVk$wKw0iianxa(^%t~ z-cxLSVXg&3R>-o4UVu|qo$JzINfoXO@ie8jA7)L968$THJ6OWazR z@pMc>>Obh}fg0j+7eWR@Ztn^1??|7#>}C>dTP?- z^7Iw6iGKua*wr#&W}Dn#1n<0p=6X)+Prc^*MqF3}u$W%7M^*~$6SkC#htnR-Z?gz< zj+_lLT0Lw%III|Q;8BKyOoU<>JTkdbDGjb1W9dSJ10O6I5R zOCx#6{HeU)V7DpZ#4kW%Ml?=DB_71)P0IvrhhG{{T1^+A=hS~2L?%+6t7hrrfRZ&-VCX=fzpn4uUW;* znWgAFJtyWj_6;(=uD$Yhwq|KX?3Hd=de)WLY(A*w+T|p-Cf8fscfE)uW}?;jp~a2` zqw!K3`?qb-juIQtH78VWObMw4JH9!!PLYP@&q5UW9^X?C-BfMLu z`{|55H$w42Db%O38MMSlwlR`4M-&Im!WE1;V*;=OE(Ecmrfi$4yn{=M@dl$^a2Nh2 zv<1zue;FNizR}(4-1wD{`V8Lp%IK(U*^`j#g^F-PE?y=WUpbJWR6P{2w~HD~m>bev z&edPrC5;8S-AATxx3r}NYH1xd#gS9-ngAYme#UY{(09PC_$(&@P;>|Z#%pL;YN$toy`xo04riv- ztWJJ8=2gGimsV*(zW-qdS0kvnTR8hEO=f-SGRZaFnD@VWASrls7Y$wIPg1a{EWp6B`>-)N zbd9X9qWcVQ&+%Rk6TmCG3CR@?oD^^fEm0F?%+(mAuIR=dro*&Ucgb5=R^Co}l6yT% zK9=N#ALX!MRzrgnZtu{g%`etgv%=L(O0ZnfJ0mECHJ;fL3-; z=Ao#(rCcS?O_=Hix2?-84!(haX4GCsWUfA8?W5Ro@MBI+{kqxkvpgnRs&sl)PQ~Gb zu)Nai=&*G7KontYX7-!4Goo9K6-tnZ&|)epfv|t!K1cvIp>myko#^ z#RBeMGboCp{oR2GxF~ek=L=8|k|lQQsZC~G8s_x&#D(Bs9%TNC{>2H?b4(x=8z7}> zhivdL->0$gp;fK@nl25@!)^v=wf$i~prM2_RG%}YoCE`qq5(c!y|~%V`ATIM0ROHx zLBf@~-y3FYsELDgw9XgG9d6;xfj!NF+QhlXQ(Sw5_H)|V*DtQ~wHQ9;==_78_&LPs zRekFvM?#`>7juZUDXC1fc7r%l!m3-ru_hDv;mIYD{jN&vMz2v)C${%QV>^4uqT- z4yV0=T%~w{z1Mq+n~3qCmRrwT&Ossn{#)V~jvygFPVh14Y@8aFqF(*3hq4UXB`}>s z|Ae1!o2dchA3Lm0N5Vhi#cf^r-F=>)gCWCo!XV_xj*GNiKe=0P2O#>-dw5`bp_9n< z;KJUW4@u{827cYvUHu79Jw^WMr+TUg^2zjb@VH?$8AaR2KXHoFH|XGcGbBx)%K;ZI zbk^=0{t5Nx@7mUfw>a>etLqar<=&7ZldQS9*yG!}&Yy_f)Ox^dn~hts{At%u^@1NW z<`cK;LjHD1@=I_%Xw=*do&@mWlV5SB%(m;U93aH4+kE?1?1gCQe)eC%qw{1A?ASIw z;*XVzDF^p;G>mB%>Itq_@p)j|*wMf5du@)%M7~m+&H^S=UyD?X`loenWixjnt>gxyV_Cf z^<7)|1N0cMkz8gGx5}5MOB>xnR z5OC!|k*V+7!$%h8faPK9v!1P5f|V2}+oo*0i~fi0zWs40&)LZlkmYiU?qb!~Q5SB_ z|I{2WCISW9%FR%Mv*YH2n^oOkxfevL|J5}9yHx*Os(-A&Um7edZ1w+UcIJmu#jqd1pKH2?zZPA(_3-}y9J79f literal 0 HcmV?d00001 diff --git a/docs/images/device42_ssot-sync-details.png b/docs/images/device42_ssot-sync-details.png new file mode 100644 index 0000000000000000000000000000000000000000..2e6fd8b049948a3cb5295ac68782fd095c0c3747 GIT binary patch literal 321470 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

plLcmEFX8` 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 z
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 literal 0 HcmV?d00001 diff --git a/docs/user/integrations/device42.md b/docs/user/integrations/device42.md new file mode 100644 index 000000000..81fc7ea47 --- /dev/null +++ b/docs/user/integrations/device42.md @@ -0,0 +1,47 @@ +# Arista CloudVision SSoT Integration + +The Device42 SSoT integration is built as part of the [Nautobot Single Source of Truth (SSoT)](../..tps://github.com/nautobot/nautobot-plugin-ssot) app. The SSoT app enables Nautobot to be the aggregation point for data coming from multiple systems of record (SoR). + +From Device42 into Nautobot, it synchronizes the following objects: + +| Device42 | Nautobot | +| ----------------------- | ---------------------------- | +| Buildings | Sites | +| Rooms | RackGroups | +| Racks | Racks | +| Vendors | Manufacturers | +| Hardware Models | DeviceTypes | +| Devices | Devices | +| Ports | Interfaces | +| VPC (VRF Groups) | VRFs | +| Subnets | Prefixes | +| IP Addresses | IP Addresses | +| VLANs | VLANs | +| Vendors | Providers | +| Telco Circuits | Circuits | + +`**` If the [Device Lifecycle Nautobot app](https://github.com/nautobot/nautobot-plugin-device-lifecycle-mgmt) is found to be installed, a matching Version will be created with a RelationshipAssociation connecting the Device and that Version. + +## Usage + +Once the plugin is installed and configured, you will be able to perform a data import from Device42 into Nautobot. From the Nautobot SSoT Dashboard view (`/plugins/ssot/`), Device42 will show as a Data Source. + +![Dashboard View](../../images/device42_dashboard.png) + +From the Dashboard, you can also view more information about the App by clicking on the Device42 link and see the Detail view. This view will show the mappings of Device42 objects to Nautobot objects, the sync history, and other configuration details for the App: + +![Detail View](../../images/device42_detail-view.png) + +To start the synchronization, simply click the `Sync Now` button on the Dashboard to start the Job. You should be presented with the Job form below: + +![Job Form](../../images/device42_job-form.png) + +If you wish to just test the synchronization but not have any data created in Nautobot you'll want to select the `Dry run` toggle. Clicking the `Debug` toggle will enable more verbose logging to inform you of what is occuring behind the scenes. Finally, the `Bulk import` option will enable bulk create and update operations to be used when the synchronization is complete. This can improve performance times for the App by forsaking validation of the imported data. Be aware that this could potentially cause bad data to be pushed into Nautobot. + +Running this Job will redirect you to a `Nautobot Job Result` view. + +![JobResult View](../../images/device42_jobresult.png) + +Once the Job has finished you can access the `SSoT Sync Details` page to see detailed information about the data that was synchronized from Device42 and the outcome of the sync Job. + +![SSoT Sync Details](../../images/device42_ssot-sync-details.png) diff --git a/mkdocs.yml b/mkdocs.yml index 496d976bb..888376495 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -105,6 +105,7 @@ nav: - "user/integrations/index.md" - Cisco ACI: "user/integrations/aci.md" - Arista CloudVision: "user/integrations/aristacv.md" + - Device42: "user/integrations/device42.md" - Infoblox: "user/integrations/infoblox.md" - IPFabric: "user/integrations/ipfabric.md" - ServiceNow: "user/integrations/servicenow.md" @@ -118,6 +119,7 @@ nav: - "admin/integrations/index.md" - Cisco ACI: "admin/integrations/aci_setup.md" - Arista CloudVision: "admin/integrations/aristacv_setup.md" + - Device42: "admin/integrations/device42_setup.md" - Infoblox: "admin/integrations/infoblox_setup.md" - IPFabric: "admin/integrations/ipfabric_setup.md" - ServiceNow: "admin/integrations/servicenow_setup.md" From 9291fe88e80b3151f2007709b7bc4f42a6902a07 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:35:13 -0500 Subject: [PATCH 29/33] =?UTF-8?q?ci:=20=F0=9F=91=B7=20Fix=20CI=20to=20use?= =?UTF-8?q?=20LTM=20releases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 620a5947c..2677de9b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: fail-fast: true matrix: python-version: ["3.8"] - nautobot-version: ["1.5.13"] + nautobot-version: ["1.6.0"] env: INVOKE_NAUTOBOT_SSOT_PYTHON_VER: "${{ matrix.python-version }}" INVOKE_NAUTOBOT_SSOT_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" @@ -121,17 +121,14 @@ jobs: matrix: python-version: ["3.8", "3.9", "3.10"] db-backend: ["postgresql"] - nautobot-version: ["latest"] + nautobot-version: ["ltm-1.6"] include: - - python-version: "3.10" - db-backend: "postgresql" - nautobot-version: "1.5.13" - python-version: "3.8" db-backend: "mysql" - nautobot-version: "1.5.13" + nautobot-version: "ltm-1.6" - python-version: "3.10" db-backend: "mysql" - nautobot-version: "stable" + nautobot-version: "ltm-1.6" runs-on: "ubuntu-20.04" env: INVOKE_NAUTOBOT_SSOT_PYTHON_VER: "${{ matrix.python-version }}" From d49912fc0f66ecd306bb7e288727fc6cabcf89f4 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Fri, 29 Sep 2023 15:49:32 -0500 Subject: [PATCH 30/33] =?UTF-8?q?docs:=20=F0=9F=93=9D=20Add=20Device42=20t?= =?UTF-8?q?o=20integration=20list=20in=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a6f18064b..e021527e6 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This Nautobot application framework includes the following integrations: - Cisco ACI - Arista CloudVision +- Device42 - Infoblox - IPFabric - ServiceNow From fdbc5b9c14231b3bf7ee3264085c796b453aa43f Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:23:49 -0500 Subject: [PATCH 31/33] =?UTF-8?q?build:=20=F0=9F=94=96=20Bump=20to=20v1.6.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 53ba973cd..6da4d73e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautobot-ssot" -version = "1.5.0" +version = "1.6.0" description = "Nautobot Single Source of Truth" authors = ["Network to Code, LLC "] license = "Apache-2.0" From 85b5cb37b862e458f71143f49bf7bf40ca14e77e Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:24:02 -0500 Subject: [PATCH 32/33] =?UTF-8?q?docs:=20=F0=9F=93=9D=20Add=20release=20no?= =?UTF-8?q?tes=20for=201.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/admin/release_notes/version_1.6.md | 19 +++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 20 insertions(+) create mode 100644 docs/admin/release_notes/version_1.6.md diff --git a/docs/admin/release_notes/version_1.6.md b/docs/admin/release_notes/version_1.6.md new file mode 100644 index 000000000..40c23d0ce --- /dev/null +++ b/docs/admin/release_notes/version_1.6.md @@ -0,0 +1,19 @@ +# v1.6 Release Notes + +## v1.6.0 - 2023-09-29 + +## Added + +- [221](https://github.com/nautobot/nautobot-plugin-ssot/pull/221) - Add Device42 integration by @jdrew82 +- [222](https://github.com/nautobot/nautobot-plugin-ssot/pull/222) - Add conflicting message link by @snaselj +- [224](https://github.com/nautobot/nautobot-plugin-ssot/pull/224) - Allow Skipping Conflicting Apps Check by @snaselj + +## Changed + +- [219](https://github.com/nautobot/nautobot-plugin-ssot/pull/219) - Attempt fixing CI error in #214 by @Kircheneer +- [220](https://github.com/nautobot/nautobot-plugin-ssot/pull/220) - Update ci.yml by @Kircheneer +- [214](https://github.com/nautobot/nautobot-plugin-ssot/pull/214) - Sync Main to Develop for 1.5.0 by @jdrew82 +- [218](https://github.com/nautobot/nautobot-plugin-ssot/pull/218) - Fixes contrib.NautobotModel not returning objects on update/delete by @Kircheneer +- [161](https://github.com/nautobot/nautobot-plugin-ssot/pull/161) - Reverts ChatOps dependency removal by @snaselj +- [213](https://github.com/nautobot/nautobot-plugin-ssot/pull/213) - fix: :bug: Several fixes in the ACI integration by @chadell +- [205](https://github.com/nautobot/nautobot-plugin-ssot/pull/205) - Migrate PR #164 from Arista Child Repo by @qduk diff --git a/mkdocs.yml b/mkdocs.yml index 888376495..6463c3954 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -128,6 +128,7 @@ nav: - Compatibility Matrix: "admin/compatibility_matrix.md" - Release Notes: - "admin/release_notes/index.md" + - v1.6: "admin/release_notes/version_1.5.md" - v1.5: "admin/release_notes/version_1.5.md" - v1.4: "admin/release_notes/version_1.4.md" - v1.3: "admin/release_notes/version_1.3.md" From ca8d2634c6e46b1983a3989855f9f64f95d0dae6 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:24:42 -0500 Subject: [PATCH 33/33] =?UTF-8?q?build:=20=E2=AC=86=EF=B8=8F=20Update=20pr?= =?UTF-8?q?oject=20dependencies=20to=20latest.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 1043 ++++++++++++++++++++++++++------------------------- 1 file changed, 529 insertions(+), 514 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9a749f26f..67877611b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "aiodns" @@ -224,13 +224,13 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "astroid" -version = "2.15.6" +version = "2.15.8" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.7.2" files = [ - {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"}, - {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"}, + {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, + {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, ] [package.dependencies] @@ -491,75 +491,63 @@ files = [ [[package]] name = "cffi" -version = "1.15.1" +version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] [package.dependencies] @@ -761,59 +749,72 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "contourpy" -version = "1.1.0" +version = "1.1.1" description = "Python library for calculating contours of 2D quadrilateral grids" optional = true python-versions = ">=3.8" files = [ - {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"}, - {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, - {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, - {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, - {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, - {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, - {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, - {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, - {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, - {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"}, - {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"}, + {file = "contourpy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b"}, + {file = "contourpy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1"}, + {file = "contourpy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d"}, + {file = "contourpy-1.1.1-cp310-cp310-win32.whl", hash = "sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431"}, + {file = "contourpy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5"}, + {file = "contourpy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62"}, + {file = "contourpy-1.1.1-cp311-cp311-win32.whl", hash = "sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33"}, + {file = "contourpy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf"}, + {file = "contourpy-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d"}, + {file = "contourpy-1.1.1-cp312-cp312-win32.whl", hash = "sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6"}, + {file = "contourpy-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8"}, + {file = "contourpy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251"}, + {file = "contourpy-1.1.1-cp38-cp38-win32.whl", hash = "sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7"}, + {file = "contourpy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85"}, + {file = "contourpy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e"}, + {file = "contourpy-1.1.1-cp39-cp39-win32.whl", hash = "sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0"}, + {file = "contourpy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c"}, + {file = "contourpy-1.1.1.tar.gz", hash = "sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab"}, ] [package.dependencies] -numpy = ">=1.16" +numpy = {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""} [package.extras] bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pillow"] test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] test-no-images = ["pytest", "pytest-cov", "wurlitzer"] @@ -896,34 +897,34 @@ dev = ["polib"] [[package]] name = "cryptography" -version = "41.0.3" +version = "41.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, - {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, - {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, - {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, - {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, - {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, - {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, + {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"}, + {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"}, + {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"}, + {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"}, + {file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"}, + {file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"}, + {file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"}, ] [package.dependencies] @@ -957,15 +958,19 @@ dev = ["check-manifest", "coverage", "pep8", "pyflakes", "pylint", "pyyaml"] [[package]] name = "cycler" -version = "0.11.0" +version = "0.12.0" description = "Composable style cycles" optional = true -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, + {file = "cycler-0.12.0-py3-none-any.whl", hash = "sha256:7896994252d006771357777d0251f3e34d266f4fa5f2c572247a80ab01440947"}, + {file = "cycler-0.12.0.tar.gz", hash = "sha256:8cc3a7b4861f91b1095157f9916f748549a617046e67eb7619abed9b34d2c94a"}, ] +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + [[package]] name = "decorator" version = "5.1.1" @@ -1489,13 +1494,13 @@ files = [ [[package]] name = "drf-spectacular" -version = "0.26.4" +version = "0.26.5" description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework" optional = false python-versions = ">=3.6" files = [ - {file = "drf-spectacular-0.26.4.tar.gz", hash = "sha256:8f5a8f87353d1bb8dcb3f3909b7109b2dcbe1d91f3e069409cf322963e140bd6"}, - {file = "drf_spectacular-0.26.4-py3-none-any.whl", hash = "sha256:afeccc6533dcdb4e78afbfcc49f3c5e9c369aeb62f965e4d1a43b165449c147a"}, + {file = "drf-spectacular-0.26.5.tar.gz", hash = "sha256:aee55330a774ba8a9cbdb125714d1c9ee05a8aafd3ce3be8bfd26527649aeb44"}, + {file = "drf_spectacular-0.26.5-py3-none-any.whl", hash = "sha256:c0002a820b11771fdbf37853deb371947caf0159d1afeeffe7598e964bc1db94"}, ] [package.dependencies] @@ -1596,45 +1601,53 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "fonttools" -version = "4.42.1" +version = "4.43.0" description = "Tools to manipulate font files" optional = true python-versions = ">=3.8" files = [ - {file = "fonttools-4.42.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ed1a13a27f59d1fc1920394a7f596792e9d546c9ca5a044419dca70c37815d7c"}, - {file = "fonttools-4.42.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9b1ce7a45978b821a06d375b83763b27a3a5e8a2e4570b3065abad240a18760"}, - {file = "fonttools-4.42.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f720fa82a11c0f9042376fd509b5ed88dab7e3cd602eee63a1af08883b37342b"}, - {file = "fonttools-4.42.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db55cbaea02a20b49fefbd8e9d62bd481aaabe1f2301dabc575acc6b358874fa"}, - {file = "fonttools-4.42.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a35981d90feebeaef05e46e33e6b9e5b5e618504672ca9cd0ff96b171e4bfff"}, - {file = "fonttools-4.42.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:68a02bbe020dc22ee0540e040117535f06df9358106d3775e8817d826047f3fd"}, - {file = "fonttools-4.42.1-cp310-cp310-win32.whl", hash = "sha256:12a7c247d1b946829bfa2f331107a629ea77dc5391dfd34fdcd78efa61f354ca"}, - {file = "fonttools-4.42.1-cp310-cp310-win_amd64.whl", hash = "sha256:a398bdadb055f8de69f62b0fc70625f7cbdab436bbb31eef5816e28cab083ee8"}, - {file = "fonttools-4.42.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:689508b918332fb40ce117131633647731d098b1b10d092234aa959b4251add5"}, - {file = "fonttools-4.42.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e36344e48af3e3bde867a1ca54f97c308735dd8697005c2d24a86054a114a71"}, - {file = "fonttools-4.42.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19b7db825c8adee96fac0692e6e1ecd858cae9affb3b4812cdb9d934a898b29e"}, - {file = "fonttools-4.42.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113337c2d29665839b7d90b39f99b3cac731f72a0eda9306165a305c7c31d341"}, - {file = "fonttools-4.42.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37983b6bdab42c501202500a2be3a572f50d4efe3237e0686ee9d5f794d76b35"}, - {file = "fonttools-4.42.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6ed2662a3d9c832afa36405f8748c250be94ae5dfc5283d668308391f2102861"}, - {file = "fonttools-4.42.1-cp311-cp311-win32.whl", hash = "sha256:179737095eb98332a2744e8f12037b2977f22948cf23ff96656928923ddf560a"}, - {file = "fonttools-4.42.1-cp311-cp311-win_amd64.whl", hash = "sha256:f2b82f46917d8722e6b5eafeefb4fb585d23babd15d8246c664cd88a5bddd19c"}, - {file = "fonttools-4.42.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:62f481ac772fd68901573956231aea3e4b1ad87b9b1089a61613a91e2b50bb9b"}, - {file = "fonttools-4.42.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2f806990160d1ce42d287aa419df3ffc42dfefe60d473695fb048355fe0c6a0"}, - {file = "fonttools-4.42.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db372213d39fa33af667c2aa586a0c1235e88e9c850f5dd5c8e1f17515861868"}, - {file = "fonttools-4.42.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d18fc642fd0ac29236ff88ecfccff229ec0386090a839dd3f1162e9a7944a40"}, - {file = "fonttools-4.42.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8708b98c278012ad267ee8a7433baeb809948855e81922878118464b274c909d"}, - {file = "fonttools-4.42.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c95b0724a6deea2c8c5d3222191783ced0a2f09bd6d33f93e563f6f1a4b3b3a4"}, - {file = "fonttools-4.42.1-cp38-cp38-win32.whl", hash = "sha256:4aa79366e442dbca6e2c8595645a3a605d9eeabdb7a094d745ed6106816bef5d"}, - {file = "fonttools-4.42.1-cp38-cp38-win_amd64.whl", hash = "sha256:acb47f6f8680de24c1ab65ebde39dd035768e2a9b571a07c7b8da95f6c8815fd"}, - {file = "fonttools-4.42.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb289b7a815638a7613d46bcf324c9106804725b2bb8ad913c12b6958ffc4ec"}, - {file = "fonttools-4.42.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:53eb5091ddc8b1199330bb7b4a8a2e7995ad5d43376cadce84523d8223ef3136"}, - {file = "fonttools-4.42.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46a0ec8adbc6ff13494eb0c9c2e643b6f009ce7320cf640de106fb614e4d4360"}, - {file = "fonttools-4.42.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cc7d685b8eeca7ae69dc6416833fbfea61660684b7089bca666067cb2937dcf"}, - {file = "fonttools-4.42.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:be24fcb80493b2c94eae21df70017351851652a37de514de553435b256b2f249"}, - {file = "fonttools-4.42.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:515607ec756d7865f23070682622c49d922901943697871fc292277cf1e71967"}, - {file = "fonttools-4.42.1-cp39-cp39-win32.whl", hash = "sha256:0eb79a2da5eb6457a6f8ab904838454accc7d4cccdaff1fd2bd3a0679ea33d64"}, - {file = "fonttools-4.42.1-cp39-cp39-win_amd64.whl", hash = "sha256:7286aed4ea271df9eab8d7a9b29e507094b51397812f7ce051ecd77915a6e26b"}, - {file = "fonttools-4.42.1-py3-none-any.whl", hash = "sha256:9398f244e28e0596e2ee6024f808b06060109e33ed38dcc9bded452fd9bbb853"}, - {file = "fonttools-4.42.1.tar.gz", hash = "sha256:c391cd5af88aacaf41dd7cfb96eeedfad297b5899a39e12f4c2c3706d0a3329d"}, + {file = "fonttools-4.43.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ab80e7d6bb01316d5fc8161a2660ca2e9e597d0880db4927bc866c76474472ef"}, + {file = "fonttools-4.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82d8e687a42799df5325e7ee12977b74738f34bf7fde1c296f8140efd699a213"}, + {file = "fonttools-4.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d08a694b280d615460563a6b4e2afb0b1b9df708c799ec212bf966652b94fc84"}, + {file = "fonttools-4.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d654d3e780e0ceabb1f4eff5a3c042c67d4428d0fe1ea3afd238a721cf171b3"}, + {file = "fonttools-4.43.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:20fc43783c432862071fa76da6fa714902ae587bc68441e12ff4099b94b1fcef"}, + {file = "fonttools-4.43.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:33c40a657fb87ff83185828c0323032d63a4df1279d5c1c38e21f3ec56327803"}, + {file = "fonttools-4.43.0-cp310-cp310-win32.whl", hash = "sha256:b3813f57f85bbc0e4011a0e1e9211f9ee52f87f402e41dc05bc5135f03fa51c1"}, + {file = "fonttools-4.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:05056a8c9af048381fdb17e89b17d45f6c8394176d01e8c6fef5ac96ea950d38"}, + {file = "fonttools-4.43.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da78f39b601ed0b4262929403186d65cf7a016f91ff349ab18fdc5a7080af465"}, + {file = "fonttools-4.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5056f69a18f3f28ab5283202d1efcfe011585d31de09d8560f91c6c88f041e92"}, + {file = "fonttools-4.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcc01cea0a121fb0c009993497bad93cae25e77db7dee5345fec9cce1aaa09cd"}, + {file = "fonttools-4.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee728d5af70f117581712966a21e2e07031e92c687ef1fdc457ac8d281016f64"}, + {file = "fonttools-4.43.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b5e760198f0b87e42478bb35a6eae385c636208f6f0d413e100b9c9c5efafb6a"}, + {file = "fonttools-4.43.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af38f5145258e9866da5881580507e6d17ff7756beef175d13213a43a84244e9"}, + {file = "fonttools-4.43.0-cp311-cp311-win32.whl", hash = "sha256:25620b738d4533cfc21fd2a4f4b667e481f7cb60e86b609799f7d98af657854e"}, + {file = "fonttools-4.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:635658464dccff6fa5c3b43fe8f818ae2c386ee6a9e1abc27359d1e255528186"}, + {file = "fonttools-4.43.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a682fb5cbf8837d1822b80acc0be5ff2ea0c49ca836e468a21ffd388ef280fd3"}, + {file = "fonttools-4.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3d7adfa342e6b3a2b36960981f23f480969f833d565a4eba259c2e6f59d2674f"}, + {file = "fonttools-4.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa67d1e720fdd902fde4a59d0880854ae9f19fc958f3e1538bceb36f7f4dc92"}, + {file = "fonttools-4.43.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e5113233a2df07af9dbf493468ce526784c3b179c0e8b9c7838ced37c98b69"}, + {file = "fonttools-4.43.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57c22e5f9f53630d458830f710424dce4f43c5f0d95cb3368c0f5178541e4db7"}, + {file = "fonttools-4.43.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:206808f9717c9b19117f461246372a2c160fa12b9b8dbdfb904ab50ca235ba0a"}, + {file = "fonttools-4.43.0-cp312-cp312-win32.whl", hash = "sha256:f19c2b1c65d57cbea25cabb80941fea3fbf2625ff0cdcae8900b5fb1c145704f"}, + {file = "fonttools-4.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c76f32051159f8284f1a5f5b605152b5a530736fb8b55b09957db38dcae5348"}, + {file = "fonttools-4.43.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e3f8acc6ef4a627394021246e099faee4b343afd3ffe2e517d8195b4ebf20289"}, + {file = "fonttools-4.43.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a68b71adc3b3a90346e4ac92f0a69ab9caeba391f3b04ab6f1e98f2c8ebe88e3"}, + {file = "fonttools-4.43.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace0fd5afb79849f599f76af5c6aa5e865bd042c811e4e047bbaa7752cc26126"}, + {file = "fonttools-4.43.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9660e70a2430780e23830476332bc3391c3c8694769e2c0032a5038702a662"}, + {file = "fonttools-4.43.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:48078357984214ccd22d7fe0340cd6ff7286b2f74f173603a1a9a40b5dc25afe"}, + {file = "fonttools-4.43.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d27d960e10cf7617d70cf3104c32a69b008dde56f2d55a9bed4ba6e3df611544"}, + {file = "fonttools-4.43.0-cp38-cp38-win32.whl", hash = "sha256:a6a2e99bb9ea51e0974bbe71768df42c6dd189308c22f3f00560c3341b345646"}, + {file = "fonttools-4.43.0-cp38-cp38-win_amd64.whl", hash = "sha256:030355fbb0cea59cf75d076d04d3852900583d1258574ff2d7d719abf4513836"}, + {file = "fonttools-4.43.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52e77f23a9c059f8be01a07300ba4c4d23dc271d33eed502aea5a01ab5d2f4c1"}, + {file = "fonttools-4.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a530fa28c155538d32214eafa0964989098a662bd63e91e790e6a7a4e9c02da"}, + {file = "fonttools-4.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70f021a6b9eb10dfe7a411b78e63a503a06955dd6d2a4e130906d8760474f77c"}, + {file = "fonttools-4.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:812142a0e53cc853964d487e6b40963df62f522b1b571e19d1ff8467d7880ceb"}, + {file = "fonttools-4.43.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ace51902ab67ef5fe225e8b361039e996db153e467e24a28d35f74849b37b7ce"}, + {file = "fonttools-4.43.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8dfd8edfce34ad135bd69de20c77449c06e2c92b38f2a8358d0987737f82b49e"}, + {file = "fonttools-4.43.0-cp39-cp39-win32.whl", hash = "sha256:e5d53eddaf436fa131042f44a76ea1ead0a17c354ab9de0d80e818f0cb1629f1"}, + {file = "fonttools-4.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:93c5b6d77baf28f306bc13fa987b0b13edca6a39dc2324eaca299a74ccc6316f"}, + {file = "fonttools-4.43.0-py3-none-any.whl", hash = "sha256:e4bc589d8da09267c7c4ceaaaa4fc01a7908ac5b43b286ac9279afe76407c384"}, + {file = "fonttools-4.43.0.tar.gz", hash = "sha256:b62a53a4ca83c32c6b78cac64464f88d02929779373c716f738af6968c8c821e"}, ] [package.extras] @@ -1775,20 +1788,20 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.36" +version = "3.1.37" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.36-py3-none-any.whl", hash = "sha256:8d22b5cfefd17c79914226982bb7851d6ade47545b1735a9d010a2a4c26d8388"}, - {file = "GitPython-3.1.36.tar.gz", hash = "sha256:4bb0c2a6995e85064140d31a33289aa5dce80133a23d36fcd372d716c54d3ebf"}, + {file = "GitPython-3.1.37-py3-none-any.whl", hash = "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33"}, + {file = "GitPython-3.1.37.tar.gz", hash = "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar", "virtualenv"] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar"] [[package]] name = "graphene" @@ -1898,60 +1911,69 @@ colorama = ">=0.4" [[package]] name = "grpcio" -version = "1.58.0" +version = "1.59.0" description = "HTTP/2-based RPC framework" optional = true python-versions = ">=3.7" files = [ - {file = "grpcio-1.58.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:3e6bebf1dfdbeb22afd95650e4f019219fef3ab86d3fca8ebade52e4bc39389a"}, - {file = "grpcio-1.58.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:cde11577d5b6fd73a00e6bfa3cf5f428f3f33c2d2878982369b5372bbc4acc60"}, - {file = "grpcio-1.58.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:a2d67ff99e70e86b2be46c1017ae40b4840d09467d5455b2708de6d4c127e143"}, - {file = "grpcio-1.58.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ed979b273a81de36fc9c6716d9fb09dd3443efa18dcc8652501df11da9583e9"}, - {file = "grpcio-1.58.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:458899d2ebd55d5ca2350fd3826dfd8fcb11fe0f79828ae75e2b1e6051d50a29"}, - {file = "grpcio-1.58.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc7ffef430b80345729ff0a6825e9d96ac87efe39216e87ac58c6c4ef400de93"}, - {file = "grpcio-1.58.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5b23d75e5173faa3d1296a7bedffb25afd2fddb607ef292dfc651490c7b53c3d"}, - {file = "grpcio-1.58.0-cp310-cp310-win32.whl", hash = "sha256:fad9295fe02455d4f158ad72c90ef8b4bcaadfdb5efb5795f7ab0786ad67dd58"}, - {file = "grpcio-1.58.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc325fed4d074367bebd465a20763586e5e1ed5b943e9d8bc7c162b1f44fd602"}, - {file = "grpcio-1.58.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:652978551af02373a5a313e07bfef368f406b5929cf2d50fa7e4027f913dbdb4"}, - {file = "grpcio-1.58.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:9f13a171281ebb4d7b1ba9f06574bce2455dcd3f2f6d1fbe0fd0d84615c74045"}, - {file = "grpcio-1.58.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:8774219e21b05f750eef8adc416e9431cf31b98f6ce9def288e4cea1548cbd22"}, - {file = "grpcio-1.58.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09206106848462763f7f273ca93d2d2d4d26cab475089e0de830bb76be04e9e8"}, - {file = "grpcio-1.58.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62831d5e251dd7561d9d9e83a0b8655084b2a1f8ea91e4bd6b3cedfefd32c9d2"}, - {file = "grpcio-1.58.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:212f38c6a156862098f6bdc9a79bf850760a751d259d8f8f249fc6d645105855"}, - {file = "grpcio-1.58.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4b12754af201bb993e6e2efd7812085ddaaef21d0a6f0ff128b97de1ef55aa4a"}, - {file = "grpcio-1.58.0-cp311-cp311-win32.whl", hash = "sha256:3886b4d56bd4afeac518dbc05933926198aa967a7d1d237a318e6fbc47141577"}, - {file = "grpcio-1.58.0-cp311-cp311-win_amd64.whl", hash = "sha256:002f228d197fea12797a14e152447044e14fb4fdb2eb5d6cfa496f29ddbf79ef"}, - {file = "grpcio-1.58.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:b5e8db0aff0a4819946215f156bd722b6f6c8320eb8419567ffc74850c9fd205"}, - {file = "grpcio-1.58.0-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:201e550b7e2ede113b63e718e7ece93cef5b0fbf3c45e8fe4541a5a4305acd15"}, - {file = "grpcio-1.58.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:d79b660681eb9bc66cc7cbf78d1b1b9e335ee56f6ea1755d34a31108b80bd3c8"}, - {file = "grpcio-1.58.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ef8d4a76d2c7d8065aba829f8d0bc0055495c998dce1964ca5b302d02514fb3"}, - {file = "grpcio-1.58.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cba491c638c76d3dc6c191d9c75041ca5b8f5c6de4b8327ecdcab527f130bb4"}, - {file = "grpcio-1.58.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6801ff6652ecd2aae08ef994a3e49ff53de29e69e9cd0fd604a79ae4e545a95c"}, - {file = "grpcio-1.58.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:24edec346e69e672daf12b2c88e95c6f737f3792d08866101d8c5f34370c54fd"}, - {file = "grpcio-1.58.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7e473a7abad9af48e3ab5f3b5d237d18208024d28ead65a459bd720401bd2f8f"}, - {file = "grpcio-1.58.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:4891bbb4bba58acd1d620759b3be11245bfe715eb67a4864c8937b855b7ed7fa"}, - {file = "grpcio-1.58.0-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:e9f995a8a421405958ff30599b4d0eec244f28edc760de82f0412c71c61763d2"}, - {file = "grpcio-1.58.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:2f85f87e2f087d9f632c085b37440a3169fda9cdde80cb84057c2fc292f8cbdf"}, - {file = "grpcio-1.58.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb6b92036ff312d5b4182fa72e8735d17aceca74d0d908a7f08e375456f03e07"}, - {file = "grpcio-1.58.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d81c2b2b24c32139dd2536972f1060678c6b9fbd106842a9fcdecf07b233eccd"}, - {file = "grpcio-1.58.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fbcecb6aedd5c1891db1d70efbfbdc126c986645b5dd616a045c07d6bd2dfa86"}, - {file = "grpcio-1.58.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92ae871a902cf19833328bd6498ec007b265aabf2fda845ab5bd10abcaf4c8c6"}, - {file = "grpcio-1.58.0-cp38-cp38-win32.whl", hash = "sha256:dc72e04620d49d3007771c0e0348deb23ca341c0245d610605dddb4ac65a37cb"}, - {file = "grpcio-1.58.0-cp38-cp38-win_amd64.whl", hash = "sha256:1c1c5238c6072470c7f1614bf7c774ffde6b346a100521de9ce791d1e4453afe"}, - {file = "grpcio-1.58.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:fe643af248442221db027da43ed43e53b73e11f40c9043738de9a2b4b6ca7697"}, - {file = "grpcio-1.58.0-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:128eb1f8e70676d05b1b0c8e6600320fc222b3f8c985a92224248b1367122188"}, - {file = "grpcio-1.58.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:039003a5e0ae7d41c86c768ef8b3ee2c558aa0a23cf04bf3c23567f37befa092"}, - {file = "grpcio-1.58.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f061722cad3f9aabb3fbb27f3484ec9d4667b7328d1a7800c3c691a98f16bb0"}, - {file = "grpcio-1.58.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0af11938acf8cd4cf815c46156bcde36fa5850518120920d52620cc3ec1830"}, - {file = "grpcio-1.58.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d4cef77ad2fed42b1ba9143465856d7e737279854e444925d5ba45fc1f3ba727"}, - {file = "grpcio-1.58.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24765a627eb4d9288ace32d5104161c3654128fe27f2808ecd6e9b0cfa7fc8b9"}, - {file = "grpcio-1.58.0-cp39-cp39-win32.whl", hash = "sha256:f0241f7eb0d2303a545136c59bc565a35c4fc3b924ccbd69cb482f4828d6f31c"}, - {file = "grpcio-1.58.0-cp39-cp39-win_amd64.whl", hash = "sha256:dcfba7befe3a55dab6fe1eb7fc9359dc0c7f7272b30a70ae0af5d5b063842f28"}, - {file = "grpcio-1.58.0.tar.gz", hash = "sha256:532410c51ccd851b706d1fbc00a87be0f5312bd6f8e5dbf89d4e99c7f79d7499"}, + {file = "grpcio-1.59.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:225e5fa61c35eeaebb4e7491cd2d768cd8eb6ed00f2664fa83a58f29418b39fd"}, + {file = "grpcio-1.59.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b95ec8ecc4f703f5caaa8d96e93e40c7f589bad299a2617bdb8becbcce525539"}, + {file = "grpcio-1.59.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:1a839ba86764cc48226f50b924216000c79779c563a301586a107bda9cbe9dcf"}, + {file = "grpcio-1.59.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6cfe44a5d7c7d5f1017a7da1c8160304091ca5dc64a0f85bca0d63008c3137a"}, + {file = "grpcio-1.59.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0fcf53df684fcc0154b1e61f6b4a8c4cf5f49d98a63511e3f30966feff39cd0"}, + {file = "grpcio-1.59.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa66cac32861500f280bb60fe7d5b3e22d68c51e18e65367e38f8669b78cea3b"}, + {file = "grpcio-1.59.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8cd2d38c2d52f607d75a74143113174c36d8a416d9472415eab834f837580cf7"}, + {file = "grpcio-1.59.0-cp310-cp310-win32.whl", hash = "sha256:228b91ce454876d7eed74041aff24a8f04c0306b7250a2da99d35dd25e2a1211"}, + {file = "grpcio-1.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:ca87ee6183421b7cea3544190061f6c1c3dfc959e0b57a5286b108511fd34ff4"}, + {file = "grpcio-1.59.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:c173a87d622ea074ce79be33b952f0b424fa92182063c3bda8625c11d3585d09"}, + {file = "grpcio-1.59.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:ec78aebb9b6771d6a1de7b6ca2f779a2f6113b9108d486e904bde323d51f5589"}, + {file = "grpcio-1.59.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:0b84445fa94d59e6806c10266b977f92fa997db3585f125d6b751af02ff8b9fe"}, + {file = "grpcio-1.59.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c251d22de8f9f5cca9ee47e4bade7c5c853e6e40743f47f5cc02288ee7a87252"}, + {file = "grpcio-1.59.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:956f0b7cb465a65de1bd90d5a7475b4dc55089b25042fe0f6c870707e9aabb1d"}, + {file = "grpcio-1.59.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:38da5310ef84e16d638ad89550b5b9424df508fd5c7b968b90eb9629ca9be4b9"}, + {file = "grpcio-1.59.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:63982150a7d598281fa1d7ffead6096e543ff8be189d3235dd2b5604f2c553e5"}, + {file = "grpcio-1.59.0-cp311-cp311-win32.whl", hash = "sha256:50eff97397e29eeee5df106ea1afce3ee134d567aa2c8e04fabab05c79d791a7"}, + {file = "grpcio-1.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:15f03bd714f987d48ae57fe092cf81960ae36da4e520e729392a59a75cda4f29"}, + {file = "grpcio-1.59.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f1feb034321ae2f718172d86b8276c03599846dc7bb1792ae370af02718f91c5"}, + {file = "grpcio-1.59.0-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d09bd2a4e9f5a44d36bb8684f284835c14d30c22d8ec92ce796655af12163588"}, + {file = "grpcio-1.59.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:2f120d27051e4c59db2f267b71b833796770d3ea36ca712befa8c5fff5da6ebd"}, + {file = "grpcio-1.59.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0ca727a173ee093f49ead932c051af463258b4b493b956a2c099696f38aa66"}, + {file = "grpcio-1.59.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5711c51e204dc52065f4a3327dca46e69636a0b76d3e98c2c28c4ccef9b04c52"}, + {file = "grpcio-1.59.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d74f7d2d7c242a6af9d4d069552ec3669965b74fed6b92946e0e13b4168374f9"}, + {file = "grpcio-1.59.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3859917de234a0a2a52132489c4425a73669de9c458b01c9a83687f1f31b5b10"}, + {file = "grpcio-1.59.0-cp312-cp312-win32.whl", hash = "sha256:de2599985b7c1b4ce7526e15c969d66b93687571aa008ca749d6235d056b7205"}, + {file = "grpcio-1.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:598f3530231cf10ae03f4ab92d48c3be1fee0c52213a1d5958df1a90957e6a88"}, + {file = "grpcio-1.59.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:b34c7a4c31841a2ea27246a05eed8a80c319bfc0d3e644412ec9ce437105ff6c"}, + {file = "grpcio-1.59.0-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:c4dfdb49f4997dc664f30116af2d34751b91aa031f8c8ee251ce4dcfc11277b0"}, + {file = "grpcio-1.59.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:61bc72a00ecc2b79d9695220b4d02e8ba53b702b42411397e831c9b0589f08a3"}, + {file = "grpcio-1.59.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f367e4b524cb319e50acbdea57bb63c3b717c5d561974ace0b065a648bb3bad3"}, + {file = "grpcio-1.59.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849c47ef42424c86af069a9c5e691a765e304079755d5c29eff511263fad9c2a"}, + {file = "grpcio-1.59.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c0488c2b0528e6072010182075615620071371701733c63ab5be49140ed8f7f0"}, + {file = "grpcio-1.59.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:611d9aa0017fa386809bddcb76653a5ab18c264faf4d9ff35cb904d44745f575"}, + {file = "grpcio-1.59.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e5378785dce2b91eb2e5b857ec7602305a3b5cf78311767146464bfa365fc897"}, + {file = "grpcio-1.59.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:fe976910de34d21057bcb53b2c5e667843588b48bf11339da2a75f5c4c5b4055"}, + {file = "grpcio-1.59.0-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:c041a91712bf23b2a910f61e16565a05869e505dc5a5c025d429ca6de5de842c"}, + {file = "grpcio-1.59.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:0ae444221b2c16d8211b55326f8ba173ba8f8c76349bfc1768198ba592b58f74"}, + {file = "grpcio-1.59.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ceb1e68135788c3fce2211de86a7597591f0b9a0d2bb80e8401fd1d915991bac"}, + {file = "grpcio-1.59.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4b1cc3a9dc1924d2eb26eec8792fedd4b3fcd10111e26c1d551f2e4eda79ce"}, + {file = "grpcio-1.59.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:871371ce0c0055d3db2a86fdebd1e1d647cf21a8912acc30052660297a5a6901"}, + {file = "grpcio-1.59.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:93e9cb546e610829e462147ce724a9cb108e61647a3454500438a6deef610be1"}, + {file = "grpcio-1.59.0-cp38-cp38-win32.whl", hash = "sha256:f21917aa50b40842b51aff2de6ebf9e2f6af3fe0971c31960ad6a3a2b24988f4"}, + {file = "grpcio-1.59.0-cp38-cp38-win_amd64.whl", hash = "sha256:14890da86a0c0e9dc1ea8e90101d7a3e0e7b1e71f4487fab36e2bfd2ecadd13c"}, + {file = "grpcio-1.59.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:34341d9e81a4b669a5f5dca3b2a760b6798e95cdda2b173e65d29d0b16692857"}, + {file = "grpcio-1.59.0-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:986de4aa75646e963466b386a8c5055c8b23a26a36a6c99052385d6fe8aaf180"}, + {file = "grpcio-1.59.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:aca8a24fef80bef73f83eb8153f5f5a0134d9539b4c436a716256b311dda90a6"}, + {file = "grpcio-1.59.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:936b2e04663660c600d5173bc2cc84e15adbad9c8f71946eb833b0afc205b996"}, + {file = "grpcio-1.59.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc8bf2e7bc725e76c0c11e474634a08c8f24bcf7426c0c6d60c8f9c6e70e4d4a"}, + {file = "grpcio-1.59.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81d86a096ccd24a57fa5772a544c9e566218bc4de49e8c909882dae9d73392df"}, + {file = "grpcio-1.59.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2ea95cd6abbe20138b8df965b4a8674ec312aaef3147c0f46a0bac661f09e8d0"}, + {file = "grpcio-1.59.0-cp39-cp39-win32.whl", hash = "sha256:3b8ff795d35a93d1df6531f31c1502673d1cebeeba93d0f9bd74617381507e3f"}, + {file = "grpcio-1.59.0-cp39-cp39-win_amd64.whl", hash = "sha256:38823bd088c69f59966f594d087d3a929d1ef310506bee9e3648317660d65b81"}, + {file = "grpcio-1.59.0.tar.gz", hash = "sha256:acf70a63cf09dd494000007b798aff88a436e1c03b394995ce450be437b8e54f"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.58.0)"] +protobuf = ["grpcio-tools (>=1.59.0)"] [[package]] name = "h11" @@ -2138,21 +2160,21 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs [[package]] name = "importlib-resources" -version = "6.0.1" +version = "6.1.0" description = "Read resources from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-6.0.1-py3-none-any.whl", hash = "sha256:134832a506243891221b88b4ae1213327eea96ceb4e407a00d790bb0626f45cf"}, - {file = "importlib_resources-6.0.1.tar.gz", hash = "sha256:4359457e42708462b9626a04657c6208ad799ceb41e5c58c57ffa0e6a098a5d4"}, + {file = "importlib_resources-6.1.0-py3-none-any.whl", hash = "sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83"}, + {file = "importlib_resources-6.1.0.tar.gz", hash = "sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"] [[package]] name = "inflection" @@ -2247,13 +2269,13 @@ PyJWT = ">=2.4.0,<3.0.0" [[package]] name = "ipython" -version = "8.12.2" +version = "8.12.3" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.8" files = [ - {file = "ipython-8.12.2-py3-none-any.whl", hash = "sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc"}, - {file = "ipython-8.12.2.tar.gz", hash = "sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea"}, + {file = "ipython-8.12.3-py3-none-any.whl", hash = "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c"}, + {file = "ipython-8.12.3.tar.gz", hash = "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363"}, ] [package.dependencies] @@ -2866,13 +2888,13 @@ requests = ">=2.26" [[package]] name = "mkdocs-material-extensions" -version = "1.1.1" +version = "1.2" description = "Extension pack for Python Markdown and MkDocs Material." optional = false python-versions = ">=3.7" files = [ - {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, - {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, + {file = "mkdocs_material_extensions-1.2-py3-none-any.whl", hash = "sha256:c767bd6d6305f6420a50f0b541b0c9966d52068839af97029be14443849fb8a1"}, + {file = "mkdocs_material_extensions-1.2.tar.gz", hash = "sha256:27e2d1ed2d031426a6e10d5ea06989d67e90bb02acd588bc5673106b5ee5eedf"}, ] [[package]] @@ -2929,74 +2951,67 @@ mkdocstrings = ">=0.20" [[package]] name = "msgpack" -version = "1.0.5" +version = "1.0.7" description = "MessagePack serializer" optional = true -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, - {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, - {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, - {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, - {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, - {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, - {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, - {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, - {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, - {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, - {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, - {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, - {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, - {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, - {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, - {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, + {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, + {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, + {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, + {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, + {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, + {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"}, + {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"}, + {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, + {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, + {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, + {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, ] [[package]] @@ -3184,17 +3199,17 @@ sso = ["social-auth-core[openidconnect,saml] (>=4.4.2,<4.5.0)"] [[package]] name = "nautobot-capacity-metrics" -version = "2.0.0" +version = "2.1.1" description = "Plugin to improve the instrumentation of Nautobot and expose additional metrics (Application Metrics, RQ Worker)." optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8,<3.12" files = [ - {file = "nautobot-capacity-metrics-2.0.0.tar.gz", hash = "sha256:02fbf65a335047252fbc25b10d8bb74f764501479a5528b2b02d09f24913cccd"}, - {file = "nautobot_capacity_metrics-2.0.0-py3-none-any.whl", hash = "sha256:f8cb1e70b876cf7c553b58c7336f7e54bfa492ce29f085436919a7d6dd09cddd"}, + {file = "nautobot_capacity_metrics-2.1.1-py3-none-any.whl", hash = "sha256:cb25c1a6b8f117351e7dd9450ba067bf25901b69132b88a0426c90a6e919b47f"}, + {file = "nautobot_capacity_metrics-2.1.1.tar.gz", hash = "sha256:29c6950a3744a752208206835129c4a574158ce62c0ee261e2390d55e0072854"}, ] [package.dependencies] -nautobot = ">=1.2.0,<2.0.0" +nautobot = ">=1.6.0,<2.0.0" [[package]] name = "nautobot-chatops" @@ -3429,67 +3444,65 @@ files = [ [[package]] name = "pillow" -version = "10.0.0" +version = "10.0.1" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "Pillow-10.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891"}, - {file = "Pillow-10.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf"}, - {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3"}, - {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992"}, - {file = "Pillow-10.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de"}, - {file = "Pillow-10.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485"}, - {file = "Pillow-10.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629"}, - {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"}, - {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"}, - {file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"}, - {file = "Pillow-10.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37"}, - {file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"}, - {file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff"}, - {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"}, - {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"}, - {file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"}, - {file = "Pillow-10.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca"}, - {file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"}, - {file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51"}, - {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86"}, - {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7"}, - {file = "Pillow-10.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0"}, - {file = "Pillow-10.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa"}, - {file = "Pillow-10.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba"}, - {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3"}, - {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017"}, - {file = "Pillow-10.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3"}, - {file = "Pillow-10.0.0.tar.gz", hash = "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, + {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, + {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, + {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, + {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, + {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, + {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, ] [package.extras] @@ -3614,71 +3627,71 @@ files = [ [[package]] name = "psycopg2-binary" -version = "2.9.7" +version = "2.9.8" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.6" files = [ - {file = "psycopg2-binary-2.9.7.tar.gz", hash = "sha256:1b918f64a51ffe19cd2e230b3240ba481330ce1d4b7875ae67305bd1d37b041c"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ea5f8ee87f1eddc818fc04649d952c526db4426d26bab16efbe5a0c52b27d6ab"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2993ccb2b7e80844d534e55e0f12534c2871952f78e0da33c35e648bf002bbff"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbbc3c5d15ed76b0d9db7753c0db40899136ecfe97d50cbde918f630c5eb857a"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:692df8763b71d42eb8343f54091368f6f6c9cfc56dc391858cdb3c3ef1e3e584"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dcfd5d37e027ec393a303cc0a216be564b96c80ba532f3d1e0d2b5e5e4b1e6e"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17cc17a70dfb295a240db7f65b6d8153c3d81efb145d76da1e4a096e9c5c0e63"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e5666632ba2b0d9757b38fc17337d84bdf932d38563c5234f5f8c54fd01349c9"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7db7b9b701974c96a88997d458b38ccb110eba8f805d4b4f74944aac48639b42"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c82986635a16fb1fa15cd5436035c88bc65c3d5ced1cfaac7f357ee9e9deddd4"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4fe13712357d802080cfccbf8c6266a3121dc0e27e2144819029095ccf708372"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-win32.whl", hash = "sha256:122641b7fab18ef76b18860dd0c772290566b6fb30cc08e923ad73d17461dc63"}, - {file = "psycopg2_binary-2.9.7-cp310-cp310-win_amd64.whl", hash = "sha256:f8651cf1f144f9ee0fa7d1a1df61a9184ab72962531ca99f077bbdcba3947c58"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ecc15666f16f97709106d87284c136cdc82647e1c3f8392a672616aed3c7151"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fbb1184c7e9d28d67671992970718c05af5f77fc88e26fd7136613c4ece1f89"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7968fd20bd550431837656872c19575b687f3f6f98120046228e451e4064df"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:094af2e77a1976efd4956a031028774b827029729725e136514aae3cdf49b87b"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26484e913d472ecb6b45937ea55ce29c57c662066d222fb0fbdc1fab457f18c5"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f309b77a7c716e6ed9891b9b42953c3ff7d533dc548c1e33fddc73d2f5e21f9"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6d92e139ca388ccfe8c04aacc163756e55ba4c623c6ba13d5d1595ed97523e4b"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2df562bb2e4e00ee064779902d721223cfa9f8f58e7e52318c97d139cf7f012d"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4eec5d36dbcfc076caab61a2114c12094c0b7027d57e9e4387b634e8ab36fd44"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1011eeb0c51e5b9ea1016f0f45fa23aca63966a4c0afcf0340ccabe85a9f65bd"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-win32.whl", hash = "sha256:ded8e15f7550db9e75c60b3d9fcbc7737fea258a0f10032cdb7edc26c2a671fd"}, - {file = "psycopg2_binary-2.9.7-cp311-cp311-win_amd64.whl", hash = "sha256:8a136c8aaf6615653450817a7abe0fc01e4ea720ae41dfb2823eccae4b9062a3"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dec5a75a3a5d42b120e88e6ed3e3b37b46459202bb8e36cd67591b6e5feebc1"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc10da7e7df3380426521e8c1ed975d22df678639da2ed0ec3244c3dc2ab54c8"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee919b676da28f78f91b464fb3e12238bd7474483352a59c8a16c39dfc59f0c5"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb1c0e682138f9067a58fc3c9a9bf1c83d8e08cfbee380d858e63196466d5c86"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00d8db270afb76f48a499f7bb8fa70297e66da67288471ca873db88382850bf4"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b0c2b466b2f4d89ccc33784c4ebb1627989bd84a39b79092e560e937a11d4ac"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:51d1b42d44f4ffb93188f9b39e6d1c82aa758fdb8d9de65e1ddfe7a7d250d7ad"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:11abdbfc6f7f7dea4a524b5f4117369b0d757725798f1593796be6ece20266cb"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f02f4a72cc3ab2565c6d9720f0343cb840fb2dc01a2e9ecb8bc58ccf95dc5c06"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-win32.whl", hash = "sha256:81d5dd2dd9ab78d31a451e357315f201d976c131ca7d43870a0e8063b6b7a1ec"}, - {file = "psycopg2_binary-2.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:62cb6de84d7767164a87ca97e22e5e0a134856ebcb08f21b621c6125baf61f16"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59f7e9109a59dfa31efa022e94a244736ae401526682de504e87bd11ce870c22"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:95a7a747bdc3b010bb6a980f053233e7610276d55f3ca506afff4ad7749ab58a"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c721ee464e45ecf609ff8c0a555018764974114f671815a0a7152aedb9f3343"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4f37bbc6588d402980ffbd1f3338c871368fb4b1cfa091debe13c68bb3852b3"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac83ab05e25354dad798401babaa6daa9577462136ba215694865394840e31f8"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:024eaeb2a08c9a65cd5f94b31ace1ee3bb3f978cd4d079406aef85169ba01f08"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1c31c2606ac500dbd26381145684d87730a2fac9a62ebcfbaa2b119f8d6c19f4"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:42a62ef0e5abb55bf6ffb050eb2b0fcd767261fa3faf943a4267539168807522"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7952807f95c8eba6a8ccb14e00bf170bb700cafcec3924d565235dffc7dc4ae8"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e02bc4f2966475a7393bd0f098e1165d470d3fa816264054359ed4f10f6914ea"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-win32.whl", hash = "sha256:fdca0511458d26cf39b827a663d7d87db6f32b93efc22442a742035728603d5f"}, - {file = "psycopg2_binary-2.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:d0b16e5bb0ab78583f0ed7ab16378a0f8a89a27256bb5560402749dbe8a164d7"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6822c9c63308d650db201ba22fe6648bd6786ca6d14fdaf273b17e15608d0852"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f94cb12150d57ea433e3e02aabd072205648e86f1d5a0a692d60242f7809b15"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5ee89587696d808c9a00876065d725d4ae606f5f7853b961cdbc348b0f7c9a1"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad5ec10b53cbb57e9a2e77b67e4e4368df56b54d6b00cc86398578f1c635f329"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:642df77484b2dcaf87d4237792246d8068653f9e0f5c025e2c692fc56b0dda70"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6a8b575ac45af1eaccbbcdcf710ab984fd50af048fe130672377f78aaff6fc1"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f955aa50d7d5220fcb6e38f69ea126eafecd812d96aeed5d5f3597f33fad43bb"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ad26d4eeaa0d722b25814cce97335ecf1b707630258f14ac4d2ed3d1d8415265"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ced63c054bdaf0298f62681d5dcae3afe60cbae332390bfb1acf0e23dcd25fc8"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b04da24cbde33292ad34a40db9832a80ad12de26486ffeda883413c9e1b1d5e"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-win32.whl", hash = "sha256:18f12632ab516c47c1ac4841a78fddea6508a8284c7cf0f292cb1a523f2e2379"}, - {file = "psycopg2_binary-2.9.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb3b8d55924a6058a26db69fb1d3e7e32695ff8b491835ba9f479537e14dcf9f"}, + {file = "psycopg2-binary-2.9.8.tar.gz", hash = "sha256:80451e6b6b7c486828d5c7ed50769532bbb04ec3a411f1e833539d5c10eb691c"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e271ad6692d50d70ca75db3bd461bfc26316de78de8fe1f504ef16dcea8f2312"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ae22a0fa5c516b84ddb189157fabfa3f12eded5d630e1ce260a18e1771f8707"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a971086db0069aef2fd22ccffb670baac427f4ee2174c4f5c7206254f1e6794"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b6928a502af71ca2ac9aad535e78c8309892ed3bfa7933182d4c760580c8af4"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f955fe6301b84b6fd13970a05f3640fbb62ca3a0d19342356585006c830e038"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3723c3f009e2b2771f2491b330edb7091846f1aad0c08fbbd9a1383d6a0c0841"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e3142c7e51b92855cff300580de949e36a94ab3bfa8f353b27fe26535e9b3542"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:de85105c568dc5f0f0efe793209ba83e4675d53d00faffc7a7c7a8bea9e0e19a"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c7ff2b6a79a92b1b169b03bb91b41806843f0cdf6055256554495bffed1d496d"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59f45cca0765aabb52a5822c72d5ff2ec46a28b1c1702de90dc0d306ec5c2001"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-win32.whl", hash = "sha256:1dbad789ebd1e61201256a19dc2e90fed4706bc966ccad4f374648e5336b1ab4"}, + {file = "psycopg2_binary-2.9.8-cp310-cp310-win_amd64.whl", hash = "sha256:15458c81b0d199ab55825007115f697722831656e6477a427783fe75c201c82b"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:395c217156723fe21809dfe8f7a433c5bf8e9bce229944668e4ec709c37c5442"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14f85ff2d5d826a7ce9e6c31e803281ed5a096789f47f52cb728c88f488de01b"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e11373d8e4f1f46cf3065bf613f0df9854803dc95aa4a35354ffac19f8c52127"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01f9731761f711e42459f87bd2ad5d744b9773b5dd05446f3b579a0f077e78e3"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bf5c27bd5867a5fa5341fad29f0d5838e2fed617ef5346884baf8b8b16dd82"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfabbd7e70785af726cc0209e8e64b926abf91741eca80678b221aad9e72135"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6369f4bd4d27944498094dccced1ae7ca43376a59dbfe4c8b6a16e9e3dc3ccce"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4879ee1d07a6b2c232ae6a74570f4788cd7a29b3cd38bc39bf60225b1d075c78"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4336afc0e81726350bd5863e3c3116d8c12aa7f457d3d0b3b3dc36137fec6feb"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:63ce1dccfd08d9c5341ac82d62aa04345bc4bf41b5e5b7b2c6c172a28e0eda27"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-win32.whl", hash = "sha256:59421806c1a0803ea7de9ed061d656c041a84db0da7e73266b98db4c7ba263da"}, + {file = "psycopg2_binary-2.9.8-cp311-cp311-win_amd64.whl", hash = "sha256:ccaa2ae03990cedde1f618ff11ec89fefa84622da73091a67b44553ca8be6711"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5aa0c99c12075c593dcdccbb8a7aaa714b716560cc99ef9206f9e75b77520801"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91719f53ed2a95ebecefac48d855d811cba9d9fe300acc162993bdfde9bc1c3b"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c68a2e1afb4f2a5bb4b7bb8f90298d21196ac1c66418523e549430b8c4b7cb1e"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278ebd63ced5a5f3af5394cb75a9a067243eee21f42f0126c6f1cf85eaeb90f9"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c84ff9682bc4520504c474e189b3de7c4a4029e529c8b775e39c95c33073767"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6f5e70e40dae47a4dc7f8eb390753bb599b0f4ede314580e6faa3b7383695d19"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:673eafbdaa4ed9f5164c90e191c3895cc5f866b9b379fdb59f3a2294e914d9bd"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:5a0a6e4004697ec98035ff3b8dfc4dba8daa477b23ee891d831cd3cd65ace6be"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d29efab3c5d6d978115855a0f2643e0ee8c6450dc536d5b4afec6f52ab99e99e"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-win32.whl", hash = "sha256:d4a19a3332f2ac6d093e60a6f1c589f97eb9f9de7e27ea80d67f188384e31572"}, + {file = "psycopg2_binary-2.9.8-cp37-cp37m-win_amd64.whl", hash = "sha256:5262713988d97a9d4cd54b682dec4a413b87b76790e5b16f480450550d11a8f7"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e46b0f4683539965ce849f2c13fc53e323bb08d84d4ba2e4b3d976f364c84210"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3fd44b52bc9c74c1512662e8da113a1c55127adeeacebaf460babe766517b049"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b6c607ecb6a9c245ebe162d63ccd9222d38efa3c858bbe38d32810b08b8f87e"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6ef615d48fa60361e57f998327046bd89679c25d06eee9e78156be5a7a76e03"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65403113ac3a4813a1409fb6a1e43c658b459cc8ed8afcc5f4baf02ec8be4334"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5debcb23a052f3fb4c165789ea513b562b2fac0f0f4f53eaf3cf4dc648907ff8"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dc145a241e1f6381efb924bcf3e3462d6020b8a147363f9111eb0a9c89331ad7"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1d669887df169a9b0c09e0f5b46891511850a9ddfcde3593408af9d9774c5c3a"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:19d40993701e39c49b50e75cd690a6af796d7e7210941ee0fe49cf12b25840e5"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b8b2cdf3bce4dd91dc035fbff4eb812f5607dda91364dc216b0920b97b521c7"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-win32.whl", hash = "sha256:4960c881471ca710b81a67ef148c33ee121c1f8e47a639cf7e06537fe9fee337"}, + {file = "psycopg2_binary-2.9.8-cp38-cp38-win_amd64.whl", hash = "sha256:aeb09db95f38e75ae04e947d283e07be34d03c4c2ace4f0b73dbb9143d506e67"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5aef3296d44d05805e634dbbd2972aa8eb7497926dd86047f5e39a79c3ecc086"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d6b592ecc8667e608b9e7344259fbfb428cc053df0062ec3ac75d8270cd5a9f"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:693a4e7641556f0b421a7d6c6a74058aead407d860ac1cb9d0bf25be0ca73de8"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf60c599c40c266a01c458e9c71db7132b11760f98f08233f19b3e0a2153cbf1"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cbe1e19f59950afd66764e3c905ecee9f2aee9f8df2ef35af6f7948ad93f620"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc37de7e3a87f5966965fc874d33c9b68d638e6c3718fdf32a5083de563428b0"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e1bb4eb0d9925d65dabaaabcbb279fab444ba66d73f86d4c07dfd11f0139c06"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e7bdc94217ae20ad03b375a991e107a31814053bee900ad8c967bf82ef3ff02e"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:565edaf9f691b17a7fdbabd368b5b3e67d0fdc8f7f6b52177c1d3289f4e763fd"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0e3071c947bda6afc6fe2e7b64ebd64fb2cad1bc0e705a3594cb499291f2dfec"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-win32.whl", hash = "sha256:205cecdd81ff4f1ddd687ce7d06879b9b80cccc428d8d6ebf36fcba08bb6d361"}, + {file = "psycopg2_binary-2.9.8-cp39-cp39-win_amd64.whl", hash = "sha256:1f279ba74f0d6b374526e5976c626d2ac3b8333b6a7b08755c513f4d380d3add"}, ] [[package]] @@ -3810,47 +3823,47 @@ files = [ [[package]] name = "pydantic" -version = "1.10.12" +version = "1.10.13" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, - {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, - {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, - {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, - {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, - {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, - {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, - {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, - {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, - {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, + {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, + {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, + {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, + {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, + {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, + {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, + {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, + {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, ] [package.dependencies] @@ -3921,17 +3934,17 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pylint" -version = "2.17.5" +version = "2.17.6" description = "python code static checker" optional = false python-versions = ">=3.7.2" files = [ - {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"}, - {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"}, + {file = "pylint-2.17.6-py3-none-any.whl", hash = "sha256:18a1412e873caf8ffb56b760ce1b5643675af23e6173a247b502406b24c716af"}, + {file = "pylint-2.17.6.tar.gz", hash = "sha256:be928cce5c76bf9acdc65ad01447a1e0b1a7bccffc609fb7fc40f2513045bd05"}, ] [package.dependencies] -astroid = ">=2.15.6,<=2.17.0-dev0" +astroid = ">=2.15.7,<=2.17.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -4286,13 +4299,13 @@ pyyaml = "*" [[package]] name = "redis" -version = "5.0.0" +version = "5.0.1" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.7" files = [ - {file = "redis-5.0.0-py3-none-any.whl", hash = "sha256:06570d0b2d84d46c21defc550afbaada381af82f5b83e5b3777600e05d8e2ed0"}, - {file = "redis-5.0.0.tar.gz", hash = "sha256:5cea6c0d335c9a7332a460ed8729ceabb4d0c489c7285b0a86dbbf8a017bd120"}, + {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, + {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"}, ] [package.dependencies] @@ -4570,24 +4583,25 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar [[package]] name = "setuptools-scm" -version = "7.1.0" +version = "8.0.3" description = "the blessed package to manage your versions by scm tags" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools_scm-7.1.0-py3-none-any.whl", hash = "sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e"}, - {file = "setuptools_scm-7.1.0.tar.gz", hash = "sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27"}, + {file = "setuptools-scm-8.0.3.tar.gz", hash = "sha256:0169fd70197efda2f8c4d0b2a7a3d614431b488116f37b79d031e9e7ec884d8c"}, + {file = "setuptools_scm-8.0.3-py3-none-any.whl", hash = "sha256:813822234453438a13c78d05c8af29918fbc06f88efb33d38f065340bbb48c39"}, ] [package.dependencies] -packaging = ">=20.0" +packaging = ">=20" setuptools = "*" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} -typing-extensions = "*" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} +typing-extensions = {version = "*", markers = "python_version < \"3.11\""} [package.extras] -test = ["pytest (>=6.2)", "virtualenv (>20)"] -toml = ["setuptools (>=42)"] +docs = ["entangled-cli[rich]", "mkdocs", "mkdocs-entangled-plugin", "mkdocs-material", "mkdocstrings[python]", "pygments"] +rich = ["rich"] +test = ["pytest", "rich", "virtualenv (>20)"] [[package]] name = "singledispatch" @@ -4632,13 +4646,13 @@ testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "We [[package]] name = "smmap" -version = "5.0.0" +version = "5.0.1" description = "A pure Python implementation of a sliding window memory map manager" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, - {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, ] [[package]] @@ -4979,18 +4993,18 @@ files = [ [[package]] name = "traitlets" -version = "5.9.0" +version = "5.10.1" description = "Traitlets Python configuration system" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, - {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, + {file = "traitlets-5.10.1-py3-none-any.whl", hash = "sha256:07ab9c5bf8a0499fd7b088ba51be899c90ffc936ffc797d7b6907fc516bcd116"}, + {file = "traitlets-5.10.1.tar.gz", hash = "sha256:db9c4aa58139c3ba850101913915c042bdba86f7c8a0dda1c6f7f92c5da8e542"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.5.1)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "types-protobuf" @@ -5005,24 +5019,24 @@ files = [ [[package]] name = "types-pyyaml" -version = "6.0.12.11" +version = "6.0.12.12" description = "Typing stubs for PyYAML" optional = true python-versions = "*" files = [ - {file = "types-PyYAML-6.0.12.11.tar.gz", hash = "sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b"}, - {file = "types_PyYAML-6.0.12.11-py3-none-any.whl", hash = "sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d"}, + {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"}, + {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"}, ] [[package]] name = "types-requests" -version = "2.31.0.2" +version = "2.31.0.6" description = "Typing stubs for requests" optional = true -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "types-requests-2.31.0.2.tar.gz", hash = "sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40"}, - {file = "types_requests-2.31.0.2-py3-none-any.whl", hash = "sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a"}, + {file = "types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0"}, + {file = "types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9"}, ] [package.dependencies] @@ -5052,13 +5066,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] [[package]] @@ -5085,13 +5099,13 @@ files = [ [[package]] name = "urllib3" -version = "2.0.4" +version = "2.0.5" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"}, + {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"}, ] [package.extras] @@ -5152,13 +5166,13 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "wcwidth" -version = "0.2.6" +version = "0.2.7" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, - {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, + {file = "wcwidth-0.2.7-py2.py3-none-any.whl", hash = "sha256:fabf3e32999d9b0dab7d19d845149f326f04fe29bac67709ee071dbd92640a36"}, + {file = "wcwidth-0.2.7.tar.gz", hash = "sha256:1b6d30a98ddd5ce9bbdb33658191fd2423fc9da203fe3ef1855407dcb7ee4e26"}, ] [[package]] @@ -5369,23 +5383,24 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.16.2" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [extras] aci = ["PyYAML"] all = ["Jinja2", "PyYAML", "cloudvision", "cvprac", "dnspython", "ijson", "ipfabric", "ipfabric-diagrams", "nautobot-device-lifecycle-mgmt", "netutils", "oauthlib", "python-magic", "pytz", "requests", "requests-oauthlib", "six"] aristacv = ["cloudvision", "cvprac"] +device42 = ["requests"] infoblox = ["dnspython"] ipfabric = ["ipfabric", "ipfabric-diagrams", "netutils"] nautobot-device-lifecycle-mgmt = ["nautobot-device-lifecycle-mgmt"] @@ -5395,4 +5410,4 @@ servicenow = ["Jinja2", "PyYAML", "ijson", "oauthlib", "python-magic", "pytz", " [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.12" -content-hash = "b4ac81644d5fe8ef27daa9ee0605720904530e4331bb74a99d62ad26364f410c" +content-hash = "dc6ccb62721c4166cec61bae26100582db540e0c83b7ba921b4ecde9a2cca111"