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

Issue:
Invalid Platforms Imported
### Environment
* Python version:  3.7
* Nautobot version:  1.4.5
* aristacv-sync version:  1.3.0

<!-- What did you expect to happen? -->
### Expected Behavior
I expect a single platform to be created and assigned to all Devices. The single Platform should be for Arista.

<!-- What happened instead? -->
### Observed Behavior
A platform for each device type is created.

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current release.
-->
### Steps to Reproduce
1. Stand-up Nautobot with CVP SSoT plugin installed.
2. Run import of devices.
3. Look at the Platforms list.

Use filter instead of test in loop
Wouldn't it be better to apply the arista as a filter, that way you don't have to load all the devices?

https://github.com/nautobot/nautobot-plugin-ssot-arista-cloudvision/blob/1429eb5be00f8dca9627d279212c40a08eaff9cb/nautobot_ssot_aristacv/diffsync/adapters/nautobot.py#L23-L26
Customfield Sync-failure
### Environment
* Python version:  3.8.12
* Nautobot version:  1.4.3
* aristacv-sync version:  1.2.0 

When syncing data from CVP to Nautobot, plugin fails when "Loading data from Nautobot".
The plugin apparently try to validate all custom fields, not just "it's own" or the ones created/handled by the plugin.
The plugin should only care about it's own fields.

The example happens because the fieldvalidation is done for all custom fields.


```
An exception occurred: ValidationError: 1 validation error for CustomField value str type expected (type=type_error.str)

Traceback (most recent call last):
  File "/opt/nautobot/lib64/python3.8/site-packages/nautobot_ssot/jobs/base.py", line 327, in run
    self.sync_data()
  File "/opt/nautobot/lib64/python3.8/site-packages/nautobot_ssot_aristacv/jobs.py", line 131, in sync_data
    nb.load()
  File "/opt/nautobot/lib64/python3.8/site-packages/nautobot_ssot_aristacv/diffsync/fromcv/nautobot.py", line 34, in load
    self.cf = CustomField(name=cf_name, value=cf_value, device_name=dev.name)
  File "pydantic/main.py", line 331, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for CustomField
value
  str type expected (type=type_error.str)__

```



An exception occurred: KeyError: 'enabledState'
### Environment
* Python version:  <3.8>
* Nautobot version:  <1.4.5>
* aristacv-sync version:  <1.3.0>

<!-- What did you expect to happen? -->

Data imported correctly into nautobot from CVP.


<!-- What happened instead? -->
### Observed Behavior

import failed with teh following error :-

An exception occurred: KeyError: 'enabledState'

Traceback (most recent call last):
  File "/opt/data/nautobot/lib64/python3.8/site-packages/nautobot_ssot/jobs/base.py", line 332, in run
    self.sync_data()
  File "/opt/data/nautobot/lib64/python3.8/site-packages/nautobot_ssot_aristacv/jobs.py", line 135, in sync_data
    cv.load()
  File "/opt/data/nautobot/lib64/python3.8/site-packages/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py", line 126, in load
    self.load_devices()
  File "/opt/data/nautobot/lib64/python3.8/site-packages/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py", line 45, in load_devices
    self.load_interfaces(device=new_device)
  File "/opt/data/nautobot/lib64/python3.8/site-packages/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py", line 58, in load_interfaces
    port_info = cloudvision.get_interfaces_chassis(client=self.conn, dId=device.serial)
  File "/opt/data/nautobot/lib64/python3.8/site-packages/nautobot_ssot_aristacv/utils/cloudvision.py", line 512, in get_interfaces_chassis
    "enabled": bool(results["enabledState"]["Name"] == "enabled"),
KeyError: 'enabledState'



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fd9bb3..1540d8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
 # Changelog
 
+## v1.4.0 - 2022-10-21
+
+### Added
+
+- #101 - Added support for the Device Lifecycle Management plug-in. Addresses #87.
+- #99 - Added support for getting interface description.
+- #103 - Added support for importing IP Addresses from CloudVision. Addresses #52.
+- #108 - Added support for hostname parsing to dynamically create Site/Role. #51
+
+### Fixed
+
+- #101 - Fix Platform creation to be single item instead of matching DeviceType.
+- #101 - Corrected Nautobot adapter load method
+- #101 - Ensure only CustomFields with `arista_` prepend are loaded. Addresses #95.
+- #101 - Validated enabledState key in chassis interface. Addresses #102.
+- #101 - Fixed MAC address string to match CVP so diff is idempotent.
+- #104 - Fixes display of plugin settings. Addresses #86.
+
+### Performance
+
+- #101 - Optimized query for Devices to improve load times. Addresses #93.
+
+### Refactor
+
+- #104 - Refactored Jobs to use load source/target adapter methods for performance metrics.
+
 ## v1.3.0 - 2022-10-13
 
 ### Added
diff --git a/README.md b/README.md
index 0248f40..ea35445 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
 # Nautobot to Arista CloudVision Sync
 
-A plugin for [Nautobot](https://github.com/nautobot/nautobot) that allows synchronization of data directly between CloudVision and Nautobot. From Nautobot into CloudVision, it synchronizes user device tags. From CloudVision into Nautobot, it synchronizes devices, their interfaces and their system tags. Here is a table showing the data mappings when syncing from CloudVision.
+A plugin for [Nautobot](https://github.com/nautobot/nautobot) that allows synchronization of data directly between CloudVision and Nautobot. From Nautobot into CloudVision, it synchronizes user device tags. From CloudVision into Nautobot, it synchronizes devices, their interfaces, associated IP addresses, and their system tags. Here is a table showing the data mappings when syncing from CloudVision.
 
 | CloudVision System Tags | Nautobot Device Custom Field |
-|-------------------------|------------------------------|
+| ----------------------- | ---------------------------- |
 | topology_network_type   | Topology Network Type        |
 | mlag                    | mlag                         |
 | mpls                    | mpls                         |
-| model                   | Device Platform*             |
+| model                   | Device Type*                 |
 | systype                 | systype                      |
 | serialnumber            | Device Serial Number         |
 | pimbidir                | pimbidir                     |
@@ -18,27 +18,29 @@ A plugin for [Nautobot](https://github.com/nautobot/nautobot) that allows synchr
 | bgp                     | bgp                          |
 | terminattr              | TerminAttr Version           |
 | ztp                     | ztp                          |
-| eos                     | EOS Version                  |
+| eos                     | EOS Version**                |
 | topology_type           | Topology Type                |
-> The model system tag is mapped to the device platform model in Nautobot.
+> *The model system tag is mapped to the device type model in Nautobot.
+
+> **If the [Device Lifecycle plug-in](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.
 
 When syncing User tags from Nautobot to CloudVision the data mappings are as follows:
 
-| Nautobot | CloudVision |
-|----------|-------------|
-| Interface| Interface   |
-|----------|-------------|
-| Tags     | Device Tags |
+| Nautobot   | CloudVision   |
+| ---------- | ------------- |
+| Interface  | Interface     |
+| ---------- | ------------- |
+| Tags       | Device Tags   |
 
 > This plugin is an extension of the [Nautobot Single Source of Truth (SSoT)](https://github.com/nautobot/nautobot-plugin-ssot) and you must have that plugin installed before installing this extension.
 
 ## Screenshots
 
-This screenshot shows the CloudVision to Nautobot home page. This contains a list of all the system tags from CloudVision and how they map to custom fields in Nautobot. This also displays current plugin configuration and sync history.
+This screenshot shows the CloudVision to Nautobot home page. This contains a list of all the system tags from CloudVision and how they map to custom fields in Nautobot. This also displays your plugin configuration and the sync history.
 
 ![cv_to_naut](https://user-images.githubusercontent.com/38091261/124859726-03557800-df76-11eb-9622-af4c29ba8d40.PNG)
 
-This screenshow shows the Nautobot to CloudVision home page. It also contains data mappings, plugin configuration and sync history.
+This screenshot shows the Nautobot to CloudVision home page. It also contains data mappings, plugin configuration and sync history.
 
 ![naut_to_cv](https://user-images.githubusercontent.com/38091261/124859903-55969900-df76-11eb-87c4-64ca2616bffe.PNG)
 
@@ -78,11 +80,12 @@ PLUGINS_CONFIG = {
     "from_cloudvision_default_site": "",
     "from_cloudvision_default_device_role": "",
     "from_cloudvision_default_device_role_color": "",
-    "from_cloudvision_default_device_status": "",
-    "from_cloudvision_default_device_status_color": "",
     "delete_devices_on_sync": is_truthy(os.getenv("NAUTOBOT_ARISTACV_DELETE_ON_SYNC", False)),
     "apply_import_tag": is_truthy(os.getenv("NAUTOBOT_ARISTACV_IMPORT_TAG", False)),
     "import_active": is_truthy(os.getenv("NAUTOBOT_ARISTACV_IMPORT_ACTIVE", False)),
+    "hostname_patterns": [""],
+    "site_mappings": {},
+    "role_mappings": {},
   }
 }
 ```
@@ -108,60 +111,70 @@ Upon installation, this plugin creates the following custom fields in Nautobot:
 
 > While these contain the prefix "arista" in the custom field admin portal, when looking at them on a device the prefix is removed.
 
-Other custom fields may need to be created by the user. When a sync is ran and a system tag for a device in CloudVision is found without a corresponding custom field, the sync log will display a message. In order to have that data synced, a custom field must be created in the Admin UI using the given name in the message.
+Other custom fields may need to be created by the user. When a sync is run and a system tag for a device in CloudVision is found without a corresponding custom field, the sync log will display a message. To have that data synced, a custom field must be created in the Admin UI using the given name in the message.
 
 ![Custom_Fields_Arista](https://user-images.githubusercontent.com/38091261/133857343-94ee262c-87ca-4e64-a3b2-c3d410755098.PNG)
 
 The plugin can connect to either on-premise or a cloud instance of CloudVision. To connect to an on-premise instance, you must set the following variables in the Nautobot configuration file.
 
 | Configuration Variable | Type    | Usage                                                                                            |
-|------------------------|---------|--------------------------------------------------------------------------------------------------|
+| ---------------------- | ------- | ------------------------------------------------------------------------------------------------ |
 | cvp_host               | string  | Hostname or ip address of the onprem instance of CloudVision.                                    |
 | cvp_port               | string  | gRPC port (defaults to 8443, but this port has changed to 443 as of CVP 2021.3.0)                |
-| cvp_user               | string  | The username used to connect to the on-prem instance of CloudVision.                              |
+| cvp_user               | string  | The username used to connect to the on-prem instance of CloudVision.                             |
 | cvp_password           | string  | The password used by the user specified above.                                                   |
 | cvp_token              | string  | Token to be used when connecting to CloudVision.                                                 |
 | verify                 | boolean | If False, the plugin will download the certificate from CloudVision and trust it for gRPC calls. |
 
 To connect to a cloud instance of CloudVision you must set the following variable:
 
-| Configuration Variable | Type   | Usage                                                         | Default            |
-|------------------------|--------|---------------------------------------------------------------|--------------------|
-| cvaas_url              | string | URL used to connect to your CvaaS instance.                   | www.arista.io:443  |
+| Configuration Variable | Type   | Usage                                       | Default           |
+| ---------------------- | ------ | ------------------------------------------- | ----------------- |
+| cvaas_url              | string | URL used to connect to your CvaaS instance. | www.arista.io:443 |
 
 When syncing from CloudVision, this plugin will create new Arista devices that do not exist in Nautobot. When creating new devices in Nautobot, a site, device role, device role color, device status, and device are required. You may define which values to use by configuring the following values in your `nautobot_config.py` file. If you define a `default_device_role` and `default_device_status` that already exist, the default color value for both of those will be ignored as it will pull that information from Nautobot.
 
-| Configuration Variable                       | Type   | Usage                                                      | Default              |
-|----------------------------------------------|--------|------------------------------------------------------------|----------------------|
-| from_cloudvision_default_site                | string | Default site created when syncing new devices to Nautobot. | cloudvision_imported |
-| from_cloudvision_default_device_role         | string | Default role created when syncing new devices to Nautobot. | network              |
-| from_cloudvision_default_device_role_color   | string | Default role color used for default role.                  | ff0000               |
-| from_cloudvision_default_device_status       | string | Default status used when syncing new devices to Nautobot.  | cloudvision_imported |
-| from_cloudvision_default_device_status_color | string | Default status color used for default status.              | ff0000               |
+| Configuration Variable                     | Type   | Usage                                                      | Default              |
+| ------------------------------------------ | ------ | ---------------------------------------------------------- | -------------------- |
+| from_cloudvision_default_site              | string | Default site created when syncing new devices to Nautobot. | cloudvision_imported |
+| from_cloudvision_default_device_role       | string | Default role created when syncing new devices to Nautobot. | network              |
+| from_cloudvision_default_device_role_color | string | Default role color used for default role.                  | ff0000               |
 
 > When these variables are not defined in the plugin settings, the plugin will use the default values mentioned.
 
 When an Arista device exists in Nautobot but not in CloudVision, this plugin can either delete or leave the device in Nautobot. That behavior can be set with the following variable in the `nautobot_config.py` file.
 
-| Configuration Variable           | Type    | Usage                                                                                                                                                              | Default |
-|----------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
+| Configuration Variable | Type    | Usage                                                                                                                                                              | Default |
+| ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
 | delete_devices_on_sync | boolean | If true, devices in Nautobot with device type manufacturer name set to Arista that do not exist in CloudVision but do exist in Nautobot upon sync will be deleted. | False   |
 
 > When this variable is not defined in the plugin settings, the plugin will default to using `False`.
 
 Optionally, an import tag with the name `cloudvision_imported` can be applied to devices that are imported from CloudVision.
 
-| Configuration Variable                       | Type    | Usage                                                      | Default              |
-|----------------------------------------------|---------|------------------------------------------------------------|----------------------|
-| apply_import_tag                             | boolean | Apply import tag to devices imported from CloudVision.     | False                |
+| Configuration Variable | Type    | Usage                                                  | Default |
+| ---------------------- | ------- | ------------------------------------------------------ | ------- |
+| apply_import_tag       | boolean | Apply import tag to devices imported from CloudVision. | False   |
 
 > If apply_import_tag is set to True, the tag value that is applied to devices is `cloudvision_imported`.
 
-Lastly, you can control whether only active devices are imported or whether all devices regardless of status are imported.
+In addition, you can control whether only active devices are imported or whether all devices regardless of status are imported.
+
+| Configuration Variable | Type    | Usage                                        | Default |
+| ---------------------- | ------- | -------------------------------------------- | ------- |
+| import_active          | boolean | Only import active devices from CloudVision. | False   |
+
+Finally, there is the option to parse device hostname's for codes that indicate the assigned site or device role. This is done through a combination of a few settings. First, the hostname_patterns setting defines a list of regex patterns that define your hostname structure. These patterns must include a named capture group using the `site` and `role` key to identify the portion of the hostname that indicates those pieces of data, ie `(?P<site>\w+)` and `(?P<role>\w+)`. Once those pieces are extracted they are then evaluated against the relevant map, ie the value for the `site` capture group is looked for in the `site_mappings` dictionary expecting the value to be a key with the map value being the name of the Site. If the Site doesn't exist it will be created in Staging status. For the Device Role, it will be created if it doesn't exist in Nautobot. Please note that the hostname is converted to all lowercase when the parsing is performed so the keys are expected to be all lowercase too.
+
+| Configuration Variable                         | Type      | Usage                                                        | Default                |
+| ---------------------------------------------- | --------- | ------------------------------------------------------------ | ---------------------- |
+| hostname_patterns                              | List[str] | Define the portions of a hostname that indicate site/role.   | []                     |
+| ---------------------------------------------- | --------- | ------------------------------------------------------------ | ---------------------- |
+| site_mappings                                  | dict      | Define the site name associated with code in hostname.       | {}                     |
+| ---------------------------------------------- | --------- | ------------------------------------------------------------ | ---------------------- |
+| role_mappings                                  | dict      | Define the role name associated with code in hostname.       | {}                     |
 
-| Configuration Variable                       | Type    | Usage                                                      | Default              |
-|----------------------------------------------|---------|------------------------------------------------------------|----------------------|
-| import_active                                | boolean | Only import active devices from CloudVision.               | False                |
+> 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.
 
 ## Usage
 
diff --git a/development/Dockerfile b/development/Dockerfile
index 9abda03..305fa74 100644
--- a/development/Dockerfile
+++ b/development/Dockerfile
@@ -9,7 +9,7 @@ WORKDIR /source
 COPY poetry.lock pyproject.toml /source/
 # --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
+RUN poetry install --no-interaction --no-ansi --no-root --extras "nautobot-device-lifecycle-mgmt"
 
 # Copy in the rest of the source code and install local Nautobot plugin
 COPY . /source
diff --git a/development/nautobot_config.py b/development/nautobot_config.py
index 063c72a..ec4d2f2 100644
--- a/development/nautobot_config.py
+++ b/development/nautobot_config.py
@@ -8,7 +8,7 @@
 import sys
 
 from nautobot.core.settings import *  # noqa: F401,F403
-from nautobot.core.settings_funcs import parse_redis_connection
+from nautobot.core.settings_funcs import parse_redis_connection, is_truthy
 
 TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
 
@@ -62,7 +62,7 @@
 SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "")
 
 # Enable installed plugins. Add the name of each plugin to the list.
-PLUGINS = ["nautobot_ssot", "nautobot_ssot_aristacv"]
+PLUGINS = ["nautobot_ssot", "nautobot_ssot_aristacv", "nautobot_device_lifecycle_mgmt"]
 
 # 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.
@@ -71,13 +71,27 @@
         "hide_example_jobs": True,  # defaults to False if unspecified
     },
     "nautobot_ssot_aristacv": {
-        "delete_devices_on_sync": True,
+        "cvp_token": os.getenv("NAUTOBOT_ARISTACV_TOKEN", ""),
+        "cvp_host": os.getenv("NAUTOBOT_ARISTACV_HOST", ""),
+        "cvp_port": os.getenv("NAUTOBOT_ARISTACV_PORT", 443),
+        "cvp_user": os.getenv("NAUTOBOT_ARISTACV_USERNAME", ""),
+        "cvp_password": os.getenv("NAUTOBOT_ARISTACV_PASSWORD", ""),
+        "verify": is_truthy(os.getenv("NAUTOBOT_ARISTACV_VERIFY", True)),
         "from_cloudvision_default_site": "cloudvision_imported",
         "from_cloudvision_default_device_role": "network",
         "from_cloudvision_default_device_role_color": "ff0000",
-        "from_cloudvision_default_device_status": "Active",
-        "from_cloudvision_default_device_status_color": "ff0000",
-        "apply_import_tag": True,
-        "import_active": False,
+        "delete_devices_on_sync": is_truthy(os.getenv("NAUTOBOT_ARISTACV_DELETE_ON_SYNC", False)),
+        "apply_import_tag": is_truthy(os.getenv("NAUTOBOT_ARISTACV_IMPORT_TAG", False)),
+        "import_active": is_truthy(os.getenv("NAUTOBOT_ARISTACV_IMPORT_ACTIVE", False)),
+        "hostname_patterns": [[r"(?P<site>\w{2,3}\d+)-(?P<role>\w+)-\d+"]],
+        "site_mappings": {"ams01": "Amsterdam", "atl01": "Atlanta"},
+        "role_mappings": {
+            "bb": "backbone",
+            "edge": "edge",
+            "dist": "distribution",
+            "leaf": "leaf",
+            "rtr": "router",
+            "spine": "spine",
+        },
     },
 }
diff --git a/nautobot_ssot_aristacv/__init__.py b/nautobot_ssot_aristacv/__init__.py
index b932ea3..5b5612d 100644
--- a/nautobot_ssot_aristacv/__init__.py
+++ b/nautobot_ssot_aristacv/__init__.py
@@ -32,6 +32,9 @@ class NautobotSSOTAristaCVConfig(PluginConfig):
         "cvp_password": os.getenv("NAUTOBOT_ARISTACV_PASSWORD"),
         "verify": os.getenv("NAUTOBOT_ARISTACVP_VERIFY"),
         "cvp_token": os.getenv("NAUTOBOT_ARISTACV_TOKEN"),
+        "hostname_patterns": [],
+        "site_mappings": {},
+        "role_mappings": {},
     }
     caching_config = {}
 
@@ -41,9 +44,13 @@ def ready(self):
 
         from .signals import (  # pylint: disable=import-outside-toplevel
             post_migrate_create_custom_fields,
+            post_migrate_create_manufacturer,
+            post_migrate_create_platform,
         )
 
         post_migrate.connect(post_migrate_create_custom_fields)
+        post_migrate.connect(post_migrate_create_manufacturer)
+        post_migrate.connect(post_migrate_create_platform)
 
 
 config = NautobotSSOTAristaCVConfig  # pylint:disable=invalid-name
diff --git a/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py b/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py
index c247458..5d058ef 100644
--- a/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py
+++ b/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py
@@ -1,14 +1,16 @@
 """DiffSync adapter for Arista CloudVision."""
+from django.conf import settings
 import distutils
 import re
 
 import arista.tag.v1 as TAG
 from diffsync import DiffSync
-from diffsync.exceptions import ObjectAlreadyExists
+from diffsync.exceptions import ObjectAlreadyExists, ObjectNotFound
 from nautobot_ssot_aristacv.diffsync.models.cloudvision import (
     CloudvisionCustomField,
     CloudvisionDevice,
     CloudvisionPort,
+    CloudvisionIPAddress,
 )
 from nautobot_ssot_aristacv.utils import cloudvision
 
@@ -18,9 +20,10 @@ class CloudvisionAdapter(DiffSync):
 
     device = CloudvisionDevice
     port = CloudvisionPort
+    ipaddr = CloudvisionIPAddress
     cf = CloudvisionCustomField
 
-    top_level = ["device", "cf"]
+    top_level = ["device", "ipaddr", "cf"]
 
     def __init__(self, *args, job=None, conn: cloudvision.CloudvisionApi, **kwargs):
         """Initialize the CloudVision DiffSync adapter."""
@@ -33,7 +36,12 @@ def load_devices(self):
         for dev in cloudvision.get_devices(client=self.conn.comm_channel):
             if dev["hostname"] != "":
                 new_device = self.device(
-                    name=dev["hostname"], serial=dev["device_id"], device_model=dev["model"], uuid=None
+                    name=dev["hostname"],
+                    serial=dev["device_id"],
+                    status=dev["status"],
+                    device_model=dev["model"],
+                    version=dev["sw_ver"],
+                    uuid=None,
                 )
                 try:
                     self.add(new_device)
@@ -43,6 +51,7 @@ def load_devices(self):
                     )
                     continue
                 self.load_interfaces(device=new_device)
+                self.load_ip_addresses(dev=new_device)
                 self.load_device_tags(device=new_device)
             else:
                 self.job.log_warning(message=f"Device {dev} is missing hostname so won't be imported.")
@@ -71,12 +80,16 @@ def load_interfaces(self, device):
                 transceiver = cloudvision.get_interface_transceiver(
                     client=self.conn, dId=device.serial, interface=base_port_name
                 )
+            port_description = cloudvision.get_interface_description(
+                client=self.conn, dId=device.serial, interface=port["interface"]
+            )
             port_status = cloudvision.get_interface_status(port_info=port)
             port_type = cloudvision.get_port_type(port_info=port, transceiver=transceiver)
             if port["interface"] != "":
                 new_port = self.port(
                     name=port["interface"],
                     device=device.name,
+                    description=port_description,
                     mac_addr=port["mac_addr"] if port.get("mac_addr") else "",
                     mode="tagged" if port_mode == "trunk" else "access",
                     mtu=port["mtu"],
@@ -93,6 +106,50 @@ def load_interfaces(self, device):
                         message=f"Duplicate port {port['interface']} found for {device.name} and ignored. {err}"
                     )
 
+    def load_ip_addresses(self, dev: device):
+        """Load IP addresses from CloudVision."""
+        dev_ip_intfs = cloudvision.get_ip_interfaces(client=self.conn, dId=dev.serial)
+        for intf in dev_ip_intfs:
+            try:
+                _ = self.get(self.port, {"name": intf["interface"], "device": dev.name})
+            except ObjectNotFound:
+                new_port = self.port(
+                    name=intf["interface"],
+                    device=dev.name,
+                    description=cloudvision.get_interface_description(
+                        client=self.conn, dId=dev.serial, interface=intf["interface"]
+                    ),
+                    mac_addr="",
+                    enabled=True,
+                    mode="access",
+                    mtu=65535,
+                    port_type=cloudvision.get_port_type(port_info={"interface": intf["interface"]}, transceiver=""),
+                    status="active",
+                    uuid=None,
+                )
+                self.add(new_port)
+                try:
+                    device = self.get(self.device, dev.name)
+                    device.add_child(new_port)
+                except ObjectNotFound as err:
+                    self.job.log_warning(
+                        message=f"Unable to find device {dev.name} to assign port {intf['interface']}. {err}"
+                    )
+
+            new_ip = self.ipaddr(
+                address=intf["address"],
+                interface=intf["interface"],
+                device=dev.name,
+                uuid=None,
+            )
+            try:
+                self.add(new_ip)
+            except ObjectAlreadyExists as err:
+                self.job.log_warning(
+                    message=f"Unable to load {intf['address']} for {dev.name} on {intf['interface']}. {err}"
+                )
+                continue
+
     def load_device_tags(self, device):
         """Load device tags from CloudVision."""
         system_tags = cloudvision.get_tags_by_type(
@@ -123,4 +180,11 @@ def load_device_tags(self, device):
 
     def load(self):
         """Load devices and associated data from CloudVision."""
+        PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
+        if PLUGIN_SETTINGS.get("hostname_patterns") and not (
+            PLUGIN_SETTINGS.get("site_mappings") and PLUGIN_SETTINGS.get("role_mappings")
+        ):
+            self.job.log_warning(
+                message="Configuration found for hostname_patterns but no site_mappings or role_mappings. Please ensure your mappings are defined."
+            )
         self.load_devices()
diff --git a/nautobot_ssot_aristacv/diffsync/adapters/nautobot.py b/nautobot_ssot_aristacv/diffsync/adapters/nautobot.py
index fd5de99..4d0b59a 100644
--- a/nautobot_ssot_aristacv/diffsync/adapters/nautobot.py
+++ b/nautobot_ssot_aristacv/diffsync/adapters/nautobot.py
@@ -1,10 +1,17 @@
 """DiffSync adapter for Nautobot."""
 from nautobot.dcim.models import Device as OrmDevice
 from nautobot.dcim.models import Interface as OrmInterface
+from nautobot.ipam.models import IPAddress as OrmIPAddress
 from diffsync import DiffSync
-from diffsync.exceptions import ObjectNotFound
+from diffsync.exceptions import ObjectNotFound, ObjectAlreadyExists
 
-from nautobot_ssot_aristacv.diffsync.models.nautobot import NautobotDevice, NautobotCustomField, NautobotPort
+from nautobot_ssot_aristacv.diffsync.models.nautobot import (
+    NautobotDevice,
+    NautobotCustomField,
+    NautobotIPAddress,
+    NautobotPort,
+)
+from nautobot_ssot_aristacv.utils import nautobot
 
 
 class NautobotAdapter(DiffSync):
@@ -12,9 +19,10 @@ class NautobotAdapter(DiffSync):
 
     device = NautobotDevice
     port = NautobotPort
+    ipaddr = NautobotIPAddress
     cf = NautobotCustomField
 
-    top_level = ["device", "cf"]
+    top_level = ["device", "ipaddr", "cf"]
 
     def __init__(self, *args, job=None, **kwargs):
         """Initialize the Nautobot DiffSync adapter."""
@@ -23,33 +31,38 @@ def __init__(self, *args, job=None, **kwargs):
 
     def load(self):
         """Load device custom field data from Nautobot and populate DiffSync models."""
-        devices = OrmDevice.objects.all()
-        for dev in devices:
+        for dev in OrmDevice.objects.filter(device_type__manufacturer__slug="arista"):
             try:
-                if dev.device_type.manufacturer.name.lower() == "arista":
-                    new_device = self.device(
-                        name=dev.name, device_model=dev.device_type.name, serial=dev.serial, uuid=dev.id
-                    )
-                    self.add(new_device)
-                    dev_custom_fields = dev.custom_field_data
+                new_device = self.device(
+                    name=dev.name,
+                    device_model=dev.device_type.model,
+                    serial=dev.serial,
+                    status=dev.status.slug,
+                    version=nautobot.get_device_version(dev),
+                    uuid=dev.id,
+                )
+                self.add(new_device)
+            except ObjectAlreadyExists as err:
+                self.job.log_warning(message=f"Unable to load {dev.name} as it appears to be a duplicate. {err}")
+                continue
 
-                    for cf_name, cf_value in dev_custom_fields.items():
-                        if cf_value is None:
-                            cf_value = ""
-                        new_cf = self.cf(name=cf_name, value=cf_value, device_name=dev.name)
+            for cf_name, cf_value in dev.custom_field_data.items():
+                if cf_name.startswith("arista_"):
+                    try:
+                        new_cf = self.cf(
+                            name=cf_name, value=cf_value if cf_value is not None else "", device_name=dev.name
+                        )
                         self.add(new_cf)
+                    except AttributeError as err:
+                        self.job.log_warning(message=f"Unable to load {cf_name}. {err}")
+                        continue
 
-                    # Gets model from device and puts it into CustomField Object.
-                    new_cf = self.cf(name="arista_model", value=str(dev.platform), device_name=dev.name)
-                    self.add(new_cf)
-            except AttributeError:
-                continue
-
-        for intf in OrmInterface.objects.all():
+        for intf in OrmInterface.objects.filter(device__device_type__manufacturer__slug="arista"):
             new_port = self.port(
                 name=intf.name,
                 device=intf.device.name,
-                mac_addr=intf.mac_address if intf.mac_address else "",
+                description=intf.description,
+                mac_addr=str(intf.mac_address).lower() if intf.mac_address else "",
                 enabled=intf.enabled,
                 mode=intf.mode,
                 mtu=intf.mtu,
@@ -65,3 +78,15 @@ def load(self):
                 self.job.log_warning(
                     message=f"Unable to find Device {intf.device.name} in diff to assign to port {intf.name}. {err}"
                 )
+
+        for ipaddr in OrmIPAddress.objects.filter(interface__device__device_type__manufacturer__slug="arista"):
+            new_ip = self.ipaddr(
+                address=str(ipaddr.address),
+                interface=ipaddr.assigned_object.name,
+                device=ipaddr.assigned_object.device.name,
+                uuid=ipaddr.id,
+            )
+            try:
+                self.add(new_ip)
+            except ObjectAlreadyExists as err:
+                self.job.log_warning(message=f"Unable to load {ipaddr.address} as appears to be a duplicate. {err}")
diff --git a/nautobot_ssot_aristacv/diffsync/models/base.py b/nautobot_ssot_aristacv/diffsync/models/base.py
index 71b0a57..e795869 100644
--- a/nautobot_ssot_aristacv/diffsync/models/base.py
+++ b/nautobot_ssot_aristacv/diffsync/models/base.py
@@ -1,6 +1,7 @@
 """DiffSyncModel subclasses for Nautobot-to-AristaCV data sync."""
 from uuid import UUID
 from diffsync import DiffSyncModel
+from diffsync.enum import DiffSyncModelFlags
 from typing import List, Optional
 
 
@@ -12,12 +13,16 @@ class Device(DiffSyncModel):
     _attributes = (
         "device_model",
         "serial",
+        "status",
+        "version",
     )
     _children = {"port": "ports"}
 
     name: str
     device_model: str
     serial: str
+    status: str
+    version: Optional[str]
     ports: List["Port"] = list()
     uuid: Optional[UUID]
 
@@ -31,6 +36,7 @@ class Port(DiffSyncModel):
         "device",
     )
     _attributes = (
+        "description",
         "mac_addr",
         "enabled",
         "mode",
@@ -42,6 +48,7 @@ class Port(DiffSyncModel):
 
     name: str
     device: str
+    description: Optional[str]
     mac_addr: str
     enabled: bool
     mode: str
@@ -51,6 +58,26 @@ class Port(DiffSyncModel):
     uuid: Optional[UUID]
 
 
+class IPAddress(DiffSyncModel):
+    """IPAddress Model."""
+
+    model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST
+
+    _modelname = "ipaddr"
+    _identifiers = (
+        "address",
+        "device",
+        "interface",
+    )
+    _attributes = ()
+    _children = {}
+
+    address: str
+    device: str
+    interface: str
+    uuid: Optional[UUID]
+
+
 class CustomField(DiffSyncModel):
     """Custom Field model."""
 
diff --git a/nautobot_ssot_aristacv/diffsync/models/cloudvision.py b/nautobot_ssot_aristacv/diffsync/models/cloudvision.py
index c47d1ae..c212e34 100644
--- a/nautobot_ssot_aristacv/diffsync/models/cloudvision.py
+++ b/nautobot_ssot_aristacv/diffsync/models/cloudvision.py
@@ -1,10 +1,8 @@
 """Cloudvision DiffSync models for AristaCV SSoT."""
 from django.conf import settings
-from nautobot_ssot_aristacv.diffsync.models.base import Device, CustomField, Port
+from nautobot_ssot_aristacv.diffsync.models.base import Device, CustomField, IPAddress, Port
 from nautobot_ssot_aristacv.utils.cloudvision import CloudvisionApi
 
-PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
-
 
 class CloudvisionDevice(Device):
     """Cloudvision Device model."""
@@ -24,7 +22,7 @@ def delete(self):
 
 
 class CloudvisionPort(Port):
-    """Nautobot Port model."""
+    """Cloudvision Port model."""
 
     @classmethod
     def create(cls, diffsync, ids, attrs):
@@ -40,12 +38,33 @@ def delete(self):
         return self
 
 
+class CloudvisionIPAddress(IPAddress):
+    """Cloudvision IPAdress model."""
+
+    @classmethod
+    def create(cls, diffsync, ids, attrs):
+        """Create IPAddress in AristaCV from IPAddress object."""
+        ...
+        return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
+
+    def update(self, attrs):
+        """Update IPAddress in AristaCV from IPAddress object."""
+        ...
+        return super().update(attrs)
+
+    def delete(self):
+        """Delete IPAddress in AristaCV from IPAddress object."""
+        ...
+        return self
+
+
 class CloudvisionCustomField(CustomField):
     """Cloudvision CustomField model."""
 
     @staticmethod
     def connect_cvp():
         """Connect to Cloudvision gRPC endpoint."""
+        PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
         return CloudvisionApi(
             cvp_host=PLUGIN_SETTINGS["cvp_host"],
             cvp_port=PLUGIN_SETTINGS.get("cvp_port", "8443"),
diff --git a/nautobot_ssot_aristacv/diffsync/models/nautobot.py b/nautobot_ssot_aristacv/diffsync/models/nautobot.py
index aed6ede..ae56d30 100644
--- a/nautobot_ssot_aristacv/diffsync/models/nautobot.py
+++ b/nautobot_ssot_aristacv/diffsync/models/nautobot.py
@@ -1,15 +1,27 @@
 """Nautobot DiffSync models for AristaCV SSoT."""
+from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.conf import settings
 from nautobot.core.settings_funcs import is_truthy
 from nautobot.dcim.models import Device as OrmDevice
 from nautobot.dcim.models import Interface as OrmInterface
 from nautobot.dcim.models import Platform as OrmPlatform
+from nautobot.extras.models import Relationship as OrmRelationship
+from nautobot.extras.models import RelationshipAssociation as OrmRelationshipAssociation
 from nautobot.extras.models import Status as OrmStatus
-from nautobot_ssot_aristacv.diffsync.models.base import Device, CustomField, Port
+from nautobot.ipam.models import IPAddress as OrmIPAddress
+from nautobot_ssot_aristacv.diffsync.models.base import Device, CustomField, IPAddress, Port
 from nautobot_ssot_aristacv.utils import nautobot
 import distutils
 
+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
+
 
 DEFAULT_SITE = "cloudvision_imported"
 DEFAULT_DEVICE_ROLE = "network"
@@ -19,7 +31,6 @@
 DEFAULT_DELETE_DEVICES_ON_SYNC = False
 APPLY_IMPORT_TAG = False
 MISSING_CUSTOM_FIELDS = []
-PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
 
 
 class NautobotDevice(Device):
@@ -28,26 +39,35 @@ class NautobotDevice(Device):
     @classmethod
     def create(cls, diffsync, ids, attrs):
         """Create device object in Nautobot."""
-        default_site_object = nautobot.verify_site(PLUGIN_SETTINGS.get("from_cloudvision_default_site", DEFAULT_SITE))
+        PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
+        site_code, role_code = nautobot.parse_hostname(ids["name"].lower())
+        site_map = PLUGIN_SETTINGS.get("site_mappings")
+        role_map = PLUGIN_SETTINGS.get("role_mappings")
 
-        device_type_cv = attrs["device_model"]
+        if site_code and site_code in site_map:
+            site = nautobot.verify_site(site_map[site_code])
+        else:
+            site = nautobot.verify_site(PLUGIN_SETTINGS.get("from_cloudvision_default_site", DEFAULT_SITE))
 
-        device_type_object = nautobot.verify_device_type_object(device_type_cv)
-        device_role_object = nautobot.verify_device_role_object(
-            PLUGIN_SETTINGS.get("from_cloudvision_default_device_role", DEFAULT_DEVICE_ROLE),
-            PLUGIN_SETTINGS.get("from_cloudvision_default_device_role_color", DEFAULT_DEVICE_ROLE_COLOR),
-        )
+        if role_code and role_code in role_map:
+            role = nautobot.verify_device_role_object(
+                role_map[role_code],
+                PLUGIN_SETTINGS.get("from_cloudvision_default_device_role_color", DEFAULT_DEVICE_ROLE_COLOR),
+            )
+        else:
+            role = nautobot.verify_device_role_object(
+                PLUGIN_SETTINGS.get("from_cloudvision_default_device_role", DEFAULT_DEVICE_ROLE),
+                PLUGIN_SETTINGS.get("from_cloudvision_default_device_role_color", DEFAULT_DEVICE_ROLE_COLOR),
+            )
 
-        device_status = nautobot.verify_device_status(
-            PLUGIN_SETTINGS.get("from_cloudvision_default_device_status", DEFAULT_DEVICE_STATUS),
-            PLUGIN_SETTINGS.get("from_cloudvision_default_device_status_color", DEFAULT_DEVICE_STATUS_COLOR),
-        )
+        device_type_object = nautobot.verify_device_type_object(attrs["device_model"])
 
         new_device = OrmDevice(
-            status=device_status,
+            status=OrmStatus.objects.get(slug=attrs["status"]),
             device_type=device_type_object,
-            device_role=device_role_object,
-            site=default_site_object,
+            device_role=role,
+            platform=OrmPlatform.objects.get(slug="arista_eos"),
+            site=site,
             name=ids["name"],
             serial=attrs["serial"] if attrs.get("serial") else "",
         )
@@ -56,6 +76,9 @@ def create(cls, diffsync, ids, attrs):
             new_device.tags.add(import_tag)
         try:
             new_device.validated_save()
+            if LIFECYCLE_MGMT and attrs.get("version"):
+                software_lcm = cls._add_software_lcm(version=attrs["version"])
+                cls._assign_version_to_device(diffsync=diffsync, device=new_device, software_lcm=software_lcm)
             return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
         except ValidationError as err:
             diffsync.job.log_warning(message=f"Unable to create Device {ids['name']}. {err}")
@@ -68,6 +91,9 @@ def update(self, attrs):
             dev.device_type = nautobot.verify_device_type_object(attrs["device_model"])
         if "serial" in attrs:
             dev.serial = attrs["serial"]
+        if "version" in attrs and LIFECYCLE_MGMT:
+            software_lcm = self._add_software_lcm(version=attrs["version"])
+            self._assign_version_to_device(diffsync=self.diffsync, device=dev, software_lcm=software_lcm)
         try:
             dev.validated_save()
             return super().update(attrs)
@@ -77,13 +103,52 @@ def update(self, attrs):
 
     def delete(self):
         """Delete device object in Nautobot."""
-        if PLUGIN_SETTINGS.get("delete_devices_on_sync", DEFAULT_DELETE_DEVICES_ON_SYNC):
+        if settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"].get(
+            "delete_devices_on_sync", DEFAULT_DELETE_DEVICES_ON_SYNC
+        ):
             self.diffsync.job.log_warning(message=f"Device {self.name} will be deleted per plugin settings.")
             device = OrmDevice.objects.get(id=self.uuid)
             device.delete()
             super().delete()
         return self
 
+    @staticmethod
+    def _add_software_lcm(version: str):
+        """Add OS Version as SoftwareLCM if Device Lifecycle Plugin found."""
+        _platform = OrmPlatform.objects.get(slug="arista_eos")
+        try:
+            os_ver = SoftwareLCM.objects.get(device_platform=_platform, version=version)
+        except SoftwareLCM.DoesNotExist:
+            os_ver = SoftwareLCM(
+                device_platform=_platform,
+                version=version,
+            )
+            os_ver.validated_save()
+        return os_ver
+
+    @staticmethod
+    def _assign_version_to_device(diffsync, device, software_lcm):
+        """Add Relationship between Device and SoftwareLCM."""
+        software_relation = OrmRelationship.objects.get(name="Software on Device")
+        relations = device.get_relationships()
+        for _, relationships in relations.items():
+            for relationship, queryset in relationships.items():
+                if relationship == software_relation:
+                    if diffsync.job.kwargs.get("debug"):
+                        diffsync.job.log_warning(
+                            message=f"Deleting Software Version Relationships for {device.name} to assign a new version."
+                        )
+                    queryset.delete()
+
+        new_assoc = OrmRelationshipAssociation(
+            relationship=software_relation,
+            source_type=ContentType.objects.get_for_model(SoftwareLCM),
+            source=software_lcm,
+            destination_type=ContentType.objects.get_for_model(OrmDevice),
+            destination=device,
+        )
+        new_assoc.validated_save()
+
 
 class NautobotPort(Port):
     """Nautobot Port model."""
@@ -95,6 +160,7 @@ def create(cls, diffsync, ids, attrs):
         new_port = OrmInterface(
             name=ids["name"],
             device=device,
+            description=attrs["description"],
             enabled=is_truthy(attrs["enabled"]),
             mac_address=attrs["mac_addr"],
             mtu=attrs["mtu"],
@@ -112,6 +178,11 @@ def create(cls, diffsync, ids, attrs):
     def update(self, attrs):
         """Update Interface in Nautobot."""
         _port = OrmInterface.objects.get(id=self.uuid)
+        if "description" in attrs:
+            description = ""
+            if attrs.get("description"):
+                description = attrs["description"]
+            _port.description = description
         if "mac_addr" in attrs:
             _port.mac_address = attrs["mac_addr"]
         if "mode" in attrs:
@@ -135,7 +206,7 @@ def update(self, attrs):
 
     def delete(self):
         """Delete Interface in Nautobot."""
-        if PLUGIN_SETTINGS.get("delete_devices_on_sync"):
+        if settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"].get("delete_devices_on_sync"):
             super().delete()
             if self.diffsync.job.kwargs.get("debug"):
                 self.diffsync.job.log_warning(message=f"Interface {self.name} for {self.device} will be deleted.")
@@ -144,29 +215,42 @@ def delete(self):
         return self
 
 
+class NautobotIPAddress(IPAddress):
+    """Nautobot IPAddress model."""
+
+    @classmethod
+    def create(cls, diffsync, ids, attrs):
+        """Create IPAddress in Nautobot."""
+        dev = OrmDevice.objects.get(name=ids["device"])
+        new_ip = OrmIPAddress(
+            address=ids["address"],
+            status=OrmStatus.objects.get(name="Active"),
+        )
+        if "loopback" in ids["interface"]:
+            new_ip.role = "loopback"
+        new_ip.validated_save()
+        try:
+            intf = OrmInterface.objects.get(device=dev, name=ids["interface"])
+            new_ip.assigned_object_type = ContentType.objects.get(app_label="dcim", model="interface")
+            new_ip.assigned_object = intf
+            new_ip.validated_save()
+            if "Management" in ids["interface"]:
+                if ":" in ids["address"]:
+                    dev.primary_ip6 = new_ip
+                else:
+                    dev.primary_ip4 = new_ip
+                dev.validated_save()
+        except OrmInterface.DoesNotExist as err:
+            diffsync.job.log_warning(message=f"Unable to find Interface {ids['interface']} for {ids['device']}. {err}")
+        return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
+
+
 class NautobotCustomField(CustomField):
     """Nautobot CustomField model."""
 
     @classmethod
     def create(cls, diffsync, ids, attrs):
         """Create Custom Field in Nautobot."""
-        if ids["name"] == "arista_model":
-            try:
-                # Try to create new platform
-                new_platform = OrmPlatform(name=attrs["value"], slug=attrs["value"].lower())
-                new_platform.validated_save()
-                # Assign new platform to device.
-                device = OrmDevice.objects.get(name=ids["device_name"])
-                device.platform = new_platform
-                device.validated_save()
-                return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
-            except ValidationError:
-                # Assign existing platform to device.
-                existing_platform = OrmPlatform.objects.get(name=attrs["value"])
-                device = OrmDevice.objects.get(name=ids["device_name"])
-                device.platform = existing_platform
-                device.validated_save()
-                return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
         try:
             attrs["value"] = bool(distutils.util.strtobool(attrs["value"]))
         except ValueError:
@@ -187,23 +271,6 @@ def create(cls, diffsync, ids, attrs):
 
     def update(self, attrs):
         """Update Custom Field in Nautobot."""
-        if self.name == "arista_model":
-            try:
-                # Try to create new platform
-                new_platform = OrmPlatform(name=attrs["value"], slug=attrs["value"].lower())
-                new_platform.validated_save()
-                # Assign new platform to device.
-                device = OrmDevice.objects.get(name=self.device_name)
-                device.platform = new_platform
-                device.validated_save()
-                return super().update(attrs)
-            except ValidationError:
-                # Assign existing platform to device.
-                existing_platform = OrmPlatform.objects.get(name=attrs["value"])
-                device = OrmDevice.objects.get(name=self.device_name)
-                device.platform = existing_platform
-                device.validated_save()
-                return super().update(attrs)
         try:
             attrs["value"] = bool(distutils.util.strtobool(attrs["value"]))
         except ValueError:
@@ -218,10 +285,7 @@ def delete(self):
         """Delete Custom Field in Nautobot."""
         try:
             device = OrmDevice.objects.get(name=self.device_name)
-            if self.name == "arista_model":
-                device.platform = None
-            else:
-                device.custom_field_data.update({self.name: None})
+            device.custom_field_data.update({self.name: None})
             device.validated_save()
             super().delete()
             return self
diff --git a/nautobot_ssot_aristacv/jobs.py b/nautobot_ssot_aristacv/jobs.py
index 13f46dc..27b44b4 100644
--- a/nautobot_ssot_aristacv/jobs.py
+++ b/nautobot_ssot_aristacv/jobs.py
@@ -1,15 +1,14 @@
 # pylint: disable=invalid-name,too-few-public-methods
 """Jobs for CloudVision integration with SSoT plugin."""
-from grpc import RpcError
-
 from django.conf import settings
 from django.templatetags.static import static
 from django.urls import reverse
 
+from nautobot.dcim.models import DeviceType
 from nautobot.extras.jobs import Job, BooleanVar
 from nautobot.extras.models.tags import Tag
 from nautobot.extras.models.customfields import CustomField
-
+from nautobot.utilities.utils import get_route_for_model
 from nautobot_ssot.jobs.base import DataTarget, DataSource, DataMapping
 
 from nautobot_ssot_aristacv.diffsync.adapters.cloudvision import CloudvisionAdapter
@@ -20,8 +19,6 @@
 
 name = "SSoT - Arista CloudVision"  # pylint: disable=invalid-name
 
-PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
-
 
 class CloudVisionDataSource(DataSource, Job):  # pylint: disable=abstract-method
     """CloudVision SSoT Data Source."""
@@ -39,53 +36,37 @@ class Meta:
     @classmethod
     def config_information(cls):
         """Dictionary describing the configuration of this DataSource."""
-        configs = settings.PLUGINS_CONFIG.get("nautobot_ssot_aristacv", {})
-        if configs.get("cvp_host"):
-            return {
-                "Server type": "On prem",
-                "CloudVision host": configs.get("cvp_host"),
-                "Username": configs.get("cvp_user"),
-                "Verify": configs.get("verify"),
-                "Delete devices on sync": configs.get(
-                    "delete_devices_on_sync", str(nautobot.DEFAULT_DELETE_DEVICES_ON_SYNC)
-                ),
-                "New device default site": configs.get("from_cloudvision_default_site", nautobot.DEFAULT_SITE),
-                "New device default role": configs.get(
-                    "from_cloudvision_default_device_role", nautobot.DEFAULT_DEVICE_ROLE
-                ),
-                "New device default role color": configs.get(
-                    "from_cloudvision_default_device_role_color", nautobot.DEFAULT_DEVICE_ROLE_COLOR
-                ),
-                "New device default status": configs.get(
-                    "from_cloudvision_default_device_status", nautobot.DEFAULT_DEVICE_STATUS
-                ),
-                "New device default status color": configs.get(
-                    "from_cloudvision_default_device_status_color", nautobot.DEFAULT_DEVICE_STATUS_COLOR
-                ),
-                "Apply import tag": str(configs.get("apply_import_tag", nautobot.APPLY_IMPORT_TAG))
-                # Password is intentionally omitted!
-            }
+        PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
+        if PLUGIN_SETTINGS.get("cvp_host"):
+            server_type = "On prem"
+            host = PLUGIN_SETTINGS.get("cvp_host")
+        else:
+            server_type = "CVaaS"
+            host = PLUGIN_SETTINGS.get("cvaas_url")
         return {
-            "Server type": "CVaaS",
-            "CloudVision host": configs.get("cvaas_url"),
-            "Delete devices on sync": configs.get(
+            "Server type": server_type,
+            "CloudVision host": host,
+            "Username": PLUGIN_SETTINGS.get("cvp_user"),
+            "Verify": str(PLUGIN_SETTINGS.get("verify")),
+            "Delete devices on sync": PLUGIN_SETTINGS.get(
                 "delete_devices_on_sync", str(nautobot.DEFAULT_DELETE_DEVICES_ON_SYNC)
             ),
-            "New device default site": configs.get("from_cloudvision_default_site", nautobot.DEFAULT_SITE),
-            "New device default role": configs.get(
+            "New device default site": PLUGIN_SETTINGS.get("from_cloudvision_default_site", nautobot.DEFAULT_SITE),
+            "New device default role": PLUGIN_SETTINGS.get(
                 "from_cloudvision_default_device_role", nautobot.DEFAULT_DEVICE_ROLE
             ),
-            "New device default role color": configs.get(
+            "New device default role color": PLUGIN_SETTINGS.get(
                 "from_cloudvision_default_device_role_color", nautobot.DEFAULT_DEVICE_ROLE_COLOR
             ),
-            "New device default status": configs.get(
+            "New device default status": PLUGIN_SETTINGS.get(
                 "from_cloudvision_default_device_status", nautobot.DEFAULT_DEVICE_STATUS
             ),
-            "New device default status color": configs.get(
+            "New device default status color": PLUGIN_SETTINGS.get(
                 "from_cloudvision_default_device_status_color", nautobot.DEFAULT_DEVICE_STATUS_COLOR
             ),
-            "Apply import tag": str(configs.get("apply_import_tag", nautobot.APPLY_IMPORT_TAG))
-            # Token is intentionally omitted!
+            "Apply import tag": str(PLUGIN_SETTINGS.get("apply_import_tag", nautobot.APPLY_IMPORT_TAG)),
+            "Import Active": str(PLUGIN_SETTINGS.get("import_active", "True"))
+            # Password and Token are intentionally omitted!
         }
 
     @classmethod
@@ -95,7 +76,7 @@ def data_mappings(cls):
             DataMapping("topology_network_type", None, "Topology Network Type", None),
             DataMapping("mlag", None, "MLAG", None),
             DataMapping("mpls", None, "mpls", None),
-            DataMapping("model", None, "Platform", reverse("dcim:platform_list")),
+            DataMapping("model", None, "Device Type", reverse(get_route_for_model(DeviceType, "list"))),
             DataMapping("systype", None, "systype", None),
             DataMapping("serialnumber", None, "Device Serial Number", None),
             DataMapping("pimbidir", None, "pimbidir", None),
@@ -110,18 +91,19 @@ def data_mappings(cls):
             DataMapping("topology_type", None, "Topology Type", None),
         )
 
-    def sync_data(self):
-        """Sync system tags from CloudVision to Nautobot custom fields."""
-        configs = settings.PLUGINS_CONFIG.get("nautobot_ssot_aristacv", {})
-        if configs.get("delete_devices_on_sync"):
-            self.log_warning(
-                message="Devices not present in Cloudvision but present in Nautobot will be deleted from Nautobot."
-            )
-        else:
-            self.log_warning(
-                message="Devices not present in Cloudvision but present in Nautobot will not be deleted from Nautobot."
-            )
-        self.log("Connecting to CloudVision")
+    def load_source_adapter(self):
+        """Load data from CloudVision into DiffSync models."""
+        PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
+        if self.kwargs.get("debug"):
+            if PLUGIN_SETTINGS.get("delete_devices_on_sync"):
+                self.log_warning(
+                    message="Devices not present in Cloudvision but present in Nautobot will be deleted from Nautobot."
+                )
+            else:
+                self.log_warning(
+                    message="Devices not present in Cloudvision but present in Nautobot will not be deleted from Nautobot."
+                )
+            self.log("Connecting to CloudVision")
         with CloudvisionApi(
             cvp_host=PLUGIN_SETTINGS["cvp_host"],
             cvp_port=PLUGIN_SETTINGS.get("cvp_port", "8443"),
@@ -129,26 +111,16 @@ def sync_data(self):
             username=PLUGIN_SETTINGS["cvp_user"],
             password=PLUGIN_SETTINGS["cvp_password"],
             cvp_token=PLUGIN_SETTINGS["cvp_token"],
-        ) as cvp:
+        ) as client:
             self.log("Loading data from CloudVision")
-            cv = CloudvisionAdapter(job=self, conn=cvp)
-            cv.load()
+            self.source_adapter = CloudvisionAdapter(job=self, conn=client)
+            self.source_adapter.load()
+
+    def load_target_adapter(self):
+        """Load data from Nautobot into DiffSync models."""
         self.log("Loading data from Nautobot")
-        nb = NautobotAdapter(job=self)
-        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:
-                nb.sync_from(cv)
-            except RpcError as e:
-                self.log_failure("Sync failed.")
-                raise e
-            self.log_success(message="Sync complete.")
+        self.target_adapter = NautobotAdapter(job=self)
+        self.target_adapter.load()
 
     def lookup_object(self, model_name, unique_id):
         """Lookup object for SSoT plugin integration."""
@@ -177,18 +149,18 @@ class Meta:
     @classmethod
     def config_information(cls):
         """Dictionary describing the configuration of this DataTarget."""
-        configs = settings.PLUGINS_CONFIG.get("nautobot_ssot_aristacv", {})
-        if configs.get("cvp_host"):
+        PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
+        if PLUGIN_SETTINGS.get("cvp_host"):
             return {
                 "Server type": "On prem",
-                "CloudVision host": configs.get("cvp_host"),
-                "Username": configs.get("cvp_user"),
-                "Verify": configs.get("verify")
+                "CloudVision host": PLUGIN_SETTINGS.get("cvp_host"),
+                "Username": PLUGIN_SETTINGS.get("cvp_user"),
+                "Verify": str(PLUGIN_SETTINGS.get("verify"))
                 # Password is intentionally omitted!
             }
         return {
             "Server type": "CVaaS",
-            "CloudVision host": "www.arista.io",
+            "CloudVision host": PLUGIN_SETTINGS.get("cvaas_url"),
             # Token is intentionally omitted!
         }
 
@@ -197,9 +169,25 @@ def data_mappings(cls):
         """List describing the data mappings involved in this DataTarget."""
         return (DataMapping("Tags", reverse("extras:tag_list"), "Device Tags", None),)
 
-    def sync_data(self):
-        """Sync device tags from CloudVision to Nautobot."""
-        self.log("Connecting to CloudVision")
+    def load_source_adapter(self):
+        """Load data from Nautobot into DiffSync models."""
+        self.log("Loading data from Nautobot")
+        self.source_adapter = NautobotAdapter(job=self)
+        self.source_adapter.load()
+
+    def load_target_adapter(self):
+        """Load data from CloudVision into DiffSync models."""
+        PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
+        if self.kwargs.get("debug"):
+            if PLUGIN_SETTINGS.get("delete_devices_on_sync"):
+                self.log_warning(
+                    message="Devices not present in Cloudvision but present in Nautobot will be deleted from Nautobot."
+                )
+            else:
+                self.log_warning(
+                    message="Devices not present in Cloudvision but present in Nautobot will not be deleted from Nautobot."
+                )
+            self.log("Connecting to CloudVision")
         with CloudvisionApi(
             cvp_host=PLUGIN_SETTINGS["cvp_host"],
             cvp_port=PLUGIN_SETTINGS.get("cvp_port", "8443"),
@@ -207,28 +195,10 @@ def sync_data(self):
             username=PLUGIN_SETTINGS["cvp_user"],
             password=PLUGIN_SETTINGS["cvp_password"],
             cvp_token=PLUGIN_SETTINGS["cvp_token"],
-        ) as cvp:
+        ) as client:
             self.log("Loading data from CloudVision")
-            cv = CloudvisionAdapter(job=self, conn=cvp)
-            cv.load()
-        self.log("Loading data from Nautobot")
-        nb = NautobotAdapter()
-        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")
+            self.target_adapter = CloudvisionAdapter(job=self, conn=client)
+            self.target_adapter.load()
 
     def lookup_object(self, model_name, unique_id):
         """Lookup object for SSoT plugin integration."""
diff --git a/nautobot_ssot_aristacv/signals.py b/nautobot_ssot_aristacv/signals.py
index ac0c601..6030ba4 100644
--- a/nautobot_ssot_aristacv/signals.py
+++ b/nautobot_ssot_aristacv/signals.py
@@ -81,3 +81,21 @@ def post_migrate_create_custom_fields(apps=global_apps, **kwargs):
     ]:
         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)])
+
+
+def post_migrate_create_manufacturer(apps=global_apps, **kwargs):
+    """Callback function for post_migrate() -- create Arista Manufacturer."""
+    Manufacturer = apps.get_model("dcim", "Manufacturer")
+    Manufacturer.objects.get_or_create(name="Arista", slug="arista")
+
+
+def post_migrate_create_platform(apps=global_apps, **kwargs):
+    """Callback function for post_migrate() -- create Arista Platform."""
+    Platform = apps.get_model("dcim", "Platform")
+    Manufacturer = apps.get_model("dcim", "Manufacturer")
+    Platform.objects.get_or_create(
+        name="arista.eos.eos",
+        slug="arista_eos",
+        napalm_driver="eos",
+        manufacturer=Manufacturer.objects.get(slug="arista"),
+    )
diff --git a/nautobot_ssot_aristacv/utils/cloudvision.py b/nautobot_ssot_aristacv/utils/cloudvision.py
index 991fa0f..6873726 100644
--- a/nautobot_ssot_aristacv/utils/cloudvision.py
+++ b/nautobot_ssot_aristacv/utils/cloudvision.py
@@ -24,7 +24,6 @@
 from nautobot_ssot_aristacv.constant import PORT_TYPE_MAP
 
 RPC_TIMEOUT = 30
-PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
 TIME_TYPE = Union[pbts.Timestamp, datetime]
 UPDATE_TYPE = Tuple[Any, Any]
 UPDATES_TYPE = List[UPDATE_TYPE]
@@ -95,7 +94,7 @@ def __init__(
             self.metadata = ((self.AUTH_KEY_PATH, self.cvp_token),)
         # Set up credentials for CVaaS using supplied token.
         else:
-            self.cvp_url = PLUGIN_SETTINGS.get("cvaas_url", "www.arista.io:443")
+            self.cvp_url = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"].get("cvaas_url", "www.arista.io:443")
             call_creds = grpc.access_token_call_credentials(self.cvp_token)
             channel_creds = grpc.ssl_channel_credentials()
         conn_creds = grpc.composite_channel_credentials(channel_creds, call_creds)
@@ -267,7 +266,7 @@ def search(  # pylint:disable=dangerous-default-value, too-many-locals
 def get_devices(client):
     """Get devices from CloudVision inventory."""
     device_stub = services.DeviceServiceStub(client)
-    if PLUGIN_SETTINGS.get("import_active"):
+    if settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"].get("import_active"):
         req = services.DeviceStreamRequest(
             partial_eq_filter=[models.Device(streaming_status=models.STREAMING_STATUS_ACTIVE)]
         )
@@ -282,6 +281,7 @@ def get_devices(client):
             "fqdn": resp.value.fqdn.value,
             "sw_ver": resp.value.software_version.value,
             "model": resp.value.model_name.value,
+            "status": "active" if resp.value.streaming_status == 2 else "offline",
             "system_mac_address": resp.value.system_mac_address.value,
         }
         devices.append(device)
@@ -509,7 +509,9 @@ def get_interfaces_chassis(client: CloudvisionApi, dId):
                             "oper_status": "up"
                             if results.get("operStatus") and results["operStatus"]["Name"] == "intfOperUp"
                             else "down",
-                            "enabled": bool(results["enabledState"]["Name"] == "enabled"),
+                            "enabled": bool(results["enabledState"]["Name"] == "enabled")
+                            if results.get("enabledState")
+                            else False,
                             "mac_addr": results["burnedInAddr"],
                             "mtu": results["mtu"],
                         }
@@ -645,3 +647,55 @@ def get_interface_status(port_info: dict) -> str:
     if port_info["oper_status"] == "down" and port_info["link_status"] == "down":
         status = "maintenance"
     return status
+
+
+def get_interface_description(client: CloudvisionApi, dId: str, interface: str):
+    """Gets interface description.
+
+    Args:
+        client (CloudvisionApi): Cloudvision connection.
+        dId (str): Device ID to get description for.
+        interface (str): Name of interface to get description for.
+    """
+    pathElts = ["Sysdb", "interface", "config", "eth", "phy", "slice", "1", "intfConfig", interface]
+    query = [create_query([(pathElts, [])], dId)]
+    query = unfreeze_frozen_dict(query)
+
+    for batch in client.get(query):
+        for notif in batch["notifications"]:
+            if notif["updates"].get("description") and notif["updates"]["description"] is not None:
+                return notif["updates"]["description"]
+    return ""
+
+
+def get_ip_interfaces(client: CloudvisionApi, dId: str):
+    """Gets interfaces with IP Addresses configured from specified device.
+
+    Args:
+        client (CloudvisionApi): Cloudvision connection.
+        dId (str): Device ID to retrieve IP Addresses and associated interfaces for.
+    """
+    pathElts = ["Sysdb", "ip", "config", "ipIntfConfig", Wildcard()]
+    query = [create_query([(pathElts, [])], dId)]
+    query = unfreeze_frozen_dict(query)
+
+    ip_intfs = []
+    for batch in client.get(query):
+        for notif in batch["notifications"]:
+            try:
+                results = notif["updates"]
+                if results.get("intfId") and results.get("addrWithMask"):
+                    ip_intfs.append(
+                        {
+                            "interface": results["intfId"],
+                            "address": results["addrWithMask"]
+                            if results["addrWithMask"] != "0.0.0.0/0"
+                            else results.get("virtualAddrWithMask"),
+                        }
+                    )
+            except KeyError as e:
+                print(
+                    f"Unknown key {e} for ip_intfs on interface {notif['updates']['intfId'] if notif['updates'].get('intfId') else notif['updates'].get('name')}.\n\nUpdate: {notif['updates']}"
+                )
+                continue
+    return ip_intfs
diff --git a/nautobot_ssot_aristacv/utils/nautobot.py b/nautobot_ssot_aristacv/utils/nautobot.py
index cedd3f2..e104068 100644
--- a/nautobot_ssot_aristacv/utils/nautobot.py
+++ b/nautobot_ssot_aristacv/utils/nautobot.py
@@ -1,8 +1,19 @@
 """Utility functions for Nautobot ORM."""
-from nautobot.dcim.models import DeviceType, DeviceRole, Site, Manufacturer
-from nautobot.extras.models.statuses import Status
-from nautobot.extras.models.tags import Tag
+import re
+from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
+from django.utils.text import slugify
+
+from nautobot.dcim.models import DeviceRole, DeviceType, Manufacturer, Site
+from nautobot.extras.models import Status, Tag, Relationship
+
+try:
+    from nautobot_device_lifecycle_mgmt.models import SoftwareLCM  # noqa: F401 # pylint: disable=unused-import
+
+    LIFECYCLE_MGMT = True
+except ImportError:
+    print("Device Lifecycle plugin isn't installed so will revert to CustomField for OS version.")
+    LIFECYCLE_MGMT = False
 
 
 def verify_site(site_name):
@@ -14,21 +25,11 @@ def verify_site(site_name):
     try:
         site_obj = Site.objects.get(name=site_name)
     except Site.DoesNotExist:
-        site_obj = Site(name=site_name, slug=site_name.lower(), status=Status.objects.get(name="Staging"))
+        site_obj = Site(name=site_name, slug=slugify(site_name), status=Status.objects.get(name="Staging"))
         site_obj.validated_save()
     return site_obj
 
 
-def verify_manufacturer():
-    """Verifies whether Arista manufactere exists in Nautobot. If not, creates Arista Manufacturer."""
-    try:
-        arista_mf = Manufacturer.objects.get(name="Arista")
-    except Manufacturer.DoesNotExist:
-        arista_mf = Manufacturer(name="Arista", slug="arista")
-        arista_mf.validated_save()
-    return arista_mf
-
-
 def verify_device_type_object(device_type):
     """Verifies whether device type object already exists in Nautobot. If not, creates specified device type.
 
@@ -38,8 +39,9 @@ def verify_device_type_object(device_type):
     try:
         device_type_obj = DeviceType.objects.get(model=device_type)
     except DeviceType.DoesNotExist:
-        manufacturer = verify_manufacturer()
-        device_type_obj = DeviceType(manufacturer=manufacturer, model=device_type, slug=device_type.lower())
+        device_type_obj = DeviceType(
+            manufacturer=Manufacturer.objects.get(name="Arista"), model=device_type, slug=device_type.lower()
+        )
         device_type_obj.validated_save()
     return device_type_obj
 
@@ -89,3 +91,73 @@ def verify_import_tag():
         import_tag = Tag(name="cloudvision_imported", slug="cloudvision_imported", color="ff0000")
         import_tag.validated_save()
     return import_tag
+
+
+def get_device_version(device):
+    """Determines Device version from Custom Field or RelationshipAssociation.
+
+    Args:
+        device (Device): The Device object to determine software version for.
+    """
+    version = ""
+    if LIFECYCLE_MGMT:
+        software_relation = Relationship.objects.get(name="Software on Device")
+        relations = device.get_relationships()
+        try:
+            version = relations["destination"][software_relation][0].source.version
+        except KeyError:
+            pass
+    else:
+        version = device.custom_field_data["arista_eos"]
+    return version
+
+
+def parse_hostname(hostname: str):
+    """Parse a device's hostname to find site and role.
+
+    Args:
+        hostname (str): Device hostname to be parsed for site and role.
+    """
+    hostname_patterns = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"].get("hostname_patterns")
+
+    site, role = None, None
+    for pattern in hostname_patterns:
+        match = re.search(pattern=pattern, string=hostname)
+        if match:
+            if match.group("site"):
+                site = match.group("site")
+            if match.group("role"):
+                role = match.group("role")
+    return (site, role)
+
+
+def get_site_from_map(site_code: str):
+    """Get name of Site from site_mapping based upon sitecode.
+
+    Args:
+        site_code (str): Site code from device hostname.
+
+    Returns:
+        str|None: Name of Site if site code found else None.
+    """
+    site_map = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"].get("site_mappings")
+    site_name = None
+    if site_code in site_map:
+        site_name = site_map[site_code]
+    return site_name
+
+
+def get_role_from_map(role_code: str):
+    """Get name of Role from role_mapping based upon role code in hostname.
+
+    Args:
+        role_code (str): Role code from device hostname.
+
+    Returns:
+        str|None: Name of Device Role if role code found else None.
+    """
+    role_map = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"].get("role_mappings")
+    role_name = None
+    if role_code in role_map:
+        role_name = role_map[role_code]
+    return role_name
diff --git a/poetry.lock b/poetry.lock
index 002cdff..7e086d2 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -99,11 +99,11 @@ python-versions = "*"
 
 [[package]]
 name = "black"
-version = "22.8.0"
+version = "22.10.0"
 description = "The uncompromising code formatter."
 category = "dev"
 optional = false
-python-versions = ">=3.6.2"
+python-versions = ">=3.7"
 
 [package.dependencies]
 click = ">=8.0.0"
@@ -262,7 +262,7 @@ six = "*"
 
 [[package]]
 name = "cloudvision"
-version = "1.5.0.post3"
+version = "1.6.0"
 description = "A Python library for Arista's CloudVision APIs."
 category = "main"
 optional = false
@@ -323,7 +323,7 @@ toml = ["tomli"]
 
 [[package]]
 name = "cryptography"
-version = "38.0.1"
+version = "38.0.2"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 category = "main"
 optional = false
@@ -340,6 +340,14 @@ sdist = ["setuptools-rust (>=0.11.4)"]
 ssh = ["bcrypt (>=3.1.5)"]
 test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
 
+[[package]]
+name = "cycler"
+version = "0.11.0"
+description = "Composable style cycles"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
 [[package]]
 name = "defusedxml"
 version = "0.7.1"
@@ -796,6 +804,28 @@ mccabe = ">=0.6.0,<0.7.0"
 pycodestyle = ">=2.7.0,<2.8.0"
 pyflakes = ">=2.3.0,<2.4.0"
 
+[[package]]
+name = "fonttools"
+version = "4.37.4"
+description = "Tools to manipulate font files"
+category = "main"
+optional = true
+python-versions = ">=3.7"
+
+[package.extras]
+all = ["fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "zopfli (>=0.1.4)", "lz4 (>=1.7.4.2)", "matplotlib", "sympy", "skia-pathops (>=0.5.0)", "uharfbuzz (>=0.23.0)", "brotlicffi (>=0.8.0)", "scipy", "brotli (>=1.0.1)", "munkres", "unicodedata2 (>=14.0.0)", "xattr"]
+graphite = ["lz4 (>=1.7.4.2)"]
+interpolatable = ["scipy", "munkres"]
+lxml = ["lxml (>=4.0,<5)"]
+pathops = ["skia-pathops (>=0.5.0)"]
+plot = ["matplotlib"]
+repacker = ["uharfbuzz (>=0.23.0)"]
+symfont = ["sympy"]
+type1 = ["xattr"]
+ufo = ["fs (>=2.2.0,<3)"]
+unicode = ["unicodedata2 (>=14.0.0)"]
+woff = ["zopfli (>=0.1.4)", "brotlicffi (>=0.8.0)", "brotli (>=1.0.1)"]
+
 [[package]]
 name = "funcy"
 version = "1.17"
@@ -831,7 +861,7 @@ smmap = ">=3.0.1,<6"
 
 [[package]]
 name = "gitpython"
-version = "3.1.27"
+version = "3.1.29"
 description = "GitPython is a python library used to interact with Git repositories"
 category = "main"
 optional = false
@@ -961,7 +991,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8
 
 [[package]]
 name = "importlib-resources"
-version = "5.9.0"
+version = "5.10.0"
 description = "Read resources from Python packages"
 category = "main"
 optional = false
@@ -971,8 +1001,8 @@ python-versions = ">=3.7"
 zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
+docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
 
 [[package]]
 name = "inflection"
@@ -1045,6 +1075,17 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
 format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
 format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"]
 
+[[package]]
+name = "kiwisolver"
+version = "1.4.4"
+description = "A fast implementation of the Cassowary constraint solver"
+category = "main"
+optional = true
+python-versions = ">=3.7"
+
+[package.dependencies]
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+
 [[package]]
 name = "kombu"
 version = "5.2.4"
@@ -1116,6 +1157,25 @@ category = "main"
 optional = false
 python-versions = ">=3.7"
 
+[[package]]
+name = "matplotlib"
+version = "3.5.3"
+description = "Python plotting package"
+category = "main"
+optional = true
+python-versions = ">=3.7"
+
+[package.dependencies]
+cycler = ">=0.10"
+fonttools = ">=4.22.0"
+kiwisolver = ">=1.0.1"
+numpy = ">=1.17"
+packaging = ">=20.0"
+pillow = ">=6.2.0"
+pyparsing = ">=2.2.1"
+python-dateutil = ">=2.7"
+setuptools_scm = ">=4,<7"
+
 [[package]]
 name = "mccabe"
 version = "0.6.1"
@@ -1134,7 +1194,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "mkdocs"
-version = "1.4.0"
+version = "1.4.1"
 description = "Project documentation with Markdown."
 category = "dev"
 optional = false
@@ -1142,18 +1202,20 @@ python-versions = ">=3.7"
 
 [package.dependencies]
 click = ">=7.0"
+colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
 ghp-import = ">=1.0"
 importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
-Jinja2 = ">=2.11.1"
-Markdown = ">=3.2.1,<3.4"
+jinja2 = ">=2.11.1"
+markdown = ">=3.2.1,<3.4"
 mergedeep = ">=1.3.4"
 packaging = ">=20.5"
-PyYAML = ">=5.1"
+pyyaml = ">=5.1"
 pyyaml-env-tag = ">=0.1"
 typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""}
 watchdog = ">=2.0"
 
 [package.extras]
+min-versions = ["watchdog (==2.0)", "typing-extensions (==3.10)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "packaging (==20.5)", "mergedeep (==1.3.4)", "markupsafe (==2.0.1)", "markdown (==3.2.1)", "jinja2 (==2.11.1)", "importlib-metadata (==4.3)", "ghp-import (==1.0)", "colorama (==0.4)", "click (==7.0)", "babel (==2.9.0)"]
 i18n = ["babel (>=2.9.0)"]
 
 [[package]]
@@ -1225,25 +1287,38 @@ social-auth-app-django = ">=5.0.0,<5.1.0"
 svgwrite = ">=1.4.2,<1.5.0"
 
 [package.extras]
-all = ["django-auth-ldap (>=4.1.0,<4.2.0)", "django-storages (>=1.12.3,<1.13.0)", "mysqlclient (>=2.1.0,<2.2.0)", "napalm (>=3.4.1,<3.5.0)", "social-auth-core[openidconnect,saml] (>=4.3.0,<4.4.0)"]
+all = ["django-auth-ldap (>=4.1.0,<4.2.0)", "django-storages (>=1.12.3,<1.13.0)", "mysqlclient (>=2.1.0,<2.2.0)", "napalm (>=3.4.1,<3.5.0)", "social-auth-core[saml,openidconnect] (>=4.3.0,<4.4.0)"]
 ldap = ["django-auth-ldap (>=4.1.0,<4.2.0)"]
 remote_storage = ["django-storages (>=1.12.3,<1.13.0)"]
 mysql = ["mysqlclient (>=2.1.0,<2.2.0)"]
 napalm = ["napalm (>=3.4.1,<3.5.0)"]
-sso = ["social-auth-core[openidconnect,saml] (>=4.3.0,<4.4.0)"]
+sso = ["social-auth-core[saml,openidconnect] (>=4.3.0,<4.4.0)"]
+
+[[package]]
+name = "nautobot-device-lifecycle-mgmt"
+version = "1.0.2"
+description = "Manages device lifecycles for platforms and software."
+category = "main"
+optional = true
+python-versions = ">=3.6.2,<4.0.0"
+
+[package.dependencies]
+matplotlib = ">=3.3.4,<4.0.0"
+pycountry = ">=20.7.3,<21.0.0"
 
 [[package]]
 name = "nautobot-ssot"
-version = "1.1.2"
+version = "1.2.0"
 description = "Nautobot Single Source of Truth"
 category = "main"
 optional = false
-python-versions = ">=3.6,<4.0"
+python-versions = ">=3.7,<4.0"
 
 [package.dependencies]
-diffsync = ">=1.3.0,<2.0.0"
+diffsync = ">=1.6.0,<2.0.0"
 Markdown = "!=3.3.5"
-nautobot = ">=1.0.3,<2.0.0"
+nautobot = "*"
+packaging = ">=21.3,<22.0"
 
 [[package]]
 name = "netaddr"
@@ -1261,6 +1336,14 @@ category = "main"
 optional = false
 python-versions = ">=3.6,<4.0"
 
+[[package]]
+name = "numpy"
+version = "1.21.1"
+description = "NumPy is the fundamental package for array computing with Python."
+category = "main"
+optional = true
+python-versions = ">=3.7"
+
 [[package]]
 name = "oauthlib"
 version = "3.2.1"
@@ -1338,7 +1421,7 @@ test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytes
 
 [[package]]
 name = "prometheus-client"
-version = "0.14.1"
+version = "0.15.0"
 description = "Python client for the Prometheus monitoring system."
 category = "main"
 optional = false
@@ -1382,7 +1465,7 @@ python-versions = ">=3.7"
 
 [[package]]
 name = "psycopg2-binary"
-version = "2.9.3"
+version = "2.9.4"
 description = "psycopg2 - Python-PostgreSQL Database Adapter"
 category = "main"
 optional = false
@@ -1396,6 +1479,14 @@ category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
+[[package]]
+name = "pycountry"
+version = "20.7.3"
+description = "ISO country, subdivision, language, currency and script definitions and their translations"
+category = "main"
+optional = true
+python-versions = "*"
+
 [[package]]
 name = "pycparser"
 version = "2.21"
@@ -1701,6 +1792,22 @@ category = "main"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "setuptools-scm"
+version = "6.4.2"
+description = "the blessed package to manage your versions by scm tags"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+packaging = ">=20.0"
+tomli = ">=1.0.0"
+
+[package.extras]
+test = ["pytest (>=6.2)", "virtualenv (>20)"]
+toml = ["setuptools (>=42)"]
+
 [[package]]
 name = "singledispatch"
 version = "3.7.0"
@@ -1785,7 +1892,7 @@ python-versions = ">=3.5"
 
 [[package]]
 name = "stevedore"
-version = "3.5.0"
+version = "3.5.1"
 description = "Manage dynamic plugins for Python applications"
 category = "dev"
 optional = false
@@ -1844,7 +1951,7 @@ python-versions = "*"
 name = "tomli"
 version = "2.0.1"
 description = "A lil' TOML parser"
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 
@@ -1874,7 +1981,7 @@ python-versions = "*"
 
 [[package]]
 name = "types-requests"
-version = "2.28.11.1"
+version = "2.28.11.2"
 description = "Typing stubs for requests"
 category = "main"
 optional = false
@@ -1885,7 +1992,7 @@ types-urllib3 = "<1.27"
 
 [[package]]
 name = "types-urllib3"
-version = "1.26.25"
+version = "1.26.25.1"
 description = "Typing stubs for urllib3"
 category = "main"
 optional = false
@@ -1893,7 +2000,7 @@ python-versions = "*"
 
 [[package]]
 name = "typing-extensions"
-version = "4.3.0"
+version = "4.4.0"
 description = "Backported and Experimental Type Hints for Python 3.7+"
 category = "main"
 optional = false
@@ -1969,20 +2076,24 @@ pyyaml = "*"
 
 [[package]]
 name = "zipp"
-version = "3.8.1"
+version = "3.9.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
+docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
+
+[extras]
+nautobot = ["nautobot"]
+nautobot-device-lifecycle-mgmt = ["nautobot-device-lifecycle-mgmt"]
 
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "337405844556baa9d2fd3b934461145a60481c3036f8e11e9bd3713fdcf595d3"
+content-hash = "9ef6a934806bbe8a9b39f50a133ce7e07a5d8911789a380c6fa00321e11ee311"
 
 [metadata.files]
 amqp = [
@@ -2119,6 +2230,10 @@ coreschema = [
 ]
 coverage = []
 cryptography = []
+cycler = [
+    {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"},
+    {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"},
+]
 defusedxml = [
     {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
     {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
@@ -2227,6 +2342,7 @@ flake8 = [
     {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
     {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
 ]
+fonttools = []
 funcy = [
     {file = "funcy-1.17-py2.py3-none-any.whl", hash = "sha256:ba7af5e58bfc69321aaf860a1547f18d35e145706b95d1b3c966abc4f0b60309"},
     {file = "funcy-1.17.tar.gz", hash = "sha256:40b9b9a88141ae6a174df1a95861f2b82f2fdc17669080788b73a3ed9370e968"},
@@ -2239,10 +2355,7 @@ gitdb = [
     {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
     {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
 ]
-gitpython = [
-    {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"},
-    {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
-]
+gitpython = []
 graphene = [
     {file = "graphene-2.1.9-py2.py3-none-any.whl", hash = "sha256:3d446eb1237c551052bc31155cf1a3a607053e4f58c9172b83a1b597beaa0868"},
     {file = "graphene-2.1.9.tar.gz", hash = "sha256:b9f2850e064eebfee9a3ef4a1f8aa0742848d97652173ab44c82cc8a62b9ed93"},
@@ -2284,6 +2397,7 @@ jinja2 = [
     {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"},
 ]
 jsonschema = []
+kiwisolver = []
 kombu = [
     {file = "kombu-5.2.4-py3-none-any.whl", hash = "sha256:8b213b24293d3417bcf0d2f5537b7f756079e3ea232a8386dcc89a59fd2361a4"},
     {file = "kombu-5.2.4.tar.gz", hash = "sha256:37cee3ee725f94ea8bb173eaab7c1760203ea53bbebae226328600f9d2799610"},
@@ -2374,6 +2488,7 @@ markupsafe = [
     {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
     {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
 ]
+matplotlib = []
 mccabe = [
     {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
     {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
@@ -2442,15 +2557,46 @@ mypy-extensions = [
     {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
 ]
 nautobot = []
-nautobot-ssot = [
-    {file = "nautobot-ssot-1.1.2.tar.gz", hash = "sha256:869b1178674fca883789a69a08956434141a9a93d83c19711f17b05305b06983"},
-    {file = "nautobot_ssot-1.1.2-py3-none-any.whl", hash = "sha256:799f43320694bfdcb2f771f23903d65b68dae176c8c3cdc82bd49c74d0fd7697"},
+nautobot-device-lifecycle-mgmt = [
+    {file = "nautobot-device-lifecycle-mgmt-1.0.2.tar.gz", hash = "sha256:bb5f412044d0bb07c4dc427abce8bf55004f41a4bb5621e76ebc2baa023ccdc2"},
+    {file = "nautobot_device_lifecycle_mgmt-1.0.2-py3-none-any.whl", hash = "sha256:ec80e365244c7b70eb89fdaab7e4929c32b3bb0a6e02a02a657ee3fec60502ee"},
 ]
+nautobot-ssot = []
 netaddr = [
     {file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"},
     {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"},
 ]
 netutils = []
+numpy = [
+    {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"},
+    {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"},
+    {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"},
+    {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"},
+    {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"},
+    {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"},
+    {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"},
+    {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"},
+    {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"},
+    {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"},
+    {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"},
+    {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"},
+    {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"},
+    {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"},
+    {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"},
+    {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"},
+    {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"},
+    {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"},
+]
 oauthlib = []
 packaging = [
     {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
@@ -2467,77 +2613,20 @@ platformdirs = [
     {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
     {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
 ]
-prometheus-client = [
-    {file = "prometheus_client-0.14.1-py3-none-any.whl", hash = "sha256:522fded625282822a89e2773452f42df14b5a8e84a86433e3f8a189c1d54dc01"},
-    {file = "prometheus_client-0.14.1.tar.gz", hash = "sha256:5459c427624961076277fdc6dc50540e2bacb98eebde99886e59ec55ed92093a"},
-]
+prometheus-client = []
 promise = [
     {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
 ]
 prompt-toolkit = []
 protobuf = []
-psycopg2-binary = [
-    {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"},
-]
+psycopg2-binary = []
 pycodestyle = [
     {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
     {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
 ]
+pycountry = [
+    {file = "pycountry-20.7.3.tar.gz", hash = "sha256:81084a53d3454344c0292deebc20fcd0a1488c136d4900312cbd465cf552cb42"},
+]
 pycparser = [
     {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
     {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
@@ -2750,6 +2839,10 @@ rx = [
     {file = "Rx-1.6.1-py2.py3-none-any.whl", hash = "sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"},
     {file = "Rx-1.6.1.tar.gz", hash = "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23"},
 ]
+setuptools-scm = [
+    {file = "setuptools_scm-6.4.2-py3-none-any.whl", hash = "sha256:acea13255093849de7ccb11af9e1fb8bde7067783450cee9ef7a93139bddf6d4"},
+    {file = "setuptools_scm-6.4.2.tar.gz", hash = "sha256:6833ac65c6ed9711a4d5d2266f8024cfa07c533a0e55f4c12f6eff280a5a9e30"},
+]
 singledispatch = [
     {file = "singledispatch-3.7.0-py2.py3-none-any.whl", hash = "sha256:bc77afa97c8a22596d6d4fc20f1b7bdd2b86edc2a65a4262bdd7cc3cc19aa989"},
     {file = "singledispatch-3.7.0.tar.gz", hash = "sha256:c1a4d5c1da310c3fd8fccfb8d4e1cb7df076148fd5d858a819e37fffe44f3092"},
@@ -2775,10 +2868,7 @@ social-auth-core = [
     {file = "social_auth_core-4.3.0-py3-none-any.whl", hash = "sha256:1e3440d104f743b02dfe258c9d4dba5b4065abf24b2f7eb362b47054d21797df"},
 ]
 sqlparse = []
-stevedore = [
-    {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"},
-    {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"},
-]
+stevedore = []
 structlog = [
     {file = "structlog-21.5.0-py3-none-any.whl", hash = "sha256:fd7922e195262b337da85c2a91c84be94ccab1f8fd1957bd6986f6904e3761c8"},
     {file = "structlog-21.5.0.tar.gz", hash = "sha256:68c4c29c003714fe86834f347cb107452847ba52414390a7ee583472bde00fc9"},
@@ -2823,10 +2913,7 @@ types-protobuf = []
 types-pyyaml = []
 types-requests = []
 types-urllib3 = []
-typing-extensions = [
-    {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
-    {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
-]
+typing-extensions = []
 uritemplate = [
     {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
     {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
diff --git a/pyproject.toml b/pyproject.toml
index 8a1764e..2a2ec6c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "nautobot-ssot-aristacv"
-version = "1.3.0"
+version = "1.4.0"
 description = "Nautobot SSoT Arista CloudVision"
 authors = ["Network to Code, LLC <info@networktocode.com>"]
 
@@ -23,6 +23,7 @@ python = "^3.7"
 nautobot = "^1.2.0"
 nautobot-ssot = "^1.0.1"
 cloudvision = "^1.5.0"
+nautobot-device-lifecycle-mgmt = {version = "^1.0.2", optional = true}
 
 [tool.poetry.dev-dependencies]
 invoke = "*"
@@ -39,6 +40,10 @@ mkdocs = "*"
 markdown-include = "*"
 parameterized = "^0.8.1"
 
+[tool.poetry.extras]
+nautobot = ["nautobot"]
+nautobot-device-lifecycle-mgmt = ["nautobot-device-lifecycle-mgmt"]
+
 [tool.black]
 line-length = 120
 target-version = ['py37']


Test patch:
diff --git a/nautobot_ssot_aristacv/tests/fixtures/fixtures.py b/nautobot_ssot_aristacv/tests/fixtures/fixtures.py
index 086d8ee..1cb4f69 100644
--- a/nautobot_ssot_aristacv/tests/fixtures/fixtures.py
+++ b/nautobot_ssot_aristacv/tests/fixtures/fixtures.py
@@ -11,3 +11,8 @@ def load_json(path):
 DEVICE_FIXTURE = load_json("./nautobot_ssot_aristacv/tests/fixtures/get_devices_response.json")
 FIXED_INTF_QUERY = load_json("./nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_client_query.json")
 INTERFACE_FIXTURE = load_json("./nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_response.json")
+INTF_DESCRIPTION_QUERY = load_json(
+    "./nautobot_ssot_aristacv/tests/fixtures/get_interface_description_client_query.json"
+)
+IP_INTF_QUERY = load_json("./nautobot_ssot_aristacv/tests/fixtures/get_ip_interfaces_client_query.json")
+IP_INTF_FIXTURE = load_json("./nautobot_ssot_aristacv/tests/fixtures/get_ip_interfaces_response.json")
diff --git a/nautobot_ssot_aristacv/tests/fixtures/get_devices_response.json b/nautobot_ssot_aristacv/tests/fixtures/get_devices_response.json
index 08dabb6..1eb7dc9 100644
--- a/nautobot_ssot_aristacv/tests/fixtures/get_devices_response.json
+++ b/nautobot_ssot_aristacv/tests/fixtures/get_devices_response.json
@@ -3,6 +3,7 @@
         "device_id": "JPE12345678",
         "hostname": "ams01-edge-01.ntc.com",
         "fqdn": "ams01-edge-01.ntc.com",
+        "status": "active",
         "sw_ver": "4.26.5M",
         "model": "DCS-7280CR2-60",
         "system_mac_address": "12:34:56:78:ab:cd"
@@ -11,6 +12,7 @@
         "device_id": "JPE12345679",
         "hostname": "ams01-edge-02.ntc.com",
         "fqdn": "ams01-edge-02.ntc.com",
+        "status": "active",
         "sw_ver": "4.26.5M",
         "model": "DCS-7280CR2-60",
         "system_mac_address": "12:34:56:78:ab:ce"
diff --git a/nautobot_ssot_aristacv/tests/fixtures/get_interface_description_client_query.json b/nautobot_ssot_aristacv/tests/fixtures/get_interface_description_client_query.json
new file mode 100644
index 0000000..d35a6c4
--- /dev/null
+++ b/nautobot_ssot_aristacv/tests/fixtures/get_interface_description_client_query.json
@@ -0,0 +1,245 @@
+[
+    {
+        "dataset": {
+            "name": "JPE12345678",
+            "type": "device"
+        },
+        "notifications": [
+            {
+                "timestamp": {
+                    "seconds": 1657780598,
+                    "nanos": 759853684
+                },
+                "deletes": [],
+                "updates": {
+                    "mtu": 1500,
+                    "addr": "00:00:00:00:00:00",
+                    "name": "",
+                    "l2Mru": 0,
+                    "l2Mtu": 0,
+                    "intfId": "Ethernet1",
+                    "fecBypass": {
+                        "Name": "fecBypassDisabled",
+                        "Value": 0
+                    },
+                    "requestor": [
+                        "Sysdb",
+                        "interface",
+                        "config",
+                        "eth",
+                        "phy",
+                        "slice",
+                        "1",
+                        "intfConfig",
+                        "Ethernet1",
+                        "requestor"
+                    ],
+                    "description": "Uplink to DC1",
+                    "loadInterval": {
+                        "val": 300.0,
+                        "useDefault": true
+                    },
+                    "loopbackMode": {
+                        "Name": "loopbackNone",
+                        "Value": 0
+                    },
+                    "defaultConfig": [
+                        "Sysdb",
+                        "interface",
+                        "config",
+                        "eth",
+                        "phy",
+                        "default",
+                        "defaultIntfConfig",
+                        "DefaultEthSwitchedPort2"
+                    ],
+                    "linkModeLocal": {
+                        "Name": "linkModeForced10GbpsFull",
+                        "Value": 9
+                    },
+                    "timestampMode": {
+                        "Name": "timestampModeDisabled",
+                        "Value": 0
+                    },
+                    "enabledDefault": true,
+                    "l3MtuConfigured": false,
+                    "fecEncodingConfig": {
+                        "fecEncodingDisabled": false,
+                        "fecEncodingFireCode": false,
+                        "fecEncodingReedSolomon": false,
+                        "fecEncodingReedSolomon544": false
+                    },
+                    "linkStatusLogging": {
+                        "Name": "useGlobal",
+                        "Value": 2
+                    },
+                    "rxFlowcontrolLocal": {
+                        "Name": "flowControlConfigUnknown",
+                        "Value": 0
+                    },
+                    "txFlowcontrolLocal": {
+                        "Name": "flowControlConfigUnknown",
+                        "Value": 0
+                    },
+                    "routerMacConfigured": false,
+                    "adminShutdownDelayed": false,
+                    "advertisedModesLocal": {
+                        "mode1GbpsFull": false,
+                        "mode1GbpsHalf": false,
+                        "mode5GbpsFull": false,
+                        "mode10GbpsFull": false,
+                        "mode10MbpsFull": false,
+                        "mode10MbpsHalf": false,
+                        "mode25GbpsFull": false,
+                        "mode40GbpsFull": false,
+                        "mode50GbpsFull": false,
+                        "mode100GbpsFull": false,
+                        "mode100MbpsFull": false,
+                        "mode100MbpsHalf": false,
+                        "mode2p5GbpsFull": false,
+                        "mode50GbpsFull1Lane": false,
+                        "mode100GbpsFull1Lane": false,
+                        "mode100GbpsFull2Lane": false,
+                        "mode200GbpsFull2Lane": false,
+                        "mode200GbpsFull4Lane": false,
+                        "mode200GbpsFull8Lane": false,
+                        "mode400GbpsFull4Lane": false,
+                        "mode400GbpsFull8Lane": false,
+                        "mode800GbpsFull8Lane": false,
+                        "modeHg11GbpsFull1Lane": false,
+                        "modeHg21GbpsFull2Lane": false,
+                        "modeHg27GbpsFull1Lane": false,
+                        "modeHg42GbpsFull4Lane": false,
+                        "modeHg53GbpsFull2Lane": false,
+                        "modeHg106GbpsFull4Lane": false
+                    },
+                    "legacySpeedCmdSyntax": false,
+                    "linkUpDebouncePeriod": 0,
+                    "adminShutdownDelayable": false,
+                    "linkDownDebouncePeriod": 0,
+                    "unidirectionalLinkMode": {
+                        "Name": "uniLinkModeDisabled",
+                        "Value": 0
+                    },
+                    "isCoherentFecConfigured": false,
+                    "negotiation25gModeConfig": {
+                        "ieee25g": true,
+                        "consortium25g": true
+                    },
+                    "congestionDropsLoggingMode": {
+                        "Name": "useGlobal",
+                        "Value": 2
+                    },
+                    "advertisedModesConfiguredLocal": false,
+                    "advertisedModesConfiguredForSfp1000BaseT": false
+                },
+                "retracts": [],
+                "path_elements": [
+                    "Sysdb",
+                    "interface",
+                    "config",
+                    "eth",
+                    "phy",
+                    "slice",
+                    "1",
+                    "intfConfig",
+                    "Ethernet1"
+                ]
+            },
+            {
+                "timestamp": {
+                    "seconds": 1661837581,
+                    "nanos": 636543313
+                },
+                "deletes": [],
+                "updates": {
+                    "globalIntfConfig": []
+                },
+                "retracts": [],
+                "path_elements": [
+                    "Sysdb",
+                    "interface",
+                    "config",
+                    "eth",
+                    "phy",
+                    "slice",
+                    "1",
+                    "intfConfig",
+                    "Ethernet1"
+                ]
+            },
+            {
+                "timestamp": {
+                    "seconds": 1657781481,
+                    "nanos": 609437412
+                },
+                "deletes": [],
+                "updates": {
+                    "enabledStateLocal": {
+                        "Name": "enabled",
+                        "Value": 1
+                    }
+                },
+                "retracts": [],
+                "path_elements": [
+                    "Sysdb",
+                    "interface",
+                    "config",
+                    "eth",
+                    "phy",
+                    "slice",
+                    "1",
+                    "intfConfig",
+                    "Ethernet1"
+                ]
+            },
+            {
+                "timestamp": {
+                    "seconds": 1657781481,
+                    "nanos": 609445674
+                },
+                "deletes": [],
+                "updates": {
+                    "enabledStateReason": ""
+                },
+                "retracts": [],
+                "path_elements": [
+                    "Sysdb",
+                    "interface",
+                    "config",
+                    "eth",
+                    "phy",
+                    "slice",
+                    "1",
+                    "intfConfig",
+                    "Ethernet1"
+                ]
+            },
+            {
+                "timestamp": {
+                    "seconds": 1661837581,
+                    "nanos": 636560560
+                },
+                "deletes": [],
+                "updates": {
+                    "adminEnabledStateLocal": {
+                        "Name": "enabled",
+                        "Value": 1
+                    }
+                },
+                "retracts": [],
+                "path_elements": [
+                    "Sysdb",
+                    "interface",
+                    "config",
+                    "eth",
+                    "phy",
+                    "slice",
+                    "1",
+                    "intfConfig",
+                    "Ethernet1"
+                ]
+            }
+        ]
+    }
+]
\ No newline at end of file
diff --git a/nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_client_query.json b/nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_client_query.json
index 595b84c..4718528 100644
--- a/nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_client_query.json
+++ b/nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_client_query.json
@@ -1,5 +1,9 @@
 [
     {
+        "dataset": {
+            "name": "JPE12345678",
+            "type": "device"
+        },
         "notifications": [
             {
                 "updates": {
diff --git a/nautobot_ssot_aristacv/tests/fixtures/get_ip_interfaces_client_query.json b/nautobot_ssot_aristacv/tests/fixtures/get_ip_interfaces_client_query.json
new file mode 100644
index 0000000..12fc8bb
--- /dev/null
+++ b/nautobot_ssot_aristacv/tests/fixtures/get_ip_interfaces_client_query.json
@@ -0,0 +1,190 @@
+[
+    {
+        "dataset": {
+            "name": "JPE12345678",
+            "type": "device"
+        },
+        "notifications": [
+            {
+                "deletes": [],
+                "path_elements": [
+                    "Sysdb",
+                    "ip",
+                    "config",
+                    "ipIntfConfig",
+                    "Loopback1"
+                ],
+                "retracts": [],
+                "timestamp": {
+                    "seconds": 1666161641,
+                    "nanos": 192093825
+                },
+                "updates": {
+                    "addrSource": {
+                        "Name": "manual",
+                        "Value": 0
+                    },
+                    "addrWithMask": "203.0.113.1/24",
+                    "attachedRoutes": true,
+                    "configSource": "00cli",
+                    "defaultRouteSource": {
+                        "Name": "manual",
+                        "Value": 0
+                    },
+                    "directedBroadcastEnabled": false,
+                    "forceDpRoutingEnabled": false,
+                    "forwarding": true,
+                    "gratuitousArpAccepted": false,
+                    "intfId": "Loopback1",
+                    "l3Config": [
+                        "Sysdb",
+                        "l3",
+                        "intf",
+                        "config",
+                        "intfConfig",
+                        "Loopback1"
+                    ],
+                    "localProxyArpEnabled": false,
+                    "mergeAddrsWithWinner": false,
+                    "name": "",
+                    "noArp": {
+                        "Name": "isInvalid",
+                        "Value": 0
+                    },
+                    "proxyArpEnabled": false,
+                    "scope": {
+                        "Name": "universe",
+                        "Value": 0
+                    },
+                    "secondaryWithMask": [
+                        "Sysdb",
+                        "ip",
+                        "config",
+                        "ipIntfConfig",
+                        "Loopback1",
+                        "secondaryWithMask"
+                    ],
+                    "tcpMssConfig": [
+                        "Sysdb",
+                        "ip",
+                        "config",
+                        "ipIntfConfig",
+                        "Loopback1",
+                        "tcpMssConfig"
+                    ],
+                    "unnumberedIntfId": "",
+                    "urpf": {
+                        "acl": "",
+                        "mode": {
+                            "Name": "disable",
+                            "Value": 0
+                        }
+                    },
+                    "virtualAddrWithMask": "0.0.0.0/0",
+                    "virtualSecondaryWithMask": [
+                        "Sysdb",
+                        "ip",
+                        "config",
+                        "ipIntfConfig",
+                        "Loopback1",
+                        "virtualSecondaryWithMask"
+                    ]
+                }
+            }
+        ]
+    },
+    {
+        "dataset": {
+            "name": "JPE12345678",
+            "type": "device"
+        },
+        "notifications": [
+            {
+                "deletes": [],
+                "path_elements": [
+                    "Sysdb",
+                    "ip",
+                    "config",
+                    "ipIntfConfig",
+                    "Loopback2"
+                ],
+                "retracts": [],
+                "timestamp": {
+                    "seconds": 1666161641,
+                    "nanos": 192093825
+                },
+                "updates": {
+                    "addrSource": {
+                        "Name": "manual",
+                        "Value": 0
+                    },
+                    "addrWithMask": "203.0.113.2/24",
+                    "attachedRoutes": true,
+                    "configSource": "00cli",
+                    "defaultRouteSource": {
+                        "Name": "manual",
+                        "Value": 0
+                    },
+                    "directedBroadcastEnabled": false,
+                    "forceDpRoutingEnabled": false,
+                    "forwarding": true,
+                    "gratuitousArpAccepted": false,
+                    "intfId": "Loopback2",
+                    "l3Config": [
+                        "Sysdb",
+                        "l3",
+                        "intf",
+                        "config",
+                        "intfConfig",
+                        "Loopback2"
+                    ],
+                    "localProxyArpEnabled": false,
+                    "mergeAddrsWithWinner": false,
+                    "name": "",
+                    "noArp": {
+                        "Name": "isInvalid",
+                        "Value": 0
+                    },
+                    "proxyArpEnabled": false,
+                    "scope": {
+                        "Name": "universe",
+                        "Value": 0
+                    },
+                    "secondaryWithMask": [
+                        "Sysdb",
+                        "ip",
+                        "config",
+                        "ipIntfConfig",
+                        "Loopback2",
+                        "secondaryWithMask"
+                    ],
+                    "tcpMssConfig": [
+                        "Sysdb",
+                        "ip",
+                        "config",
+                        "ipIntfConfig",
+                        "Loopback2",
+                        "tcpMssConfig"
+                    ],
+                    "unnumberedIntfId": "",
+                    "urpf": {
+                        "acl": "",
+                        "mode": {
+                            "Name": "disable",
+                            "Value": 0
+                        }
+                    },
+                    "virtualAddrWithMask": "0.0.0.0/0",
+                    "virtualSecondaryWithMask": [
+                        "Sysdb",
+                        "ip",
+                        "config",
+                        "ipIntfConfig",
+                        "Loopback2",
+                        "virtualSecondaryWithMask"
+                    ]
+                }
+            }
+        ]
+    }
+]
diff --git a/nautobot_ssot_aristacv/tests/fixtures/get_ip_interfaces_response.json b/nautobot_ssot_aristacv/tests/fixtures/get_ip_interfaces_response.json
new file mode 100644
index 0000000..f10eab3
--- /dev/null
+++ b/nautobot_ssot_aristacv/tests/fixtures/get_ip_interfaces_response.json
@@ -0,0 +1,10 @@
+[
+    {
+        "interface": "Loopback1",
+        "address": "203.0.113.1/24"
+    },
+    {
+        "interface": "Loopback2",
+        "address": "203.0.113.2/24"
+    }
+]
diff --git a/nautobot_ssot_aristacv/tests/test_cloudvision_adapter.py b/nautobot_ssot_aristacv/tests/test_cloudvision_adapter.py
index ce96f1a..fbea346 100644
--- a/nautobot_ssot_aristacv/tests/test_cloudvision_adapter.py
+++ b/nautobot_ssot_aristacv/tests/test_cloudvision_adapter.py
@@ -33,6 +33,10 @@ def setUp(self):
         self.cloudvision.get_interface_mode.return_value = "access"
         self.cloudvision.get_interface_transceiver = MagicMock()
         self.cloudvision.get_interface_transceiver.return_value = "1000BASE-T"
+        self.cloudvision.get_interface_description = MagicMock()
+        self.cloudvision.get_interface_description.return_value = "Uplink to DC1"
+        self.cloudvision.get_ip_interfaces = MagicMock()
+        self.cloudvision.get_ip_interfaces.return_value = fixtures.IP_INTF_FIXTURE
 
         self.job = CloudVisionDataSource()
         self.job.job_result = JobResult.objects.create(
@@ -74,8 +78,30 @@ def test_load_interfaces(self):
                         "nautobot_ssot_aristacv.utils.cloudvision.get_interface_transceiver",
                         self.cloudvision.get_interface_transceiver,
                     ):
-                        self.cvp.load_interfaces(mock_device)
+                        with patch(
+                            "nautobot_ssot_aristacv.utils.cloudvision.get_interface_description",
+                            self.cloudvision.get_interface_description,
+                        ):
+                            self.cvp.load_interfaces(mock_device)
         self.assertEqual(
             {f"{port['interface']}__mock_device" for port in fixtures.INTERFACE_FIXTURE},
             {port.get_unique_id() for port in self.cvp.get_all("port")},
         )
+
+    def test_load_ip_addresses(self):
+        """Test the load_ip_addresses() adapter method."""
+        mock_device = MagicMock()
+        mock_device.name = "mock_device"
+        mock_device.serial = MagicMock()
+        mock_device.serial.return_value = "JPE12345678"
+
+        with patch("nautobot_ssot_aristacv.utils.cloudvision.get_ip_interfaces", self.cloudvision.get_ip_interfaces):
+            with patch(
+                "nautobot_ssot_aristacv.utils.cloudvision.get_interface_description",
+                self.cloudvision.get_interface_description,
+            ):
+                self.cvp.load_ip_addresses(dev=mock_device)
+        self.assertEqual(
+            {f"{ipaddr['address']}__mock_device__{ipaddr['interface']}" for ipaddr in fixtures.IP_INTF_FIXTURE},
+            {ipaddr.get_unique_id() for ipaddr in self.cvp.get_all("ipaddr")},
+        )
diff --git a/nautobot_ssot_aristacv/tests/test_utils_cloudvision.py b/nautobot_ssot_aristacv/tests/test_utils_cloudvision.py
index 3021cd1..477f2c1 100644
--- a/nautobot_ssot_aristacv/tests/test_utils_cloudvision.py
+++ b/nautobot_ssot_aristacv/tests/test_utils_cloudvision.py
@@ -23,6 +23,7 @@ def test_get_devices(self):
         device1.value.hostname.value = "ams01-edge-01.ntc.com"
         device1.value.fqdn.value = "ams01-edge-01.ntc.com"
         device1.value.software_version.value = "4.26.5M"
+        device1.value.streaming_status = 2
         device1.value.model_name.value = "DCS-7280CR2-60"
         device1.value.system_mac_address.value = "12:34:56:78:ab:cd"
 
@@ -31,6 +32,7 @@ def test_get_devices(self):
         device2.value.hostname.value = "ams01-edge-02.ntc.com"
         device2.value.fqdn.value = "ams01-edge-02.ntc.com"
         device2.value.software_version.value = "4.26.5M"
+        device2.value.streaming_status = 2
         device2.value.model_name.value = "DCS-7280CR2-60"
         device2.value.system_mac_address.value = "12:34:56:78:ab:ce"
 
@@ -112,3 +114,49 @@ def test_get_port_type(self, name, sent, received):  # pylint: disable=unused-ar
     def test_get_interface_status(self, name, sent, received):  # pylint: disable=unused-argument
         """Test the get_interface_status method."""
         self.assertEqual(cloudvision.get_interface_status(port_info=sent), received)
+
+    def test_get_interface_description(self):
+        """Test get_interface_description method."""
+        mock_query = MagicMock()
+        mock_query.dataset.type = "device"
+        mock_query.dataset.name = "JPE12345678"
+        mock_query.paths.path_elements = [
+            "\304\005Sysdb",
+            "\304\tinterface",
+            "\304\006config",
+            "\304\003eth",
+            "\304\003phy",
+            "\304\005slice",
+            "\304\0011",
+            "\304\nintfStatus",
+            "\304\tEthernet1",
+        ]
+
+        with patch("cloudvision.Connector.grpc_client.grpcClient.create_query", mock_query):
+            self.client.get = MagicMock()
+            self.client.get.return_value = fixtures.INTF_DESCRIPTION_QUERY
+            results = cloudvision.get_interface_description(
+                client=self.client, dId="JPE12345678", interface="Ethernet1"
+            )
+        expected = "Uplink to DC1"
+        self.assertEqual(results, expected)
+
+    def test_get_ip_interfaces(self):
+        """Test the get_ip_interfaces method."""
+        mock_query = MagicMock()
+        mock_query.dataset.type = "device"
+        mock_query.dataset.name = "JPE12345678"
+        mock_query.paths.path_elements = [
+            "\304\005Sysdb",
+            "\304\002ip",
+            "\304\006config",
+            "\304\014ipIntfConfig",
+            "\307\00\001",
+        ]
+
+        with patch("cloudvision.Connector.grpc_client.grpcClient.create_query", mock_query):
+            self.client.get = MagicMock()
+            self.client.get.return_value = fixtures.IP_INTF_QUERY
+            results = cloudvision.get_ip_interfaces(client=self.client, dId="JPE12345678")
+        expected = fixtures.IP_INTF_FIXTURE
+        self.assertEqual(results, expected)
diff --git a/nautobot_ssot_aristacv/tests/test_utils_nautobot.py b/nautobot_ssot_aristacv/tests/test_utils_nautobot.py
new file mode 100644
index 0000000..f948387
--- /dev/null
+++ b/nautobot_ssot_aristacv/tests/test_utils_nautobot.py
@@ -0,0 +1,62 @@
+"""Tests of Cloudvision utility methods."""
+from django.test import override_settings
+from nautobot.dcim.models import Site
+from nautobot.utilities.testing import TestCase
+from nautobot_ssot_aristacv.utils import nautobot
+
+
+class TestNautobotUtils(TestCase):
+    """Test Nautobot utility methods."""
+
+    databases = ("default", "job_logs")
+
+    def test_verify_site_success(self):
+        """Test the verify_site method for existing Site."""
+        test_site, _ = Site.objects.get_or_create(name="Test")
+        result = nautobot.verify_site(site_name="Test")
+        self.assertEqual(result, test_site)
+
+    def test_verify_site_fail(self):
+        """Test the verify_site method for non-existing Site."""
+        result = nautobot.verify_site(site_name="Test2")
+        self.assertEqual(result.name, "Test2")
+        self.assertEqual(result.slug, "test2")
+        self.assertTrue(isinstance(result, Site))
+
+    @override_settings(
+        PLUGINS_CONFIG={"nautobot_ssot_aristacv": {"hostname_patterns": [r"(?P<site>\w{2,3}\d+)-(?P<role>\w+)-\d+"]}}
+    )
+    def test_parse_hostname(self):
+        """Test the parse_hostname method."""
+        host = "ams01-leaf-01"
+        results = nautobot.parse_hostname(host)
+        expected = ("ams01", "leaf")
+        self.assertEqual(results, expected)
+
+    @override_settings(PLUGINS_CONFIG={"nautobot_ssot_aristacv": {"site_mappings": {"ams01": "Amsterdam"}}})
+    def test_get_site_from_map_success(self):
+        """Test the get_site_from_map method with response."""
+        results = nautobot.get_site_from_map("ams01")
+        expected = "Amsterdam"
+        self.assertEqual(results, expected)
+
+    @override_settings(PLUGINS_CONFIG={"nautobot_ssot_aristacv": {"site_mappings": {}}})
+    def test_get_site_from_map_fail(self):
+        """Test the get_site_from_map method with failed response."""
+        results = nautobot.get_site_from_map("dc01")
+        expected = None
+        self.assertEqual(results, expected)
+
+    @override_settings(PLUGINS_CONFIG={"nautobot_ssot_aristacv": {"role_mappings": {"edge": "Edge Router"}}})
+    def test_get_role_from_map_success(self):
+        """Test the get_role_from_map method with response."""
+        results = nautobot.get_role_from_map("edge")
+        expected = "Edge Router"
+        self.assertEqual(results, expected)
+
+    @override_settings(PLUGINS_CONFIG={"nautobot_ssot_aristacv": {"role_mappings": {}}})
+    def test_get_role_from_map_fail(self):
+        """Test the get_role_from_map method with failed response."""
+        results = nautobot.get_role_from_map("rtr")
+        expected = None
+        self.assertEqual(results, expected)