Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External Integrations #398

Merged
merged 10 commits into from
Apr 16, 2024
1 change: 1 addition & 0 deletions changes/398.changed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Changed Arista Cloud Vision jobs to optionally use ExternalIntegration.
1 change: 1 addition & 0 deletions nautobot_ssot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class NautobotSSOTAppConfig(NautobotAppConfig):
"aristacv_from_cloudvision_default_site": "",
"aristacv_hostname_patterns": [],
"aristacv_import_active": False,
"aristacv_external_integration_name": "",
"aristacv_role_mappings": {},
"aristacv_site_mappings": {},
"aristacv_verify": True,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"""Storage of data that will not change throughout the life cycle of the application."""

from django.conf import settings


def _read_settings() -> dict:
config = settings.PLUGINS_CONFIG["nautobot_ssot"]
return config


APP_SETTINGS = _read_settings()
ARISTA_PLATFORM = "arista.eos.eos"
CLOUDVISION_PLATFORM = "Arista EOS-CloudVision"
DEFAULT_APPLY_IMPORT_TAG = False
DEFAULT_CREATE_CONTROLLER = False
DEFAULT_CVAAS_URL = "https://www.arista.io"
DEFAULT_DELETE_DEVICES_ON_SYNC = False
DEFAULT_DEVICE_ROLE = "network"
DEFAULT_DEVICE_ROLE_COLOR = "ff0000"
DEFAULT_DEVICE_STATUS = "cloudvision_imported"
DEFAULT_DEVICE_STATUS_COLOR = "ff0000"
DEFAULT_IMPORT_ACTIVE = False
DEFAULT_SITE = "cloudvision_imported"
DEFAULT_VERIFY_SSL = True

PORT_TYPE_MAP = {
"xcvr1000BaseT": "1000base-t",
Expand Down Expand Up @@ -83,7 +87,3 @@ def _read_settings() -> dict:
"400GBASE-2FR4": "400gbase-x-osfp",
"400GBASE-ZR": "400gbase-x-qsfpdd",
}

CLOUDVISION_PLATFORM = "Arista EOS-CloudVision"

ARISTA_PLATFORM = "arista.eos.eos"
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from diffsync import DiffSync
from diffsync.exceptions import ObjectAlreadyExists, ObjectNotFound

from nautobot_ssot.integrations.aristacv.constant import APP_SETTINGS
from nautobot_ssot.integrations.aristacv.diffsync.models.cloudvision import (
CloudvisionCustomField,
CloudvisionDevice,
Expand All @@ -17,6 +16,7 @@
CloudvisionIPAddress,
CloudvisionIPAssignment,
)
from nautobot_ssot.integrations.aristacv.types import CloudVisionAppConfig
from nautobot_ssot.integrations.aristacv.utils import cloudvision


Expand All @@ -41,8 +41,13 @@ def __init__(self, *args, job=None, conn: cloudvision.CloudvisionApi, **kwargs):

def load_devices(self):
"""Load devices from CloudVision."""
if APP_SETTINGS.get("aristacv_create_controller"):
cvp_version = cloudvision.get_cvp_version()
config: CloudVisionAppConfig = self.job.app_config
if config.hostname_patterns and not (config.site_mappings and config.role_mappings):
self.job.logger.warning(
"Configuration found for aristacv_hostname_patterns but no aristacv_site_mappings or aristacv_role_mappings. Please ensure your mappings are defined."
)
if config.create_controller:
cvp_version = cloudvision.get_cvp_version(config)
cvp_ver_cf = self.cf(name="arista_eos", value=cvp_version, device_name="CloudVision")
try:
self.add(cvp_ver_cf)
Expand Down Expand Up @@ -258,10 +263,4 @@ def load_device_tags(self, device):

def load(self):
"""Load devices and associated data from CloudVision."""
if APP_SETTINGS.get("aristacv_hostname_patterns") and not (
APP_SETTINGS.get("aristacv_site_mappings") and APP_SETTINGS.get("aristacv_role_mappings")
):
self.job.logger.warning(
"Configuration found for aristacv_hostname_patterns but no aristacv_site_mappings or aristacv_role_mappings. Please ensure your mappings are defined."
)
self.load_devices()
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from diffsync import DiffSync
from diffsync.exceptions import ObjectNotFound, ObjectAlreadyExists

from nautobot_ssot.integrations.aristacv.constant import APP_SETTINGS
from nautobot_ssot.integrations.aristacv.diffsync.models.nautobot import (
NautobotDevice,
NautobotCustomField,
Expand All @@ -21,6 +20,7 @@
NautobotIPAssignment,
NautobotPort,
)
from nautobot_ssot.integrations.aristacv.types import CloudVisionAppConfig
from nautobot_ssot.integrations.aristacv.utils import nautobot


Expand Down Expand Up @@ -166,8 +166,9 @@ def sync_complete(self, source: DiffSync, *args, **kwargs):
self.job.logger.warning(f"Deletion failed for protected object: {nautobot_object}. {err}")
self.objects_to_delete[grouping] = []

config: CloudVisionAppConfig = self.job.app_config # type: ignore
# if Controller is created we need to ensure all imported Devices have RelationshipAssociation to it.
if APP_SETTINGS.get("aristacv_create_controller"):
if config.create_controller:
self.job.logger.info("Creating Relationships between CloudVision and connected Devices.")
controller_relation = OrmRelationship.objects.get(label="Controller -> Device")
device_ct = ContentType.objects.get_for_model(OrmDevice)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Cloudvision DiffSync models for AristaCV SSoT."""
from nautobot_ssot.integrations.aristacv.constant import APP_SETTINGS
"""CloudVision DiffSync models for AristaCV SSoT."""
from nautobot_ssot.integrations.aristacv.diffsync.models.base import (
Device,
CustomField,
Expand All @@ -9,11 +8,12 @@
IPAssignment,
Port,
)
from nautobot_ssot.integrations.aristacv.types import CloudVisionAppConfig
from nautobot_ssot.integrations.aristacv.utils.cloudvision import CloudvisionApi


class CloudvisionDevice(Device):
"""Cloudvision Device model."""
"""CloudVision Device model."""

@classmethod
def create(cls, diffsync, ids, attrs):
Expand All @@ -30,7 +30,7 @@ def delete(self):


class CloudvisionPort(Port):
"""Cloudvision Port model."""
"""CloudVision Port model."""

@classmethod
def create(cls, diffsync, ids, attrs):
Expand All @@ -47,7 +47,7 @@ def delete(self):


class CloudvisionNamespace(Namespace):
"""Cloudvision Namespace model."""
"""CloudVision Namespace model."""

@classmethod
def create(cls, diffsync, ids, attrs):
Expand All @@ -67,7 +67,7 @@ def delete(self):


class CloudvisionPrefix(Prefix):
"""Cloudvision IPAdress model."""
"""CloudVision IPAdress model."""

@classmethod
def create(cls, diffsync, ids, attrs):
Expand All @@ -87,7 +87,7 @@ def delete(self):


class CloudvisionIPAddress(IPAddress):
"""Cloudvision IPAdress model."""
"""CloudVision IPAdress model."""

@classmethod
def create(cls, diffsync, ids, attrs):
Expand All @@ -107,7 +107,7 @@ def delete(self):


class CloudvisionIPAssignment(IPAssignment):
"""Cloudvision IPAssignment model."""
"""CloudVision IPAssignment model."""

@classmethod
def create(cls, diffsync, ids, attrs):
Expand All @@ -127,24 +127,19 @@ def delete(self):


class CloudvisionCustomField(CustomField):
"""Cloudvision CustomField model."""
"""CloudVision CustomField model."""

@staticmethod
def connect_cvp():
"""Connect to Cloudvision gRPC endpoint."""
return CloudvisionApi(
cvp_host=APP_SETTINGS["aristacv_cvp_host"],
cvp_port=APP_SETTINGS.get("aristacv_cvp_port", "8443"),
verify=APP_SETTINGS["aristacv_verify"],
username=APP_SETTINGS["aristacv_cvp_user"],
password=APP_SETTINGS["aristacv_cvp_password"],
cvp_token=APP_SETTINGS["aristacv_cvp_token"],
)
def connect_cvp(config: CloudVisionAppConfig):
"""Connect to CloudVision gRPC endpoint."""
return CloudvisionApi(config)

@classmethod
def create(cls, diffsync, ids, attrs):
"""Create a user tag in cvp."""
cvp = cls.connect_cvp()
config: CloudVisionAppConfig = diffsync.job.app_config # type: ignore
# TBD: Isn't this a performance bottleneck? We are connecting to CVP for each operation.
cvp = cls.connect_cvp(config)
cvp.create_tag(ids["name"], attrs["value"])
# Create mapping from device_name to CloudVision device_id
device_ids = {dev["hostname"]: dev["device_id"] for dev in cvp.get_devices()}
Expand All @@ -159,7 +154,9 @@ def create(cls, diffsync, ids, attrs):

def update(self, attrs):
"""Update user tag in cvp."""
cvp = self.connect_cvp()
config: CloudVisionAppConfig = self.diffsync.job.app_config # type: ignore
# TBD: Isn't this a performance bottleneck? We are connecting to CVP for each operation.
cvp = self.connect_cvp(config)
remove = set(self.device_name) - set(attrs["devices"])
add = set(attrs["devices"]) - set(self.device_name)
# Create mapping from device_name to CloudVision device_id
Expand All @@ -180,7 +177,9 @@ def update(self, attrs):

def delete(self):
"""Delete user tag applied to devices in cvp."""
cvp = self.connect_cvp()
config: CloudVisionAppConfig = self.diffsync.job.app_config # type: ignore
# TBD: Isn't this performance bottleneck? We are connecting to CVP for each operation.
cvp = self.connect_cvp(config)
device_ids = {dev["hostname"]: dev["device_id"] for dev in cvp.get_devices()}
for device in self.device_name:
cvp.remove_tag_from_device(device_ids[device], self.name, self.value)
Expand Down
49 changes: 19 additions & 30 deletions nautobot_ssot/integrations/aristacv/diffsync/models/nautobot.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
from nautobot.ipam.models import IPAddressToInterface
import distutils

from nautobot_ssot.integrations.aristacv.constant import (
APP_SETTINGS,
from nautobot_ssot.integrations.aristacv.constants import (
ARISTA_PLATFORM,
CLOUDVISION_PLATFORM,
DEFAULT_DEVICE_ROLE_COLOR,
)
from nautobot_ssot.integrations.aristacv.diffsync.models.base import (
Device,
Expand All @@ -28,6 +28,7 @@
Port,
Prefix,
)
from nautobot_ssot.integrations.aristacv.types import CloudVisionAppConfig
from nautobot_ssot.integrations.aristacv.utils import nautobot

try:
Expand All @@ -38,15 +39,6 @@
print("Device Lifecycle app isn't installed so will revert to CustomField for OS version.")
LIFECYCLE_MGMT = False


# TODO: Move to constant.py
DEFAULT_SITE = "cloudvision_imported"
DEFAULT_DEVICE_ROLE = "network"
DEFAULT_DEVICE_ROLE_COLOR = "ff0000"
DEFAULT_DEVICE_STATUS = "cloudvision_imported"
DEFAULT_DEVICE_STATUS_COLOR = "ff0000"
DEFAULT_DELETE_DEVICES_ON_SYNC = False
APPLY_IMPORT_TAG = False
MISSING_CUSTOM_FIELDS = []


Expand All @@ -56,40 +48,35 @@ class NautobotDevice(Device):
@classmethod
def create(cls, diffsync, ids, attrs):
"""Create device object in Nautobot."""
site_code, role_code = nautobot.parse_hostname(ids["name"].lower())
site_map = APP_SETTINGS.get("aristacv_site_mappings")
role_map = APP_SETTINGS.get("aristacv_role_mappings")
config: CloudVisionAppConfig = diffsync.job.app_config # type: ignore
site_code, role_code = nautobot.parse_hostname(ids["name"].lower(), config.hostname_patterns)
site_map = config.site_mappings
role_map = config.role_mappings

if site_code and site_code in site_map:
site = nautobot.verify_site(site_map[site_code])
elif "CloudVision" in ids["name"]:
if APP_SETTINGS.get("aristacv_controller_site"):
site = nautobot.verify_site(APP_SETTINGS["aristacv_controller_site"])
if config.controller_site:
site = nautobot.verify_site(config.controller_site)
else:
site = nautobot.verify_site("CloudVision")
else:
site = nautobot.verify_site(APP_SETTINGS.get("aristacv_from_cloudvision_default_site", DEFAULT_SITE))
site = nautobot.verify_site(config.from_cloudvision_default_site)

if role_code and role_code in role_map:
role = nautobot.verify_device_role_object(
role_map[role_code],
APP_SETTINGS.get(
"aristacv_from_cloudvision_default_device_role_color",
DEFAULT_DEVICE_ROLE_COLOR,
),
config.from_cloudvision_default_device_role_color,
)
elif "CloudVision" in ids["name"]:
role = nautobot.verify_device_role_object("Controller", DEFAULT_DEVICE_ROLE_COLOR)
else:
role = nautobot.verify_device_role_object(
APP_SETTINGS.get("aristacv_from_cloudvision_default_device_role", DEFAULT_DEVICE_ROLE),
APP_SETTINGS.get(
"aristacv_from_cloudvision_default_device_role_color",
DEFAULT_DEVICE_ROLE_COLOR,
),
config.from_cloudvision_default_device_role,
config.from_cloudvision_default_device_role_color,
)

if APP_SETTINGS.get("aristacv_create_controller") and "CloudVision" in ids["name"]:
if config.create_controller and "CloudVision" in ids["name"]:
platform = OrmPlatform.objects.get(name=CLOUDVISION_PLATFORM)
else:
platform = OrmPlatform.objects.get(name=ARISTA_PLATFORM)
Expand All @@ -106,7 +93,7 @@ def create(cls, diffsync, ids, attrs):
serial=attrs["serial"] if attrs.get("serial") else "",
)

if APP_SETTINGS.get("aristacv_apply_import_tag", APPLY_IMPORT_TAG):
if config.apply_import_tag:
import_tag = nautobot.verify_import_tag()
new_device.tags.add(import_tag)
try:
Expand Down Expand Up @@ -143,7 +130,8 @@ def update(self, attrs):

def delete(self):
"""Delete device object in Nautobot."""
if APP_SETTINGS.get("aristacv_delete_devices_on_sync", DEFAULT_DELETE_DEVICES_ON_SYNC):
config: CloudVisionAppConfig = self.diffsync.job.app_config # type: ignore
if config.delete_devices_on_sync:
self.diffsync.job.logger.warning(f"Device {self.name} will be deleted per app settings.")
device = OrmDevice.objects.get(id=self.uuid)
self.diffsync.objects_to_delete["devices"].append(device)
Expand Down Expand Up @@ -242,7 +230,8 @@ def update(self, attrs):

def delete(self):
"""Delete Interface in Nautobot."""
if APP_SETTINGS.get("aristacv_delete_devices_on_sync"):
config: CloudVisionAppConfig = self.diffsync.job.app_config # type: ignore
if config.delete_devices_on_sync:
super().delete()
if self.diffsync.job.debug:
self.diffsync.job.logger.warning(f"Interface {self.name} for {self.device} will be deleted.")
Expand Down