Repository: nautobot/nautobot-plugin-ssot-arista-cloudvision (language: python)

Issue:
fromcv working



Discussion / hints:

Fix patch:
diff --git a/aristacv_sync/__init__.py b/aristacv_sync/__init__.py
deleted file mode 100644
index e150e6c..0000000
--- a/aristacv_sync/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""Plugin declaration for aristacv_sync."""
-
-__version__ = "0.1.0"
-
-from nautobot.extras.plugins import PluginConfig
-
-
-class AristaCVSyncConfig(PluginConfig):
-    """Plugin configuration for the aristacv_sync plugin."""
-
-    name = "aristacv_sync"
-    verbose_name = "Nautobot to Arista CloudVision Sync"
-    version = __version__
-    author = "Network to Code, LLC"
-    description = "Nautobot to Arista CloudVision Sync."
-    base_url = "aristacv-sync"
-    required_settings = []
-    min_version = "1.0.0"
-    max_version = "1.9999"
-    default_settings = {}
-    caching_config = {}
-
-
-config = AristaCVSyncConfig  # pylint:disable=invalid-name
diff --git a/aristacv_sync/jobs.py b/aristacv_sync/jobs.py
deleted file mode 100644
index f5f11f4..0000000
--- a/aristacv_sync/jobs.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from grpc import RpcError
-
-from nautobot.extras.jobs import Job, BooleanVar
-
-from aristacv_sync.diffsync.tocv.cloudvision import CloudVision
-from aristacv_sync.diffsync.tocv.nautobot import Nautobot
-
-
-class FormEntry:
-    debug = BooleanVar(description="Enable for more verbose debug logging")
-
-
-class CVSyncFromJob(Job, FormEntry):
-    debug = FormEntry.debug
-
-    class Meta:
-        name = "Sync from CloudVision"
-        description = "Sync system tag data from CloudVision to Nautobot"
-
-    def run(self, data, commit):
-        cv = CloudVision()
-        self.log("Loading data from CloudVision")
-        cv.load()
-
-
-class CVSyncToJob(Job, FormEntry):
-    debug = FormEntry.debug
-
-    class Meta:
-        name = "Sync to CloudVision"
-        description = "Sync custom tag data from Nautobot to CloudVision"
-
-    def run(self, data, commit):
-        self.log("Loading data from CloudVision")
-        cv = CloudVision()
-        cv.load()
-        self.log("Loading data from Nautobot")
-        nb = Nautobot()
-        nb.load()
-        self.log("Performing diff between Nautobot and Cloudvision")
-        diff_nb_cv = cv.diff_from(nb)
-        self.log(diff_nb_cv.summary())
-        if data["debug"]:
-            self.log_debug(diff_nb_cv.dict())
-        if commit:
-            self.log("Syncing to CloudVision")
-            try:
-                nb.sync_to(cv)
-            except RpcError as e:
-                self.log_failure("Sync failed.")
-                raise e
-            self.log_success(message="Sync complete")
-
-
-jobs = [CVSyncFromJob, CVSyncToJob]
diff --git a/aristacv_sync/navigation.py b/aristacv_sync/navigation.py
deleted file mode 100644
index 863e710..0000000
--- a/aristacv_sync/navigation.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""Add Nautobot AristaCV Sync Navigation."""
-from nautobot.extras.plugins import PluginMenuItem
-
-
-menu_items = (
-    PluginMenuItem(
-        link="plugins:aristacv_sync:syncfromcv",
-        link_text="Sync from Arista CloudVision",
-    ),
-    PluginMenuItem(
-        link="plugins:aristacv_sync:synctocv",
-        link_text="Sync to Arista CloudVision",
-    ),
-)
diff --git a/aristacv_sync/urls.py b/aristacv_sync/urls.py
deleted file mode 100644
index 2769024..0000000
--- a/aristacv_sync/urls.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""Django urlpatterns declaration for aristacv_sync plugin."""
-from django.urls import path
-
-from aristacv_sync import views
-
-app_name = "aristacv_sync"
-
-urlpatterns = [
-    path("syncfromcv/", views.CVSyncFromJobRedirect.as_view(), name="syncfromcv"),
-    path("synctocv/", views.CVSyncToJobRedirect.as_view(), name="synctocv"),
-]
diff --git a/aristacv_sync/views.py b/aristacv_sync/views.py
deleted file mode 100644
index 339d57f..0000000
--- a/aristacv_sync/views.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from django.views.generic import RedirectView
-from django.urls import reverse
-
-
-class CVSyncFromJobRedirect(RedirectView):
-    def get_redirect_url(self, *args, **kwargs):
-        return reverse("extras:job", kwargs={"class_path": "plugins/aristacv_sync.jobs/CVSyncFromJob"})
-
-
-class CVSyncToJobRedirect(RedirectView):
-    def get_redirect_url(self, *args, **kwargs):
-        return reverse("extras:job", kwargs={"class_path": "plugins/aristacv_sync.jobs/CVSyncToJob"})
diff --git a/development/Dockerfile b/development/Dockerfile
index c9d5637..9ece4b6 100644
--- a/development/Dockerfile
+++ b/development/Dockerfile
@@ -8,6 +8,10 @@ WORKDIR /source
 
 # Copy in only pyproject.toml/poetry.lock to help with caching this layer if no updates to dependencies
 COPY poetry.lock pyproject.toml /source/
+
+# Copy packages directory with wheel files
+COPY packages /source/packages/
+
 # --no-root declares not to install the project package since we're wanting to take advantage of caching dependency installation
 # and the project is copied in and installed after this step
 RUN poetry install --no-interaction --no-ansi --no-root
diff --git a/development/nautobot_config.py b/development/nautobot_config.py
index d640a01..e5e6e9a 100644
--- a/development/nautobot_config.py
+++ b/development/nautobot_config.py
@@ -244,7 +244,7 @@ def is_truthy(arg):
 PAGINATE_COUNT = int(os.environ.get("PAGINATE_COUNT", 50))
 
 # Enable installed plugins. Add the name of each plugin to the list.
-PLUGINS = ["aristacv_sync"]
+PLUGINS = ["nautobot_ssot", "nautobot_ssot_aristacv"]
 
 # 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.
diff --git a/nautobot_ssot_aristacv/__init__.py b/nautobot_ssot_aristacv/__init__.py
new file mode 100644
index 0000000..39b3596
--- /dev/null
+++ b/nautobot_ssot_aristacv/__init__.py
@@ -0,0 +1,35 @@
+"""Plugin declaration for aristacv_sync."""
+
+__version__ = "0.1.0"
+
+from nautobot.extras.plugins import PluginConfig
+from django.db.models.signals import post_migrate
+
+
+class NautobotSSOTAristaCVConfig(PluginConfig):
+    """Plugin configuration for the nautobot_ssot_aristacv plugin."""
+
+    name = "nautobot_ssot_aristacv"
+    verbose_name = "Nautobot SSoT Arista CloudVision"
+    version = __version__
+    author = "Network to Code, LLC"
+    description = "Nautobot SSoT Arista CloudVision."
+    base_url = "nautobot-sot_aristacv"
+    required_settings = []
+    min_version = "1.0.0"
+    max_version = "1.9999"
+    default_settings = {}
+    caching_config = {}
+
+    def ready(self):
+        """Callback invoked after the plugin is loaded."""
+        super().ready()
+
+        from .signals import (  # pylint: disable=import-outside-toplevel
+            post_migrate_create_custom_fields,
+        )
+
+        post_migrate.connect(post_migrate_create_custom_fields)
+
+
+config = NautobotSSOTAristaCVConfig  # pylint:disable=invalid-name
diff --git a/aristacv_sync/api/__init__.py b/nautobot_ssot_aristacv/api/__init__.py
similarity index 100%
rename from aristacv_sync/api/__init__.py
rename to nautobot_ssot_aristacv/api/__init__.py
diff --git a/aristacv_sync/diffsync/cvutils.py b/nautobot_ssot_aristacv/diffsync/cvutils.py
similarity index 100%
rename from aristacv_sync/diffsync/cvutils.py
rename to nautobot_ssot_aristacv/diffsync/cvutils.py
diff --git a/nautobot_ssot_aristacv/diffsync/fromcv/cloudvision.py b/nautobot_ssot_aristacv/diffsync/fromcv/cloudvision.py
new file mode 100644
index 0000000..a458a51
--- /dev/null
+++ b/nautobot_ssot_aristacv/diffsync/fromcv/cloudvision.py
@@ -0,0 +1,29 @@
+"""DiffSync adapter for Arista CloudVision."""
+from diffsync import DiffSync
+import arista.tag.v1 as TAG
+import nautobot_ssot_aristacv.diffsync.cvutils as cvutils
+
+from .models import Device, CustomField
+
+
+class CloudVision(DiffSync):
+    """DiffSync adapter implementation for CloudVision user-defined device tags."""
+
+    device = Device
+    cf = CustomField
+
+    top_level = ["device"]
+
+    def load(self):
+        devices = cvutils.get_devices()
+        system_tags = cvutils.get_tags_by_type(TAG.models.CREATOR_TYPE_SYSTEM)
+        for dev in devices:
+            self.device = Device(name=dev["hostname"], device_id=dev["device_id"])
+            self.add(self.device)
+            dev_tags = [tag for tag in cvutils.get_device_tags(device_id=dev["device_id"]) if tag in system_tags]
+            for tag in dev_tags:
+                if any(tag["label"] in string for string in ["hostname", "serialnumber"]):
+                    continue
+                self.cf = CustomField(name=f"arista_{tag['label']}", value=tag["value"], device_name=dev["hostname"])
+                self.add(self.cf)
+                self.device.add_child(self.cf)
diff --git a/nautobot_ssot_aristacv/diffsync/fromcv/models.py b/nautobot_ssot_aristacv/diffsync/fromcv/models.py
new file mode 100644
index 0000000..f1e2c14
--- /dev/null
+++ b/nautobot_ssot_aristacv/diffsync/fromcv/models.py
@@ -0,0 +1,90 @@
+"""Diffsync models for Nautobot <-> CloudVision sync."""
+from diffsync import DiffSyncModel
+from django.core.exceptions import ValidationError
+from nautobot.dcim.models import Device as NautobotDevice
+from nautobot.dcim.models import Platform as NautobotPlatform
+from typing import List
+
+class Device(DiffSyncModel):
+    """Device Model"""
+
+    _modelname = "device"
+    _identifiers = ("name",)
+    _shortname = ()
+    _attributes = ()
+    _children = {"cf": "cfs"}
+
+    name: str
+    cfs: List = list()
+
+    @classmethod
+    def create(cls, diffsync, ids, attrs):
+        # Call the super().create() method to create the in-memory DiffSyncModel instance
+        #new_device = NautobotDevice(status = "", device_type = "", device_role="", site="", name=ids["name"])
+        #new_device.validated_save()
+        return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
+
+    def update(self, diffsync, attrs):
+        ## TODO add your own logic here to update the device on the remote system
+        # Call the super().update() method to update the in-memory DiffSyncModel instance
+        return super().update(attrs, diffsync)
+
+    def delete(self):
+        ## TODO add your own logic here to delete the device on the remote system
+        # Call the super().delete() method to remove the DiffSyncModel instance from its parent DiffSync adapter
+        super().delete()
+        return self
+
+class CustomField(DiffSyncModel):
+    """Custom Field model"""
+
+    _modelname = "cf"
+    _identifiers = ("name", "device_name")
+    _shortname = ()
+    _attributes = ("value",)
+
+    name: str
+    value: str
+    device_name: str
+
+    @classmethod
+    def create(cls, diffsync, ids, attrs):
+        ## TODO add your own logic here to create the device on the remote system
+        # Call the super().create() method to create the in-memory DiffSyncModel instance
+        return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
+
+    def update(self, attrs):
+        if self.name == "arista_model":
+            try:
+                # Try to create new platform
+                new_platform = NautobotPlatform(name=attrs["value"], slug=attrs["value"].lower())
+                new_platform.validated_save()
+                # Assign new platform to device.
+                device = NautobotDevice.objects.get(name=self.device_name)
+                device.platform = new_platform
+                device.validated_save()
+                return super().update(attrs)
+            except ValidationError as e:
+                # Assign existing platform to device.
+                existing_platform = NautobotPlatform.objects.get(name=attrs["value"])
+                device = NautobotDevice.objects.get(name=self.device_name)
+                device.platform = existing_platform
+                device.validated_save()
+                return super().update(attrs)
+        if attrs["value"] == "false":
+            attrs["value"] = False
+        elif attrs["value"] == "true":
+            attrs["value"] == True
+        device = NautobotDevice.objects.get(name=self.device_name)
+        device.custom_field_data.update({self.name: attrs["value"]})
+        device.validated_save()
+        return super().update(attrs)
+
+
+    def delete(self):
+        # Call the super().delete() method to remove the DiffSyncModel instance from its parent DiffSync adapter
+        device = NautobotDevice.objects.get(name=self.device_name)
+        device.custom_field_data.pop(self.name)
+        device.validated_save()
+        super().delete()
+        return self
diff --git a/nautobot_ssot_aristacv/diffsync/fromcv/nautobot.py b/nautobot_ssot_aristacv/diffsync/fromcv/nautobot.py
new file mode 100644
index 0000000..92c8abe
--- /dev/null
+++ b/nautobot_ssot_aristacv/diffsync/fromcv/nautobot.py
@@ -0,0 +1,38 @@
+"""DiffSync adapter for Nautobot."""
+from nautobot.dcim.models import Device as NautobotDevice
+from diffsync import DiffSync
+
+from .models import Device, CustomField
+
+
+class Nautobot(DiffSync):
+    """DiffSync adapter implementation for Nautobot custom fields."""
+
+    device = Device
+    cf = CustomField
+
+    top_level = ["device"]
+
+    def load(self):
+        """Load device custom field data from Nautobot and populate DiffSync models."""
+        devices = NautobotDevice.objects.all()
+        for dev in devices:
+            self.device = Device(name=dev.name)
+            self.add(self.device)
+            dev_custom_fields = dev.custom_field_data
+            for cf_name, cf_value in dev_custom_fields.items():
+                if cf_value == None:
+                    cf_value = ""
+                if cf_name == "arista_mpls" or cf_name == "arista_ztp":
+                    if cf_value:
+                        cf_value = "true"
+                    else:
+                        cf_value = "false"
+                self.cf = CustomField(name=cf_name, value=cf_value, device_name=dev.name)
+                self.add(self.cf)
+                self.device.add_child(self.cf)
+            # Gets model from device and puts it into CustomField Object.
+            self.cf = CustomField(name="arista_model", value=str(dev.platform), device_name=dev.name)
+            self.add(self.cf)
+            self.device.add_child(self.cf)
+
diff --git a/aristacv_sync/diffsync/tocv/cloudvision.py b/nautobot_ssot_aristacv/diffsync/tocv/cloudvision.py
similarity index 82%
rename from aristacv_sync/diffsync/tocv/cloudvision.py
rename to nautobot_ssot_aristacv/diffsync/tocv/cloudvision.py
index b89350c..ed9902a 100644
--- a/aristacv_sync/diffsync/tocv/cloudvision.py
+++ b/nautobot_ssot_aristacv/diffsync/tocv/cloudvision.py
@@ -1,7 +1,7 @@
 """DiffSync adapter for Arista CloudVision."""
 from diffsync import DiffSync
 
-import aristacv_sync.diffsync.cvutils as cvutils
+import nautobot_ssot_aristacv.diffsync.cvutils as cvutils
 
 from .models import UserTag
 
@@ -15,8 +15,6 @@ class CloudVision(DiffSync):
 
     type = "CloudVision"
 
-    nb = None
-
     def load(self):
         user_tags = cvutils.get_tags_by_type()
         for tag in user_tags:
@@ -31,3 +29,7 @@ def load(self):
             for tag in dev_tags:
                 cur_tag = self.get(UserTag, f"{tag['label']}__{tag['value']}")
                 cur_tag.devices.append(hostname)
+
+        # Sort device list of all tags for diffsync
+        for tag in self.get_all(UserTag):
+            tag.devices = sorted(tag.devices)
diff --git a/aristacv_sync/diffsync/tocv/models.py b/nautobot_ssot_aristacv/diffsync/tocv/models.py
similarity index 97%
rename from aristacv_sync/diffsync/tocv/models.py
rename to nautobot_ssot_aristacv/diffsync/tocv/models.py
index e1306df..67fbccb 100644
--- a/aristacv_sync/diffsync/tocv/models.py
+++ b/nautobot_ssot_aristacv/diffsync/tocv/models.py
@@ -3,7 +3,7 @@
 from diffsync import DiffSyncModel
 from typing import List
 
-import aristacv_sync.diffsync.cvutils as cvutils
+import nautobot_ssot_aristacv.diffsync.cvutils as cvutils
 
 
 class UserTag(DiffSyncModel):
diff --git a/aristacv_sync/diffsync/tocv/nautobot.py b/nautobot_ssot_aristacv/diffsync/tocv/nautobot.py
similarity index 90%
rename from aristacv_sync/diffsync/tocv/nautobot.py
rename to nautobot_ssot_aristacv/diffsync/tocv/nautobot.py
index 36de509..33b6873 100644
--- a/aristacv_sync/diffsync/tocv/nautobot.py
+++ b/nautobot_ssot_aristacv/diffsync/tocv/nautobot.py
@@ -15,8 +15,6 @@ class Nautobot(DiffSync):
 
     type = "Nautobot"
 
-    nb = None
-
     def load(self):
         """Load device tag data from Nautobot and populate DiffSync models."""
         tags = Tag.objects.all()
@@ -30,4 +28,7 @@ def load(self):
             tagged_devices = Device.objects.filter(tags__name__exact=cur_tag.name)
             for dev in tagged_devices:
                 self.tag.devices.append(dev.name)
+
+            # Sort device list for diffsync
+            self.tag.devices = sorted(self.tag.devices)
             self.add(self.tag)
diff --git a/nautobot_ssot_aristacv/jobs.py b/nautobot_ssot_aristacv/jobs.py
new file mode 100644
index 0000000..929124a
--- /dev/null
+++ b/nautobot_ssot_aristacv/jobs.py
@@ -0,0 +1,101 @@
+from grpc import RpcError
+
+from django.templatetags.static import static
+
+from nautobot.extras.jobs import Job, BooleanVar
+from nautobot.extras.models.tags import Tag
+from nautobot.extras.models.customfields import CustomField
+
+from nautobot_ssot.jobs.base import DataTarget
+from nautobot_ssot.jobs.base import DataSource
+
+from nautobot_ssot_aristacv.diffsync.tocv.cloudvision import CloudVision
+from nautobot_ssot_aristacv.diffsync.tocv.nautobot import Nautobot
+
+from nautobot_ssot_aristacv.diffsync.fromcv.cloudvision import CloudVision as C
+from nautobot_ssot_aristacv.diffsync.fromcv.nautobot import Nautobot as N
+
+
+class CloudVisionDataSource(DataSource, Job):
+    debug = BooleanVar(description="Enable for more verbose debug logging")
+
+    class Meta:
+        name = "Sync from CloudVision"
+        data_source = "Cloudvision"
+        data_source_icon = static("nautobot_ssot_aristacv/cvp_logo.png")
+        description = "Sync system tag data from CloudVision to Nautobot"
+
+    def sync_data(self):
+        self.log("Loading data from CloudVision")
+        cv = C()
+        cv.load()
+        self.log("Loading data from Nautobot")
+        nb = N()
+        nb.load()
+        self.log("Performing diff between Cloudvision and Nautobot.")
+        diff = nb.diff_from(cv)
+        self.sync.diff = diff.dict()
+        self.sync.save()
+        self.log(diff.summary())
+        if not self.kwargs["dry_run"]:
+            self.log("Syncing to Nautbot")
+            try:
+                cv.sync_to(nb)
+            except RpcError as e:
+                self.log_failure("Sync failed.")
+                raise e
+            self.log_success(message="Sync complete.")
+
+    def lookup_object(self, model_name, unique_id):
+        if model_name == "cf":
+            try:
+                cf_name, value = unique_id.split("__")
+                return (CustomField.objects.get(name=f"{cf_name}"), None)
+            except CustomField.DoesNotExist:
+                pass
+        return (None, None)
+
+
+class CloudVisionDataTarget(DataTarget, Job):
+    debug = BooleanVar(description="Enable for more verbose debug logging")
+
+    class Meta:
+        name = "CloudVision"
+        data_target = "CloudVision"
+        data_target_icon = static("nautobot_ssot_aristacv/cvp_logo.png")
+        description = "Sync tag data from Nautobot to CloudVision"
+
+    def sync_data(self):
+        self.log("Loading data from CloudVision")
+        cv = CloudVision()
+        cv.load()
+        self.log("Loading data from Nautobot")
+        nb = Nautobot()
+        nb.load()
+        self.log("Performing diff between Nautobot and Cloudvision")
+        diff = cv.diff_from(nb)
+        self.sync.diff = diff.dict()
+        self.sync.save()
+        self.log(diff.summary())
+        # if self.kwargs["debug"]:
+        #     self.log_debug(diff_nb_cv.dict())
+        if not self.kwargs["dry_run"]:
+            self.log("Syncing to CloudVision")
+            try:
+                nb.sync_to(cv)
+            except RpcError as e:
+                self.log_failure("Sync failed.")
+                raise e
+            self.log_success(message="Sync complete")
+
+    def lookup_object(self, model_name, unique_id):
+        if model_name == "tag":
+            try:
+                tag_name, value = unique_id.split("__")
+                return (Tag.objects.get(name=f"{tag_name}:{value}"), None)
+            except Tag.DoesNotExist:
+                pass
+        return (None, None)
+
+
+jobs = [CloudVisionDataSource, CloudVisionDataTarget]
diff --git a/aristacv_sync/migrations/__init__.py b/nautobot_ssot_aristacv/migrations/__init__.py
similarity index 100%
rename from aristacv_sync/migrations/__init__.py
rename to nautobot_ssot_aristacv/migrations/__init__.py
diff --git a/nautobot_ssot_aristacv/signals.py b/nautobot_ssot_aristacv/signals.py
new file mode 100644
index 0000000..e10ca33
--- /dev/null
+++ b/nautobot_ssot_aristacv/signals.py
@@ -0,0 +1,87 @@
+"""Nautobot signal handler functions for aristavc_sync."""
+
+from django.db.models.signals import pre_delete
+from django.dispatch import receiver
+
+from nautobot.extras.choices import CustomFieldTypeChoices
+
+def post_migrate_create_custom_fields(apps, **kwargs):
+    """Callback function for post_migrate() -- create CustomField records."""
+    ContentType = apps.get_model("contenttypes", "ContentType")
+    Device = apps.get_model("dcim", "Device")
+    CustomField = apps.get_model("extras", "CustomField")
+
+    for device_cf_dict in [
+            {
+                "name": "arista_eostrain",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "EOS Train",
+            },
+            {
+                "name": "arista_eos",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "EOS Version",
+            },
+            {
+                "name": "arista_ztp",
+                "type": CustomFieldTypeChoices.TYPE_BOOLEAN,
+                "label": "ztp",
+            },
+            {
+                "name": "arista_pimbidir",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "pimbidir",
+            },
+            {
+                "name": "arista_pim",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "pim",
+            },
+            {
+                "name": "arista_bgp",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "bgp",
+            },
+            {
+                "name": "arista_mpls",
+                "type": CustomFieldTypeChoices.TYPE_BOOLEAN,
+                "label": "mpls",
+            },
+            {
+                "name": "arista_systype",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "systype",
+            },
+            {
+                "name": "arista_mlag",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "MLAG",
+            },
+            {
+                "name": "arista_tapagg",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "TAP Aggregation",
+            },
+            {
+                "name": "arista_sflow",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "sFlow",
+            },
+            {
+                "name": "arista_terminattr",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "TerminAttr Version",
+            },
+            {
+                "name": "arista_topology_network_type",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "Topology Network Type",
+            },
+            {
+                "name": "arista_topology_type",
+                "type": CustomFieldTypeChoices.TYPE_TEXT,
+                "label": "Topology Type"
+            }
+        ]:
+            field, _ = CustomField.objects.get_or_create(name=device_cf_dict["name"], defaults=device_cf_dict)
+            field.content_types.set([ContentType.objects.get_for_model(Device)])
\ No newline at end of file
diff --git a/nautobot_ssot_aristacv/static/nautobot_ssot_aristacv/cvp_logo.png b/nautobot_ssot_aristacv/static/nautobot_ssot_aristacv/cvp_logo.png
new file mode 100644
index 0000000..6a7dc8d
Binary files /dev/null and b/nautobot_ssot_aristacv/static/nautobot_ssot_aristacv/cvp_logo.png differ
diff --git a/packages/nautobot_ssot-0.1.0-py3-none-any.whl b/packages/nautobot_ssot-0.1.0-py3-none-any.whl
new file mode 100644
index 0000000..3d1cb07
Binary files /dev/null and b/packages/nautobot_ssot-0.1.0-py3-none-any.whl differ
diff --git a/poetry.lock b/poetry.lock
index 2f9d882..ce223da 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -95,7 +95,7 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
 
 [[package]]
 name = "certifi"
-version = "2020.12.5"
+version = "2021.5.30"
 description = "Python package for providing Mozilla's CA Bundle."
 category = "main"
 optional = false
@@ -520,11 +520,11 @@ smmap = ">=3.0.1,<5"
 
 [[package]]
 name = "gitpython"
-version = "3.1.17"
+version = "3.1.18"
 description = "Python Git Library"
 category = "main"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 
 [package.dependencies]
 gitdb = ">=4.0.1,<5"
@@ -603,7 +603,7 @@ six = ">=1.12"
 
 [[package]]
 name = "grpcio"
-version = "1.38.0"
+version = "1.38.1"
 description = "HTTP/2-based RPC framework"
 category = "main"
 optional = false
@@ -613,7 +613,7 @@ python-versions = "*"
 six = ">=1.5.2"
 
 [package.extras]
-protobuf = ["grpcio-tools (>=1.38.0)"]
+protobuf = ["grpcio-tools (>=1.38.1)"]
 
 [[package]]
 name = "idna"
@@ -843,7 +843,7 @@ python-versions = "*"
 
 [[package]]
 name = "nautobot"
-version = "1.0.3-beta.1"
+version = "1.0.4-beta.1"
 description = "Source of truth and network automation platform."
 category = "main"
 optional = false
@@ -882,9 +882,25 @@ svgwrite = "~1.4.1"
 
 [package.source]
 type = "git"
-url = "https://github.com/nautobot/nautobot"
+url = "https://github.com/nautobot/nautobot.git"
 reference = "develop"
-resolved_reference = "65bccb1064e7078330db36ecc61faad1843c87df"
+resolved_reference = "92ac42c75dc6e67b31f7097fe438fbdd45470622"
+
+[[package]]
+name = "nautobot-ssot"
+version = "0.1.0"
+description = "Nautobot Single Source of Truth"
+category = "main"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+diffsync = ">=1.3.0,<2.0.0"
+nautobot = "rev develop"
+
+[package.source]
+type = "file"
+url = "packages/nautobot_ssot-0.1.0-py3-none-any.whl"
 
 [[package]]
 name = "netaddr"
@@ -921,16 +937,16 @@ twitter = ["twython"]
 
 [[package]]
 name = "oauthlib"
-version = "3.1.0"
+version = "3.1.1"
 description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.6"
 
 [package.extras]
-rsa = ["cryptography"]
-signals = ["blinker"]
-signedtoken = ["cryptography", "pyjwt (>=1.0.0)"]
+rsa = ["cryptography (>=3.0.0,<4)"]
+signals = ["blinker (>=1.4.0)"]
+signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"]
 
 [[package]]
 name = "packaging"
@@ -969,7 +985,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "prometheus-client"
-version = "0.10.1"
+version = "0.11.0"
 description = "Python client for the Prometheus monitoring system."
 category = "main"
 optional = false
@@ -1089,14 +1105,14 @@ tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"]
 
 [[package]]
 name = "pylint"
-version = "2.8.2"
+version = "2.8.3"
 description = "python code static checker"
 category = "dev"
 optional = false
 python-versions = "~=3.6"
 
 [package.dependencies]
-astroid = ">=2.5.6,<2.7"
+astroid = "2.5.6"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 isort = ">=4.2.5,<6"
 mccabe = ">=0.6,<0.7"
@@ -1250,7 +1266,7 @@ redis = ">=3.5.0"
 
 [[package]]
 name = "ruamel.yaml"
-version = "0.17.4"
+version = "0.17.10"
 description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
 category = "main"
 optional = false
@@ -1437,7 +1453,7 @@ python-versions = ">= 3.5"
 
 [[package]]
 name = "tqdm"
-version = "4.61.0"
+version = "4.61.1"
 description = "Fast, Extensible Progress Meter"
 category = "dev"
 optional = false
@@ -1520,7 +1536,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6"
-content-hash = "1d27ee01557a5dbc9b4f37707a88b25480d3056c13a35170f0429f206af63623"
+content-hash = "49393c520d584dad9d7b4ac9c8759940571996c7c0b30dbf8b6584f9a5954782"
 
 [metadata.files]
 aniso8601 = [
@@ -1551,8 +1567,8 @@ black = [
     {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
 ]
 certifi = [
-    {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
-    {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
+    {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
+    {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
 ]
 cffi = [
     {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"},
@@ -1784,8 +1800,8 @@ gitdb = [
     {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"},
 ]
 gitpython = [
-    {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"},
-    {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"},
+    {file = "GitPython-3.1.18-py3-none-any.whl", hash = "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"},
+    {file = "GitPython-3.1.18.tar.gz", hash = "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b"},
 ]
 graphene = [
     {file = "graphene-2.1.8-py2.py3-none-any.whl", hash = "sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896"},
@@ -1804,57 +1820,57 @@ graphql-relay = [
     {file = "graphql_relay-2.0.1-py3-none-any.whl", hash = "sha256:ac514cb86db9a43014d7e73511d521137ac12cf0101b2eaa5f0a3da2e10d913d"},
 ]
 grpcio = [
-    {file = "grpcio-1.38.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:db01eaea57e7a1898c69271e35a84341cf8150cfdec5f0411eddcfb65b5f590e"},
-    {file = "grpcio-1.38.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:f6f6d51c9efbfe56af9eb9eeb4881cad1b869e4c0e2a32c1d345897fd0979ee3"},
-    {file = "grpcio-1.38.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7466eef3b57b5ac8c7585251b26b917b093ab015750bf98aab4e0836c39e2a2b"},
-    {file = "grpcio-1.38.0-cp27-cp27m-win32.whl", hash = "sha256:604da72df5bece8844a15990ce0b3f2f8c5243a1333d3dcc02371048bf6f9269"},
-    {file = "grpcio-1.38.0-cp27-cp27m-win_amd64.whl", hash = "sha256:924552099365ea1dd32237dc161849452cd567d931efc57e8427260d8f0eacdb"},
-    {file = "grpcio-1.38.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f19bd4b5bcf88ee059f478c4ab46a1607f09835587750294038fbd0120f1a9dc"},
-    {file = "grpcio-1.38.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:c47e85eae34af5d17d1c2007a1f0b13a0293d4b7a6d8c8ae23761d718293803e"},
-    {file = "grpcio-1.38.0-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:d631304e66c908d5d2d1a3cc9c02d372d2f8bed8c3632902d6f3f77d7ca34ac2"},
-    {file = "grpcio-1.38.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4a1dd16ccf76ddc18c1cde900049c04ed996e6c02e0588d88d06396c398e6023"},
-    {file = "grpcio-1.38.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:32067791bd56a13614399f1994454afea9e2475019fcabc4abd3112f09892005"},
-    {file = "grpcio-1.38.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:0247b045feb7b138754c91efcca9ea7f7d2cc9f0bd2cc73f6588c523f38873c3"},
-    {file = "grpcio-1.38.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:07abf6b36c138bf247cef7ac0bad9f8ab1c8242f7c1302af23bb8e6877d08163"},
-    {file = "grpcio-1.38.0-cp35-cp35m-win32.whl", hash = "sha256:2c26cb7566e8942542ff1aee71f10ed35e2f9ee95c2f27179b198af0727fbebb"},
-    {file = "grpcio-1.38.0-cp35-cp35m-win_amd64.whl", hash = "sha256:e72dd202c982a5922c3b846976cae3b699e3fa8d2355d9d5bad119d066cf23ee"},
-    {file = "grpcio-1.38.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:34fb08d46a70750bef6566c9556a16b98e08af6345a3bad6574477eb0b08c3dd"},
-    {file = "grpcio-1.38.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1d157a2ac6632d31a3ff21f56bbe73420e1d7a21e4fe89d8c7ac792b79c1a589"},
-    {file = "grpcio-1.38.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3cacfee78310b5a89769b2fac20b8cd723470130f5b1ba0c960da8db39e74a97"},
-    {file = "grpcio-1.38.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e83ab148911e6c8ae4ec5e1334e6d800c6b84c432b92eb0ebf0808087117cb39"},
-    {file = "grpcio-1.38.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:a836f21a1d08d28c8344e149b28729649ff4732c318a59a3628451bbd6c3c9ac"},
-    {file = "grpcio-1.38.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b4cd8fb4e3725e8852b1da734904edb3579c76660ae26a72283ac580779e5bf0"},
-    {file = "grpcio-1.38.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:d3566acd87a65a0bc93875125a7064293ab2b6ffb9327333030939681d269f4f"},
-    {file = "grpcio-1.38.0-cp36-cp36m-win32.whl", hash = "sha256:f2c4ff0e8c98418c5d55c28ba4ff954e3a5d3c723af5008e8d3ddeae8f0ecb41"},
-    {file = "grpcio-1.38.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9a2216df1be9fdbc3c1ebd3c5184d1ef4afb387c29224fce00346b9ddec7c7e3"},
-    {file = "grpcio-1.38.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:9f1747b81d44daed0649ff10395b58c4f29b03139a628afeb058f3e942ba6893"},
-    {file = "grpcio-1.38.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ac05434a7a7f444b2ddd109d72f87f4704364be159aea42a04bd6ea2ba6e10e4"},
-    {file = "grpcio-1.38.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:a4789014f9d9e9ff29551314a004266b1ac90225c8a009dc87947aaf823fd83c"},
-    {file = "grpcio-1.38.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e3072b9ebb573fe1f6757a55b610e4975979d2d58247cbe18ff4385f5aaa81a5"},
-    {file = "grpcio-1.38.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:7b74c48b2e41dd506f889a4a9974d40b3eead965b0fd2e0b1d55a9b3c0e3bc6e"},
-    {file = "grpcio-1.38.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:291dcde4139bc25629de6a743cfcc0ca861e289e3547421ecd2273b51d95b8e1"},
-    {file = "grpcio-1.38.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:0ab78d4c16d7f3924718711689f5e301aec52cfcf38eb4143bed0f74b7c4fd10"},
-    {file = "grpcio-1.38.0-cp37-cp37m-win32.whl", hash = "sha256:10a6c62e0cddd456db91f9f04b53a8cccf67d86d7ca814d989423939099c2723"},
-    {file = "grpcio-1.38.0-cp37-cp37m-win_amd64.whl", hash = "sha256:be83ca2d24321c8bf6516b9cd1064da15ac3ff3978c6c502643be114e2a54af2"},
-    {file = "grpcio-1.38.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:897bcd54890e6ec6359063affb35e19a61a58ba37bc61c9e8ac6b464b854233e"},
-    {file = "grpcio-1.38.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:f8dd51b05e7fde843d7a3140b058f02801fbec5784a036d5f6abb374450d4608"},
-    {file = "grpcio-1.38.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:277faad1d8d462bd1b986f43a47a2c2fe795b2e0de72c9318e11826d921e665a"},
-    {file = "grpcio-1.38.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a0d7c88b4cf9748147cd6c16e14569a124b683a3eb5d7787f43eb9d48cf86755"},
-    {file = "grpcio-1.38.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2736b109ec5bd9fcf580bf871b5fd4f136c6ae9728407f344a3c64ad87bb6519"},
-    {file = "grpcio-1.38.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:cbd2754da81bf5f18454c7808b4afe5b57c6736955a742fb599b32b6353fe99f"},
-    {file = "grpcio-1.38.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a8f9fcf5623282e4804595166a4ee1401cf4ccfc16fe84bb69e1eb23ffd836ac"},
-    {file = "grpcio-1.38.0-cp38-cp38-win32.whl", hash = "sha256:1c11041ecb69d962d49e8a38a35736cdc6fc74230867b5f0ac6138770387a950"},
-    {file = "grpcio-1.38.0-cp38-cp38-win_amd64.whl", hash = "sha256:1d212af1008bdbfd4b8a287e17a8e63e14d72ac450475307452f20c1bbb6bae4"},
-    {file = "grpcio-1.38.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:8c4f39ad529fb4a33cd6e58d1d860c3b583902208547614b4b5b75fc306f13f6"},
-    {file = "grpcio-1.38.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:f9b3678920017842a1b576de3524ecf8f6a2bf4b39f86fb25b870693141e0584"},
-    {file = "grpcio-1.38.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:1e88a8d5d961df958362f61b1b79ad3981a8b051f99224717b09308546298902"},
-    {file = "grpcio-1.38.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6461d69a8ae20e7abce4c6d9cc2603e9f16f4d6b64865eddd0e664127349c04d"},
-    {file = "grpcio-1.38.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:cd220606259f8aa2403bc0f4a4483bae5e36be879364ca3e256f0304ac44f575"},
-    {file = "grpcio-1.38.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:25028420d004fe33d64015f5d4d97207c53530acdb493310e217fac76dcd2513"},
-    {file = "grpcio-1.38.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:b86a1b0654804b5f2248d9262c77a9d5f19882481fc21df53eb2823d0875b86d"},
-    {file = "grpcio-1.38.0-cp39-cp39-win32.whl", hash = "sha256:752593a275e26ef250dc4d93a6f7917dd9986396b41eabcc114b0e0315ec1bf6"},
-    {file = "grpcio-1.38.0-cp39-cp39-win_amd64.whl", hash = "sha256:6824567e2372dde1bd70214427d23b709d09f1a02a552133d1e0f504b616c84e"},
-    {file = "grpcio-1.38.0.tar.gz", hash = "sha256:abbf9c8c3df4d5233d5888c6cfa85c1bb68a6923749bd4dd1abc6e1e93986f17"},
+    {file = "grpcio-1.38.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:118479436bda25b369e2dc1cd0921790fbfaea1ec663e4ee7095c4c325694495"},
+    {file = "grpcio-1.38.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:7adfbd4e22647f880c9ed86b2be7f6d7a7dbbb8adc09395808cc7a4d021bc328"},
+    {file = "grpcio-1.38.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:87b4b1977b52d5e0873a5e396340d2443640ba760f4fa23e93a38997ecfbcd5b"},
+    {file = "grpcio-1.38.1-cp27-cp27m-win32.whl", hash = "sha256:3a25e1a46f51c80d06b66223f61938b9ffda37f2824ca65749c49b758137fac2"},
+    {file = "grpcio-1.38.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b5ea9902fc2990af993b74862282b49ae0b8de8a64ca3b4a8dda26a3163c3bb4"},
+    {file = "grpcio-1.38.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:8ccde1df51eeaddf5515edc41bde2ea43a834a288914eae9ce4287399be108f5"},
+    {file = "grpcio-1.38.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:0e193feaf4ebc72f6af57d7b8a08c0b8e43ebbd76f81c6f1e55d013557602dfd"},
+    {file = "grpcio-1.38.1-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:b16e1967709392a0ec4b10b4374a72eb062c47c168a189606c9a7ea7b36593a8"},
+    {file = "grpcio-1.38.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4bc60f8372c3ab06f41279163c5d558bf95195bb3f68e35ed19f95d4fbd53d71"},
+    {file = "grpcio-1.38.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a433d3740a9ef7bc34a18e2b12bf72b25e618facdfd09871167b30fd8e955fed"},
+    {file = "grpcio-1.38.1-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:d49f250c3ffbe83ba2d03e3500e03505576a985f7c5f77172a9531058347aa68"},
+    {file = "grpcio-1.38.1-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:6e137d014cf4162e5a796777012452516d92547717c1b4914fb71ce4e41817b5"},
+    {file = "grpcio-1.38.1-cp35-cp35m-win32.whl", hash = "sha256:5ff4802d9b3704e680454289587e1cc146bb0d953cf3c9296e2d96441a6a8e88"},
+    {file = "grpcio-1.38.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4c19578b35715e110c324b27c18ab54a56fccc4c41b8f651b1d1da5a64e0d605"},
+    {file = "grpcio-1.38.1-cp36-cp36m-linux_armv7l.whl", hash = "sha256:6edf68d4305e08f6f8c45bfaa9dc04d527ab5a1562aaf0c452fa921fbe90eb23"},
+    {file = "grpcio-1.38.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:ddd33c90b0c95eca737c9f6db7e969a48d23aed72cecb23f3b8aac009ca2cfb4"},
+    {file = "grpcio-1.38.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c83481501533824fe341c17d297bbec1ec584ec46b352f98ce12bf16740615c4"},
+    {file = "grpcio-1.38.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e85bba6f0e0c454a90b8fea16b59db9c6d19ddf9cc95052b2d4ca77b22d46d6"},
+    {file = "grpcio-1.38.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:dcfcb147c18272a22a592251a49830b3c7abc82385ffff34916c2534175d885e"},
+    {file = "grpcio-1.38.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:419af4f577a3d5d9f386aeacf4c4992f90016f84cbceb11ecd832101b1f7f9c9"},
+    {file = "grpcio-1.38.1-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:cd7ddb5b6ffcbd3691990df20f260a888c8bd770d57480a97da1b756fb1be5c0"},
+    {file = "grpcio-1.38.1-cp36-cp36m-win32.whl", hash = "sha256:d4179d96b0ce27602756185c1a00d088c9c1feb0cc17a36f8a66eec6ddddbc0c"},
+    {file = "grpcio-1.38.1-cp36-cp36m-win_amd64.whl", hash = "sha256:96d78d9edf3070770cefd1822bc220d8cccad049b818a70a3c630052e9f15490"},
+    {file = "grpcio-1.38.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:8ab27a6626c2038e13c1b250c5cd22da578f182364134620ec298b4ccfc85722"},
+    {file = "grpcio-1.38.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:532ab738351aad2cdad80f4355123652e08b207281f3923ce51fb2b58692dd4c"},
+    {file = "grpcio-1.38.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:e4a8a371ad02bf31576bcd99093cea3849e19ca1e9eb63fc0b2c0f1db1132f7d"},
+    {file = "grpcio-1.38.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89af675d38bf490384dae85151768b8434e997cece98e5d1eb6fcb3c16d6af12"},
+    {file = "grpcio-1.38.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ff9ebc416e815161d89d2fd22d1a91acf3b810ef800dae38c402d19d203590bf"},
+    {file = "grpcio-1.38.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:3db0680fee9e55022677abda186e73e3c019c59ed83e1550519250dc97cf6793"},
+    {file = "grpcio-1.38.1-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:a77d1f47e5e82504c531bc9dd22c093ff093b6706ec8bcdad228464ef3a5dd54"},
+    {file = "grpcio-1.38.1-cp37-cp37m-win32.whl", hash = "sha256:549beb5646137b78534a312a3b80b2b8b1ea01058b38a711d42d6b54b20b6c2b"},
+    {file = "grpcio-1.38.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3eb960c2f9e031f0643b53bab67733a9544d82f42d0714338183d14993d2a23c"},
+    {file = "grpcio-1.38.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:e90cda2ccd4bdb89a3cd5dc11771c3b8394817d5caaa1ae36042bc96a428c10e"},
+    {file = "grpcio-1.38.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:26af85ae0a7ff8e8f8f550255bf85551df86a89883c11721c0756b71bc1019be"},
+    {file = "grpcio-1.38.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:947bdba3ebcd93a7cef537d6405bc5667d1caf818fa8bbd2e2cc952ec8f97e09"},
+    {file = "grpcio-1.38.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6d898441ada374f76e0b5354d7e240e1c0e905a1ebcb1e95d9ffd99c88f63700"},
+    {file = "grpcio-1.38.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:59f5fb4ba219a11fdc1c23e17c93ca3090480a8cde4370c980908546ffc091e6"},
+    {file = "grpcio-1.38.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:cddd61bff66e42ef334f8cb9e719951e479b5ad2cb75c00338aac8de28e17484"},
+    {file = "grpcio-1.38.1-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:c323265a4f18f586e8de84fda12b48eb3bd48395294aa2b8c05307ac1680299d"},
+    {file = "grpcio-1.38.1-cp38-cp38-win32.whl", hash = "sha256:72e8358c751da9ab4f8653a3b67b2a3bb7e330ee57cb26439c6af358d6eac032"},
+    {file = "grpcio-1.38.1-cp38-cp38-win_amd64.whl", hash = "sha256:278e131bfbc57bab112359b98930b0fdbf81aa0ba2cdfc6555c7a5119d7e2117"},
+    {file = "grpcio-1.38.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:44efa41ac36f6bcbf4f64d6479b3031cceea28cf6892a77f15bd1c22611bff9d"},
+    {file = "grpcio-1.38.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:cf6c3bfa403e055380fe90844beb4fe8e9448edab5d2bf40d37d208dbb2f768c"},
+    {file = "grpcio-1.38.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:5efa68fc3fe0c439e2858215f2224bfb7242c35079538d58063f68a0d5d5ec33"},
+    {file = "grpcio-1.38.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:2a179b2565fa85a134933acc7845f9d4c12e742c802b4f50bf2fd208bf8b741e"},
+    {file = "grpcio-1.38.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:b1624123710fa701988a8a43994de78416e5010ac1508f64ed41e2577358604a"},
+    {file = "grpcio-1.38.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:6a225440015db88ec4625a2a41c21582a50cce7ffbe38dcbbb416c7180352516"},
+    {file = "grpcio-1.38.1-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:e891b0936aab73550d673dd3bbf89fa9577b3db1a61baecea480afd36fdb1852"},
+    {file = "grpcio-1.38.1-cp39-cp39-win32.whl", hash = "sha256:889518ce7c2a0609a3cffb7b667669a39b3410e869ff38e087bf7eeadad62e5d"},
+    {file = "grpcio-1.38.1-cp39-cp39-win_amd64.whl", hash = "sha256:77054f24d46498d9696c809da7810b67bccf6153f9848ea48331708841926d82"},
+    {file = "grpcio-1.38.1.tar.gz", hash = "sha256:1f79d8a24261e3c12ec3a6c25945ff799ae09874fd24815bc17c2dc37715ef6c"},
 ]
 idna = [
     {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
@@ -2007,6 +2023,9 @@ mypy-extensions = [
     {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
 ]
 nautobot = []
+nautobot-ssot = [
+    {file = "nautobot_ssot-0.1.0-py3-none-any.whl", hash = "sha256:d41be7cc997be11dfc772e5062bfe65a85d383d15153f526e9d0f9d22e3e3775"},
+]
 netaddr = [
     {file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"},
     {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"},
@@ -2016,8 +2035,8 @@ nltk = [
     {file = "nltk-3.6.2.zip", hash = "sha256:57d556abed621ab9be225cc6d2df1edce17572efb67a3d754630c9f8381503eb"},
 ]
 oauthlib = [
-    {file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"},
-    {file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"},
+    {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
+    {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"},
 ]
 packaging = [
     {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
@@ -2067,8 +2086,8 @@ pillow = [
     {file = "Pillow-8.1.2.tar.gz", hash = "sha256:b07c660e014852d98a00a91adfbe25033898a9d90a8f39beb2437d22a203fc44"},
 ]
 prometheus-client = [
-    {file = "prometheus_client-0.10.1-py2.py3-none-any.whl", hash = "sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa"},
-    {file = "prometheus_client-0.10.1.tar.gz", hash = "sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d"},
+    {file = "prometheus_client-0.11.0-py2.py3-none-any.whl", hash = "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"},
+    {file = "prometheus_client-0.11.0.tar.gz", hash = "sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86"},
 ]
 promise = [
     {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
@@ -2212,8 +2231,8 @@ pyjwt = [
     {file = "PyJWT-2.1.0.tar.gz", hash = "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"},
 ]
 pylint = [
-    {file = "pylint-2.8.2-py3-none-any.whl", hash = "sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b"},
-    {file = "pylint-2.8.2.tar.gz", hash = "sha256:586d8fa9b1891f4b725f587ef267abe2a1bad89d6b184520c7f07a253dd6e217"},
+    {file = "pylint-2.8.3-py3-none-any.whl", hash = "sha256:792b38ff30903884e4a9eab814ee3523731abd3c463f3ba48d7b627e87013484"},
+    {file = "pylint-2.8.3.tar.gz", hash = "sha256:0a049c5d47b629d9070c3932d13bff482b12119b6a241a93bc460b0be16953c8"},
 ]
 pylint-django = [
     {file = "pylint-django-2.4.4.tar.gz", hash = "sha256:f63f717169b0c2e4e19c28f1c32c28290647330184fcb7427805ae9b6994f3fc"},
@@ -2338,8 +2357,8 @@ rq = [
     {file = "rq-1.8.1.tar.gz", hash = "sha256:feec0f914e69cae2380e2f46d82901e2abbd6fb09db320acb295f486a338f89f"},
 ]
 "ruamel.yaml" = [
-    {file = "ruamel.yaml-0.17.4-py3-none-any.whl", hash = "sha256:ac79fb25f5476e8e9ed1c53b8a2286d2c3f5dde49eb37dbcee5c7eb6a8415a22"},
-    {file = "ruamel.yaml-0.17.4.tar.gz", hash = "sha256:44bc6b54fddd45e4bc0619059196679f9e8b79c027f4131bb072e6a22f4d5e28"},
+    {file = "ruamel.yaml-0.17.10-py3-none-any.whl", hash = "sha256:ffb9b703853e9e8b7861606dfdab1026cf02505bade0653d1880f4b2db47f815"},
+    {file = "ruamel.yaml-0.17.10.tar.gz", hash = "sha256:106bc8d6dc6a0ff7c9196a47570432036f41d556b779c6b4e618085f57e39e67"},
 ]
 "ruamel.yaml.clib" = [
     {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc"},
@@ -2468,8 +2487,8 @@ tornado = [
     {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
 ]
 tqdm = [
-    {file = "tqdm-4.61.0-py2.py3-none-any.whl", hash = "sha256:736524215c690621b06fc89d0310a49822d75e599fcd0feb7cc742b98d692493"},
-    {file = "tqdm-4.61.0.tar.gz", hash = "sha256:cd5791b5d7c3f2f1819efc81d36eb719a38e0906a7380365c556779f585ea042"},
+    {file = "tqdm-4.61.1-py2.py3-none-any.whl", hash = "sha256:aa0c29f03f298951ac6318f7c8ce584e48fa22ec26396e6411e43d038243bdb2"},
+    {file = "tqdm-4.61.1.tar.gz", hash = "sha256:24be966933e942be5f074c29755a95b315c69a91f839a29139bf26ffffe2d3fd"},
 ]
 typed-ast = [
     {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
diff --git a/pyproject.toml b/pyproject.toml
index 4bf1e9d..099ddd5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,14 +1,14 @@
 [tool.poetry]
-name = "aristacv-sync"
+name = "nautobot-ssot-aristacv"
 version = "0.1.0"
-description = "Nautobot to Arista CloudVision Sync"
+description = "Nautobot SSoT Arista CloudVision"
 authors = ["Network to Code, LLC <info@networktocode.com>"]
 
 license = "Apache-2.0"
 
 readme = "README.md"
-homepage = "https://github.com/nautobot/nautobot-plugin-aristacv-sync"
-repository = "https://github.com/nautobot/nautobot-plugin-aristacv-sync"
+homepage = "https://github.com/nautobot/nautobot-plugin-ssot-aristacv"
+repository = "https://github.com/nautobot/nautobot-plugin-ssot-aristacv"
 keywords = ["nautobot", "nautobot-plugin"]
 include = [
 
@@ -17,12 +17,13 @@ include = [
     "README.md",
 ]
 packages = [
-    { include = "aristacv_sync" },
+    { include = "nautobot_ssot_aristacv" },
 ]
 
 [tool.poetry.dependencies]
 python = "^3.6"
-nautobot = {git = "https://github.com/nautobot/nautobot", rev = "develop"}
+nautobot = {git = "https://github.com/nautobot/nautobot.git", rev = "develop"}
+nautobot-ssot = {path = "./packages/nautobot_ssot-0.1.0-py3-none-any.whl"}
 diffsync = "^1.3.0"
 cloudvision = "^1.0.0"
 


Test patch:
diff --git a/aristacv_sync/tests/__init__.py b/nautobot_ssot_aristacv/tests/__init__.py
similarity index 100%
rename from aristacv_sync/tests/__init__.py
rename to nautobot_ssot_aristacv/tests/__init__.py
diff --git a/aristacv_sync/tests/test_api.py b/nautobot_ssot_aristacv/tests/test_api.py
similarity index 100%
rename from aristacv_sync/tests/test_api.py
rename to nautobot_ssot_aristacv/tests/test_api.py
diff --git a/aristacv_sync/tests/test_basic.py b/nautobot_ssot_aristacv/tests/test_basic.py
similarity index 100%
rename from aristacv_sync/tests/test_basic.py
rename to nautobot_ssot_aristacv/tests/test_basic.py