Repository: nautobot/nautobot-app-golden-config (language: python)

Issue:
Multiple pr



Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 675b8367..5a4abe45 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,6 +19,8 @@ services:
 # --------------------------------------------------------------------------
 # Tests
 # --------------------------------------------------------------------------
+before_install:
+  - 'docker login -u "$DOCKER_HUB_USERNAME" --password "$DOCKER_HUB_PASSWORD"'  # yamllint disable-line rule:quoted-strings
 before_script:
   - "pip install invoke toml docker-compose poetry"
 script:
diff --git a/README.md b/README.md
index bbe46b3c..7b6ba257 100644
--- a/README.md
+++ b/README.md
@@ -2,36 +2,36 @@
 
 A plugin for [Nautobot](https://github.com/nautobot/nautobot) that intends to provide context around golden configuration.
 
-In addition to this overview, documentation can be found for:
-
-- [Navigating Overview](./docs/navigating-golden.md)
-- [Navigating Backup](./docs/navigating-backup.md)
-- [Navigating Compliance](./docs/navigating-compliance.md)
-- [Navigating Intended](./docs/navigating-intended.md)
-- [Navigating SoTAgg](./docs/navigating-sotagg.md)
-- [FAQ](./docs/FAQ.md)
+**This version is currently in Beta and will require a rebuild of the database for a 1.0 release.**
 
 # Overview
 
+You may see the [Navigating Overview](./docs/navigating-golden.md) documentation for an overview of navigating through the different areas of this plugin. You may also see the [FAQ](./docs/FAQ.md) for commonly asked questions.
+
 The golden configuration plugin performs four primary actions, each of which can be toggled on with a respective `enable_*` setting, covered in detail 
 later in the readme. 
 
 * Configuration Backup - Is a Nornir process to connect to devices, optionally parse out lines/secrets, backup the configuration, and save to a Git repository.
+    * see [Navigating Backup](./docs/navigating-backup.md) for more information
 * Configuration Intended - Is a Nornir process to generate configuration based on a Git repo of Jinja files and a Git repo to store the intended configuration.
+    * see [Navigating Intended](./docs/navigating-intended.md) for more information
 * Source of Truth Aggregation - Is a GraphQL query per device with that creates a data structure used in the generation of configuration.
-* Configuration Compliance - This is a comparison of the actual (via backups) and intended (via Jinja file creation) cli configurations.
+    * see [Navigating SoTAgg](./docs/navigating-sot-agg.md) for more information
+* Configuration Compliance - Is a Nornir process to run comparison of the actual (via backups) and intended (via Jinja file creation) CLI configurations.
+    * see [Navigating Compliance](./docs/navigating-compliance.md) for more information
 
 The operator's of their own Nautobot instance are welcome to use any combination of these features. Though the appearance may seem like they are tightly 
-coupled, they are not actually. As an example, once can obtain backup configurations from their current RANCID/Oxidized process and simply provide a Git Repo
-of the location of the backup configurations and the compliance process would work the same way. Another user may only want to generate configurations.
+coupled, this isn't actually the case. For example, one can obtain backup configurations from their current RANCID/Oxidized process and simply provide a Git Repo
+of the location of the backup configurations, and the compliance process would work the same way. Also, another user may only want to generate configurations,
+but not want to use other features, which is perfectly fine to do so.
 
 ## Screenshots
 
-There are many features and capabilities the plugin provides, the following is intended to provide a quick visual overview of some of those features.
+There are many features and capabilities the plugin provides into the Nautobot ecosystem. The following screenshots are intended to provide a quick visual overview of some of these features.
 
 The golden configuration is driven by jobs that run a series of tasks and the result is captured in this overview.
 
-![Compliance Feature](./docs/img/golden-overview.png)
+![Overview](./docs/img/golden-overview.png)
 
 The compliance report provides a high-level overview on the compliance of your network.
 ![Compliance Report](./docs/img/compliance-report.png)
@@ -39,194 +39,28 @@ The compliance report provides a high-level overview on the compliance of your n
 The compliance overview will provide a per device and feature overview on the compliance of your network devices.
 ![Compliance Overview](./docs/img/compliance-overview.png)
 
-Drilling into a specfic device and feature, you can get an immediate detailed understanding of your device.
+Drilling into a specific device and feature, you can get an immediate detailed understanding of your device.
 ![Compliance Device](./docs/img/compliance-device.png)
 
-![Compliance Feature](./docs/img/compliance-feature.png)
+![Compliance Rule](./docs/img/compliance-rule.png)
 
-## Settings
+## Plugin Settings
 
-There are three primary controls to determine the inclusion of a device within one of the four given components.
+There is a setting to determine the inclusion of any of the four given components.
 
-* The `allowed_os` will allow list the specific operating systems that can be included.
 * The `enable_backup`, `enable_compliance`, `enable_intended`, and `enable_sotagg` will toggle inclusion of the entire component.
-* There is the ability to manage on a per device element TODO: Needs to be updated after more correctly implemented.
-
-## Configuration Backup
-
-The backup configuration process relies on the ability for the nautobot worker to connect via Nornir to the device, run the `show run` or equivalent command 
-and save the configuration. The high-level process to run backups is:
-
-* Download the latest Git repository, based on `backup config` type Git repo within Nautobot.
-* Run a Nornir play to obtain the cli configurations.
-* Optionally perform some lightweight processing of the backup.
-* Store the backup configurations locally.
-* Push configurations to the remote Git repository.
-
-### Configuration Backup Settings
-
-Backup configurations often need some amount of parsing to stay sane. The two obvious use cases are the ability to remove lines such as the "Last 
-Configuration" changed date, as this will cause unnecessary changes the second is to strip out secrets from the configuration. In an effort to support these 
-uses cases, the following settings are available.
-
-* Remove Lines - provides the ability to remove a line based on a regex match.
-* Substitute Lines - provides the ability to swap out parts of a line based on a regex match.
-
-TODO: Document when ideally it is in the UI and not in config context, for now, can reference [here](https://github.com/networktocode-llc/nautobot-gc-data/blob/be82d7f686a573ad33f85b2313e632d9bc2e7910/config_contexts/all.yml#L197-L214)
-
-Backup configurations rely on a Git Repo, and the plugin registers an additional repository for Git source this access. Within the Nautobot Git repositories, there will be a `backup config` option, which there must be one and only one configured for the process to work.
-
-TODO: insert screenshot
-
-The `backup_path_template` provides the ability to dynamically state per device where the configurations should end up in the file structure. Every device 
-there is a Django ORM object, tied to the model instance of a `Device` model, and that is represented as `obj`. That means that any valid Device model method is available. This is then compiled via Jinja. This may seem complicated, but the equivalent of `obj` by example would be:
-
-```python
-obj = Device.objects.get(name="nyc-rt01")
-```
-
-An example would be:
-```python
-backup_path_template = "{{obj.site.slug}}/{{obj.name}}.cfg"
-```
-
-The backup process will automatically create folders as required based on the path definition. 
-
-## Configuration Compliance 
-
-The following should be noted by what is meant by configuration compliance. Configurations are considered to be compliant if the generated configuration 
-(generally by merging data and Jinja2, will be referred to as the intended configuration from hence forth) matches "exactly" as the actual configuration is 
-on the backup. This may confusing to some, as for example to the average network engineer, there is no difference between `int g0/0` and
-`interface GigabitEthernet0/0` but for the purpose of configuration compliance, it is not a match... full stop.
-
-It's helpful to understand what are some common reasons a device is not compliant.
-
-* There is missing configuration on the device.
-* There is extra configuration on the device.
-* The data used to generate the configuration is incorrect, and created a "false positive".
-* The template used to generate the configuration is incorrect, and created a "false positive".
-* The parser used to obtain the configuration from the feature is incorrect, and created a "false positive".
-
-There is no magic to determine the state of configuration. You still must define what is good configuration and compare it. There are several reasons why 
-configuration may be as a network engineer wants it, but the tool correctly considers it non-compliant, since the tool is only comparing two configurations.
-The tool makes no assumptions to determine what an engineer may want to do, but did not document via the configuration generation process.
-
-### Configuration Compliance Parsing Engine
-
-Configuration compliance is different then a simple unix diff. While the UI provides both, the compliance metrics are not influenced by the unix diff 
-capabilities. One of the challenges of getting a device into compliance is the ramp up it takes to model and generate configurations for an entire 
-configuration. The compliance engine has several features to better build out this process.
-
-1. The ability to parse into smaller sections, given a list of root configuration elements.
-2. The ability to consider ordered and non-ordered configurations.
-3. The ability to dynamically understand parent/child relationships within the configurations.
-
-In regards to `1`, consider the following example of how to obtain service configurations:
-```
-service
-no service
-```
-Specific configurations that start with either of these commands can be grouped together.
-
-In regards to `2`, consider the configurations of SNMP on a nexus switch. 
-```
-snmp-server community secure group network-admin
-snmp-server community networktocode group network-operator
-```
-
-The above configurations are rendered based on the order in which they were entered, not based on the a deterministic way. The comparison process takes this into consideration. 
-
-In regards to `3`, consider the following example of BGP configuration. 
-```
-router bgp
-prefix-list
-```
-All configurations that are a parent and child relationships would be considered within the parsing engine. Additionally, if one configuration line was 
-wrong, only that line and the parents would be shown, not all lines or only the missing configuration without context of the parents, e.g. Given:
-
-Actual
-```
-router bgp 65250
-  router-id 10.0.10.5
-  log-neighbor-changes
-  address-family ipv4 unicast
-    redistribute direct route-map PERMIT_CONN_ROUTES
-  neighbor 10.10.10.5
-    remote-as 65250
-    address-family ipv4 unicast
-```
-
-Intended
-```
-router bgp 65250
-  router-id 10.0.10.6
-  log-neighbor-changes
-  address-family ipv4 unicast
-    redistribute direct route-map PERMIT_CONN_ROUTES
-  neighbor 10.10.10.5
-    remote-as 65250
-    address-family ipv4 unicast
-```
-Would result in the identfying the missing configurations as:
-
-```
-router bgp 65250
-  router-id 10.0.10.6
-```
-
-### Configuration Compliance Settings
-
-Configuration compliance requires the Git Repo settings for `config backups` and `intended configs`--which are covered in their respective sections--regardless if they are actually managed via the plugin or not.
-
-The Configuration compliance feature map must be created per the operator. 
-
-TODO: Once move the UI, document for now an example can be found [here](https://github.com/networktocode-llc/nautobot-gc-data/blob/be82d7f686a573ad33f85b2313e632d9bc2e7910/config_contexts/all.yml#L7-L196)
-
-## Intended Configuration Generation
-
-The Golden Config plugin provides the ability to generate configurations. The process is a Nornir play that points to a single Jinja template per 
-device that generates the configurations. Data is provided via the Source of Truth aggregation and is currently a hard requirement to be turned on if 
-generating configuration via the Golden Config plugin. Whatever data is returned by the Source of Truth Aggregation is available to the Jinja template.
-
-As previous stated, there can only be a single template per device. It is often advantageous to break configurations into smaller snippets. A common pattern 
-to overcome is:
-
-```jinja
-!
-{% include os ~ '/services.j2' %}
-!
-{% include os ~ '/hostname.j2' %}
-!
-{% include os ~ '/ntp.j2' %}
-!
-```
-or 
-
-```jinja
-!
-{% set features = ['services', 'hostname', 'ntp'] %}
-{% for feature in features %}
-{% include os ~ '/' ~ feature ~ '.j2' %}
-!
-{% endfor %}
-```
-
-### Intended Configuration Settings
-
-The configuration generation requires three components in addition to what was covered in the overall settings.
-
-1. The creation of a Git Repo for "Intended Config".
-2. The creation of a Git Repo for "Jinja Templates".
-3. The `intended_path_template` configuration parameter.
-4. The `jinja_path_template` configuration parameter.
-
-The Git Repo's and path templates follow the same philosophy as those reviewed in the backup configuration. Please refer to that section for further details.
 
 # Installation
 
+Plugins can be installed manually or use Python's `pip`. See the [nautobot documentation](https://nautobot.readthedocs.io/en/latest/plugins/#install-the-package) for more details. The pip package name for this plugin is [`nautobot-golden-config`](https://pypi.org/project/nautobot-golden-config/)
+
 > The plugin is compatible with Nautobot 1.0.0 and higher
 
-Once installed, the plugin needs to be enabled in your `configuration.py` for both plugins
+**Prerequisite:** The plugin relies on [`nautobot_plugin_nornir`](https://pypi.org/project/nautobot-plugin-nornir/) to be installed and both plugins to be enabled in your configuration settings.
+
+**Required:** The following block of code below shows the additional configuration required to be added to your `nautobot_config.py` file:
+- append `"nautobot_golden_config"` to the `PLUGINS` list
+- append the `"nautobot_golden_config"` dictionary to the `PLUGINS_CONFIG` dictionary
 
 ```python
 PLUGINS = ["nautobot_plugin_nornir", "nautobot_golden_config"]
@@ -244,15 +78,15 @@ PLUGINS_CONFIG = {
         },
     },
     "nautobot_golden_config": {
-        "allowed_os": os.environ.get("ALLOWED_OS", "all").split(","),
-        "per_feature_bar_width": float(os.environ.get("PER_FEATURE_BAR_WIDTH", 0.15)),
-        "per_feature_width": int(os.environ.get("PER_FEATURE_WIDTH", 13)),
-        "per_feature_height": int(os.environ.get("PER_FEATURE_HEIGHT", 4)),
-        "enable_backup": is_truthy(os.environ.get("ENABLE_BACKUP", True)),
-        "enable_compliance": is_truthy(os.environ.get("ENABLE_COMPLIANCE", True)),
-        "enable_intended": is_truthy(os.environ.get("ENABLE_INTENDED", True)),
-        "enable_sotagg": is_truthy(os.environ.get("ENABLE_SOTAGG", True)),
-        "sot_agg_transposer": os.environ.get("SOT_AGG_TRANSPOSER"),
+        "per_feature_bar_width": 0.15,
+        "per_feature_width": 13,
+        "per_feature_height": 4,
+        "enable_backup": True,
+        "enable_compliance": True,
+        "enable_intended": True,
+        "enable_sotagg": True,
+        "sot_agg_transposer": None,
+        "default_drivers_mapping": None,
     },
 }
 
@@ -262,36 +96,34 @@ The plugin behavior can be controlled with the following list of settings.
 
 | Key     | Example | Default | Description                          |
 | ------- | ------ | -------- | ------------------------------------- |
-| allowed_os | [cisco_ios, arista_eos] | [all] | A list of platforms supported, identified by the `platform_slug`, with special consideration for `all`. |
 | enable_backup | True | True | A boolean to represent whether or not to run backup configurations within the plugin. |
 | enable_compliance | True | True | A boolean to represent whether or not to run the compliance process within the plugin. |
 | enable_intended | True | True | A boolean to represent whether or not to generate intended configurations within the plugin. |
 | enable_sotagg | True | True | A boolean to represent whether or not to provide a GraphQL query per device to allow the intended configuration to provide data variables to the plugin. |
+| default_drivers_mapping | {"newos": "dispatcher.newos"} | None | A dictionary in which the key is a platform slug and the value is the import path of the dispatcher in string format|
+| sot_agg_transposer | mypkg.transposer | - | A string representation of a function that can post-process the graphQL data. |
 | per_feature_bar_width | 0.15 | 0.15 | The width of the table bar within the overview report | 
 | per_feature_width | 13 | 13 | The width in inches that the overview table can be. | 
 | per_feature_height | 4 | 4 | The height in inches that the overview table can be. | 
-| sot_agg_transposer | mypkg.transposer | - | A string representation of a function that can post-process the graphQL data. |
-
-> Note: Over time the intention is to make the compliance report more dynamic, but for now allow users to configure in a way that fits best for them.
 
-
-## API
-
-There is no way to currently run the script via an API, this would be helpful to use in the configuration compliance workflow, and will be a future feature.
+> Note: Over time the intention is to make the compliance report more dynamic, but for now allow users to configure the `per_*` configs in a way that fits best for them.
 
 # Contributing
 
-Pull requests are welcomed and automatically built and tested against multiple version of Python and multiple version of Nautobot through TravisCI.
+Pull requests are welcomed and automatically built and tested against multiple versions of Python and Nautobot through TravisCI.
 
-The project is packaged with a light development environment based on `docker-compose` to help with the local development of the project and to run the tests within TravisCI.
+The project is packaged with a light development environment based on `docker-compose` to help with the local development of the project and to run tests within TravisCI.
 
-The project is following Network to Code software development guideline and is leveraging:
+The project is following Network to Code software development guidelines and are leveraging the following:
 - Black, Pylint, Bandit, flake8, and pydocstyle for Python linting and formatting.
 - Django unit test to ensure the plugin is working properly.
 
 ## CLI Helper Commands
 
-The project is coming with a CLI helper based on [invoke](http://www.pyinvoke.org/) to help setup the development environment. The commands are listed below in 3 categories `dev environment`, `utility` and `testing`. 
+The project features a CLI helper based on [invoke](http://www.pyinvoke.org/) to help setup the development environment. The commands are listed below in 3 categories:
+- `dev environment`
+- `utility`
+- `testing`. 
 
 Each command can be executed with `invoke <command>`. All commands support the arguments `--nautobot-ver` and `--python-ver` if you want to manually define the version of Python and Nautobot to use. Each command also has its own help `invoke <command> --help`
 
@@ -321,8 +153,8 @@ Each command can be executed with `invoke <command>`. All commands support the a
   bandit           Run bandit to validate basic static code security analysis.
   black            Run black to check that Python files adhere to its style standards.
   flake8           Run flake8 to check that Python files adhere to its style standards.
-  pylint           Run pylint code analysis.
   pydocstyle       Run pydocstyle to validate docstring formatting adheres to NTC defined standards.
+  pylint           Run pylint code analysis.
   tests            Run all tests for this plugin.
   unittest         Run Django unit tests for the plugin.
 ```
diff --git a/development/Dockerfile b/development/Dockerfile
index 3e135ceb..a08300df 100644
--- a/development/Dockerfile
+++ b/development/Dockerfile
@@ -1,19 +1,17 @@
-ARG python_ver=3.7
-FROM python:${python_ver}
+ARG PYTHON_VER
+ARG NAUTOBOT_VER
+FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER}
 
-# Set env vars that won't change per image
-ENV PYTHONUNBUFFERED=1 \
-    PATH="/root/.poetry/bin:$PATH" \
-    NAUTOBOT_CONFIG="/source/development/nautobot_config.py"
+WORKDIR /source
 
-RUN pip install --upgrade pip\
-  && pip install poetry
+# Copy in only pyproject.toml/poetry.lock to help with caching this layer if no updates to dependencies
+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
 
-# -------------------------------------------------------------------------------------
-# Install Nautobot Plugin
-# -------------------------------------------------------------------------------------
-RUN mkdir -p /source
-WORKDIR /source
+# Copy in the rest of the source code and install local Nautobot plugin
 COPY . /source
-RUN poetry config virtualenvs.create false \
-  && poetry install --no-interaction --no-ansi
\ No newline at end of file
+RUN poetry install --no-interaction --no-ansi
+
+COPY development/nautobot_config.py /opt/nautobot/nautobot_config.py
\ No newline at end of file
diff --git a/development/docker-compose.yml b/development/docker-compose.yml
index da09551b..2c3e7f20 100644
--- a/development/docker-compose.yml
+++ b/development/docker-compose.yml
@@ -1,45 +1,59 @@
+
 ---
-x-nautobot-common: &nautobot-common
+x-nautobot-build: &nautobot-build
   build:
     args:
-      python_ver: "${PYTHON_VER}"
+      NAUTOBOT_VER: "${NAUTOBOT_VER}"
+      PYTHON_VER: "${PYTHON_VER}"
     context: "../"
     dockerfile: "development/Dockerfile"
+x-nautobot-base: &nautobot-base
   image: "nautobot-golden-config/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}"
-  depends_on:
-    - "postgres"
-    - "redis"
   env_file:
-    - "./dev.env"
-  volumes:
-    - "../:/source"
+    - "dev.env"
   tty: true
 
-version: "3"
+version: "3.4"
 services:
   nautobot:
-    <<: *nautobot-common
-    command: >
-      sh -c "nautobot-server migrate &&
-             nautobot-server runserver 0.0.0.0:8000"
+    command: "nautobot-server runserver 0.0.0.0:8080 --insecure"
+    volumes:
+      - "./nautobot_config.py:/opt/nautobot/nautobot_config.py"
+      - "../:/source"
     ports:
-      - "8000:8000"
+      - "8080:8080"
+    depends_on:
+      - "postgres"
+      - "redis"
+    <<: *nautobot-build
+    <<: *nautobot-base
   worker:
-    <<: *nautobot-common
-    command: "sh -c 'nautobot-server rqworker'"
-    ports:
-      - "8001:8001"
+    entrypoint: "nautobot-server rqworker"
+    volumes:
+      - "./nautobot_config.py:/opt/nautobot/nautobot_config.py"
+      - "../:/source"
+    depends_on:
+      - "nautobot"
+    healthcheck:
+      disable: true
+    <<: *nautobot-base
   postgres:
-    image: "postgres:13"
-    env_file: "dev.env"
+    image: "postgres:13-alpine"
+    env_file:
+      - "dev.env"
     volumes:
       - "pgdata_nautobot_golden_config:/var/lib/postgresql/data"
+    ports:
+      - "5432:5432"
   redis:
     image: "redis:6-alpine"
     command:
       - "sh"
-      - "-c"  # this is to evaluate the $REDIS_PASSWORD from the env
-      - "redis-server --appendonly yes --requirepass $$REDIS_PASSWORD"  ## $$ because of docker-compose
-    env_file: "./dev.env"
+      - "-c" # this is to evaluate the $REDIS_PASSWORD from the env
+      - "redis-server --appendonly yes --requirepass $$REDIS_PASSWORD"
+    env_file:
+      - "dev.env"
+    ports:
+      - "6379:6379"
 volumes:
   pgdata_nautobot_golden_config:  # yamllint disable-line rule:empty-values
\ No newline at end of file
diff --git a/development/nautobot_config.py b/development/nautobot_config.py
index 2c36a9da..26b0791b 100644
--- a/development/nautobot_config.py
+++ b/development/nautobot_config.py
@@ -1,11 +1,31 @@
-"""Nautobot configuration."""
+"""Nautobot config."""
+# pylint: disable=invalid-envvar-default
+#########################
+#                       #
+#   Required settings   #
+#                       #
+#########################
+
 import os
 import sys
 
 from distutils.util import strtobool
-
 from django.core.exceptions import ImproperlyConfigured
-from nautobot.core.settings import *  # noqa: F401,F403 pylint: disable=wildcard-import,unused-wildcard-import
+from nautobot.core.settings import *  # noqa: F401,F403 #pylint: disable=wildcard-import, unused-wildcard-import
+
+# Enforce required configuration parameters
+for key in [
+    "ALLOWED_HOSTS",
+    "POSTGRES_DB",
+    "POSTGRES_USER",
+    "POSTGRES_HOST",
+    "POSTGRES_PASSWORD",
+    "REDIS_HOST",
+    "REDIS_PASSWORD",
+    "SECRET_KEY",
+]:
+    if not os.environ.get(key):
+        raise ImproperlyConfigured(f"Required environment variable {key} is missing.")
 
 
 def is_truthy(arg):
@@ -24,47 +44,25 @@ def is_truthy(arg):
     return bool(strtobool(arg))
 
 
-# Enforce required configuration parameters
-for key in [
-    "ALLOWED_HOSTS",
-    "POSTGRES_DB",
-    "POSTGRES_USER",
-    "POSTGRES_HOST",
-    "POSTGRES_PASSWORD",
-    "REDIS_HOST",
-    "REDIS_PASSWORD",
-    "SECRET_KEY",
-]:
-    if not os.environ.get(key):
-        raise ImproperlyConfigured(f"Required environment variable {key} is missing.")
-
-# For reference see https://nautobot.readthedocs.io/en/latest/configuration/required-settings/
-# Based on config from nautobot-server init
-
-#########################
-#                       #
-#   Required settings   #
-#                       #
-#########################
+TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
 
 # This is a list of valid fully-qualified domain names (FQDNs) for the Nautobot server. Nautobot will not permit write
 # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
 #
 # Example: ALLOWED_HOSTS = ['nautobot.example.com', 'nautobot.internal.local']
-ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(" ")
+ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS").split(" ")
 
 # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
 #   https://docs.djangoproject.com/en/stable/ref/settings/#databases
-DATABASE_PORT = 5432 if not os.environ.get("POSTGRES_PORT", False) else int(os.environ["POSTGRES_PORT"])
 DATABASES = {
     "default": {
-        "NAME": os.getenv("POSTGRES_DB", ""),  # Database name
+        "NAME": os.getenv("POSTGRES_DB", "nautobot"),  # Database name
         "USER": os.getenv("POSTGRES_USER", ""),  # Database username
         "PASSWORD": os.getenv("POSTGRES_PASSWORD", ""),  # Datbase password
         "HOST": os.getenv("POSTGRES_HOST", "localhost"),  # Database server
-        "PORT": DATABASE_PORT,  # Database port (leave blank for default)
-        "CONN_MAX_AGE": int(os.environ.get("NAUTOBOT_DB_TIMEOUT", 300)),  # Database timeout
-        "ENGINE": "django.db.backends.postgresql",  # Database driver (Postres only supported!)
+        "PORT": os.getenv("POSTGRES_PORT", ""),  # Database port (leave blank for default)
+        "CONN_MAX_AGE": os.getenv("POSTGRES_TIMEOUT", 300),  # Database timeout
+        "ENGINE": "django.db.backends.postgresql",  # Database driver (Postgres only supported!)
     }
 }
 
@@ -72,11 +70,27 @@ def is_truthy(arg):
 # For detailed configuration see: https://github.com/rq/django-rq#installation
 RQ_QUEUES = {
     "default": {
-        "HOST": os.environ["REDIS_HOST"],
-        "PORT": int(os.environ.get("REDIS_PORT", 6379)),
+        "HOST": os.getenv("REDIS_HOST", "localhost"),
+        "PORT": os.getenv("REDIS_PORT", 6379),
+        "DB": 0,
+        "PASSWORD": os.getenv("REDIS_PASSWORD", ""),
+        "SSL": os.getenv("REDIS_SSL", False),
+        "DEFAULT_TIMEOUT": 300,
+    },
+    "webhooks": {
+        "HOST": os.getenv("REDIS_HOST", "localhost"),
+        "PORT": os.getenv("REDIS_PORT", 6379),
         "DB": 0,
-        "PASSWORD": os.environ["REDIS_PASSWORD"],
-        "SSL": is_truthy(os.environ.get("REDIS_SSL", False)),
+        "PASSWORD": os.getenv("REDIS_PASSWORD", ""),
+        "SSL": os.getenv("REDIS_SSL", False),
+        "DEFAULT_TIMEOUT": 300,
+    },
+    "custom_fields": {
+        "HOST": os.getenv("REDIS_HOST", "localhost"),
+        "PORT": os.getenv("REDIS_PORT", 6379),
+        "DB": 0,
+        "PASSWORD": os.getenv("REDIS_PASSWORD", ""),
+        "SSL": os.getenv("REDIS_SSL", False),
         "DEFAULT_TIMEOUT": 300,
     },
     # "with-sentinel": {
@@ -93,23 +107,21 @@ def is_truthy(arg):
     #     },
     # },
     "check_releases": {
-        "HOST": os.environ["REDIS_HOST"],
-        "PORT": int(os.environ.get("REDIS_PORT", 6379)),
+        "HOST": os.getenv("REDIS_HOST", "localhost"),
+        "PORT": os.getenv("REDIS_PORT", 6379),
         "DB": 0,
-        "PASSWORD": os.environ["REDIS_PASSWORD"],
-        "SSL": is_truthy(os.environ.get("REDIS_SSL", False)),
+        "PASSWORD": os.getenv("REDIS_PASSWORD", ""),
+        "SSL": os.getenv("REDIS_SSL", False),
         "DEFAULT_TIMEOUT": 300,
     },
 }
 
 # Nautobot uses Cacheops for database query caching. These are the following defaults.
 # For detailed configuration see: https://github.com/Suor/django-cacheops#setup
-# REDIS_URL used for healthchecks
-if is_truthy(os.environ.get("REDIS_SSL", False)):
-    REDIS_PROTOCOL = "rediss://"
-else:
-    REDIS_PROTOCOL = "redis://"
-CACHEOPS_REDIS = f"{REDIS_PROTOCOL}://{os.environ['REDIS_HOST']}:{os.environ.get('REDIS_PORT', 6379)}/1"
+REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
+REDIS_PORT = os.getenv("REDIS_PORT", 6379)
+REDIS_PASS = os.getenv("REDIS_PASSWORD", "")
+CACHEOPS_REDIS = f"redis://:{REDIS_PASS}@{REDIS_HOST}:{REDIS_PORT}/1"
 
 # This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
 # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
@@ -117,6 +129,7 @@ def is_truthy(arg):
 # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
 SECRET_KEY = os.environ["SECRET_KEY"]
 
+
 #########################
 #                       #
 #   Optional settings   #
@@ -159,9 +172,12 @@ def is_truthy(arg):
 # BASE_PATH = 'nautobot/'
 BASE_PATH = os.environ.get("BASE_PATH", "")
 
-# Cache timeout in seconds. Set to 0 to dissable caching. Defaults to 900 (15 minutes)
+# Cache timeout in seconds. Cannot be 0. Defaults to 900 (15 minutes). To disable caching, set CACHEOPS_ENABLED to False
 CACHEOPS_DEFAULTS = {"timeout": 900}
 
+# Set to False to disable caching with cacheops. (Default: True)
+CACHEOPS_ENABLED = True
+
 # Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
 CHANGELOG_RETENTION = int(os.environ.get("CHANGELOG_RETENTION", 90))
 
@@ -175,6 +191,7 @@ def is_truthy(arg):
 CORS_ALLOWED_ORIGINS = [
     # 'https://hostname.example.com',
 ]
+
 # A list of strings representing regexes that match Origins that are authorized to make cross-site
 # HTTP requests. Defaults to [].
 CORS_ALLOWED_ORIGIN_REGEXES = [
@@ -203,10 +220,6 @@ def is_truthy(arg):
     # 'ipam.prefix',
 ]
 
-# A list of strings designating all applications that are enabled in this Django installation. Each string should be a dotted Python path to an application configuration class (preferred), or a package containing an application.
-# http://nautobot.readthedocs.io/configuration/optional-settings/#extra-applications
-EXTRA_INSTALLED_APPS = []
-
 # HTTP proxies Nautobot should use when sending outbound HTTP requests (e.g. for webhooks).
 # HTTP_PROXIES = {
 #     'http': 'http://10.10.1.10:3128',
@@ -217,10 +230,6 @@ def is_truthy(arg):
 # Nautobot from an internal IP.
 INTERNAL_IPS = ("127.0.0.1", "::1")
 
-# The file path where jobs will be stored. A trailing slash is not needed. Note that the default value of
-# this setting is derived from the installed location.
-JOBS_ROOT = os.environ.get("JOBS_ROOT", os.path.expanduser("~/.nautobot/jobs"))
-
 LOG_LEVEL = os.environ.get("LOG_LEVEL", "DEBUG" if DEBUG else "INFO")
 
 # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
@@ -239,12 +248,8 @@ def is_truthy(arg):
     "root": {"handlers": ["console"], "level": LOG_LEVEL},
 }
 
-# Setting this to True will permit only authenticated users to access any part of Nautobot. By default, anonymous users
-# are permitted to access most data in Nautobot (excluding secrets) but not make any changes.
-LOGIN_REQUIRED = is_truthy(os.environ.get("LOGIN_REQUIRED", False))
-
 # Setting this to True will display a "maintenance mode" banner at the top of every page.
-MAINTENANCE_MODE = is_truthy(os.environ.get("MAINTENANCE_MODE", False))
+MAINTENANCE_MODE = False
 
 # An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
 # "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
@@ -253,10 +258,20 @@ def is_truthy(arg):
 
 # The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that
 # the default value of this setting is within the invoking user's home directory
-MEDIA_ROOT = os.environ.get("MEDIA_ROOT", os.path.expanduser("~/.nautobot/media"))
+# MEDIA_ROOT = os.path.expanduser('~/.nautobot/media')
+
+# By default uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the
+# class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example:
+# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage'
+# STORAGE_CONFIG = {
+#     'AWS_ACCESS_KEY_ID': 'Key ID',
+#     'AWS_SECRET_ACCESS_KEY': 'Secret',
+#     'AWS_STORAGE_BUCKET_NAME': 'nautobot',
+#     'AWS_S3_REGION_NAME': 'eu-west-1',
+# }
 
 # Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics'
-METRICS_ENABLED = True
+METRICS_ENABLED = False
 
 # Credentials that Nautobot will uses to authenticate to devices when connecting via NAPALM.
 NAPALM_USERNAME = os.environ.get("NAPALM_USERNAME", "")
@@ -267,10 +282,7 @@ def is_truthy(arg):
 
 # NAPALM optional arguments (see https://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
 # be provided as a dictionary.
-NAPALM_ARGS = {
-    "secret": NAPALM_PASSWORD,
-    # Include any additional args here
-}
+NAPALM_ARGS = {}
 
 # Determine how many objects to display per page within a list. (Default: 50)
 PAGINATE_COUNT = int(os.environ.get("PAGINATE_COUNT", 50))
@@ -278,7 +290,6 @@ def is_truthy(arg):
 # Enable installed plugins. Add the name of each plugin to the list.
 PLUGINS = ["nautobot_plugin_nornir", "nautobot_golden_config"]
 
-# Enable installed plugins. Add the name of each plugin to the list.
 # Plugins configuration settings. These settings are used by various plugins that the user may have installed.
 # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings.
 PLUGINS_CONFIG = {
@@ -294,7 +305,6 @@ def is_truthy(arg):
         },
     },
     "nautobot_golden_config": {
-        "allowed_os": os.environ.get("ALLOWED_OS", "all").split(","),
         "per_feature_bar_width": float(os.environ.get("PER_FEATURE_BAR_WIDTH", 0.15)),
         "per_feature_width": int(os.environ.get("PER_FEATURE_WIDTH", 13)),
         "per_feature_height": int(os.environ.get("PER_FEATURE_HEIGHT", 4)),
@@ -346,21 +356,11 @@ def is_truthy(arg):
 # Configure SSO, for more information see docs/configuration/authentication/sso.md
 SOCIAL_AUTH_ENABLED = False
 
-# By default uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the
-# class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example:
-# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage'
-# STORAGE_CONFIG = {
-#     'AWS_ACCESS_KEY_ID': 'Key ID',
-#     'AWS_SECRET_ACCESS_KEY': 'Secret',
-#     'AWS_STORAGE_BUCKET_NAME': 'nautobot',
-#     'AWS_S3_REGION_NAME': 'eu-west-1',
-# }
-
 # Time zone (default: UTC)
 TIME_ZONE = os.environ.get("TIME_ZONE", "UTC")
 
 # Date/time formatting. See the following link for supported formats:
-# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
+# https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
 DATE_FORMAT = os.environ.get("DATE_FORMAT", "N j, Y")
 SHORT_DATE_FORMAT = os.environ.get("SHORT_DATE_FORMAT", "Y-m-d")
 TIME_FORMAT = os.environ.get("TIME_FORMAT", "g:i a")
@@ -368,6 +368,14 @@ def is_truthy(arg):
 DATETIME_FORMAT = os.environ.get("DATETIME_FORMAT", "N j, Y g:i a")
 SHORT_DATETIME_FORMAT = os.environ.get("SHORT_DATETIME_FORMAT", "Y-m-d H:i")
 
+# A list of strings designating all applications that are enabled in this Django installation. Each string should be a dotted Python path to an application configuration class (preferred), or a package containing an application.
+# https://nautobot.readthedocs.io/en/latest/configuration/optional-settings/#extra-applications
+EXTRA_INSTALLED_APPS = os.environ["EXTRA_INSTALLED_APPS"].split(",") if os.environ.get("EXTRA_INSTALLED_APPS") else []
+
 # Django Debug Toolbar
-TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
 DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda _request: DEBUG and not TESTING}
+
+if "debug_toolbar" not in EXTRA_INSTALLED_APPS:
+    EXTRA_INSTALLED_APPS.append("debug_toolbar")
+if "debug_toolbar.middleware.DebugToolbarMiddleware" not in MIDDLEWARE:
+    MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
diff --git a/docs/FAQ.md b/docs/FAQ.md
index ddbc9d7d..35fec71f 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -1,9 +1,5 @@
 # Frequently Asked Questions
 
-_Why must a device name be unique?_
-
-The APIs and other components rely on a single device, such as the ability to perform an API call to `sotagg/<str:device_name>/`. This is a challenge that may be affected by decisions made to the core platform. For the time being, it is a requirement.
-
 _Why don't the configurations match like the vendor cli?_
 
 A vendor processes configuration understanding constructs such as knowing that `int g0/0` and `interface GigabitEthernet0/0` are the same. Each one of these 
@@ -15,8 +11,8 @@ Instead, it is up to the operator to ensure their configurations match exactly a
 
 _Why doesn't the config overview page reflect the inclusion changes immediately?_
 
-On a technical level, those changes enable the model `GoldenConfiguration` to *not* filter out the newly included devices, but this does not add to the
-model. In order to be included, a new job needs to be ran which will create an entry within `GoldenConfiguration`, any of the 3 jobs that successfully run
+On a technical level, those changes enable the model `GoldenConfig` to *not* filter out the newly included devices, but this does not add to the
+model. In order to be included, a new job needs to be ran which will create an entry within `GoldenConfig`, any of the 3 jobs that successfully run
 will create such an entry.
 
 _Why aren't configurations generated or compliance generated real time?_
@@ -39,5 +35,34 @@ operate as they intend it, without burdening the plugins goals.
 
 _Why not predefine the configuration feature map?_
 
-The process is based on an opinion on what defines a feature, for one organization bgp may include the prefix configuration and another it would not.
-Understanding that there will never be consensus on what should go into a feature it becomes obvious why the users must maintain such configuration.
\ No newline at end of file
+The process is based on an opinion on what defines a feature, for one organization BGP may include the prefix configuration and another it would not.
+Understanding that there will never be consensus on what should go into a feature it becomes obvious why the users must maintain such configuration.
+
+_What are the supported platforms? How do I configure a device with a specific OS?_
+
+The current supported platform and the associated *required* platform slug names are the following:
+
+* arista_eos
+* cisco_ios
+* cisco_nxos
+* cisco_aireos
+* linux
+* bigip_f5
+* juniper_junos
+
+In many use cases, this can be extended with a custom dispatcher, which is controlled in the [nornir-nautobot](https://github.com/nautobot/nornir-nautobot) repository.
+
+_Why not provide the corrective configurations?_
+
+Configuration enforcement is a difficult problem to attack. While potentially could integrate with a system to provide the enforcement, this is currently 
+out-of-scope for the plugin.
+
+_Why does the compliance section scroll so much?_
+
+The real estate optimizations is not the best for the configuration compliance overview right now. Users are suggested to review the best practices
+described in the configuration compliance section. Over time, the hope is to optimize this.
+
+_Why can't I get access to the name key when generating configuration?_
+
+All data created by GraphQL is unpacked with the `**data` operator. There is a namespace issue with Nornir using name as a keyword as well. The recommended
+approach is to use GraphQL aliasing. An example would be `hostname: name` or `inventory_hostname: name` to workaround this issue.
\ No newline at end of file
diff --git a/docs/img/00-navigating-backup.png b/docs/img/00-navigating-backup.png
new file mode 100644
index 00000000..10a8bff9
Binary files /dev/null and b/docs/img/00-navigating-backup.png differ
diff --git a/docs/img/01-navigating-backup.png b/docs/img/01-navigating-backup.png
new file mode 100644
index 00000000..f1aaf327
Binary files /dev/null and b/docs/img/01-navigating-backup.png differ
diff --git a/docs/img/backup-git-step2.png b/docs/img/backup-git-step2.png
new file mode 100644
index 00000000..bf4b4563
Binary files /dev/null and b/docs/img/backup-git-step2.png differ
diff --git a/docs/img/backup-git-step3.png b/docs/img/backup-git-step3.png
new file mode 100644
index 00000000..ef9dc52a
Binary files /dev/null and b/docs/img/backup-git-step3.png differ
diff --git a/docs/img/compliance-feature-edit.png b/docs/img/compliance-feature-edit.png
new file mode 100644
index 00000000..b4615487
Binary files /dev/null and b/docs/img/compliance-feature-edit.png differ
diff --git a/docs/img/compliance-features.png b/docs/img/compliance-features.png
new file mode 100644
index 00000000..3bdfb33f
Binary files /dev/null and b/docs/img/compliance-features.png differ
diff --git a/docs/img/device-compliance.png b/docs/img/device-compliance.png
new file mode 100644
index 00000000..81fb6681
Binary files /dev/null and b/docs/img/device-compliance.png differ
diff --git a/docs/img/git-step1.png b/docs/img/git-step1.png
new file mode 100644
index 00000000..65850587
Binary files /dev/null and b/docs/img/git-step1.png differ
diff --git a/docs/img/intended-git-step2.png b/docs/img/intended-git-step2.png
new file mode 100644
index 00000000..827c61ef
Binary files /dev/null and b/docs/img/intended-git-step2.png differ
diff --git a/docs/img/job-overview.png b/docs/img/job-overview.png
new file mode 100644
index 00000000..b935a9be
Binary files /dev/null and b/docs/img/job-overview.png differ
diff --git a/docs/img/job-result.png b/docs/img/job-result.png
new file mode 100644
index 00000000..f72f3ec2
Binary files /dev/null and b/docs/img/job-result.png differ
diff --git a/docs/img/navigate-compliance-rules.png b/docs/img/navigate-compliance-rules.png
new file mode 100644
index 00000000..da8e609f
Binary files /dev/null and b/docs/img/navigate-compliance-rules.png differ
diff --git a/docs/img/templates-git-step2.png b/docs/img/templates-git-step2.png
new file mode 100644
index 00000000..38a6971a
Binary files /dev/null and b/docs/img/templates-git-step2.png differ
diff --git a/docs/navigating-backup.md b/docs/navigating-backup.md
index e69de29b..44881068 100644
--- a/docs/navigating-backup.md
+++ b/docs/navigating-backup.md
@@ -0,0 +1,66 @@
+# Configuration Backup
+
+The backup configuration process relies on the ability for the nautobot worker to connect via Nornir to the device, run the `show run` or equivalent command 
+and save the configuration. The high-level process to run backups is:
+
+* Download the latest Git repository, based on `backup config` type Git repo within Nautobot.
+* Run a Nornir play to obtain the cli configurations.
+* Optionally perform some lightweight processing of the backup.
+* Store the backup configurations locally.
+* Push configurations to the remote Git repository.
+
+# Configuration Backup Settings
+
+Backup configurations often need some amount of parsing to stay sane. The two obvious use cases are the ability to remove lines such as the "Last 
+Configuration" changed date, as this will cause unnecessary changes the second is to strip out secrets from the configuration. In an effort to support these 
+uses cases, the following settings are available and further documented below.
+
+* Config Removals - provides the ability to remove a line based on a regex match.
+* Config Replacements - provides the ability to swap out parts of a line based on a regex match.
+
+Backup configurations rely on a Git Repo, and the plugin registers an additional repository for Git source this access. Within the Nautobot Git 
+repositories, there will be a `backup config` option, which there must be one and only one configured for the process to work. For further details, refer 
+[to](./navigating-golden.md#git-settings).
+
+The `backup_path_template` provides the ability to dynamically state per device where the configurations should end up in the file structure. Every device is a Django ORM object, tied to the model instance of a `Device` model, and that is represented as `obj`. That means that any valid Device model method is available. This is then compiled via Jinja. This may seem complicated, but the equivalent of `obj` by example would be:
+
+```python
+obj = Device.objects.get(name="nyc-rt01")
+```
+
+An example would be:
+```python
+backup_path_template = "{{obj.site.slug}}/{{obj.name}}.cfg"
+```
+
+The backup process will automatically create folders as required based on the path definition. 
+
+The `backup_path_template` can be set in the UI.  For navigation details [see](./navigating-golden.md#application-settings).
+
+The credentials/secrets management is further described within the [nautbot-plugin-nornir](https://github.com/nautobot/nautobot-plugin-nornir)
+repo. For the simplist use case you can set environment variables for `NAPALM_USERNAME`, `NAPALM_PASSWORD`, and `DEVICE_SECRET`. For more
+complicated use cases, please refer to the plugin documentation linked above.
+
+# Config Removals
+
+The line removals settings is a series of regex patterns to identify lines that should be removed. This is helpful as there are usually parts of the
+configurations that will change each time. A match simply means to remove.
+
+In order to specify line removals. Navigate to **Plugins -> Config Removals**.  Click the **Add** button and fill out the details.
+
+The remove setting is based on `Platform`.  An example is shown below.
+![Config Removals View](./img/00-navigating-backup.png)
+
+# Config Replacements
+
+This is a replacement config with a regex pattern with a single capture groups to replace. This is helpful to strip out secrets.
+
+The replace lines setting is based on `Platform`.  An example is shown below.
+
+![Config Replacements View](./img/01-navigating-backup.png)
+
+The line replace uses Python's `re.sub` method. As shown, a common pattern is to obtain the non-confidential data in a capture group e.g. `()`, and return the rest of the string returned in the backrefence, e.g. `\2`.
+
+```python
+re.sub(r"(username\s+\S+\spassword\s+5\s+)\S+(\s+role\s+\S+)", r"\1<redacted_config>\2", config, flags=re.MULTILINE))
+```
\ No newline at end of file
diff --git a/docs/navigating-compliance.md b/docs/navigating-compliance.md
index ccca351d..23bfe9e7 100644
--- a/docs/navigating-compliance.md
+++ b/docs/navigating-compliance.md
@@ -1,31 +1,150 @@
+# Configuration Compliance 
+
+The following should be noted by what is meant by configuration compliance. Configurations are considered to be compliant if the generated configuration 
+(generally by merging data and Jinja2, will be referred to as the intended configuration from hence forth) matches "exactly" as the actual configuration is 
+on the backup. This may confusing to some, as for example to the average network engineer, there is no difference between `int g0/0` and
+`interface GigabitEthernet0/0` but for the purpose of configuration compliance, it is not a match... full stop.
+
+It's helpful to understand what are some common reasons a device is not compliant.
+
+* There is missing configuration on the device.
+* There is extra configuration on the device.
+* The data used to generate the configuration is incorrect, and created a "false positive".
+* The template used to generate the configuration is incorrect, and created a "false positive".
+* The parser used to obtain the configuration from the feature is incorrect, and created a "false positive".
+
+There is no magic to determine the state of configuration. You still must define what is good configuration and compare it. There are several reasons why 
+configuration may be as a network engineer wants it, but the tool correctly considers it non-compliant, since the tool is only comparing two configurations.
+The tool makes no assumptions to determine what an engineer may want to do, but did not document via the configuration generation process.
+
+# Compliance Configuration Settings
+
+In order to generate the intended configurations two repositories are needed.
 
-# Usage
+1. A repo to save [intended configurations](./navigating-golden.md#git-settings) to once generated.
+2. A repo that stores [Backups](./navigating-golden.md#git-settings) used to as the actual configurations.
+3. The [intended_path_template](./navigating-golden.md#application-settings) configuration parameter.
+4. The [backup_path_template](./navigating-golden.md#application-settings) configuration parameter.
 
-There is a single process and several views that the plugin provides.
+# Configuration Compliance Parsing Engine
 
-## Plugins Buttons
+Configuration compliance is different than a simple UNIX diff. While the UI provides both, the compliance metrics are not influenced by the UNIX diff 
+capabilities. One of the challenges of getting a device into compliance is the ramp up it takes to model and generate configurations for an entire 
+configuration. The compliance engine has several features to better build work through this process.
 
-The plugins buttons provides you the ability to navigate to Run the script, overview report, and detailed report.
+1. The ability to parse into smaller sections, given a list of root configuration elements.
+2. The ability to consider ordered and non-ordered configurations.
+3. The ability to dynamically understand parent/child relationships within the configurations.
 
+In regards to `1`, consider the following example of how to obtain service configurations:
+```
+service
+no service
+```
+Specific configurations that start with either of these commands can be grouped together.
 
-## Run Script
+In regards to `2`, consider the configurations of SNMP on a nexus switch. 
+```
+snmp-server community secure group network-admin
+snmp-server community networktocode group network-operator
+```
 
-This can be accessed via the Plugins drop-down via `Run Script` button, it will immediately run the script once the it starts.
+The above configurations are rendered based on the order in which they were entered, not based on the a deterministic way. The comparison process takes this into consideration, to ensure that the following is not non-compliant when ordering option is not considered.
 
+```
+snmp-server community networktocode group network-operator
+snmp-server community secure group network-admin
+```
 
-## Detail Report
+In regards to `3`, consider the following example of BGP configuration. 
+```
+router bgp
+prefix-list
+```
+All configurations that are a parent and child relationships would be considered within the parsing engine. Additionally, if one configuration line was 
+wrong, only that line and the parents would be shown, not all lines or only the missing configuration without context of the parents, e.g. Given:
 
-This can be accessed via the Plugins drop-down via `Compliance` details button. From there you can filter the devices via the form on the right side, limit the columns with the `Configure` button, or 
-bulk delete with the `Delete` button. Additionally each device is click-able to view the details of that individual device. 
+Actual
+```
+router bgp 65250
+  router-id 10.0.10.5
+  log-neighbor-changes
+  address-family ipv4 unicast
+    redistribute direct route-map PERMIT_CONN_ROUTES
+  neighbor 10.10.10.5
+    remote-as 65250
+    address-family ipv4 unicast
+```
 
-You can configure the columns to limit how much is showing on one screen.
+Intended
+```
+router bgp 65250
+  router-id 10.0.10.6
+  log-neighbor-changes
+  address-family ipv4 unicast
+    redistribute direct route-map PERMIT_CONN_ROUTES
+  neighbor 10.10.10.5
+    remote-as 65250
+    address-family ipv4 unicast
+```
+Would result in the identifying the missing configurations as:
+
+```
+router bgp 65250
+  router-id 10.0.10.6
+```
+
+> Note: A platform will not run successfully against a device unless at least one compliance rule is set. 
+
+# Configuration Compliance Settings
+
+Configuration compliance requires the Git Repo settings for `config backups` and `intended configs`--which are covered in their respective sections--regardless if they are actually managed via the plugin or not. The same is true for the `Backup Path` and `Intended Path`.
+
+The Configuration compliance rule map must be created per the operator/user. You can find these configurations via `Plugins -> Compliance Rules`
+links, which brings up the specific configurations.
+
+![Configuration Features](./img/compliance-rules.png)
+
+Each configuration can be added and edits from this table. When editing/adding the configurations, the following should be noted.
+
+![Configuration Feature Edit](./img/compliance-rule-edit.png)
+
+The platform must refer to a platform with a valid slug supported by the configuration compliance engine. While there is no enforcement of this data from
+a database perspective, the job will never run successfully, rendering the additional configuration ineffective. 
+
+The Feature is a unique identifier, that should prefer shorter names, as this effects the width of the compliance overview and thus it's readability as a
+best practice.
 
-## Device Details
+The "Configs to Match" section represents the configuration root elements. This would be the parent most key only. Additionally, the match is based on
+what a line starts with only. Meaning, there is an implicit greediness to the matching. All matches must start form the beginning of the line.
 
-You can get to the device details form either the Compliance details page, or there is a `content_template` on the device model page is Nautobot's core instance (more details later.)
+> Note: If accidentally the data is "corrupted" with a bad tested match, simply delete the devices an re-run the compliance process.
 
+# Compliance View
 
-## Overview Report
+The compliance overview will provide a per device and feature overview on the compliance of your network devices. From here you can navigate to the details view.
+![Compliance Overview](./img/compliance-overview.png)
+
+# Compliance Details View
+
+Drilling into a specific device and feature, you can get an immediate detailed understanding of your device.
+
+![Compliance Device](./img/compliance-device.png)
+
+![Compliance Rule](./img/compliance-rule.png)
+
+Please note the following about the compliance details page.
+
+* The device Intended and Actual configuration will become a single cell configuration if there is an exact match.
+* The device Intended and Actual configuration will both show if the configuration is matched, but not ordered the same.
+* The icon next to the status will indicate whether or not the configuration is ordered.
+* The icons on top of the page can be used to help navigate the page easier.
+
+# Supported Platforms
+
+Platforms support technically come from the options provided by nornir_nautobot. However, for reference, the valid slug's of the platforms are provided in the [FAQ](./FAQ.md).
+
+# Overview Report
 
 There is a global overview or executive summary that provides a high level snapshot of the compliance. There are 3 points of data captured.
 
@@ -33,12 +152,15 @@ There is a global overview or executive summary that provides a high level snaps
 * Features - This is the total number of features for all devices, and how many are compliant, and how many are non-compliant.
 * Per Feature - This is a breakdown of that feature and how many within that feature are compliant of not.
 
-## Device Template Content
+# Detail Report
+
+This can be accessed via the Plugins drop-down via `Compliance` details button. From there you can filter the devices via the form on the right side, limit the columns with the `Configure` button, or 
+bulk delete with the `Delete` button. Additionally each device is click-able to view the details of that individual device. 
 
-The plugin makes use of template content `right_page` in order to use display in-line the status of that device in the traditional Nautobot view. From here you can click the link to see the
-detail compliance view.
+You can configure the columns to limit how much is showing on one screen.
 
+# Device Details
 
-## Site Template Content
+You can get to the device details form either the Compliance details page, or there is a `content_template` on the device model page is Nautobot's core instance.
 
-The plugin makes use of template content `right_page` in order to use display in-line the status of that entire site in the traditional Nautobot view. 
+![Configuration Features](./img/device-compliance.png)
diff --git a/docs/navigating-golden.md b/docs/navigating-golden.md
index e69de29b..2842c6b0 100644
--- a/docs/navigating-golden.md
+++ b/docs/navigating-golden.md
@@ -0,0 +1,156 @@
+# Golden Configuration 
+
+A navigation overview of the entire plugin.
+
+# Home
+
+The Home view is a portal to understand what the status of the devices are. 
+
+![Home Overview](./img/golden-overview.png)
+
+Some of the information described in this view, may not be immediately obvious.
+
+* The Backup/Intended/Compliance status will always show the last time the job was successfully ran for that device, but there are several conditions it may be in.
+  * Green with a date indicates that the ran was successful, which was the last time the job ran. 
+  * Red with a data indicates the last time the job ran successfully, with the last time the job was attempted in be shown when you mouse over the date.
+  * A red double-dashed icon indicated the job has never been successful
+* The icons are provided in the following order, that largely matches the status.
+  * Backups
+  * Intended
+  * Unix Diff
+  * SoT aggregation data
+  * Run job
+
+The first four bring up a "modal" or "dialogue box" which has a detailed view for a dedicated page. The run job brings the user to a job to run all three 
+components against all of the devices.
+
+# Jobs
+
+There are a series of Jobs that are registered via the Plugin. They can be viewed from the standard Jobs view.
+
+![Job Overview](./img/job-overview.png)
+
+Each Job attempts to provide sane error handling, and respects the `debug` flag to provide more information.
+
+![Job Result](./img/job-result.png)
+
+# Application Settings
+
+The golden configuration plugin settings can be found by navigating to `Plugins -> Settings` button. Under the `Golden Configuration` section.
+
+![Navigate to Settings](./img/navigate-compliance-rules.png)
+
+To configure or update the settings click the pencil icon to edit.
+
+|Setting|Explanation|
+|:--|:--|
+|Backup Repository | This is the Git Repository where your backup configurations will be found. |
+|Backup Path|This represents the Jinja path where the backup files will be found.  The variable `obj` is available as the device instance object of a given device, as is the case for all Jinja templates. e.g. `{{obj.site.slug}}/{{obj.name}}.cfg`|
+|Intended Repository | This is the Git Repository where your backup configurations will be found. |
+|Intended Path|The Jinja path representation of where the generated file will be places. e.g. `{{obj.site.slug}}/{{obj.name}}.cfg`|
+|Jinja Repository | This is the Git Repository where your jinja templates will be found. |
+|Jinja Path|The Jinja path representation of where the Jinja temaplte can be found. e.g. `{{obj.platform.slug}}.j2`|
+|Scope| This is where the scope of devices to be considered within Golden Config is defined. |
+|GraphQL Query|A query that is evaluated and used to render the config. The query must start with `query ($device_id: ID!)`.|
+
+> Note: Each of these will be further detailed in their respective sections.
+
+## Scope
+
+The scope, is a JSON blob that describes a filter that will provide the list of devices to be allowed whenever a job is ran. A job can optionally further refine the scope, but the outbound would be based on what is defined here. The options are best described by leveraging the Devices list view, search features (the filtering shown on the side of the Devices.) Building a query there, will provide the exact keys expected.
+
+Filtering to specific platforms, based on their slug.
+
+```json
+{
+  "platform": [
+    "cisco_ios",
+    "cisco_nxos",
+    "arista_eos",
+    "juniper_junos"
+  ]
+}
+```
+
+> Note: The Platform slug is an important value, see the [FAQ](./FAQ.md) for further details.
+
+Adding a "has_primary_ip" check. 
+
+```json
+{
+  "platform": [
+    "cisco_ios",
+    "cisco_nxos",
+    "arista_eos",
+    "juniper_junos"
+  ],
+  "has_primary_ip": "True"
+}
+```
+
+When viewing the settings, the scope of devices is actually a link to the query built in the Devices view. Click that link to understand which devices are permitted by the filter.
+
+# Git Settings
+
+The plugin makes heavy use of the Nautobot git data sources feature. There are up to three repositories used in the application. This set of instructions will walk an operator through setting up the backup repository. The steps are the same, except for the "Provides" field name chosen.
+
+In order to setup this repository, go to Nautobot and navigate to the Data Sources Git integration. `Extensibility -> Git Repositories`.
+
+![Backup Git Navigation](./img/git-step1.png)
+
+From the Git Repositories page we can add the **Backup** repository.
+
+Click on `Add`.
+
+You will now be presented with a page to fill in the repository details.
+
+Parameters:
+|Field|Explanation|
+|:--|:--|
+|Name|User friendly name for the backup repo.|
+|Slug|Auto-generated based on the `name` provided.|
+|Remote URL|The URL pointing to the Git repo that stores the backup configuration files. Current git url usage is limited to `http` or `https`.|
+|Branch|The branch in the Git repo to use. Defaults to `main`.|
+|Token|The token is a personal access token for the `username` provided.  For more information on generating a personal access token. [Github Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token)
+|Username|The Git username that corresponds with the personal access token above.|
+|Provides|Valid providers for Git Repo.|
+<br>
+
+![Example Git Backups](./img/backup-git-step2.png)
+
+Select `backup configs` and click on `Create`.
+
+Once you click `Create` and the repository syncs, the main page will now show the repo along with its status.
+![Git Backup Repo Status](./img/backup-git-step3.png)
+
+For their respective features, the "Provides" field could be backup intended configs and jinja templates.
+
+# Plugins Buttons
+
+The plugins buttons provides you with the ability to navigate to Run the script, overview report, and detailed report.
+
+# Run Script
+
+This can be accessed via the Plugins drop-down via `Run Script` button of the `Home` view, the user will be provided a form of the Job (as described 
+above), which will allow the user to limit the scope of the request.
+
+# Device Template Content
+
+The plugin makes use of template content `right_page` in order to use display in-line the status of that device in the traditional Nautobot view. From here you can click the link to see the detail compliance view.
+
+# Site Template Content
+
+The plugin makes use of template content `right_page` in order to use display in-line the status of that entire site in the traditional Nautobot view. 
+
+# API
+
+To run the job programmactially, reference the [nautobot documentation](https://nautobot.readthedocs.io/en/stable/additional-features/jobs/#via-the-api) for the proper API call. Pay special attention to the `class_path` defintion.
+
+# Feature Enablement
+
+Enabling features such as backup or compliance, will render those parts of the UI visible. It is worth noting that disabling features does not provide any
+garbage collection and it is up to the operator to remove such data.
+
+# Network Operating System Support
+
+The version of OS's supported is documented in the [FAQ](./FAQ.md) and is controlled the platform slug. The platform slug must be exactly as expected for the plugin to work.
\ No newline at end of file
diff --git a/docs/navigating-intended.md b/docs/navigating-intended.md
index e69de29b..a0e21621 100644
--- a/docs/navigating-intended.md
+++ b/docs/navigating-intended.md
@@ -0,0 +1,41 @@
+# Configuration Generation
+
+The Golden Config plugin provides the ability to generate configurations. The process is a Nornir play that points to a single Jinja template per 
+device that generates the configurations. Data is provided via the Source of Truth aggregation and is currently a hard requirement to be turned on if 
+generating configuration via the Golden Config plugin. Whatever data is returned by the Source of Truth Aggregation is available to the Jinja template.
+
+As previous stated, there can only be a single template per device. It is often advantageous to break configurations into smaller snippets. A common pattern 
+to overcome is:
+
+```jinja
+!
+{% include os ~ '/services.j2' %}
+!
+{% include os ~ '/hostname.j2' %}
+!
+{% include os ~ '/ntp.j2' %}
+!
+```
+or 
+
+```jinja
+!
+{% set features = ['services', 'hostname', 'ntp'] %}
+{% for feature in features %}
+{% include os ~ '/' ~ feature ~ '.j2' %}
+!
+{% endfor %}
+```
+
+# Intended Configuration Settings
+
+In order to generate the intended configurations two repositories are needed.
+
+1. A repo to save [intended configurations](./navigating-golden.md#git-settings) to once generated.
+2. A repo that stores [backups](./navigating-golden.md#git-settings) used to as the actual configurations.
+3. The [intended_path_template](./navigating-golden.md#application-settings) configuration parameter.
+4. The [jinja_path_template](./navigating-golden.md#application-settings) configuration parameter.
+
+# Data
+
+The data provided while rendering the configuration of a device is described in the [SoT Aggregation](./navigating-sot-agg.md) overview. 
diff --git a/docs/navigating-sot-agg.md b/docs/navigating-sot-agg.md
index e69de29b..f6262a68 100644
--- a/docs/navigating-sot-agg.md
+++ b/docs/navigating-sot-agg.md
@@ -0,0 +1,162 @@
+# SoT Aggregation Overview 
+
+The Source of Truth Aggregation Overview is driven by a few key components.
+
+* The ability to have a single GraphQL query to aggregate information.
+* The ability to modify data with a "transposer" function.
+* The usage of config contexts and the Nautobot's native git platform.
+
+# GraphQL
+
+There is currently support to make an arbitrary GraphQL query that has "device_id" as a variable. It is likely best to use the GraphiQL interface to model
+your data, and then save that query to the configuration. The application configuration ensures the following two components.
+
+* The query is a valid GraphQL query.
+* The query starts with exactly "query ($device_id: ID!)"". This is to help fail fast and help with overall user experience of clear expectations.
+
+It is worth noting that the graphQL query returned is modified to remove the root key of `device`, so instead of all data being within device, such as
+`{"device": {"site": {"slug": "jcy"}}}`, it is simply `{"site": {"slug": "jcy"}}` as an example.
+
+It is helpful to make adjustments to the query, and then view the data from the Plugin's home page and clicking on a given device's `code-json` icon.
+
+# Transposer Function
+
+The transposer function is an optional function to make arbitrary changes to the data after the fact. There is a Plugin configuration that allows the
+operator to point to a function within the python path by a string. The function will receive a single variable, that by convention should be called
+`data`. The function should return a valid Python json serializable data structure.
+
+```python
+def transposer(data):
+    """Some."""
+    if data["platform"]["slug"] == "cisco_ios":
+        data["platform"].update({"support-number": "1-800-ciscohelp"})
+    if data["platform"]["slug"] == "arista_eos":
+        data["platform"].update({"support-number": "1-800-aristahelp"})
+    return data
+```
+
+While the example transposer is silly and untested, it provides the structure for which a transposer can be use. The possibilities are obviously endless,
+such as reaching out to an external system but operators should use caution not to overload complexity into the transposer. 
+
+The configuration required in the Plugin configuration is as described below.
+
+```python
+PLUGINS_CONFIG["nautobot_golden_config"]["sot_agg_transposer"] = "nautobot_golden_config.transposer.transposer"
+```
+The path described must be within the Python path of your worker. It is up to the operator to ensure that happens.
+
+# Config Context
+
+Outside of the scope of this document, but it is worth mentioning the power that configuration context's with integration to Git can provide in this
+solution.
+
+# Performance
+
+The GraphQL and transposer functionality could seriously impact the performance of the server. There are no restrictions imposed as it is up to the
+operator to weigh the pros and cons of the solution.
+
+# Sample Query
+
+To test your query in the GraphiQL UI, obtain a device's uuid, which can be seen in the url of the detailed device view. Once you have a valid device uuid, you can use the "Query Variables" portion of the UI, which is on the bottom left-hand side of the screen.
+
+Example: Query Variables
+```
+{
+  "device_id": "c2dfa612-3c6b-4a67-8492-a7ca346641f9"
+}
+```
+
+GraphQL may be new to many users, and while the GraphiQL interface is great way to get started, the following query is for reference. It is
+highly recommended to alias name (as in `hostname: name` shown below), as there will be a namespace issue with nornir tasks, which often
+take in name as a parameter. 
+
+```
+query ($device_id: ID!) {
+  device(id: $device_id) {
+    config_context
+    hostname: name
+    position
+    serial
+    primary_ip4 {
+      id
+      primary_ip4_for {
+        id
+        name
+      }
+    }
+    tenant {
+      name
+    }
+    tags {
+      name
+      slug
+    }
+    device_role {
+      name
+    }
+    platform {
+      name
+      slug
+      manufacturer {
+        name
+      }
+      napalm_driver
+    }
+    site {
+      name
+      slug
+      vlans {
+        id
+        name
+        vid
+      }
+      vlan_groups {
+        id
+      }
+    }
+    interfaces {
+      description
+      mac_address
+      enabled
+      name
+      ip_addresses {
+        address
+        tags {
+          id
+        }
+      }
+      connected_circuit_termination {
+        circuit {
+          cid
+          commit_rate
+          provider {
+            name
+          }
+        }
+      }
+      tagged_vlans {
+        id
+      }
+      untagged_vlan {
+        id
+      }
+      cable {
+        termination_a_type
+        status {
+          name
+        }
+        color
+      }
+      tagged_vlans {
+        site {
+          name
+        }
+        id
+      }
+      tags {
+        id
+      }
+    }
+  }
+}
+```
\ No newline at end of file
diff --git a/nautobot_golden_config/__init__.py b/nautobot_golden_config/__init__.py
index 8821580c..5f841a80 100644
--- a/nautobot_golden_config/__init__.py
+++ b/nautobot_golden_config/__init__.py
@@ -1,11 +1,11 @@
 """Plugin declaration for nautobot_golden_config."""
 
-__version__ = "0.9.0"
+__version__ = "0.9.2"
 
 from nautobot.extras.plugins import PluginConfig
 
 
-class GoldenConfiguration(PluginConfig):
+class GoldenConfig(PluginConfig):
     """Plugin configuration for the nautobot_golden_config plugin."""
 
     name = "nautobot_golden_config"
@@ -19,7 +19,6 @@ class GoldenConfiguration(PluginConfig):
     # min_version = "0"
     # max_version = "100"
     default_settings = {
-        "allowed_os": ["all"],
         "enable_backup": True,
         "enable_golden": True,
         "enable_compliance": True,
@@ -31,4 +30,4 @@ class GoldenConfiguration(PluginConfig):
     caching_config = {}
 
 
-config = GoldenConfiguration  # pylint:disable=invalid-name
+config = GoldenConfig  # pylint:disable=invalid-name
diff --git a/nautobot_golden_config/api/serializer.py b/nautobot_golden_config/api/serializer.py
deleted file mode 100644
index c8ef5316..00000000
--- a/nautobot_golden_config/api/serializer.py
+++ /dev/null
@@ -1,9 +0,0 @@
-"""REST API serializer capabilities for graphql plugin."""
-
-from rest_framework import serializers
-
-
-class GraphQLSerializer(serializers.Serializer):  # pylint: disable=abstract-method
-    """Serializer for a GraphQL object."""
-
-    data = serializers.JSONField()
diff --git a/nautobot_golden_config/api/serializers.py b/nautobot_golden_config/api/serializers.py
new file mode 100644
index 00000000..5f3b4baa
--- /dev/null
+++ b/nautobot_golden_config/api/serializers.py
@@ -0,0 +1,103 @@
+"""REST API serializer capabilities for graphql plugin."""
+# pylint: disable=too-many-ancestors
+from rest_framework import serializers
+from nautobot.extras.api.customfields import CustomFieldModelSerializer
+from nautobot.extras.api.serializers import TaggedObjectSerializer
+
+from nautobot_golden_config import models
+
+
+class GraphQLSerializer(serializers.Serializer):  # pylint: disable=abstract-method
+    """Serializer for a GraphQL object."""
+
+    data = serializers.JSONField()
+
+
+class ComplianceFeatureSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+    """Serializer for ComplianceFeature object."""
+
+    url = serializers.HyperlinkedIdentityField(
+        view_name="plugins-api:nautobot_golden_config-api:compliancefeature-detail"
+    )
+
+    class Meta:
+        """Set Meta Data for ComplianceFeature, will serialize all fields."""
+
+        model = models.ComplianceFeature
+        fields = "__all__"
+
+
+class ComplianceRuleSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+    """Serializer for ComplianceRule object."""
+
+    url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_golden_config-api:compliancerule-detail")
+
+    class Meta:
+        """Set Meta Data for ComplianceRule, will serialize all fields."""
+
+        model = models.ComplianceRule
+        fields = "__all__"
+
+
+class ConfigComplianceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+    """Serializer for ConfigCompliance object."""
+
+    url = serializers.HyperlinkedIdentityField(
+        view_name="plugins-api:nautobot_golden_config-api:configcompliance-detail"
+    )
+
+    class Meta:
+        """Set Meta Data for ConfigCompliance, will serialize all fields."""
+
+        model = models.ConfigCompliance
+        fields = "__all__"
+
+
+class GoldenConfigSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+    """Serializer for GoldenConfig object."""
+
+    url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_golden_config-api:goldenconfig-detail")
+
+    class Meta:
+        """Set Meta Data for GoldenConfig, will serialize all fields."""
+
+        model = models.GoldenConfig
+        fields = "__all__"
+
+
+class GoldenConfigSettingSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+    """Serializer for GoldenConfigSetting object."""
+
+    url = serializers.HyperlinkedIdentityField(
+        view_name="plugins-api:nautobot_golden_config-api:goldenconfigsetting-detail"
+    )
+
+    class Meta:
+        """Set Meta Data for GoldenConfigSetting, will serialize all fields."""
+
+        model = models.GoldenConfigSetting
+        fields = "__all__"
+
+
+class ConfigRemoveSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+    """Serializer for ConfigRemove object."""
+
+    url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_golden_config-api:configremove-detail")
+
+    class Meta:
+        """Set Meta Data for ConfigRemove, will serialize all fields."""
+
+        model = models.ConfigRemove
+        fields = "__all__"
+
+
+class ConfigReplaceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+    """Serializer for ConfigReplace object."""
+
+    url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_golden_config-api:configreplace-detail")
+
+    class Meta:
+        """Set Meta Data for ConfigReplace, will serialize all fields."""
+
+        model = models.ConfigReplace
+        fields = "__all__"
diff --git a/nautobot_golden_config/api/urls.py b/nautobot_golden_config/api/urls.py
index c51c826a..93ac95d5 100644
--- a/nautobot_golden_config/api/urls.py
+++ b/nautobot_golden_config/api/urls.py
@@ -1,14 +1,24 @@
 """API for Custom Jobs ."""
 
 from django.urls import path
+from nautobot.core.api import OrderedDefaultRouter
 
-from . import views
+from nautobot_golden_config.api import views
 
-app_name = "nautobot_golden_config"
-urlpatterns = [
+router = OrderedDefaultRouter()
+router.APIRootView = views.GoldenConfigRootView
+router.register("compliance-feature", views.ComplianceFeatureViewSet)
+router.register("compliance-rule", views.ComplianceRuleViewSet)
+router.register("config-compliance", views.ConfigComplianceViewSet)
+router.register("golden-config", views.GoldenConfigViewSet)
+router.register("golden-config-settings", views.GoldenConfigSettingViewSet)
+router.register("config-remove", views.ConfigRemoveViewSet)
+router.register("config-replace", views.ConfigReplaceViewSet)
+urlpatterns = router.urls
+urlpatterns.append(
     path(
-        "sotagg/<str:device_name>/",
+        "sotagg/<uuid:pk>/",
         views.SOTAggDeviceDetailView.as_view(),
         name="device_detail",
-    ),
-]
+    )
+)
diff --git a/nautobot_golden_config/api/views.py b/nautobot_golden_config/api/views.py
index fbca7811..d9246fff 100644
--- a/nautobot_golden_config/api/views.py
+++ b/nautobot_golden_config/api/views.py
@@ -3,13 +3,22 @@
 
 from rest_framework.views import APIView
 from rest_framework.response import Response
+from rest_framework.routers import APIRootView
 from rest_framework.permissions import AllowAny
-
+from nautobot.extras.api.views import CustomFieldModelViewSet
 from nautobot.dcim.models import Device
 
-from ..models import GoldenConfigSettings
-from ..utilities.graphql import graph_ql_query
-from .serializer import GraphQLSerializer
+from nautobot_golden_config.api import serializers
+from nautobot_golden_config import models
+from nautobot_golden_config.utilities.graphql import graph_ql_query
+
+
+class GoldenConfigRootView(APIRootView):
+    """Golden Config API root view."""
+
+    def get_view_name(self):
+        """Golden Config API root view boilerplate."""
+        return "Golden Config"
 
 
 class SOTAggDeviceDetailView(APIView):
@@ -19,8 +28,57 @@ class SOTAggDeviceDetailView(APIView):
 
     def get(self, request, *args, **kwargs):
         """Get method serialize for a dictionary to json response."""
-        device = Device.objects.get(name=kwargs["device_name"])
-        global_settings = GoldenConfigSettings.objects.get(id="aaaaaaaa-0000-0000-0000-000000000001")
+        device = Device.objects.get(pk=kwargs["pk"])
+        global_settings = models.GoldenConfigSetting.objects.first()
         status_code, data = graph_ql_query(request, device, global_settings.sot_agg_query)
         data = json.loads(json.dumps(data))
-        return Response(GraphQLSerializer(data=data).initial_data, status=status_code)
+        return Response(serializers.GraphQLSerializer(data=data).initial_data, status=status_code)
+
+
+class ComplianceRuleViewSet(CustomFieldModelViewSet):  # pylint:disable=too-many-ancestors
+    """API viewset for interacting with ComplianceRule objects."""
+
+    queryset = models.ComplianceRule.objects.all()
+    serializer_class = serializers.ComplianceRuleSerializer
+
+
+class ComplianceFeatureViewSet(CustomFieldModelViewSet):  # pylint:disable=too-many-ancestors
+    """API viewset for interacting with ComplianceFeature objects."""
+
+    queryset = models.ComplianceFeature.objects.all()
+    serializer_class = serializers.ComplianceFeatureSerializer
+
+
+class ConfigComplianceViewSet(CustomFieldModelViewSet):  # pylint:disable=too-many-ancestors
+    """API viewset for interacting with ConfigCompliance objects."""
+
+    queryset = models.ConfigCompliance.objects.all()
+    serializer_class = serializers.ConfigComplianceSerializer
+
+
+class GoldenConfigViewSet(CustomFieldModelViewSet):  # pylint:disable=too-many-ancestors
+    """API viewset for interacting with GoldenConfig objects."""
+
+    queryset = models.GoldenConfig.objects.all()
+    serializer_class = serializers.GoldenConfigSerializer
+
+
+class GoldenConfigSettingViewSet(CustomFieldModelViewSet):  # pylint:disable=too-many-ancestors
+    """API viewset for interacting with GoldenConfigSetting objects."""
+
+    queryset = models.GoldenConfigSetting.objects.all()
+    serializer_class = serializers.GoldenConfigSettingSerializer
+
+
+class ConfigRemoveViewSet(CustomFieldModelViewSet):  # pylint:disable=too-many-ancestors
+    """API viewset for interacting with ConfigRemove objects."""
+
+    queryset = models.ConfigRemove.objects.all()
+    serializer_class = serializers.ConfigRemoveSerializer
+
+
+class ConfigReplaceViewSet(CustomFieldModelViewSet):  # pylint:disable=too-many-ancestors
+    """API viewset for interacting with ConfigReplace objects."""
+
+    queryset = models.ConfigReplace.objects.all()
+    serializer_class = serializers.ConfigReplaceSerializer
diff --git a/nautobot_golden_config/choices.py b/nautobot_golden_config/choices.py
new file mode 100644
index 00000000..98ad7c47
--- /dev/null
+++ b/nautobot_golden_config/choices.py
@@ -0,0 +1,14 @@
+"""Choicesets for golden config."""
+from nautobot.utilities.choices import ChoiceSet
+
+
+class ComplianceRuleTypeChoice(ChoiceSet):
+    """Choiceset used by ComplianceRule."""
+
+    TYPE_CLI = "cli"
+    TYPE_JSON = "json"
+
+    CHOICES = (
+        (TYPE_CLI, "CLI"),
+        (TYPE_JSON, "JSON"),
+    )
diff --git a/nautobot_golden_config/datasources.py b/nautobot_golden_config/datasources.py
index 704b52cd..8f744975 100644
--- a/nautobot_golden_config/datasources.py
+++ b/nautobot_golden_config/datasources.py
@@ -1,8 +1,8 @@
-"""Data source plugin extenstion to register additional git repo types."""
+"""Data source plugin extension to register additional git repo types."""
 from nautobot.extras.choices import LogLevelChoices
 from nautobot.extras.registry import DatasourceContent
 
-from .utilities.constant import ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_INTENDED
+from nautobot_golden_config.utilities.constant import ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_INTENDED
 
 
 def refresh_git_jinja(repository_record, job_result, delete=False):  # pylint: disable=unused-argument
diff --git a/nautobot_golden_config/filters.py b/nautobot_golden_config/filters.py
index 4eaac23c..722d4280 100644
--- a/nautobot_golden_config/filters.py
+++ b/nautobot_golden_config/filters.py
@@ -6,15 +6,15 @@
 
 from nautobot.dcim.models import Device, Platform, Region, Site, DeviceRole, DeviceType, Manufacturer, RackGroup, Rack
 from nautobot.extras.models import Status
-from nautobot.extras.filters import CreatedUpdatedFilterSet, StatusFilter
+from nautobot.extras.filters import CreatedUpdatedFilterSet, StatusFilter, CustomFieldModelFilterSet
 from nautobot.tenancy.models import Tenant, TenantGroup
 from nautobot.utilities.filters import TreeNodeMultipleChoiceFilter
 
-from .models import ConfigCompliance, ComplianceFeature, GoldenConfiguration
+from nautobot_golden_config import models
 
 
-class GoldenConfigurationFilter(CreatedUpdatedFilterSet):
-    """Filter capabilities for GoldenConfiguration instances."""
+class GoldenConfigFilter(CreatedUpdatedFilterSet):
+    """Filter capabilities for GoldenConfig instances."""
 
     q = django_filters.CharFilter(
         method="search",
@@ -142,7 +142,7 @@ def search(self, queryset, name, value):  # pylint: disable=unused-argument,no-s
     class Meta:
         """Meta class attributes for GoldenConfig."""
 
-        model = GoldenConfiguration
+        model = models.GoldenConfig
         distinct = True
         fields = [
             "q",
@@ -169,12 +169,14 @@ class Meta:
         ]
 
 
-class ConfigComplianceFilter(GoldenConfigurationFilter):
+class ConfigComplianceFilter(GoldenConfigFilter):
     """Filter capabilities for ConfigCompliance instances."""
 
     device = django_filters.ModelMultipleChoiceFilter(
         field_name="device__name",
-        queryset=Device.objects.filter(id__in=Subquery(ConfigCompliance.objects.distinct("device").values("device"))),
+        queryset=Device.objects.filter(
+            id__in=Subquery(models.ConfigCompliance.objects.distinct("device").values("device"))
+        ),
         to_field_name="name",
         label="Device Name",
     )
@@ -182,7 +184,7 @@ class ConfigComplianceFilter(GoldenConfigurationFilter):
     class Meta:
         """Meta class attributes for ConfigComplianceFilter."""
 
-        model = ConfigCompliance
+        model = models.ConfigCompliance
         distinct = True
         fields = [
             "q",
@@ -209,8 +211,8 @@ class Meta:
         ]
 
 
-class ComplianceFeatureFilter(django_filters.FilterSet):
-    """Boilerplate filter for compliance feature."""
+class ComplianceFeatureFilter(CustomFieldModelFilterSet):
+    """Inherits Base Class CustomFieldModelFilterSet."""
 
     q = django_filters.CharFilter(
         method="search",
@@ -220,5 +222,35 @@ class ComplianceFeatureFilter(django_filters.FilterSet):
     class Meta:
         """Boilerplate filter Meta data for compliance feature."""
 
-        model = ComplianceFeature
-        fields = ["name", "platform"]
+        model = models.ComplianceFeature
+        fields = ["q", "name"]
+
+
+class ComplianceRuleFilter(CustomFieldModelFilterSet):
+    """Inherits Base Class CustomFieldModelFilterSet."""
+
+    class Meta:
+        """Boilerplate filter Meta data for compliance rule."""
+
+        model = models.ComplianceRule
+        fields = ["platform", "feature"]
+
+
+class ConfigRemoveFilter(CustomFieldModelFilterSet):
+    """Inherits Base Class CustomFieldModelFilterSet."""
+
+    class Meta:
+        """Boilerplate filter Meta data for Config Remove."""
+
+        model = models.ConfigRemove
+        fields = ["platform", "name"]
+
+
+class ConfigReplaceFilter(CustomFieldModelFilterSet):
+    """Inherits Base Class CustomFieldModelFilterSet."""
+
+    class Meta:
+        """Boilerplate filter Meta data for Config Remove."""
+
+        model = models.ConfigReplace
+        fields = ["platform", "name"]
diff --git a/nautobot_golden_config/forms.py b/nautobot_golden_config/forms.py
index 30e372a6..c0eacac2 100644
--- a/nautobot_golden_config/forms.py
+++ b/nautobot_golden_config/forms.py
@@ -3,19 +3,19 @@
 from django import forms
 from django.db.models import Subquery
 
+import nautobot.extras.forms as extras_forms
+import nautobot.utilities.forms as utilities_forms
 from nautobot.dcim.models import Device, Platform, Region, Site, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup
 from nautobot.extras.models import Status
-from nautobot.extras.forms import CustomFieldFilterForm
 from nautobot.tenancy.models import Tenant, TenantGroup
-from nautobot.utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, DynamicModelChoiceField
 
-from .models import ConfigCompliance, ComplianceFeature, GoldenConfigSettings, GoldenConfiguration
+from nautobot_golden_config import models
 
 
-class GoldenConfigurationFilterForm(BootstrapMixin, CustomFieldFilterForm):
-    """Filter Form for GoldenConfiguration instances."""
+class GoldenConfigFilterForm(utilities_forms.BootstrapMixin, extras_forms.CustomFieldFilterForm):
+    """Filter Form for GoldenConfig instances."""
 
-    model = GoldenConfiguration
+    model = models.GoldenConfig
 
     class Meta:
         """Meta definitions of searchable fields."""
@@ -36,24 +36,26 @@ class Meta:
         "device",
     ]
     q = forms.CharField(required=False, label="Search")
-    tenant_group = DynamicModelMultipleChoiceField(
+    tenant_group = utilities_forms.DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(), to_field_name="slug", required=False, null_option="None"
     )
-    tenant = DynamicModelMultipleChoiceField(
+    tenant = utilities_forms.DynamicModelMultipleChoiceField(
         queryset=Tenant.objects.all(),
         to_field_name="slug",
         required=False,
         null_option="None",
         query_params={"group": "$tenant_group"},
     )
-    region = DynamicModelMultipleChoiceField(queryset=Region.objects.all(), to_field_name="slug", required=False)
-    site = DynamicModelMultipleChoiceField(
+    region = utilities_forms.DynamicModelMultipleChoiceField(
+        queryset=Region.objects.all(), to_field_name="slug", required=False
+    )
+    site = utilities_forms.DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(), to_field_name="slug", required=False, query_params={"region": "$region"}
     )
-    rack_group_id = DynamicModelMultipleChoiceField(
+    rack_group_id = utilities_forms.DynamicModelMultipleChoiceField(
         queryset=RackGroup.objects.all(), required=False, label="Rack group", query_params={"site": "$site"}
     )
-    rack_id = DynamicModelMultipleChoiceField(
+    rack_id = utilities_forms.DynamicModelMultipleChoiceField(
         queryset=Rack.objects.all(),
         required=False,
         label="Rack",
@@ -63,28 +65,30 @@ class Meta:
             "group_id": "$rack_group_id",
         },
     )
-    role = DynamicModelMultipleChoiceField(queryset=DeviceRole.objects.all(), to_field_name="slug", required=False)
-    manufacturer = DynamicModelMultipleChoiceField(
+    role = utilities_forms.DynamicModelMultipleChoiceField(
+        queryset=DeviceRole.objects.all(), to_field_name="slug", required=False
+    )
+    manufacturer = utilities_forms.DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(), to_field_name="slug", required=False, label="Manufacturer"
     )
-    device_type_id = DynamicModelMultipleChoiceField(
+    device_type_id = utilities_forms.DynamicModelMultipleChoiceField(
         queryset=DeviceType.objects.all(),
         required=False,
         label="Model",
         display_field="model",
         query_params={"manufacturer": "$manufacturer"},
     )
-    platform = DynamicModelMultipleChoiceField(
+    platform = utilities_forms.DynamicModelMultipleChoiceField(
         queryset=Platform.objects.all(), to_field_name="slug", required=False, null_option="None"
     )
-    device = DynamicModelMultipleChoiceField(
+    device = utilities_forms.DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(), required=False, null_option="None", label="Device"
     )
 
     def __init__(self, *args, **kwargs):
         """Required for status to work."""
         super().__init__(*args, **kwargs)
-        self.fields["device_status_id"] = DynamicModelMultipleChoiceField(
+        self.fields["device_status_id"] = utilities_forms.DynamicModelMultipleChoiceField(
             required=False,
             queryset=Status.objects.all(),
             query_params={"content_types": Device._meta.label_lower},
@@ -95,58 +99,257 @@ def __init__(self, *args, **kwargs):
         self.order_fields(self.field_order)  # Reorder fields again
 
 
-class ConfigComplianceFilterForm(GoldenConfigurationFilterForm):
+# ConfigCompliance
+
+
+class ConfigComplianceFilterForm(GoldenConfigFilterForm):
     """Filter Form for ConfigCompliance instances."""
 
-    model = ConfigCompliance
-    device = DynamicModelMultipleChoiceField(
-        queryset=Device.objects.filter(id__in=Subquery(ConfigCompliance.objects.distinct("device").values("device"))),
+    model = models.ConfigCompliance
+    device = utilities_forms.DynamicModelMultipleChoiceField(
+        queryset=Device.objects.filter(
+            id__in=Subquery(models.ConfigCompliance.objects.distinct("device").values("device"))
+        ),
         to_field_name="name",
         required=False,
         null_option="None",
     )
 
 
-class ComplianceFeatureFilterForm(BootstrapMixin, forms.Form):
-    """Form for ComplianceFeature instances."""
+# ComplianceRule
 
-    model = ComplianceFeature
-    platform = DynamicModelChoiceField(queryset=Platform.objects.all(), required=False)
-    name = forms.CharField(required=False)
 
+class ComplianceRuleForm(
+    utilities_forms.BootstrapMixin, extras_forms.CustomFieldModelForm, extras_forms.RelationshipModelForm
+):
+    """Filter Form for ComplianceRule instances."""
 
-class ComplianceFeatureForm(BootstrapMixin, forms.ModelForm):
-    """Filter Form for ComplianceFeature instances."""
+    platform = utilities_forms.DynamicModelChoiceField(queryset=Platform.objects.all())
+
+    class Meta:
+        """Boilerplate form Meta data for compliance rule."""
 
-    platform = DynamicModelChoiceField(queryset=Platform.objects.all())
+        model = models.ComplianceRule
+        fields = (
+            "platform",
+            "feature",
+            "description",
+            "config_ordered",
+            "match_config",
+            "config_type",
+        )
+
+
+class ComplianceRuleFilterForm(utilities_forms.BootstrapMixin, extras_forms.CustomFieldFilterForm):
+    """Form for ComplianceRule instances."""
+
+    model = models.ComplianceRule
+    platform = utilities_forms.DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), required=False)
+    feature = utilities_forms.DynamicModelMultipleChoiceField(
+        queryset=models.ComplianceFeature.objects.all(), required=False
+    )
+
+
+class ComplianceRuleBulkEditForm(
+    utilities_forms.BootstrapMixin, extras_forms.AddRemoveTagsForm, extras_forms.CustomFieldBulkEditForm
+):
+    """BulkEdit form for ComplianceRule instances."""
+
+    pk = forms.ModelMultipleChoiceField(queryset=models.ComplianceRule.objects.all(), widget=forms.MultipleHiddenInput)
+
+    class Meta:
+        """Boilerplate form Meta data for ComplianceRule."""
+
+        nullable_fields = []
+
+
+class ComplianceRuleCSVForm(extras_forms.CustomFieldModelCSVForm):
+    """CSV Form for ComplianceRule instances."""
+
+    class Meta:
+        """Boilerplate form Meta data for ComplianceRule."""
+
+        model = models.ComplianceRule
+        fields = models.ComplianceRule.csv_headers
+
+
+# ComplianceFeature
+
+
+class ComplianceFeatureForm(
+    utilities_forms.BootstrapMixin, extras_forms.CustomFieldModelForm, extras_forms.RelationshipModelForm
+):
+    """Filter Form for ComplianceFeature instances."""
 
     class Meta:
         """Boilerplate form Meta data for compliance feature."""
 
-        model = ComplianceFeature
+        model = models.ComplianceFeature
+        fields = ("name", "slug", "description")
+
+
+class ComplianceFeatureFilterForm(utilities_forms.BootstrapMixin, extras_forms.CustomFieldFilterForm):
+    """Form for ComplianceFeature instances."""
+
+    model = models.ComplianceFeature
+
+    name = utilities_forms.DynamicModelChoiceField(queryset=models.ComplianceFeature.objects.all(), required=False)
+
+
+class ComplianceFeatureBulkEditForm(
+    utilities_forms.BootstrapMixin, extras_forms.AddRemoveTagsForm, extras_forms.CustomFieldBulkEditForm
+):
+    """BulkEdit form for ComplianceFeature instances."""
+
+    pk = forms.ModelMultipleChoiceField(
+        queryset=models.ComplianceFeature.objects.all(), widget=forms.MultipleHiddenInput
+    )
+
+    class Meta:
+        """Boilerplate form Meta data for ComplianceFeature."""
+
+        nullable_fields = []
+
+
+class ComplianceFeatureCSVForm(extras_forms.CustomFieldModelCSVForm):
+    """CSV Form for ComplianceFeature instances."""
+
+    class Meta:
+        """Boilerplate form Meta data for ComplianceFeature."""
+
+        model = models.ComplianceFeature
+        fields = models.ComplianceFeature.csv_headers
+
+
+# ConfigRemove
+
+
+class ConfigRemoveForm(
+    utilities_forms.BootstrapMixin, extras_forms.CustomFieldModelForm, extras_forms.RelationshipModelForm
+):
+    """Filter Form for Line Removal instances."""
+
+    platform = utilities_forms.DynamicModelChoiceField(queryset=Platform.objects.all())
+
+    class Meta:
+        """Boilerplate form Meta data for removal feature."""
+
+        model = models.ConfigRemove
         fields = (
             "platform",
             "name",
             "description",
-            "config_ordered",
-            "match_config",
+            "regex",
         )
 
 
-class GoldenConfigSettingsFeatureForm(BootstrapMixin, forms.ModelForm):
-    """Filter Form for GoldenConfigSettingsFeatureForm instances."""
+class ConfigRemoveFeatureFilterForm(utilities_forms.BootstrapMixin, extras_forms.CustomFieldFilterForm):
+    """Filter Form for Line Removal."""
+
+    model = models.ConfigRemove
+
+    platform = utilities_forms.DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), required=False)
+    name = utilities_forms.DynamicModelChoiceField(queryset=models.ConfigRemove.objects.all(), required=False)
+
+
+class ConfigRemoveBulkEditForm(
+    utilities_forms.BootstrapMixin, extras_forms.AddRemoveTagsForm, extras_forms.CustomFieldBulkEditForm
+):
+    """BulkEdit form for ConfigRemove instances."""
+
+    pk = forms.ModelMultipleChoiceField(queryset=models.ConfigRemove.objects.all(), widget=forms.MultipleHiddenInput)
+
+    class Meta:
+        """Boilerplate form Meta data for ConfigRemove."""
+
+        nullable_fields = []
+
+
+class ConfigRemoveCSVForm(extras_forms.CustomFieldModelCSVForm):
+    """CSV Form for ConfigRemove instances."""
+
+    class Meta:
+        """Boilerplate form Meta data for ConfigRemove."""
+
+        model = models.ConfigRemove
+        fields = models.ConfigRemove.csv_headers
+
+
+# ConfigReplace
+
+
+class ConfigReplaceForm(
+    utilities_forms.BootstrapMixin, extras_forms.CustomFieldModelForm, extras_forms.RelationshipModelForm
+):
+    """Filter Form for Line Removal instances."""
+
+    platform = utilities_forms.DynamicModelChoiceField(queryset=Platform.objects.all())
+
+    class Meta:
+        """Boilerplate form Meta data for removal feature."""
+
+        model = models.ConfigReplace
+        fields = (
+            "platform",
+            "name",
+            "description",
+            "regex",
+            "replace",
+        )
+
+
+class ConfigReplaceFeatureFilterForm(utilities_forms.BootstrapMixin, extras_forms.CustomFieldFilterForm):
+    """Filter Form for Line Replacement."""
+
+    model = models.ConfigReplace
+
+    platform = utilities_forms.DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), required=False)
+    name = utilities_forms.DynamicModelChoiceField(queryset=models.ConfigReplace.objects.all(), required=False)
+
+
+class ConfigReplaceCSVForm(extras_forms.CustomFieldModelCSVForm):
+    """CSV Form for ConfigRemove instances."""
+
+    class Meta:
+        """Boilerplate form Meta data for ConfigRemove."""
+
+        model = models.ConfigReplace
+        fields = models.ConfigReplace.csv_headers
+
+
+class ConfigReplaceBulkEditForm(
+    utilities_forms.BootstrapMixin, extras_forms.AddRemoveTagsForm, extras_forms.CustomFieldBulkEditForm
+):
+    """BulkEdit form for ConfigReplace instances."""
+
+    pk = forms.ModelMultipleChoiceField(queryset=models.ConfigReplace.objects.all(), widget=forms.MultipleHiddenInput)
+
+    class Meta:
+        """Boilerplate form Meta data for ConfigReplace."""
+
+        nullable_fields = []
+
+
+# GoldenConfigSetting
+
+
+class GoldenConfigSettingFeatureForm(
+    utilities_forms.BootstrapMixin, extras_forms.CustomFieldModelForm, extras_forms.RelationshipModelForm
+):
+    """Filter Form for GoldenConfigSettingFeatureForm instances."""
 
     class Meta:
-        """Filter Form Meta Data for GoldenConfigSettingsFeatureForm instances."""
+        """Filter Form Meta Data for GoldenConfigSettingFeatureForm instances."""
 
-        model = GoldenConfigSettings
+        model = models.GoldenConfigSetting
         fields = (
+            "backup_repository",
             "backup_path_template",
+            "intended_repository",
             "intended_path_template",
+            "jinja_repository",
             "jinja_path_template",
             "backup_test_connectivity",
-            "shorten_sot_query",
+            "scope",
             "sot_agg_query",
-            "remove_lines",
-            "substitute_lines",
         )
diff --git a/nautobot_golden_config/graphql/types.py b/nautobot_golden_config/graphql/types.py
new file mode 100644
index 00000000..90b294fe
--- /dev/null
+++ b/nautobot_golden_config/graphql/types.py
@@ -0,0 +1,82 @@
+"""GraphQL implementation for golden config plugin."""
+import graphene
+from graphene_django import DjangoObjectType
+from graphene_django.converter import convert_django_field
+from taggit.managers import TaggableManager
+
+from nautobot.extras.graphql.types import TagType
+from nautobot_golden_config import models
+from nautobot_golden_config import filters
+
+
+@convert_django_field.register(TaggableManager)
+def convert_field_to_list_tags(field, registry=None):
+    """Convert TaggableManager to List of Tags."""
+    return graphene.List(TagType)
+
+
+class ConfigComplianceType(DjangoObjectType):
+    """Graphql Type Object for Config Compliance model."""
+
+    class Meta:
+        """Meta object boilerplate for ConfigComplianceType."""
+
+        model = models.ConfigCompliance
+        filterset_class = filters.ConfigComplianceFilter
+
+
+class GoldenConfigType(DjangoObjectType):
+    """Graphql Type Object for Golden Configuration model."""
+
+    class Meta:
+        """Meta object boilerplate for GoldenConfigType."""
+
+        model = models.GoldenConfig
+        filterset_class = filters.GoldenConfigFilter
+
+
+class ComplianceRuleType(DjangoObjectType):
+    """Graphql Type Object for Compliance Rule model."""
+
+    class Meta:
+        """Meta object boilerplate for GoldenConfigType."""
+
+        model = models.ComplianceRule
+        filterset_class = filters.ComplianceRuleFilter
+
+
+class GoldenConfigSettingType(DjangoObjectType):
+    """Graphql Type Object for Golden Config Settings model."""
+
+    class Meta:
+        """Meta object boilerplate for GoldenConfigSettingType."""
+
+        model = models.GoldenConfigSetting
+
+
+class ConfigRemoveType(DjangoObjectType):
+    """Graphql Type Object for Backup Config Line Remove model."""
+
+    class Meta:
+        """Meta object boilerplate for ConfigRemoveType."""
+
+        model = models.ConfigRemove
+
+
+class ConfigReplaceType(DjangoObjectType):
+    """Graphql Type Object for Backup Config Line Replace model."""
+
+    class Meta:
+        """Meta object boilerplate for ConfigReplaceType."""
+
+        model = models.ConfigReplace
+
+
+graphql_types = [
+    ConfigComplianceType,
+    GoldenConfigType,
+    ComplianceRuleType,
+    GoldenConfigSettingType,
+    ConfigRemoveType,
+    ConfigReplaceType,
+]
diff --git a/nautobot_golden_config/jobs.py b/nautobot_golden_config/jobs.py
index 5afcdd9c..c91b6de2 100644
--- a/nautobot_golden_config/jobs.py
+++ b/nautobot_golden_config/jobs.py
@@ -2,40 +2,31 @@
 import logging
 
 from datetime import datetime
-from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
 
 from nautobot.extras.jobs import Job, MultiObjectVar, ObjectVar, BooleanVar
 from nautobot.extras.models import Tag
 from nautobot.extras.datasources.git import ensure_git_repository
-from nautobot.extras.models.datasources import GitRepository
 from nautobot.dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site, Platform, Region, Rack, RackGroup
 from nautobot.tenancy.models import Tenant, TenantGroup
 
-from .utilities.git import GitRepo
-from .nornir_plays.config_intended import config_intended
-from .nornir_plays.config_backup import config_backup
-from .nornir_plays.config_compliance import config_compliance
-from .utilities.constant import ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_INTENDED
+from nautobot_golden_config.models import GoldenConfigSetting
+from nautobot_golden_config.nornir_plays.config_intended import config_intended
+from nautobot_golden_config.nornir_plays.config_backup import config_backup
+from nautobot_golden_config.nornir_plays.config_compliance import config_compliance
+from nautobot_golden_config.utilities.constant import ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_INTENDED
+from nautobot_golden_config.utilities.git import GitRepo
 
 LOGGER = logging.getLogger(__name__)
 
 
-def git_wrapper(obj, git_type):
+def git_wrapper(obj, orm_obj, git_type):
     """Small wrapper to pull latest branch, and return a GitRepo plugin specific object."""
-    try:
-        orm_obj = GitRepository.objects.get(provided_contents__contains=git_type)
-    except ObjectDoesNotExist:
+    if not orm_obj:
         obj.log_failure(
             obj,
-            f"FATAL ERROR: There is not a valid Git repositories for Git type {git_type}, please see pre-requisite instructions to configure a appropriate Git repositories.",
+            f"FATAL ERROR: There is not a valid Git repositories for Git type {git_type}, please see pre-requisite instructions to configure an appropriate Git repositories.",
         )
-        raise
-    except MultipleObjectsReturned:
-        obj.log_failure(
-            obj,
-            f"FATAL ERROR: There are more than one Git repositories for Git type {git_type}, there is no deterministic way to find which repo should be used, please refer to documentation.",
-        )
-        raise
+        raise  # pylint: disable=misplaced-bare-raise
 
     ensure_git_repository(orm_obj, obj.job_result)
     git_repo = GitRepo(orm_obj)
@@ -103,8 +94,8 @@ def run(self, data, commit):  # pylint: disable=too-many-branches
         # pylint: disable-msg=too-many-locals
         # pylint: disable=unused-argument
 
-        backup_repo = git_wrapper(self, "nautobot_golden_config.backupconfigs")
-        intended_repo = git_wrapper(self, "nautobot_golden_config.intendedconfigs")
+        backup_repo = git_wrapper(self, GoldenConfigSetting.objects.first().backup_repository, "backup")
+        intended_repo = git_wrapper(self, GoldenConfigSetting.objects.first().intended_repository, "intended")
 
         config_compliance(self, data, backup_repo.path, intended_repo.path)
 
@@ -137,9 +128,9 @@ def run(self, data, commit):
         """Run config generation script."""
         now = datetime.now()
         LOGGER.debug("Pull Jinja template repo.")
-        jinja_repo = git_wrapper(self, "nautobot_golden_config.jinjatemplate")
+        jinja_repo = git_wrapper(self, GoldenConfigSetting.objects.first().jinja_repository, "jinja")
         LOGGER.debug("Pull Intended config repo.")
-        intended_repo = git_wrapper(self, "nautobot_golden_config.intendedconfigs")
+        intended_repo = git_wrapper(self, GoldenConfigSetting.objects.first().intended_repository, "intended")
 
         LOGGER.debug("Run config intended nornir play.")
         config_intended(self, data, jinja_repo.path, intended_repo.path)
@@ -177,7 +168,7 @@ def run(self, data, commit):
         """Run config backup process."""
         now = datetime.now()
         LOGGER.debug("Pull Backup config repo.")
-        backup_repo = git_wrapper(self, "nautobot_golden_config.backupconfigs")
+        backup_repo = git_wrapper(self, GoldenConfigSetting.objects.first().backup_repository, "backup")
 
         LOGGER.debug("Run nornir play.")
         config_backup(self, data, backup_repo.path)
diff --git a/nautobot_golden_config/migrations/0001_initial.py b/nautobot_golden_config/migrations/0001_initial.py
index 2ef12db6..21e1d518 100644
--- a/nautobot_golden_config/migrations/0001_initial.py
+++ b/nautobot_golden_config/migrations/0001_initial.py
@@ -1,7 +1,9 @@
-# Generated by Django 3.1.7 on 2021-02-23 22:54
+# Generated by Django 3.1.8 on 2021-05-09 18:33
 
+import django.core.serializers.json
 from django.db import migrations, models
 import django.db.models.deletion
+import taggit.managers
 import uuid
 
 
@@ -10,12 +12,13 @@ class Migration(migrations.Migration):
     initial = True
 
     dependencies = [
+        ("extras", "0004_populate_default_status_records"),
         ("dcim", "0004_initial_part_4"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name="GoldenConfigSettings",
+            name="ComplianceFeature",
             fields=[
                 (
                     "id",
@@ -23,21 +26,123 @@ class Migration(migrations.Migration):
                         default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
                     ),
                 ),
+                ("created", models.DateField(auto_now_add=True, null=True)),
+                ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "_custom_field_data",
+                    models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
+                ),
+                ("name", models.CharField(max_length=100, unique=True)),
+                ("slug", models.SlugField(max_length=100, unique=True)),
+                ("description", models.CharField(blank=True, max_length=200)),
+                ("tags", taggit.managers.TaggableManager(through="extras.TaggedItem", to="extras.Tag")),
+            ],
+            options={
+                "ordering": ("slug",),
+            },
+        ),
+        migrations.CreateModel(
+            name="ComplianceRule",
+            fields=[
+                (
+                    "id",
+                    models.UUIDField(
+                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
+                    ),
+                ),
+                ("created", models.DateField(auto_now_add=True, null=True)),
+                ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "_custom_field_data",
+                    models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
+                ),
+                ("description", models.CharField(blank=True, max_length=200)),
+                ("config_ordered", models.BooleanField()),
+                ("match_config", models.TextField(null=True)),
+                ("config_type", models.CharField(default="cli", max_length=20)),
+                (
+                    "feature",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="feature",
+                        to="nautobot_golden_config.compliancefeature",
+                    ),
+                ),
+                (
+                    "platform",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, related_name="compliance_rules", to="dcim.platform"
+                    ),
+                ),
+                ("tags", taggit.managers.TaggableManager(through="extras.TaggedItem", to="extras.Tag")),
+            ],
+            options={
+                "ordering": ("platform", "feature__name"),
+                "unique_together": {("feature", "platform")},
+            },
+        ),
+        migrations.CreateModel(
+            name="GoldenConfigSetting",
+            fields=[
+                (
+                    "id",
+                    models.UUIDField(
+                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
+                    ),
+                ),
+                ("created", models.DateField(auto_now_add=True, null=True)),
+                ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "_custom_field_data",
+                    models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
+                ),
                 ("backup_path_template", models.CharField(blank=True, max_length=255)),
                 ("intended_path_template", models.CharField(blank=True, max_length=255)),
                 ("jinja_path_template", models.CharField(blank=True, max_length=255)),
                 ("backup_test_connectivity", models.BooleanField(default=True)),
-                ("shorten_sot_query", models.BooleanField(default=False)),
+                (
+                    "scope",
+                    models.JSONField(blank=True, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True),
+                ),
                 ("sot_agg_query", models.TextField(blank=True)),
-                ("remove_lines", models.TextField(blank=True)),
-                ("substitute_lines", models.TextField(blank=True)),
+                (
+                    "backup_repository",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="backup_repository",
+                        to="extras.gitrepository",
+                    ),
+                ),
+                (
+                    "intended_repository",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="intended_repository",
+                        to="extras.gitrepository",
+                    ),
+                ),
+                (
+                    "jinja_repository",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="jinja_template",
+                        to="extras.gitrepository",
+                    ),
+                ),
+                ("tags", taggit.managers.TaggableManager(through="extras.TaggedItem", to="extras.Tag")),
             ],
             options={
                 "abstract": False,
             },
         ),
         migrations.CreateModel(
-            name="GoldenConfiguration",
+            name="GoldenConfig",
             fields=[
                 (
                     "id",
@@ -45,6 +150,12 @@ class Migration(migrations.Migration):
                         default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
                     ),
                 ),
+                ("created", models.DateField(auto_now_add=True, null=True)),
+                ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "_custom_field_data",
+                    models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
+                ),
                 ("backup_config", models.TextField(blank=True)),
                 ("backup_last_attempt_date", models.DateTimeField(null=True)),
                 ("backup_last_success_date", models.DateTimeField(null=True)),
@@ -55,13 +166,14 @@ class Migration(migrations.Migration):
                 ("compliance_last_attempt_date", models.DateTimeField(null=True)),
                 ("compliance_last_success_date", models.DateTimeField(null=True)),
                 ("device", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="dcim.device")),
+                ("tags", taggit.managers.TaggableManager(through="extras.TaggedItem", to="extras.Tag")),
             ],
             options={
-                "abstract": False,
+                "ordering": ["device"],
             },
         ),
         migrations.CreateModel(
-            name="ConfigCompliance",
+            name="ConfigReplace",
             fields=[
                 (
                     "id",
@@ -69,21 +181,33 @@ class Migration(migrations.Migration):
                         default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
                     ),
                 ),
-                ("feature", models.CharField(max_length=32)),
-                ("compliance", models.BooleanField(null=True)),
-                ("actual", models.TextField(blank=True)),
-                ("intended", models.TextField(blank=True)),
-                ("missing", models.TextField(blank=True)),
-                ("extra", models.TextField(blank=True)),
-                ("ordered", models.BooleanField(default=True)),
-                ("device", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="dcim.device")),
+                ("created", models.DateField(auto_now_add=True, null=True)),
+                ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "_custom_field_data",
+                    models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
+                ),
+                ("name", models.CharField(max_length=255)),
+                ("description", models.CharField(blank=True, max_length=200)),
+                ("regex", models.CharField(max_length=200)),
+                ("replace", models.CharField(max_length=200)),
+                (
+                    "platform",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="backup_line_replace",
+                        to="dcim.platform",
+                    ),
+                ),
+                ("tags", taggit.managers.TaggableManager(through="extras.TaggedItem", to="extras.Tag")),
             ],
             options={
-                "unique_together": {("device", "feature")},
+                "ordering": ("platform", "name"),
+                "unique_together": {("name", "platform")},
             },
         ),
         migrations.CreateModel(
-            name="ComplianceFeature",
+            name="ConfigRemove",
             fields=[
                 (
                     "id",
@@ -91,22 +215,66 @@ class Migration(migrations.Migration):
                         default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
                     ),
                 ),
+                ("created", models.DateField(auto_now_add=True, null=True)),
+                ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "_custom_field_data",
+                    models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
+                ),
                 ("name", models.CharField(max_length=255)),
                 ("description", models.CharField(blank=True, max_length=200)),
-                ("config_ordered", models.BooleanField()),
-                ("match_config", models.TextField()),
+                ("regex", models.CharField(max_length=200)),
                 (
                     "platform",
                     models.ForeignKey(
                         on_delete=django.db.models.deletion.CASCADE,
-                        related_name="compliance_features",
+                        related_name="backup_line_remove",
                         to="dcim.platform",
                     ),
                 ),
+                ("tags", taggit.managers.TaggableManager(through="extras.TaggedItem", to="extras.Tag")),
             ],
             options={
-                "ordering": ("name", "platform"),
+                "ordering": ("platform", "name"),
                 "unique_together": {("name", "platform")},
             },
         ),
+        migrations.CreateModel(
+            name="ConfigCompliance",
+            fields=[
+                (
+                    "id",
+                    models.UUIDField(
+                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
+                    ),
+                ),
+                ("created", models.DateField(auto_now_add=True, null=True)),
+                ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "_custom_field_data",
+                    models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
+                ),
+                ("compliance", models.BooleanField(blank=True, null=True)),
+                ("actual", models.TextField(blank=True)),
+                ("intended", models.TextField(blank=True)),
+                ("missing", models.TextField(blank=True)),
+                ("extra", models.TextField(blank=True)),
+                ("ordered", models.BooleanField(default=True)),
+                ("compliance_int", models.IntegerField(blank=True, null=True)),
+                ("device", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="dcim.device")),
+                (
+                    "rule",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="rule",
+                        to="nautobot_golden_config.compliancerule",
+                    ),
+                ),
+                ("tags", taggit.managers.TaggableManager(through="extras.TaggedItem", to="extras.Tag")),
+            ],
+            options={
+                "ordering": ["device"],
+                "unique_together": {("device", "rule")},
+            },
+        ),
     ]
diff --git a/nautobot_golden_config/migrations/0002_custom_data.py b/nautobot_golden_config/migrations/0002_custom_data.py
index 18f85e1b..e0496e78 100644
--- a/nautobot_golden_config/migrations/0002_custom_data.py
+++ b/nautobot_golden_config/migrations/0002_custom_data.py
@@ -10,8 +10,8 @@ class Migration(migrations.Migration):
     ]
 
     def insertData(apps, schema_editor):
-        GoldenConfigSettings = apps.get_model("nautobot_golden_config", "GoldenConfigSettings")
-        global_settings = GoldenConfigSettings.objects.create(id="aaaaaaaa-0000-0000-0000-000000000001")
+        GoldenConfigSetting = apps.get_model("nautobot_golden_config", "GoldenConfigSetting")
+        global_settings = GoldenConfigSetting.objects.create()
         global_settings.save()
 
     operations = [
diff --git a/nautobot_golden_config/migrations/0003_auto_20210510_2356.py b/nautobot_golden_config/migrations/0003_auto_20210510_2356.py
new file mode 100644
index 00000000..b115efa7
--- /dev/null
+++ b/nautobot_golden_config/migrations/0003_auto_20210510_2356.py
@@ -0,0 +1,51 @@
+# Generated by Django 3.1.8 on 2021-05-10 23:56
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("extras", "0004_populate_default_status_records"),
+        ("nautobot_golden_config", "0002_custom_data"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="goldenconfigsetting",
+            name="backup_repository",
+            field=models.ForeignKey(
+                blank=True,
+                limit_choices_to={"provided_contents__contains": "nautobot_golden_config.backupconfigs"},
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="backup_repository",
+                to="extras.gitrepository",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="goldenconfigsetting",
+            name="intended_repository",
+            field=models.ForeignKey(
+                blank=True,
+                limit_choices_to={"provided_contents__contains": "nautobot_golden_config.intendedconfigs"},
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="intended_repository",
+                to="extras.gitrepository",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="goldenconfigsetting",
+            name="jinja_repository",
+            field=models.ForeignKey(
+                blank=True,
+                limit_choices_to={"provided_contents__contains": "nautobot_golden_config.jinjatemplate"},
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="jinja_template",
+                to="extras.gitrepository",
+            ),
+        ),
+    ]
diff --git a/nautobot_golden_config/models.py b/nautobot_golden_config/models.py
index 88ea02d4..c3824654 100644
--- a/nautobot_golden_config/models.py
+++ b/nautobot_golden_config/models.py
@@ -4,49 +4,229 @@
 
 from django.db import models
 from django.core.exceptions import ValidationError
+from django.core.serializers.json import DjangoJSONEncoder
 from django.shortcuts import reverse
 from graphene_django.settings import graphene_settings
 from graphql import get_default_backend
 from graphql.error import GraphQLSyntaxError
 
-from nautobot.core.models import BaseModel
+from nautobot.dcim.models import Device
+from nautobot.extras.models import ObjectChange
+from nautobot.extras.utils import extras_features
+from nautobot.utilities.utils import get_filterset_for_model, serialize_object
+from nautobot.core.models.generics import PrimaryModel
+from netutils.config.compliance import feature_compliance
+
+from nautobot_golden_config.choices import ComplianceRuleTypeChoice
+
 
 LOGGER = logging.getLogger(__name__)
+GRAPHQL_STR_START = "query ($device_id: ID!)"
+
+
+def null_to_empty(val):
+    """Convert to empty string if the value is currently null."""
+    if not val:
+        return ""
+    return val
+
+
+@extras_features(
+    "custom_fields",
+    "custom_validators",
+    "export_templates",
+    "relationships",
+    "graphql",
+    "webhooks",
+)
+class ComplianceFeature(PrimaryModel):
+    """ComplianceFeature details."""
+
+    name = models.CharField(max_length=100, unique=True)
+    slug = models.SlugField(max_length=100, unique=True)
+    description = models.CharField(max_length=200, blank=True)
+
+    csv_headers = ["name", "slug", "description"]
+
+    def to_csv(self):
+        """Indicates model fields to return as csv."""
+        return (self.name, self.slug, self.description)
+
+    class Meta:
+        """Meta information for ComplianceFeature model."""
+
+        ordering = ("slug",)
+
+    def __str__(self):
+        """Return a sane string representation of the instance."""
+        return self.slug
+
+    def get_absolute_url(self):
+        """Absolute url for the ComplianceFeature instance."""
+        return reverse("plugins:nautobot_golden_config:compliancefeature", args=[self.pk])
+
+
+@extras_features(
+    "custom_fields",
+    "custom_validators",
+    "export_templates",
+    "relationships",
+    "graphql",
+    "webhooks",
+)
+class ComplianceRule(PrimaryModel):
+    """ComplianceRule details."""
+
+    feature = models.ForeignKey(to="ComplianceFeature", on_delete=models.CASCADE, blank=False, related_name="feature")
+
+    platform = models.ForeignKey(
+        to="dcim.Platform",
+        on_delete=models.CASCADE,
+        related_name="compliance_rules",
+        null=False,
+        blank=False,
+    )
+    description = models.CharField(
+        max_length=200,
+        blank=True,
+    )
+    config_ordered = models.BooleanField(
+        null=False,
+        blank=False,
+        verbose_name="Configured Ordered",
+        help_text="Whether or not the configuration order matters, such as in ACLs.",
+    )
+    match_config = models.TextField(
+        null=True,
+        blank=False,
+        verbose_name="Config to Match",
+        help_text="The config to match that is matched based on the parent most configuration. e.g. `router bgp` or `ntp`.",
+    )
+    config_type = models.CharField(
+        max_length=20,
+        default=ComplianceRuleTypeChoice.TYPE_CLI,
+        choices=ComplianceRuleTypeChoice,
+        help_text="Whether the config is in cli or json/structured format.",
+    )
 
+    csv_headers = ["platform", "feature", "description", "config_ordered", "match_config", "config_type"]
 
-class ConfigCompliance(BaseModel):
+    def to_csv(self):
+        """Indicates model fields to return as csv."""
+        return (
+            self.platform.slug,
+            self.feature.name,
+            self.description,
+            self.config_ordered,
+            self.match_config,
+            self.config_type,
+        )
+
+    class Meta:
+        """Meta information for ComplianceRule model."""
+
+        ordering = ("platform", "feature__name")
+        unique_together = (
+            "feature",
+            "platform",
+        )
+
+    def __str__(self):
+        """Return a sane string representation of the instance."""
+        return f"{self.platform} - {self.feature.name}"
+
+    def get_absolute_url(self):
+        """Absolute url for the ComplianceRule instance."""
+        return reverse("plugins:nautobot_golden_config:compliancerule", args=[self.pk])
+
+    def clean(self):
+        """Verify that if cli, then match_config is set."""
+        if self.config_type == ComplianceRuleTypeChoice.TYPE_CLI and not self.match_config:
+            raise ValidationError("CLI configuration set, but no configuration set to match.")
+        if self.config_type == ComplianceRuleTypeChoice.TYPE_JSON:
+            raise ValidationError("JSON currently not supported.")
+
+
+@extras_features(
+    "custom_fields",
+    "custom_links",
+    "custom_validators",
+    "export_templates",
+    "graphql",
+    "relationships",
+    "webhooks",
+)
+class ConfigCompliance(PrimaryModel):
     """Configuration compliance details."""
 
     device = models.ForeignKey(to="dcim.Device", on_delete=models.CASCADE, help_text="The device", blank=False)
-
-    feature = models.CharField(max_length=32)
-    compliance = models.BooleanField(null=True)
+    rule = models.ForeignKey(to="ComplianceRule", on_delete=models.CASCADE, blank=False, related_name="rule")
+    compliance = models.BooleanField(null=True, blank=True)
     actual = models.TextField(blank=True, help_text="Actual Configuration for feature")
     intended = models.TextField(blank=True, help_text="Intended Configuration for feature")
     missing = models.TextField(blank=True, help_text="Configuration that should be on the device.")
     extra = models.TextField(blank=True, help_text="Configuration that should not be on the device.")
     ordered = models.BooleanField(default=True)
+    compliance_int = models.IntegerField(null=True, blank=True)
 
     csv_headers = ["Device Name", "Feature", "Compliance"]
 
+    def get_absolute_url(self):
+        """Return absolute URL for instance."""
+        return reverse("plugins:nautobot_golden_config:configcompliance", args=[self.pk])
+
     def to_csv(self):
         """Indicates model fields to return as csv."""
-        return self.device.name, self.feature, self.compliance
+        return (self.device.name, self.rule.feature.name, self.compliance)
+
+    def to_objectchange(self, action):
+        """Remove actual and intended configuration from changelog."""
+        return ObjectChange(
+            changed_object=self,
+            object_repr=str(self),
+            action=action,
+            object_data=serialize_object(self, exclude=["actual", "intended"]),
+        )
 
     class Meta:
         """Set unique together fields for model."""
 
-        unique_together = (
-            "device",
-            "feature",
-        )
+        ordering = ["device"]
+        unique_together = ("device", "rule")
 
     def __str__(self):
         """String representation of a the compliance."""
-        return f"{self.device} -> {self.feature} -> {self.compliance}"
-
-
-class GoldenConfiguration(BaseModel):
+        return f"{self.device} -> {self.rule} -> {self.compliance}"
+
+    def save(self, *args, **kwargs):
+        """Perform that actual compliance check."""
+        feature = {
+            "ordered": self.rule.config_ordered,
+            "name": self.rule.feature.name,
+            "section": self.rule.match_config.splitlines(),
+        }
+        value = feature_compliance(feature, self.actual, self.intended, self.device.platform.slug)
+        self.compliance = value["compliant"]
+        if self.compliance:
+            self.compliance_int = 1
+        else:
+            self.compliance_int = 0
+        self.ordered: value["ordered_compliant"]
+        self.missing = null_to_empty(value["missing"])
+        self.extra = null_to_empty(value["extra"])
+        super().save(*args, **kwargs)
+
+
+@extras_features(
+    "custom_fields",
+    "custom_links",
+    "custom_validators",
+    "export_templates",
+    "graphql",
+    "relationships",
+    "webhooks",
+)
+class GoldenConfig(PrimaryModel):
     """Configuration Management Model."""
 
     device = models.ForeignKey(
@@ -89,66 +269,50 @@ def to_csv(self):
             self.compliance_last_success_date,
         )
 
+    def to_objectchange(self, action):
+        """Remove actual and intended configuration from changelog."""
+        return ObjectChange(
+            changed_object=self,
+            object_repr=str(self),
+            action=action,
+            object_data=serialize_object(self, exclude=["backup_config", "intended_config", "compliance_config"]),
+        )
+
+    class Meta:
+        """Set unique together fields for model."""
+
+        ordering = ["device"]
+
     def __str__(self):
         """String representation of a the compliance."""
         return f"{self.device}"
 
 
-class ComplianceFeature(BaseModel):
-    """Configuration compliance details."""
+class GoldenConfigSetting(PrimaryModel):
+    """GoldenConfigSetting Model defintion. This provides global configs instead of via configs.py."""
 
-    name = models.CharField(max_length=255, null=False, blank=False)
-    platform = models.ForeignKey(
-        to="dcim.Platform",
+    backup_repository = models.ForeignKey(
+        to="extras.GitRepository",
         on_delete=models.CASCADE,
-        related_name="compliance_features",
-        null=False,
-        blank=False,
-    )
-    description = models.CharField(
-        max_length=200,
+        null=True,
         blank=True,
+        related_name="backup_repository",
+        limit_choices_to={"provided_contents__contains": "nautobot_golden_config.backupconfigs"},
     )
-    config_ordered = models.BooleanField(
-        null=False,
-        blank=False,
-        verbose_name="Configured Ordered",
-        help_text="Whether or not the configuration is ordered determentistically.",
-    )
-    match_config = models.TextField(
-        null=False,
-        blank=False,
-        verbose_name="Config to Match",
-        help_text="The config to match that is matched based on the parent most configuration. e.g. `router bgp` or `ntp`.",
-    )
-
-    class Meta:
-        """Meta information for ComplianceFeature model."""
-
-        ordering = ("name", "platform")
-        unique_together = (
-            "name",
-            "platform",
-        )
-
-    def __str__(self):
-        """Return a sane string representation of the instance."""
-        return f"{self.platform} - {self.name}"
-
-    def get_absolute_url(self):  # pylint: disable=no-self-use
-        """Absolute url for the Compliance instance."""
-        return reverse("plugins:nautobot_golden_config:compliancefeature_list")
-
-
-class GoldenConfigSettings(BaseModel):
-    """GoldenConfigSettings Model defintion. This provides global configs instead of via configs.py."""
-
     backup_path_template = models.CharField(
         max_length=255,
         null=False,
         blank=True,
         verbose_name="Backup Path in Jinja Template Form",
-        help_text="The Jinja path representation of where the backup file will be found. The variable `obj` is availiable as the device instance object of a given device, as is the case for all Jinja templates. e.g. `{{obj.site.slug}}/{{obj.name}}.cfg`",
+        help_text="The Jinja path representation of where the backup file will be found. The variable `obj` is available as the device instance object of a given device, as is the case for all Jinja templates. e.g. `{{obj.site.slug}}/{{obj.name}}.cfg`",
+    )
+    intended_repository = models.ForeignKey(
+        to="extras.GitRepository",
+        on_delete=models.CASCADE,
+        null=True,
+        blank=True,
+        related_name="intended_repository",
+        limit_choices_to={"provided_contents__contains": "nautobot_golden_config.intendedconfigs"},
     )
     intended_path_template = models.CharField(
         max_length=255,
@@ -157,12 +321,20 @@ class GoldenConfigSettings(BaseModel):
         verbose_name="Intended Path in Jinja Template Form",
         help_text="The Jinja path representation of where the generated file will be places. e.g. `{{obj.site.slug}}/{{obj.name}}.cfg`",
     )
+    jinja_repository = models.ForeignKey(
+        to="extras.GitRepository",
+        on_delete=models.CASCADE,
+        null=True,
+        blank=True,
+        related_name="jinja_template",
+        limit_choices_to={"provided_contents__contains": "nautobot_golden_config.jinjatemplate"},
+    )
     jinja_path_template = models.CharField(
         max_length=255,
         null=False,
         blank=True,
         verbose_name="Template Path in Jinja Template Form",
-        help_text="The Jinja path representation of where the Jinja temaplte can be found. e.g. `{{obj.platform.slug}}.j2`",
+        help_text="The Jinja path representation of where the Jinja template can be found. e.g. `{{obj.platform.slug}}.j2`",
     )
     backup_test_connectivity = models.BooleanField(
         null=False,
@@ -170,39 +342,40 @@ class GoldenConfigSettings(BaseModel):
         verbose_name="Backup Test",
         help_text="Whether or not to pretest the connectivity of the device by verifying there is a resolvable IP that can connect to port 22.",
     )
-    shorten_sot_query = models.BooleanField(
-        null=False,
-        default=False,
-        verbose_name="Shorten the SoT data returned",
-        help_text="This will shorten the response from `devices[0]{data}` to `{data}`",
+    scope = models.JSONField(
+        encoder=DjangoJSONEncoder,
+        blank=True,
+        null=True,
+        help_text="Queryset filter matching the list of devices for the scope of devices to be considered.",
     )
     sot_agg_query = models.TextField(
         null=False,
         blank=True,
         verbose_name="GraphQL Query",
-        help_text="A query that is evaluated and used to render the config. The query must start with `query ($device: String!)`.",
-    )
-    remove_lines = models.TextField(
-        null=False,
-        blank=True,
-        verbose_name="Lines to remove from backup config",
-        help_text="Configuration lines to remove that match these patterns, one pattern per line.",
-    )
-    substitute_lines = models.TextField(
-        null=False,
-        blank=True,
-        verbose_name="Lines to substitute from backup config",
-        help_text="Lines substitute, using a regex pattern with replacement config three pipes (|||) and a regex pattern with a capture group. e.g. `redacted_config|||username\s+\S+\spassword\s+5\s+(\S+)\s+role\s+\S+`",  # pylint: disable=anomalous-backslash-in-string
+        help_text=f"A query starting with `{GRAPHQL_STR_START}` that is used to render the config. Please make sure to alias name, see FAQ for more details.",
     )
 
+    def get_absolute_url(self):  # pylint: disable=no-self-use
+        """Return absolute URL for instance."""
+        return reverse("plugins:nautobot_golden_config:goldenconfigsetting")
+
     def __str__(self):
         """Return a simple string if model is called."""
-        return "Settings"
+        return "Golden Config Settings"
+
+    def delete(self, *args, **kwargs):
+        """Enforce the singleton pattern, there is no way to delete the configurations."""
+
+    @classmethod
+    def load(cls):
+        """Enforce the singleton pattern, fail it somehow more than one instance."""
+        if len(cls.objects.all()) != 1:
+            raise ValidationError("There was an error where more than one instance existed for a setting.")
+        return cls.objects.first()
 
     def clean(self):
         """Validate there is only one model and if there is a GraphQL query, that it is valid."""
-        if str(self.pk) != "aaaaaaaa-0000-0000-0000-000000000001":
-            raise ValidationError("Only one entry with pk aaaaaaaa-0000-0000-0000-000000000001 is permitted")
+        super().clean()
 
         if self.sot_agg_query:
             try:
@@ -212,6 +385,173 @@ def clean(self):
                 backend.document_from_string(schema, str(self.sot_agg_query))
             except GraphQLSyntaxError as error:
                 raise ValidationError(str(error))  # pylint: disable=raise-missing-from
-            graphql_start = "query ($device: String!)"
-            if not str(self.sot_agg_query).startswith(graphql_start):
-                raise ValidationError(f"The GraphQL query must start with exactly `{graphql_start}`")
+
+            LOGGER.debug("GraphQL - test  query start with: `%s`", GRAPHQL_STR_START)
+            if not str(self.sot_agg_query).startswith(GRAPHQL_STR_START):
+                raise ValidationError(f"The GraphQL query must start with exactly `{GRAPHQL_STR_START}`")
+
+        if self.scope:
+            filterset_class = get_filterset_for_model(Device)
+            filterset = filterset_class(self.scope, Device.objects.all())
+
+            if filterset.errors:
+                for key in filterset.errors:
+                    error_message = ", ".join(filterset.errors[key])
+                    raise ValidationError({"scope": f"{key}: {error_message}"})
+
+            filterset_params = set(filterset.get_filters().keys())
+            for key in self.scope.keys():
+                if key not in filterset_params:
+                    raise ValidationError({"scope": f"'{key}' is not a valid filter parameter for Device object"})
+
+    def get_queryset(self):
+        """Generate a Device QuerySet from the filter."""
+        if not self.scope:
+            return Device.objects.all()
+
+        filterset_class = get_filterset_for_model(Device)
+        filterset = filterset_class(self.scope, Device.objects.all())
+
+        return filterset.qs
+
+    def device_count(self):
+        """Return the number of devices in the group."""
+        return self.get_queryset().count()
+
+    def get_filter_as_string(self):
+        """Get filter as string."""
+        if not self.scope:
+            return None
+
+        result = ""
+
+        for key, value in self.scope.items():
+            if isinstance(value, list):
+                for item in value:
+                    if result != "":
+                        result += "&"
+                    result += f"{key}={item}"
+            else:
+                result += "&"
+                result += f"{key}={value}"
+
+        return result
+
+    def get_url_to_filtered_device_list(self):
+        """Get url to all devices that are matching the filter."""
+        base_url = reverse("dcim:device_list")
+        filter_str = self.get_filter_as_string()
+
+        if filter_str:
+            return f"{base_url}?{filter_str}"
+
+        return base_url
+
+
+@extras_features(
+    "custom_fields",
+    "custom_links",
+    "custom_validators",
+    "export_templates",
+    "graphql",
+    "relationships",
+    "webhooks",
+)
+class ConfigRemove(PrimaryModel):
+    """ConfigRemove for Regex Line Removals from Backup Configuration Model defintion."""
+
+    name = models.CharField(max_length=255, null=False, blank=False)
+    platform = models.ForeignKey(
+        to="dcim.Platform",
+        on_delete=models.CASCADE,
+        related_name="backup_line_remove",
+        null=False,
+        blank=False,
+    )
+    description = models.CharField(
+        max_length=200,
+        blank=True,
+    )
+    regex = models.CharField(
+        max_length=200,
+        verbose_name="Regex Pattern",
+        help_text="Regex pattern used to remove a line from the backup configuration.",
+    )
+
+    clone_fields = ["platform", "description", "regex"]
+    csv_headers = ["name", "platform", "description", "regex"]
+
+    def to_csv(self):
+        """Indicates model fields to return as csv."""
+        return (self.name, self.platform.slug, self.regex)
+
+    class Meta:
+        """Meta information for ConfigRemove model."""
+
+        ordering = ("platform", "name")
+        unique_together = ("name", "platform")
+
+    def __str__(self):
+        """Return a simple string if model is called."""
+        return self.name
+
+    def get_absolute_url(self):  # pylint: disable=no-self-use
+        """Return absolute URL for instance."""
+        return reverse("plugins:nautobot_golden_config:configremove", args=[self.pk])
+
+
+@extras_features(
+    "custom_fields",
+    "custom_links",
+    "custom_validators",
+    "export_templates",
+    "graphql",
+    "relationships",
+    "webhooks",
+)
+class ConfigReplace(PrimaryModel):
+    """ConfigReplace for Regex Line Replacements from Backup Configuration Model defintion."""
+
+    name = models.CharField(max_length=255, null=False, blank=False)
+    platform = models.ForeignKey(
+        to="dcim.Platform",
+        on_delete=models.CASCADE,
+        related_name="backup_line_replace",
+        null=False,
+        blank=False,
+    )
+    description = models.CharField(
+        max_length=200,
+        blank=True,
+    )
+    regex = models.CharField(
+        max_length=200,
+        verbose_name="Regex Pattern to Substitute",
+        help_text="Regex pattern that will be found and replaced with 'replaced text'.",
+    )
+    replace = models.CharField(
+        max_length=200,
+        verbose_name="Replaced Text",
+        help_text="Text that will be inserted in place of Regex pattern match.",
+    )
+
+    clone_fields = ["platform", "description", "regex", "replace"]
+    csv_headers = ["name", "platform", "description", "regex", "replace"]
+
+    def to_csv(self):
+        """Indicates model fields to return as csv."""
+        return (self.name, self.platform.slug, self.regex, self.replace)
+
+    class Meta:
+        """Meta information for ConfigReplace model."""
+
+        ordering = ("platform", "name")
+        unique_together = ("name", "platform")
+
+    def get_absolute_url(self):
+        """Return absolute URL for instance."""
+        return reverse("plugins:nautobot_golden_config:configreplace", args=[self.pk])
+
+    def __str__(self):
+        """Return a simple string if model is called."""
+        return self.name
diff --git a/nautobot_golden_config/navigation.py b/nautobot_golden_config/navigation.py
index e340b5bc..a35fcf44 100644
--- a/nautobot_golden_config/navigation.py
+++ b/nautobot_golden_config/navigation.py
@@ -2,34 +2,50 @@
 
 from nautobot.extras.plugins import PluginMenuItem, PluginMenuButton
 from nautobot.utilities.choices import ButtonColorChoices
-from .utilities.constant import ENABLE_COMPLIANCE
+from nautobot_golden_config.utilities.constant import ENABLE_COMPLIANCE
 
 
 plugin_items = [
     PluginMenuItem(
-        link="plugins:nautobot_golden_config:home",
+        link="plugins:nautobot_golden_config:goldenconfig_list",
         link_text="Home",
-        permissions=["nautobot_golden_config.view_configstatus"],
+        permissions=["nautobot_golden_config.view_goldenconfig"],
     )
 ]
 
 if ENABLE_COMPLIANCE:
     plugin_items.append(
         PluginMenuItem(
-            link="plugins:nautobot_golden_config:config_report",
+            link="plugins:nautobot_golden_config:configcompliance_list",
             link_text="Configuration Compliance",
             permissions=["nautobot_golden_config.view_configcompliance"],
         )
     )
     plugin_items.append(
         PluginMenuItem(
-            link="plugins:nautobot_golden_config:compliancefeature_list",
+            link="plugins:nautobot_golden_config:compliancerule_list",
             link_text="Compliance Rules",
+            permissions=["nautobot_golden_config.view_compliancerule"],
+            buttons=(
+                PluginMenuButton(
+                    link="plugins:nautobot_golden_config:compliancerule_add",
+                    title="Compliance Rules",
+                    icon_class="mdi mdi-plus-thick",
+                    color=ButtonColorChoices.GREEN,
+                    permissions=["nautobot_golden_config.add_compliancerule"],
+                ),
+            ),
+        )
+    )
+    plugin_items.append(
+        PluginMenuItem(
+            link="plugins:nautobot_golden_config:compliancefeature_list",
+            link_text="Compliance Features",
             permissions=["nautobot_golden_config.view_compliancefeature"],
             buttons=(
                 PluginMenuButton(
                     link="plugins:nautobot_golden_config:compliancefeature_add",
-                    title="Compliance Rules",
+                    title="Compliance Features",
                     icon_class="mdi mdi-plus-thick",
                     color=ButtonColorChoices.GREEN,
                     permissions=["nautobot_golden_config.add_compliancefeature"],
@@ -37,5 +53,53 @@
             ),
         )
     )
+    plugin_items.append(
+        PluginMenuItem(
+            link="plugins:nautobot_golden_config:configremove_list",
+            link_text="Config Removals",
+            permissions=["nautobot_golden_config.view_configremove"],
+            buttons=(
+                PluginMenuButton(
+                    link="plugins:nautobot_golden_config:configremove_add",
+                    title="Config Remove",
+                    icon_class="mdi mdi-plus-thick",
+                    color=ButtonColorChoices.GREEN,
+                    permissions=["nautobot_golden_config.add_configremove"],
+                ),
+            ),
+        )
+    )
+    plugin_items.append(
+        PluginMenuItem(
+            link="plugins:nautobot_golden_config:configreplace_list",
+            link_text="Config Replacements",
+            permissions=["nautobot_golden_config.view_compliancereplace"],
+            buttons=(
+                PluginMenuButton(
+                    link="plugins:nautobot_golden_config:configreplace_add",
+                    title="Config Replace",
+                    icon_class="mdi mdi-plus-thick",
+                    color=ButtonColorChoices.GREEN,
+                    permissions=["nautobot_golden_config.add_compliancereplace"],
+                ),
+            ),
+        )
+    )
+    plugin_items.append(
+        PluginMenuItem(
+            link="plugins:nautobot_golden_config:goldenconfigsetting",
+            link_text="Settings",
+            permissions=["nautobot_golden_config.view_compliancereplace"],
+            buttons=(
+                PluginMenuButton(
+                    link="plugins:nautobot_golden_config:goldenconfigsetting_edit",
+                    title="Golden Config Settings",
+                    icon_class="mdi mdi-pencil",
+                    color=ButtonColorChoices.YELLOW,
+                    permissions=["nautobot_golden_config.edit_goldenconfigsetting"],
+                ),
+            ),
+        ),
+    )
 
 menu_items = tuple(plugin_items)
diff --git a/nautobot_golden_config/nornir_plays/config_backup.py b/nautobot_golden_config/nornir_plays/config_backup.py
index 4d4bc15a..37630843 100644
--- a/nautobot_golden_config/nornir_plays/config_backup.py
+++ b/nautobot_golden_config/nornir_plays/config_backup.py
@@ -13,36 +13,41 @@
 from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory
 from nautobot_plugin_nornir.constants import NORNIR_SETTINGS
 
-from nautobot_golden_config.utilities.helper import get_allowed_os, verify_global_settings, check_jinja_template
-from nautobot_golden_config.models import GoldenConfigSettings, GoldenConfiguration
-from .processor import ProcessGoldenConfig
+from nautobot_golden_config.utilities.helper import (
+    get_job_filter,
+    get_dispatcher,
+    verify_global_settings,
+    check_jinja_template,
+)
+from nautobot_golden_config.models import (
+    GoldenConfigSetting,
+    GoldenConfig,
+    ConfigRemove,
+    ConfigReplace,
+)
+from nautobot_golden_config.nornir_plays.processor import ProcessGoldenConfig
 
 InventoryPluginRegister.register("nautobot-inventory", NautobotORMInventory)
 
 
-def get_substitute_lines(text):
-    """Helper functionality to split on filter."""
-    substitute_lines = []
-    for line in text.splitlines():
-        regex_replacement, regex_search = line.split("|||")
-        substitute_lines.append({"regex_replacement": regex_replacement, "regex_search": regex_search})
-    return substitute_lines
-
-
-def run_backup(task: Task, logger, global_settings, backup_root_folder) -> Result:
-    """Backup configurations to disk.
+def run_backup(  # pylint: disable=too-many-arguments
+    task: Task, logger, global_settings, remove_regex_dict, replace_regex_dict, backup_root_folder
+) -> Result:
+    r"""Backup configurations to disk.
 
     Args:
         task (Task): Nornir task individual object
+        remove_regex_dict (dict): {'cisco_ios': ['^Building\\s+configuration.*\\n', '^Current\\s+configuration.*\\n', '^!\\s+Last\\s+configuration.*'], 'arista_eos': ['.s*']}
+        replace_regex_dict (dict): {'cisco_ios': [{'regex_replacement': '<redacted_config>', 'regex_search': 'username\\s+\\S+\\spassword\\s+5\\s+(\\S+)\\s+role\\s+\\S+'}]}
 
     Returns:
         result (Result): Result from Nornir task
     """
     obj = task.host.data["obj"]
 
-    backup_obj = GoldenConfiguration.objects.filter(device=obj).first()
+    backup_obj = GoldenConfig.objects.filter(device=obj).first()
     if not backup_obj:
-        backup_obj = GoldenConfiguration.objects.create(
+        backup_obj = GoldenConfig.objects.create(
             device=obj,
         )
     backup_obj.backup_last_attempt_date = task.host.defaults.data["now"]
@@ -50,7 +55,6 @@ def run_backup(task: Task, logger, global_settings, backup_root_folder) -> Resul
 
     backup_path_template_obj = check_jinja_template(obj, logger, global_settings.backup_path_template)
     backup_file = os.path.join(backup_root_folder, backup_path_template_obj)
-    substitute_lines = get_substitute_lines(global_settings.substitute_lines)
 
     if global_settings.backup_test_connectivity is not False:
         task.run(
@@ -59,6 +63,7 @@ def run_backup(task: Task, logger, global_settings, backup_root_folder) -> Resul
             method="check_connectivity",
             obj=obj,
             logger=logger,
+            default_drivers_mapping=get_dispatcher(),
         )
     running_config = task.run(
         task=dispatcher,
@@ -67,8 +72,9 @@ def run_backup(task: Task, logger, global_settings, backup_root_folder) -> Resul
         obj=obj,
         logger=logger,
         backup_file=backup_file,
-        remove_lines=global_settings.remove_lines.splitlines(),
-        substitute_lines=substitute_lines,
+        remove_lines=remove_regex_dict.get(obj.platform.slug, []),
+        substitute_lines=replace_regex_dict.get(obj.platform.slug, []),
+        default_drivers_mapping=get_dispatcher(),
     )[1].result["config"]
 
     backup_obj.backup_last_success_date = task.host.defaults.data["now"]
@@ -83,8 +89,22 @@ def config_backup(job_result, data, backup_root_folder):
     """Nornir play to backup configurations."""
     now = datetime.now()
     logger = NornirLogger(__name__, job_result, data.get("debug"))
-    global_settings = GoldenConfigSettings.objects.get(id="aaaaaaaa-0000-0000-0000-000000000001")
-    verify_global_settings(logger, global_settings, ["backup_path_template", "intended_path_template"])
+    global_settings = GoldenConfigSetting.objects.first()
+    verify_global_settings(logger, global_settings, ["backup_path_template"])
+
+    # Build a dictionary, with keys of platform.slug, and the regex line in it for the netutils func.
+    remove_regex_dict = {}
+    for regex in ConfigRemove.objects.all():
+        if not remove_regex_dict.get(regex.platform.slug):
+            remove_regex_dict[regex.platform.slug] = []
+        remove_regex_dict[regex.platform.slug].append({"regex": regex.regex})
+
+    # Build a dictionary, with keys of platform.slug, and the regex and replace keys for the netutils func.
+    replace_regex_dict = {}
+    for regex in ConfigReplace.objects.all():
+        if not replace_regex_dict.get(regex.platform.slug):
+            replace_regex_dict[regex.platform.slug] = []
+        replace_regex_dict[regex.platform.slug].append({"replace": regex.replace, "regex": regex.regex})
     nornir_obj = InitNornir(
         runner=NORNIR_SETTINGS.get("runner"),
         logging={"enabled": False},
@@ -93,7 +113,7 @@ def config_backup(job_result, data, backup_root_folder):
             "options": {
                 "credentials_class": NORNIR_SETTINGS.get("credentials"),
                 "params": NORNIR_SETTINGS.get("inventory_params"),
-                "queryset": get_allowed_os(data),
+                "queryset": get_job_filter(data),
                 "defaults": {"now": now},
             },
         },
@@ -106,6 +126,8 @@ def config_backup(job_result, data, backup_root_folder):
         name="BACKUP CONFIG",
         logger=logger,
         global_settings=global_settings,
+        remove_regex_dict=remove_regex_dict,
+        replace_regex_dict=replace_regex_dict,
         backup_root_folder=backup_root_folder,
     )
 
diff --git a/nautobot_golden_config/nornir_plays/config_compliance.py b/nautobot_golden_config/nornir_plays/config_compliance.py
index 1338b84f..dbac7afc 100644
--- a/nautobot_golden_config/nornir_plays/config_compliance.py
+++ b/nautobot_golden_config/nornir_plays/config_compliance.py
@@ -6,26 +6,24 @@
 
 from datetime import datetime
 
+from netutils.config.compliance import parser_map, section_config, _open_file_config
 from nornir import InitNornir
 from nornir.core.plugins.inventory import InventoryPluginRegister
 from nornir.core.task import Result, Task
 
-from nornir_nautobot.plugins.tasks.dispatcher import dispatcher
-from nornir_nautobot.plugins.tasks.dispatcher.utils.compliance import parser_map
 from nornir_nautobot.utils.logger import NornirLogger
 from nornir_nautobot.exceptions import NornirNautobotException
 
 from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory
 from nautobot_plugin_nornir.constants import NORNIR_SETTINGS
 
-from nautobot_golden_config.models import ComplianceFeature, ConfigCompliance, GoldenConfigSettings, GoldenConfiguration
+from nautobot_golden_config.models import ComplianceRule, ConfigCompliance, GoldenConfigSetting, GoldenConfig
 from nautobot_golden_config.utilities.helper import (
-    get_allowed_os,
-    null_to_empty,
+    get_job_filter,
     verify_global_settings,
     check_jinja_template,
 )
-from .processor import ProcessGoldenConfig
+from nautobot_golden_config.nornir_plays.processor import ProcessGoldenConfig
 
 
 InventoryPluginRegister.register("nautobot-inventory", NautobotORMInventory)
@@ -36,13 +34,11 @@ def get_features():
     """A serializer of sorts to return feature mappings as a dictionary."""
     # TODO: Review if creating a proper serializer is the way to go.
     features = {}
-    for obj in ComplianceFeature.objects.all():
+    for obj in ComplianceRule.objects.all():
         platform = str(obj.platform.slug)
         if not features.get(platform):
             features[platform] = []
-        features[platform].append(
-            {"ordered": obj.config_ordered, "name": obj.name, "section": obj.match_config.splitlines()}
-        )
+        features[platform].append({"ordered": obj.config_ordered, "obj": obj, "section": obj.match_config.splitlines()})
     return features
 
 
@@ -73,9 +69,9 @@ def run_compliance(  # pylint: disable=too-many-arguments,too-many-locals
     """
     obj = task.host.data["obj"]
 
-    compliance_obj = GoldenConfiguration.objects.filter(device=obj).first()
+    compliance_obj = GoldenConfig.objects.filter(device=obj).first()
     if not compliance_obj:
-        compliance_obj = GoldenConfiguration.objects.create(device=obj)
+        compliance_obj = GoldenConfig.objects.create(device=obj)
     compliance_obj.compliance_last_attempt_date = task.host.defaults.data["now"]
     compliance_obj.save()
 
@@ -83,9 +79,17 @@ def run_compliance(  # pylint: disable=too-many-arguments,too-many-locals
 
     intended_file = os.path.join(intended_root_folder, intended_path_template_obj)
 
+    if not os.path.exists(intended_file):
+        logger.log_failure(obj, f"Unable to locate intended file for device at {intended_file}")
+        raise NornirNautobotException()
+
     backup_template = check_jinja_template(obj, logger, global_settings.backup_path_template)
     backup_file = os.path.join(backup_root_path, backup_template)
 
+    if not os.path.exists(backup_file):
+        logger.log_failure(obj, f"Unable to locate backup file for device at {backup_file}")
+        raise NornirNautobotException()
+
     platform = obj.platform.slug
     if not features.get(platform):
         logger.log_failure(obj, f"There is no `user` defined feature mapping for platform slug {platform}.")
@@ -95,40 +99,25 @@ def run_compliance(  # pylint: disable=too-many-arguments,too-many-locals
         logger.log_failure(obj, f"There is currently no parser support for platform slug {platform}.")
         raise NornirNautobotException()
 
-    feature_data = task.run(
-        task=dispatcher,
-        name="GET COMPLIANCE FOR CONFIG",
-        method="compliance_config",
-        obj=obj,
-        logger=logger,
-        backup_file=backup_file,
-        intended_file=intended_file,
-        features=features[platform],
-        platform=platform,
-    )[1].result["feature_data"]
-
-    for feature, value in feature_data.items():
-        defaults = {
-            "actual": null_to_empty(value["actual"]),
-            "intended": null_to_empty(value["intended"]),
-            "missing": null_to_empty(value["missing"]),
-            "extra": null_to_empty(value["extra"]),
-            "compliance": value["compliant"],
-            "ordered": value["ordered_compliant"],
-        }
+    backup_cfg = _open_file_config(backup_file)
+    intended_cfg = _open_file_config(intended_file)
+
+    # TODO: Make this atomic with compliance_obj step.
+    for feature in features[obj.platform.slug]:
         # using update_or_create() method to conveniently update actual obj or create new one.
         ConfigCompliance.objects.update_or_create(
             device=obj,
-            feature=feature,
-            defaults=defaults,
+            rule=feature["obj"],
+            actual=section_config(feature, backup_cfg, platform),
+            intended=section_config(feature, intended_cfg, platform),
         )
 
     compliance_obj.compliance_last_success_date = task.host.defaults.data["now"]
     compliance_obj.compliance_config = "\n".join(diff_files(backup_file, intended_file))
     compliance_obj.save()
-    logger.log_success(obj, "Successfully tested complinace.")
+    logger.log_success(obj, "Successfully tested compliance.")
 
-    return Result(host=task.host, result=feature_data)
+    return Result(host=task.host)
 
 
 def config_compliance(job_result, data, backup_root_path, intended_root_folder):
@@ -136,7 +125,7 @@ def config_compliance(job_result, data, backup_root_path, intended_root_folder):
     now = datetime.now()
     features = get_features()
     logger = NornirLogger(__name__, job_result, data.get("debug"))
-    global_settings = GoldenConfigSettings.objects.get(id="aaaaaaaa-0000-0000-0000-000000000001")
+    global_settings = GoldenConfigSetting.objects.first()
     verify_global_settings(logger, global_settings, ["backup_path_template", "intended_path_template"])
     nornir_obj = InitNornir(
         runner=NORNIR_SETTINGS.get("runner"),
@@ -146,7 +135,7 @@ def config_compliance(job_result, data, backup_root_path, intended_root_folder):
             "options": {
                 "credentials_class": NORNIR_SETTINGS.get("credentials"),
                 "params": NORNIR_SETTINGS.get("inventory_params"),
-                "queryset": get_allowed_os(data),
+                "queryset": get_job_filter(data),
                 "defaults": {"now": now},
             },
         },
diff --git a/nautobot_golden_config/nornir_plays/config_intended.py b/nautobot_golden_config/nornir_plays/config_intended.py
index a86cda23..3b446925 100644
--- a/nautobot_golden_config/nornir_plays/config_intended.py
+++ b/nautobot_golden_config/nornir_plays/config_intended.py
@@ -16,10 +16,15 @@
 from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory
 from nautobot_plugin_nornir.constants import NORNIR_SETTINGS
 
-from nautobot_golden_config.models import GoldenConfigSettings, GoldenConfiguration
-from nautobot_golden_config.utilities.helper import get_allowed_os, verify_global_settings, check_jinja_template
+from nautobot_golden_config.models import GoldenConfigSetting, GoldenConfig
+from nautobot_golden_config.utilities.helper import (
+    get_job_filter,
+    get_dispatcher,
+    verify_global_settings,
+    check_jinja_template,
+)
 from nautobot_golden_config.utilities.graphql import graph_ql_query
-from .processor import ProcessGoldenConfig
+from nautobot_golden_config.nornir_plays.processor import ProcessGoldenConfig
 
 InventoryPluginRegister.register("nautobot-inventory", NautobotORMInventory)
 LOGGER = logging.getLogger(__name__)
@@ -40,9 +45,9 @@ def run_template(  # pylint: disable=too-many-arguments
     """
     obj = task.host.data["obj"]
 
-    intended_obj = GoldenConfiguration.objects.filter(device=obj).first()
+    intended_obj = GoldenConfig.objects.filter(device=obj).first()
     if not intended_obj:
-        intended_obj = GoldenConfiguration.objects.create(device=obj)
+        intended_obj = GoldenConfig.objects.create(device=obj)
     intended_obj.intended_last_attempt_date = task.host.defaults.data["now"]
     intended_obj.save()
 
@@ -66,6 +71,7 @@ def run_template(  # pylint: disable=too-many-arguments
         jinja_template=jinja_template,
         jinja_root_path=jinja_root_path,
         output_file_location=output_file_location,
+        default_drivers_mapping=get_dispatcher(),
     )[1].result["config"]
     intended_obj.intended_last_success_date = task.host.defaults.data["now"]
     intended_obj.intended_config = generated_config
@@ -80,7 +86,7 @@ def config_intended(job_result, data, jinja_root_path, intended_root_folder):
     """Nornir play to generate configurations."""
     now = datetime.now()
     logger = NornirLogger(__name__, job_result, data.get("debug"))
-    global_settings = GoldenConfigSettings.objects.get(id="aaaaaaaa-0000-0000-0000-000000000001")
+    global_settings = GoldenConfigSetting.objects.first()
     verify_global_settings(logger, global_settings, ["jinja_path_template", "intended_path_template", "sot_agg_query"])
     nornir_obj = InitNornir(
         runner=NORNIR_SETTINGS.get("runner"),
@@ -90,7 +96,7 @@ def config_intended(job_result, data, jinja_root_path, intended_root_folder):
             "options": {
                 "credentials_class": NORNIR_SETTINGS.get("credentials"),
                 "params": NORNIR_SETTINGS.get("inventory_params"),
-                "queryset": get_allowed_os(data),
+                "queryset": get_job_filter(data),
                 "defaults": {"now": now},
             },
         },
diff --git a/nautobot_golden_config/tables.py b/nautobot_golden_config/tables.py
index cd268200..97d3f741 100644
--- a/nautobot_golden_config/tables.py
+++ b/nautobot_golden_config/tables.py
@@ -2,13 +2,15 @@
 import copy
 
 from django.utils.html import format_html
-from django_tables2 import Column, TemplateColumn
-
-from nautobot.utilities.tables import BaseTable, ToggleColumn
-
-from .models import ConfigCompliance, ComplianceFeature, GoldenConfiguration
-from .utilities.constant import ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_INTENDED, CONFIG_FEATURES
+from django_tables2 import Column, LinkColumn, TemplateColumn
+from django_tables2.utils import A
 
+from nautobot.utilities.tables import (
+    BaseTable,
+    ToggleColumn,
+)
+from nautobot_golden_config import models
+from nautobot_golden_config.utilities.constant import ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_INTENDED, CONFIG_FEATURES
 
 BACKUP_SUCCESS = """
 {% if record.backup_last_success_date and record.backup_last_attempt_date == record.backup_last_success_date %}
@@ -59,7 +61,7 @@
 ALL_ACTIONS = """
 {% if backup == True %}
     {% if record.backup_config %}
-        <a value="{% url 'plugins:nautobot_golden_config:config_details' device_name=record.device config_type='backup' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:config_details' device_name=record.device config_type='backup' %}?modal=true"> 
+        <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='backup' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='backup' %}?modal=true"> 
             <i class="mdi mdi-file-document-outline"></i>
         </a>
     {% else %}
@@ -68,7 +70,7 @@
 {% endif %}
 {% if intended == True %}
     {% if record.intended_config %}
-        <a value="{% url 'plugins:nautobot_golden_config:config_details' device_name=record.device config_type='intended' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:config_details' device_name=record.device config_type='intended' %}?modal=true"> 
+        <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='intended' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='intended' %}?modal=true"> 
             <i class="mdi mdi-text-box-check-outline"></i>
         </a>
     {% else %}
@@ -77,7 +79,7 @@
 {% endif %}
 {% if compliance == True %}
     {% if record.compliance_config %}
-        <a value="{% url 'plugins:nautobot_golden_config:config_details' device_name=record.device config_type='compliance' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:config_details' device_name=record.device config_type='compliance' %}?modal=true"> 
+        <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='compliance' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='compliance' %}?modal=true"> 
             <i class="mdi mdi-file-compare"></i>
         </a>
     {% else %}
@@ -85,7 +87,7 @@
     {% endif %}
 {% endif %}
 {% if sotagg == True %}
-    <a value="{% url 'plugins:nautobot_golden_config:config_details' device_name=record.device config_type='sotagg' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:config_details' device_name=record.device config_type='sotagg' %}?modal=true"> 
+    <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='sotagg' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='sotagg' %}?modal=true"> 
         <i class="mdi mdi-code-json"></i>
     </a>
     <a href="{% url 'extras:job' class_path='plugins/nautobot_golden_config.jobs/AllGoldenConfig' %}?device={{ record.device.pk }}"
@@ -96,10 +98,6 @@
 {% endif %}
 """
 
-COMPLIANCE_FEATURE_NAME = (
-    """<a href="{% url 'plugins:nautobot_golden_config:compliancefeature_edit' pk=record.pk %}">{{ record.name }}</a>"""
-)
-
 MATCH_CONFIG = """{{ record.match_config|linebreaksbr }}"""
 
 
@@ -116,25 +114,43 @@ def actual_fields():
     return tuple(active_fields)
 
 
+#
+# Columns
+#
+
+
+class PercentageColumn(Column):
+    """Column used to display percentage."""
+
+    def render(self, value):
+        """Render percentage value."""
+        return f"{value} %"
+
+
 class ComplianceColumn(Column):
     """Column used to display config compliance status (True/False/None)."""
 
     def render(self, value):
         """Render an entry in this column."""
-        if value is True:  # pylint: disable=no-else-return
+        if value == 1:  # pylint: disable=no-else-return
             return format_html('<span class="text-success"><i class="mdi mdi-check-bold"></i></span>')
-        elif value is False:
+        elif value == 0:
             return format_html('<span class="text-danger"><i class="mdi mdi-close-thick"></i></span>')
         else:  # value is None
             return format_html('<span class="mdi mdi-minus"></span>')
 
 
+#
+# Tables
+#
+
+# ConfigCompliance
 class ConfigComplianceTable(BaseTable):
     """Table for rendering a listing of Device entries and their associated ConfigCompliance record status."""
 
-    pk = ToggleColumn()
-    device__name = TemplateColumn(
-        template_code="""<a href="{% url 'plugins:nautobot_golden_config:device_report' device_name=record.device  %}" <strong>{{ record.device }}</strong></a> """
+    pk = ToggleColumn(accessor=A("device"))
+    device = TemplateColumn(
+        template_code="""<a href="{% url 'plugins:nautobot_golden_config:configcompliance_devicedetail' pk=record.device  %}" <strong>{{ record.device__name }}</strong></a> """
     )
 
     def __init__(self, *args, **kwargs):
@@ -142,7 +158,11 @@ def __init__(self, *args, **kwargs):
         # Used ConfigCompliance.objects on purpose, vs queryset (set in args[0]), as there were issues with that as
         # well as not as expected from user standpoint (e.g. not always the same values on columns depending on
         # filtering)
-        features = list(ConfigCompliance.objects.order_by("feature").values_list("feature", flat=True).distinct())
+        features = list(
+            models.ConfigCompliance.objects.order_by("rule__feature__name")
+            .values_list("rule__feature__name", flat=True)
+            .distinct()
+        )
         extra_columns = [(feature, ComplianceColumn(verbose_name=feature)) for feature in features]
         kwargs["extra_columns"] = extra_columns
         # Nautobot's BaseTable.configurable_columns() only recognizes columns in self.base_columns,
@@ -155,38 +175,30 @@ def __init__(self, *args, **kwargs):
     class Meta(BaseTable.Meta):
         """Metaclass attributes of ConfigComplianceTable."""
 
-        model = ConfigCompliance
+        model = models.ConfigCompliance
         fields = (
             "pk",
-            "device__name",
+            "device",
         )
-        # All other fields (ConfigCompliance features) are constructed dynamically at instantiation time - see views.py
-
-
-class PercentageColumn(Column):
-    """Column used to display percentage."""
-
-    def render(self, value):
-        """Render percentage value."""
-        return f"{value} %"
+        # All other fields (ConfigCompliance names) are constructed dynamically at instantiation time - see views.py
 
 
 class ConfigComplianceGlobalFeatureTable(BaseTable):
     """Table for feature compliance report."""
 
+    name = Column(accessor="rule__feature__slug", verbose_name="Feature")
     count = Column(accessor="count", verbose_name="Total")
     compliant = Column(accessor="compliant", verbose_name="Compliant")
     non_compliant = Column(accessor="non_compliant", verbose_name="Non-Compliant")
     comp_percent = PercentageColumn(accessor="comp_percent", verbose_name="Compliance (%)")
-    feature = Column(accessor="feature", verbose_name="Feature")
 
     class Meta(BaseTable.Meta):
         """Metaclass attributes of ConfigComplianceGlobalFeatureTable."""
 
-        model = ConfigCompliance
-        fields = ["feature", "count", "compliant", "non_compliant", "comp_percent"]
+        model = models.ConfigCompliance
+        fields = ["name", "count", "compliant", "non_compliant", "comp_percent"]
         default_columns = [
-            "feature",
+            "name",
             "count",
             "compliant",
             "non_compliant",
@@ -197,18 +209,21 @@ class Meta(BaseTable.Meta):
 class ConfigComplianceDeleteTable(BaseTable):
     """Table for device compliance report."""
 
+    feature = Column(accessor="rule__feature__name", verbose_name="Feature")
+
     class Meta(BaseTable.Meta):
         """Metaclass attributes of ConfigComplianceDeleteTable."""
 
-        feature = Column(accessor="feature", verbose_name="Feature")
-        device__name = Column(accessor="device__name", verbose_name="Device Name")
+        device = Column(accessor="device__name", verbose_name="Device Name")
         compliance = Column(accessor="compliance", verbose_name="Compliance")
+        model = models.ConfigCompliance
+        fields = ("device", "feature", "compliance")
+
 
-        model = ConfigCompliance
-        fields = ("device__name", "feature", "compliance")
+# GoldenConfig
 
 
-class GoldenConfigurationTable(BaseTable):
+class GoldenConfigTable(BaseTable):
     """Table to display Config Management Status."""
 
     pk = ToggleColumn()
@@ -232,20 +247,74 @@ class GoldenConfigurationTable(BaseTable):
     class Meta(BaseTable.Meta):
         """Meta for class CircuitMaintenanceTable."""
 
-        model = GoldenConfiguration
+        model = models.GoldenConfig
         fields = actual_fields()
 
 
+# ComplianceFeature
+
+
 class ComplianceFeatureTable(BaseTable):
     """Table to display Compliance Features."""
 
     pk = ToggleColumn()
-    name = TemplateColumn(template_code=COMPLIANCE_FEATURE_NAME)
-    match_config = TemplateColumn(template_code=MATCH_CONFIG)
+    name = LinkColumn("plugins:nautobot_golden_config:compliancefeature", args=[A("pk")])
 
     class Meta(BaseTable.Meta):
         """Table to display Compliance Features Meta Data."""
 
-        model = ComplianceFeature
-        fields = ("pk", "name", "slug", "platform", "description", "config_ordered", "match_config")
-        default_columns = ("pk", "name", "slug", "platform", "description", "config_ordered", "match_config")
+        model = models.ComplianceFeature
+        fields = ("pk", "name", "slug", "description")
+        default_columns = ("pk", "name", "slug", "description")
+
+
+# ComplianceRule
+
+
+class ComplianceRuleTable(BaseTable):
+    """Table to display Compliance Rules."""
+
+    pk = ToggleColumn()
+    feature = LinkColumn("plugins:nautobot_golden_config:compliancerule", args=[A("pk")])
+    match_config = TemplateColumn(template_code=MATCH_CONFIG)
+
+    class Meta(BaseTable.Meta):
+        """Table to display Compliance Rules Meta Data."""
+
+        model = models.ComplianceRule
+        fields = ("pk", "feature", "platform", "description", "config_ordered", "match_config", "config_type")
+        default_columns = ("pk", "feature", "platform", "description", "config_ordered", "match_config", "config_type")
+
+
+# ConfigRemove
+
+
+class ConfigRemoveTable(BaseTable):
+    """Table to display Compliance Rules."""
+
+    pk = ToggleColumn()
+    name = LinkColumn("plugins:nautobot_golden_config:configremove", args=[A("pk")])
+
+    class Meta(BaseTable.Meta):
+        """Table to display Compliance Rules Meta Data."""
+
+        model = models.ConfigRemove
+        fields = ("pk", "name", "platform", "description", "regex")
+        default_columns = ("pk", "name", "platform", "description", "regex")
+
+
+# ConfigReplace
+
+
+class ConfigReplaceTable(BaseTable):
+    """Table to display Compliance Rules."""
+
+    pk = ToggleColumn()
+    name = LinkColumn("plugins:nautobot_golden_config:configreplace", args=[A("pk")])
+
+    class Meta(BaseTable.Meta):
+        """Table to display Compliance Rules Meta Data."""
+
+        model = models.ConfigReplace
+        fields = ("pk", "name", "platform", "description", "regex", "replace")
+        default_columns = ("pk", "name", "platform", "description", "regex", "replace")
diff --git a/nautobot_golden_config/template_content.py b/nautobot_golden_config/template_content.py
index 3462a238..7095af98 100644
--- a/nautobot_golden_config/template_content.py
+++ b/nautobot_golden_config/template_content.py
@@ -2,9 +2,8 @@
 from django.db.models import Count, Q
 from nautobot.extras.plugins import PluginTemplateExtension
 
-from .models import ConfigCompliance, GoldenConfiguration
-from .utilities.constant import ENABLE_COMPLIANCE, CONFIG_FEATURES
-from .utilities.helper import get_allowed_os_from_nested
+from nautobot_golden_config.models import ConfigCompliance, GoldenConfig
+from nautobot_golden_config.utilities.constant import ENABLE_COMPLIANCE, CONFIG_FEATURES
 
 
 class ConfigComplianceDeviceCheck(PluginTemplateExtension):  # pylint: disable=abstract-method
@@ -18,11 +17,7 @@ def get_device(self):
 
     def right_page(self):
         """Content to add to the configuration compliance."""
-        comp_obj = (
-            ConfigCompliance.objects.filter(**get_allowed_os_from_nested())
-            .filter(device=self.get_device())
-            .values("feature", "compliance")
-        )
+        comp_obj = ConfigCompliance.objects.filter(device=self.get_device()).values("rule__feature__name", "compliance")
         extra_context = {
             "compliance": comp_obj,
             "device": self.get_device(),
@@ -46,16 +41,16 @@ def get_site_slug(self):
     def right_page(self):
         """Content to add to the configuration compliance."""
         comp_obj = (
-            ConfigCompliance.objects.values("feature")
-            .filter(**get_allowed_os_from_nested())
+            ConfigCompliance.objects.values("rule__feature__name")
             .filter(device__site__slug=self.get_site_slug().slug)
             .annotate(
-                compliant=Count("feature", filter=Q(compliance=True)),
-                non_compliant=Count("feature", filter=~Q(compliance=True)),
+                count=Count("rule__feature__name"),
+                compliant=Count("rule__feature__name", filter=Q(compliance=True)),
+                non_compliant=Count("rule__feature__name", filter=~Q(compliance=True)),
             )
-            .values("feature", "compliant", "non_compliant")
+            .order_by("rule__feature__name")
+            .values("rule__feature__name", "compliant", "non_compliant")
         )
-
         extra_context = {"compliance": comp_obj, "template_type": "site"}
         return self.render(
             "nautobot_golden_config/content_template.html",
@@ -74,9 +69,7 @@ def get_device(self):
 
     def right_page(self):
         """Content to add to the configuration compliance."""
-        golden_config = (
-            GoldenConfiguration.objects.filter(**get_allowed_os_from_nested()).filter(device=self.get_device()).first()
-        )
+        golden_config = GoldenConfig.objects.filter(device=self.get_device()).first()
         extra_context = {
             "device": self.get_device(),  # device,
             "golden_config": golden_config,
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/device_report.html b/nautobot_golden_config/templates/nautobot_golden_config/compliance_device_report.html
similarity index 89%
rename from nautobot_golden_config/templates/nautobot_golden_config/device_report.html
rename to nautobot_golden_config/templates/nautobot_golden_config/compliance_device_report.html
index 3d0d9262..df9cab23 100644
--- a/nautobot_golden_config/templates/nautobot_golden_config/device_report.html
+++ b/nautobot_golden_config/templates/nautobot_golden_config/compliance_device_report.html
@@ -55,13 +55,13 @@
         transform: scale(1.1);
     }
 </style>
-<h1>{% block title %}Configuration Compliance - {{ device_name }}{% endblock %}</h1>
+<h1>{% block title %}Configuration Compliance - {{ device.name }}{% endblock %}</h1>
 {% block navigation %}
 <div class="panel panel-default" style="width:100%">
         <div class="panel-heading"><strong>Feature Navigation</strong> 
-            <a href="{% url 'plugins:nautobot_golden_config:device_filter_report' device_name=device_name compliance='compliant' %}" class="btn btn-success">Compliant</a> 
-            <a href="{% url 'plugins:nautobot_golden_config:device_filter_report' device_name=device_name compliance='non-compliant' %}" class="btn btn-danger">Non-Compliant</a>
-            <a href="{% url 'plugins:nautobot_golden_config:device_report' device_name=device_name %}" class="btn btn-info">Clear</a>  
+            <a href="{% url 'plugins:nautobot_golden_config:configcompliance_filter_report' pk=device.pk compliance='compliant' %}" class="btn btn-success">Compliant</a> 
+            <a href="{% url 'plugins:nautobot_golden_config:configcompliance_filter_report' pk=device.pk compliance='non-compliant' %}" class="btn btn-danger">Non-Compliant</a>
+            <a href="{% url 'plugins:nautobot_golden_config:configcompliance_devicedetail' pk=device.pk %}" class="btn btn-info">Clear</a>  
         </div>
     <div id="navigation">
         <table>
@@ -72,8 +72,8 @@ <h1>{% block title %}Configuration Compliance - {{ device_name }}{% endblock %}<
                 {% else %}
                     <td style="background-color: #d9534f;">
                 {% endif %}
-                    <a href="#{{ item.feature }}">
-                        {{ item.feature }}
+                    <a href="#{{ item.rule }}">
+                        {{ item.rule }}
                     </a>
                 </td>
                 {% if forloop.counter|divisibleby:"5" %}
@@ -87,12 +87,12 @@ <h1>{% block title %}Configuration Compliance - {{ device_name }}{% endblock %}<
 </div>
 {% endblock %}
 {% for item in compliance_details %}
-    <div id="{{ item.feature }}" class="panel panel-default" style="width:100%">
-        <div class="panel-heading"><strong>{{ item.feature|upper }}</strong></div>
+    <div id="{{ item.rule }}" class="panel panel-default" style="width:100%">
+        <div class="panel-heading"><strong>{{ item.rule.feature.name|upper }}</strong></div>
         <table id="content">
         <tr>
             <td style="width:250px">Status</td>
-            {% if item.ordered %}
+            {% if item.rule.config_ordered %}
                 {% if item.compliance %}
                     <td><span class="label label-success">Compliant</span> <span><i class="mdi mdi-sort" title="Ordered Configuration Test"></i></span></td>
                 {% else %}
@@ -107,7 +107,7 @@ <h1>{% block title %}Configuration Compliance - {{ device_name }}{% endblock %}<
             {% endif %}
 
         </tr>
-        {% if item.compliance and not item.ordered %}
+        {% if item.compliance and not item.rule.config_ordered %}
             <tr>
                 <td style="width:250px">Intended Configuration</td>
                 <td class="config_hover">
@@ -205,7 +205,7 @@ <h1>{% block title %}Configuration Compliance - {{ device_name }}{% endblock %}<
 {% endfor %}
 <script type="text/javascript">
     function copyToClipboard(config_value) {
-        var $temp = $("<input>");
+        var $temp = $("<textarea>");
         $("body").append($temp);
         $temp.val(config_value).select();
         document.execCommand("copy");
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/compliance_features.html b/nautobot_golden_config/templates/nautobot_golden_config/compliance_features.html
deleted file mode 100644
index 44b35042..00000000
--- a/nautobot_golden_config/templates/nautobot_golden_config/compliance_features.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{% extends 'base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.nautobot_golden_config.add_compliancefeature %}
-        {% add_button 'plugins:nautobot_golden_config:compliancefeature_add' %}
-    {% endif %}
-    {% if perms.nautobot_golden_config.change_goldenconfigsettings %}
-        <a href="{% url 'plugins:nautobot_golden_config:goldenconfigsettings_edit' pk='aaaaaaaa-0000-0000-0000-000000000001' %}" class="btn btn-warning">
-            <span class="mdi mdi-pencil" aria-hidden="true"></span> Setting
-        </a>
-    {% endif %}
-</div>
-<h1>{% block title %}Compliance Rules{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_delete_url="plugins:nautobot_golden_config:compliancefeature_bulk_delete" %}
-    </div>
-	<div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-	</div>
-</div>
-{% endblock %}
\ No newline at end of file
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/overview_report.html b/nautobot_golden_config/templates/nautobot_golden_config/compliance_overview_report.html
similarity index 100%
rename from nautobot_golden_config/templates/nautobot_golden_config/overview_report.html
rename to nautobot_golden_config/templates/nautobot_golden_config/compliance_overview_report.html
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/compliance_report.html b/nautobot_golden_config/templates/nautobot_golden_config/compliance_report.html
index 7cba3243..6436dedf 100644
--- a/nautobot_golden_config/templates/nautobot_golden_config/compliance_report.html
+++ b/nautobot_golden_config/templates/nautobot_golden_config/compliance_report.html
@@ -12,7 +12,7 @@
     </button>
     <ul class="dropdown-menu">
         {% if compliance %}
-            <li><a href="{% url 'plugins:nautobot_golden_config:compliance_overview_report' %}">Compliance</a></li>
+            <li><a href="{% url 'plugins:nautobot_golden_config:configcompliance_report' %}">Compliance</a></li>
         {% endif %}
     </ul>
 </div>
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature.html b/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature.html
new file mode 100644
index 00000000..030c86b2
--- /dev/null
+++ b/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature.html
@@ -0,0 +1,84 @@
+{% extends 'base.html' %}
+{% load buttons %}
+{% load static %}
+{% load custom_links %}
+{% load helpers %}
+
+{% block title %}{{ object }}{% endblock %}
+
+{% block header %}
+    <div class="row noprint">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li>Compliance Feature</li>
+                <li><a href="{% url 'plugins:nautobot_golden_config:compliancefeature_list' %}">Compliance Features</a></li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'plugins:nautobot_golden_config:compliancefeature_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="mdi mdi-magnify" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
+    </div>
+    <div class="pull-right noprint">
+        {% if perms.nautobot_golden_config.add_compliancefeature %}
+            {% clone_button object %}
+        {% endif %}
+        {% if perms.nautobot_golden_config.change_compliancefeature %}
+            {% edit_button object use_pk=1 %}
+        {% endif %}
+        {% if perms.nautobot_golden_config.delete_compliancefeature %}
+            {% delete_button object use_pk=1 %}
+        {% endif %}
+    </div>
+    <h1>{{ object }}</h1>
+    {% include 'inc/created_updated.html' %}
+    <div class="pull-right noprint">
+        {% custom_links object %}
+    </div>
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ object.get_absolute_url }}">Compliance Feature</a>
+        </li>
+        {% if perms.extras.view_objectchange %}
+            <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+                <a href="{% url 'plugins:nautobot_golden_config:compliancefeature_changelog' pk=object.pk %}">Change Log</a>
+            </li>
+        {% endif %}
+    </ul>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-4">
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Details</strong>
+            </div>
+            <table class="table table-hover panel-body attr-table">
+                <tr>
+                    <td>Name</td>
+                    <td>{{ object.name }}</td>
+                </tr>
+                <tr>
+                    <td>Slug</td>
+                    <td>{{ object.slug }}</a></td>
+                </tr>
+                <tr>
+                    <td>Description</td>
+                    <td>{{ object.description }}</a></td>
+                </tr>
+            </table>
+        </div>
+        {% include 'inc/custom_fields_panel.html' %}
+        {% include 'inc/relationships_panel.html' %}
+    </div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/compliancerule.html b/nautobot_golden_config/templates/nautobot_golden_config/compliancerule.html
new file mode 100644
index 00000000..b81c46df
--- /dev/null
+++ b/nautobot_golden_config/templates/nautobot_golden_config/compliancerule.html
@@ -0,0 +1,96 @@
+{% extends 'base.html' %}
+{% load buttons %}
+{% load static %}
+{% load custom_links %}
+{% load helpers %}
+
+{% block title %}{{ object }}{% endblock %}
+
+{% block header %}
+    <div class="row noprint">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li>Compliance Rule</li>
+                <li><a href="{% url 'plugins:nautobot_golden_config:compliancerule_list' %}">Compliance Rules</a></li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'plugins:nautobot_golden_config:compliancerule_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="mdi mdi-magnify" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
+    </div>
+    <div class="pull-right noprint">
+        {% if perms.nautobot_golden_config.add_compliancerule %}
+            {% clone_button object %}
+        {% endif %}
+        {% if perms.nautobot_golden_config.change_compliancerule %}
+            {% edit_button object use_pk=1 %}
+        {% endif %}
+        {% if perms.nautobot_golden_config.delete_compliancerule %}
+            {% delete_button object use_pk=1 %}
+        {% endif %}
+    </div>
+    <h1>{{ object }}</h1>
+    {% include 'inc/created_updated.html' %}
+    <div class="pull-right noprint">
+        {% custom_links object %}
+    </div>
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ object.get_absolute_url }}">Compliance Rule</a>
+        </li>
+        {% if perms.extras.view_objectchange %}
+            <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+                <a href="{% url 'plugins:nautobot_golden_config:compliancerule_changelog' pk=object.pk %}">Change Log</a>
+            </li>
+        {% endif %}
+    </ul>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-4">
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Details</strong>
+            </div>
+            <table class="table table-hover panel-body attr-table">
+                <tr>
+                    <td>Platform</td>
+                    <td>{{ object.platform.name }}</td>
+                </tr>
+                <tr>
+                    <td>Feature</td>
+                    <td>{{ object.feature.name }}</a></td>
+                </tr>
+                <tr>
+                    <td>Description</td>
+                    <td>{{ object.description }}</a></td>
+                </tr>
+                <tr>
+                    <td>Config Ordered</td>
+                    <td>{{ object.config_ordered }}</a></td>
+                </tr>
+                <tr>
+                    <td>Match Config</td>
+                    <td><pre>{{ object.match_config }}</pre></a></td>
+                </tr>
+                <tr>
+                    <td>Config Type</td>
+                    <td>{{ object.config_type }}</a></td>
+                </tr>
+            </table>
+        </div>
+        {% include 'inc/custom_fields_panel.html' %}
+        {% include 'inc/relationships_panel.html' %}
+    </div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/configcompliance.html b/nautobot_golden_config/templates/nautobot_golden_config/configcompliance.html
new file mode 100644
index 00000000..3994c4ef
--- /dev/null
+++ b/nautobot_golden_config/templates/nautobot_golden_config/configcompliance.html
@@ -0,0 +1,111 @@
+{% extends 'base.html' %}
+{% load buttons %}
+{% load static %}
+{% load custom_links %}
+{% load helpers %}
+
+{% block title %}{{ object }}{% endblock %}
+
+{% block header %}
+<div class="row noprint">
+    <div class="col-sm-8 col-md-9">
+        <ol class="breadcrumb">
+            <li>Compliance Feature</li>
+            <li><a href="{% url 'plugins:nautobot_golden_config:configcompliance_list' %}">Compliance Device</a>
+            </li>
+        </ol>
+    </div>
+    <div class="col-sm-4 col-md-3">
+        <form action="{% url 'plugins:nautobot_golden_config:configcompliance_list' %}" method="get">
+            <div class="input-group">
+                <input type="text" name="q" class="form-control" />
+                <span class="input-group-btn">
+                    <button type="submit" class="btn btn-primary">
+                        <span class="mdi mdi-magnify" aria-hidden="true"></span>
+                    </button>
+                </span>
+            </div>
+        </form>
+    </div>
+</div>
+<div class="pull-right noprint">
+{% if perms.nautobot_golden_config.delete_compliancerule %}
+    {% delete_button object use_pk=1 %}
+{% endif %}
+</div>
+<h1>{{ object }}</h1>
+{% include 'inc/created_updated.html' %}
+<div class="pull-right noprint">
+    {% custom_links object %}
+</div>
+<ul class="nav nav-tabs">
+    <li role="presentation" {% if not active_tab %} class="active" {% endif %}>
+        <a href="{{ object.get_absolute_url }}">Config Compliance</a>
+    </li>
+    {% if perms.extras.view_objectchange %}
+    <li role="presentation">
+        <a href="{% url 'plugins:nautobot_golden_config:configcompliance_changelog' pk=object.pk %}">Change Log</a>
+    </li>
+    {% endif %}
+</ul>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+    <div class="col-md-8">
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Details</strong>
+            </div>
+            <table class="table table-hover panel-body attr-table">
+                <tr>
+                    <td>Device Name</td>
+                    <td>{{ object.device }}</td>
+                </tr>
+                <tr>
+                    <td>Feature</td>
+                    <td>{{ object.rule.feature.name }}</td>
+                </tr>
+                <tr>
+                    <td>Compliance Status</td>
+                    <td>
+                    {% if object.compliance %}
+                        <span class="text-success"><i class="mdi mdi-check-bold"></i></span>
+                    {% else %}
+                        <span class="text-danger"><i class="mdi mdi-close"></i></span>
+                    {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Actual Configuration</td>
+                    <td><pre>{{ object.actual }}</pre></td>
+                </tr>
+                <tr>
+                    <td>Intended Configuration</td>
+                    <td><pre>{{ object.intended }}</pre></td>
+                </tr>
+                <tr>
+                    <td>Extra Configuration</td>
+                    <td><pre>{{ object.extra }}</pre></td>
+                </tr>
+                <tr>
+                    <td>Missing Configuration</td>
+                    <td><pre>{{ object.missing }}</pre></td>
+                </tr>
+                <tr>
+                    <td>Ordered</td>
+                    <td>
+                    {% if object.ordered %}
+                        <span class="text-success"><i class="mdi mdi-check-bold"></i></span>
+                    {% else %}
+                        <span class="text-danger"><i class="mdi mdi-close"></i></span>
+                    {% endif %}
+                    </td>
+                </tr>
+            </table>
+        </div>
+        {% include 'inc/custom_fields_panel.html' %}
+        {% include 'inc/relationships_panel.html' %}
+    </div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/config_details.html b/nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails.html
similarity index 100%
rename from nautobot_golden_config/templates/nautobot_golden_config/config_details.html
rename to nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails.html
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/config_details_modal.html b/nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails_modal.html
similarity index 100%
rename from nautobot_golden_config/templates/nautobot_golden_config/config_details_modal.html
rename to nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails_modal.html
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/configremove.html b/nautobot_golden_config/templates/nautobot_golden_config/configremove.html
new file mode 100644
index 00000000..a8be5122
--- /dev/null
+++ b/nautobot_golden_config/templates/nautobot_golden_config/configremove.html
@@ -0,0 +1,88 @@
+{% extends 'base.html' %}
+{% load buttons %}
+{% load static %}
+{% load custom_links %}
+{% load helpers %}
+
+{% block title %}{{ object }}{% endblock %}
+
+{% block header %}
+    <div class="row noprint">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li>Config Removal</li>
+                <li><a href="{% url 'plugins:nautobot_golden_config:configremove_list' %}">Config Removals</a></li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'plugins:nautobot_golden_config:configremove_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="mdi mdi-magnify" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
+    </div>
+    <div class="pull-right noprint">
+        {% if perms.nautobot_golden_config.add_configremove %}
+            {% clone_button object %}
+        {% endif %}
+        {% if perms.nautobot_golden_config.change_configremove %}
+            {% edit_button object use_pk=1 %}
+        {% endif %}
+        {% if perms.nautobot_golden_config.delete_configremove %}
+            {% delete_button object use_pk=1 %}
+        {% endif %}
+    </div>
+    <h1>{{ object }}</h1>
+    {% include 'inc/created_updated.html' %}
+    <div class="pull-right noprint">
+        {% custom_links object %}
+    </div>
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ object.get_absolute_url }}">Config Removals</a>
+        </li>
+        {% if perms.extras.view_objectchange %}
+            <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+                <a href="{% url 'plugins:nautobot_golden_config:configremove_changelog' pk=object.pk %}">Change Log</a>
+            </li>
+        {% endif %}
+    </ul>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-4">
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Details</strong>
+            </div>
+            <table class="table table-hover panel-body attr-table">
+                <tr>
+                    <td>Name</td>
+                    <td>{{ object.name }}</a></td>
+                </tr>
+                <tr>
+                    <td>Platform</td>
+                    <td>{{ object.platform.name }}</td>
+                </tr>
+                <tr>
+                    <td>Description</td>
+                    <td>{{ object.description }}</a></td>
+                </tr>
+                <tr>
+                    <td>Regex</td>
+                    <td><pre>{{ object.regex }}</pre></a></td>
+                </tr>
+            </table>
+        </div>
+        {% include 'inc/custom_fields_panel.html' %}
+        {% include 'inc/relationships_panel.html' %}
+    </div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/configreplace.html b/nautobot_golden_config/templates/nautobot_golden_config/configreplace.html
new file mode 100644
index 00000000..c8a73a2e
--- /dev/null
+++ b/nautobot_golden_config/templates/nautobot_golden_config/configreplace.html
@@ -0,0 +1,92 @@
+{% extends 'base.html' %}
+{% load buttons %}
+{% load static %}
+{% load custom_links %}
+{% load helpers %}
+
+{% block title %}{{ object }}{% endblock %}
+
+{% block header %}
+    <div class="row noprint">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li>Config Replace</li>
+                <li><a href="{% url 'plugins:nautobot_golden_config:configreplace_list' %}">Config Replacements</a></li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'plugins:nautobot_golden_config:configreplace_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="mdi mdi-magnify" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
+    </div>
+    <div class="pull-right noprint">
+        {% if perms.nautobot_golden_config.add_configreplace %}
+            {% clone_button object %}
+        {% endif %}
+        {% if perms.nautobot_golden_config.change_configreplace %}
+            {% edit_button object use_pk=1 %}
+        {% endif %}
+        {% if perms.nautobot_golden_config.delete_configreplace %}
+            {% delete_button object use_pk=1 %}
+        {% endif %}
+    </div>
+    <h1>{{ object }}</h1>
+    {% include 'inc/created_updated.html' %}
+    <div class="pull-right noprint">
+        {% custom_links object %}
+    </div>
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ object.get_absolute_url }}">Config Replace</a>
+        </li>
+        {% if perms.extras.view_objectchange %}
+            <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+                <a href="{% url 'plugins:nautobot_golden_config:configreplace_changelog' pk=object.pk %}">Change Log</a>
+            </li>
+        {% endif %}
+    </ul>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+	<div class="col-md-8">
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Details</strong>
+            </div>
+            <table class="table table-hover panel-body attr-table">
+                <tr>
+                    <td>Name</td>
+                    <td>{{ object.name }}</a></td>
+                </tr>
+                <tr>
+                    <td>Platform</td>
+                    <td>{{ object.platform.name }}</td>
+                </tr>
+                <tr>
+                    <td>Description</td>
+                    <td>{{ object.description }}</a></td>
+                </tr>
+                <tr>
+                    <td>Regex</td>
+                    <td><pre>{{ object.regex }}</pre></a></td>
+                </tr>
+                <tr>
+                    <td>Replaced</td>
+                    <td><pre>{{ object.replace }}</pre></a></td>
+                </tr>
+            </table>
+        </div>
+        {% include 'inc/custom_fields_panel.html' %}
+        {% include 'inc/relationships_panel.html' %}
+    </div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/content_template.html b/nautobot_golden_config/templates/nautobot_golden_config/content_template.html
index 76abd6fc..bfc41a5d 100644
--- a/nautobot_golden_config/templates/nautobot_golden_config/content_template.html
+++ b/nautobot_golden_config/templates/nautobot_golden_config/content_template.html
@@ -1,7 +1,7 @@
 <div class="panel panel-default">
-{% if template_type == "device-compliance" %}
+    {% if template_type == "device-compliance" %}
     <div class="panel-heading">
-        <a href="{% url 'plugins:nautobot_golden_config:device_report' device_name=device.name %}"><strong>Config Compliance Validation</strong></a>
+        <a href="{% url 'plugins:nautobot_golden_config:configcompliance_devicedetail' pk=device.pk %}"><strong>Config Compliance Validation</strong></a>
     </div>
         <div class="panel panel-default">
         <table class="table table-hover panel-body report">
@@ -14,7 +14,7 @@
             <tbody>
             {% for item in compliance %}
                 <tr>
-                    <td>{{ item.feature }}</td>
+                    <td>{{ item.rule__feature__name }}</td>
                     <td>
                     {% if item.compliance == False %}
                         <span class="label label-danger">Non-Compliant</span>
@@ -43,7 +43,7 @@
             <tbody>
             {% for item in compliance %}
                 <tr>
-                    <td>{{ item.feature }}</td>
+                    <td>{{ item.rule__feature__name }}</td>
                     <td><span class="label label-danger">{{ item.non_compliant }}</span></td>
                     <td><span class="label label-success">{{ item.compliant }}</span></td>
                 </tr>
@@ -69,7 +69,7 @@
                 <tr>
                     <td>Compliance</td>
                     <td>
-                        <a href={% url 'plugins:nautobot_golden_config:config_details' device_name=device.name config_type='compliance' %} <i class="mdi mdi-file-compare"></i></a>
+                        <a href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=device.pk config_type='compliance' %}"><i class="mdi mdi-file-compare"></i></a>
                     </td>
                 </tr>
             {% endif %}
@@ -77,7 +77,7 @@
                 <tr>
                     <td>Intended</td>
                     <td>
-                        <a href={% url 'plugins:nautobot_golden_config:config_details' device_name=device.name config_type='intended' %} <i class="mdi mdi-text-box-check-outline"></i></a>
+                        <a href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=device.pk config_type='intended' %}"><i class="mdi mdi-text-box-check-outline"></i></a>
                     </td>
                 </tr>
             {% endif %}
@@ -85,7 +85,7 @@
                 <tr>
                     <td>Actual</td>
                     <td>
-                        <a href={% url 'plugins:nautobot_golden_config:config_details' device_name=device.name config_type='backup' %} <i class="mdi mdi-file-document-outline"></i></a>
+                        <a href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=device.pk config_type='backup' %}"><i class="mdi mdi-file-document-outline"></i></a>
                     </td>
                 </tr>
             {% endif %}
@@ -93,7 +93,7 @@
                 <tr>
                     <td>SoT Aggregation Data</td>
                     <td>
-                        <a href={% url 'plugins:nautobot_golden_config:config_details' device_name=device.name config_type='sotagg' %} <i class="mdi mdi-code-json"></i></a>
+                        <a href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=device.pk config_type='sotagg' %}"><i class="mdi mdi-code-json"></i></a>
                     </td>
                 </tr>
             {% endif %}
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/home.html b/nautobot_golden_config/templates/nautobot_golden_config/goldenconfig_list.html
similarity index 95%
rename from nautobot_golden_config/templates/nautobot_golden_config/home.html
rename to nautobot_golden_config/templates/nautobot_golden_config/goldenconfig_list.html
index 659ea2ef..cf019adf 100644
--- a/nautobot_golden_config/templates/nautobot_golden_config/home.html
+++ b/nautobot_golden_config/templates/nautobot_golden_config/goldenconfig_list.html
@@ -34,7 +34,7 @@
 <h1>{% block title %}Configuration Overview{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_delete_url="plugins:nautobot_golden_config:home_bulk_delete" %}
+        {% include 'utilities/obj_table.html' with bulk_delete_url="plugins:nautobot_golden_config:goldenconfig_bulk_delete" %}
     </div>
 	<div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
@@ -96,5 +96,8 @@ <h1 class="modal-title">Golden Configuration</h4>
             });
         });
     });
+    $('.openBtn').hover(function(){
+        $(this).css('cursor', 'pointer');
+    });
 </script>
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/goldenconfigsetting.html b/nautobot_golden_config/templates/nautobot_golden_config/goldenconfigsetting.html
new file mode 100644
index 00000000..97ff4404
--- /dev/null
+++ b/nautobot_golden_config/templates/nautobot_golden_config/goldenconfigsetting.html
@@ -0,0 +1,134 @@
+{% extends 'base.html' %}
+{% load buttons %}
+{% load static %}
+{% load custom_links %}
+{% load helpers %}
+
+{% block title %}{{ object }}{% endblock %}
+
+{% block header %}
+    <div class="row noprint">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li>Golden Configuration Settings</li>
+                <li><a href="{% url 'plugins:nautobot_golden_config:goldenconfigsetting_edit' %}">Settings</a></li>
+                <li>{{ object }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'plugins:nautobot_golden_config:goldenconfigsetting' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="mdi mdi-magnify" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
+    </div>
+    <div class="pull-right noprint">
+        {% if perms.nautobot_golden_config.change_goldenconfigsetting %}
+            <a href="{% url 'plugins:nautobot_golden_config:goldenconfigsetting_edit' %}" class="btn btn-warning">
+                <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
+            </a>
+        {% endif %}
+    </div>
+    <h1>{{ object }}</h1>
+    {% include 'inc/created_updated.html' %}
+    <div class="pull-right noprint">
+        {% custom_links object %}
+    </div>
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ object.get_absolute_url }}">Settings</a>
+        </li>
+        {% if perms.extras.view_objectchange %}
+            <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+                <a href="{% url 'plugins:nautobot_golden_config:goldenconfigsetting_changelog' %}">Change Log</a>
+            </li>
+        {% endif %}
+    </ul>
+{% endblock %}
+
+{% block content %}
+<style>
+    pre {
+        font-size: 10px;
+        display: inline-block;
+    }
+</style>
+<div class="row">
+	<div class="col-md-8">
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Settings</strong>
+            </div>
+            <table class="table table-hover panel-body attr-table">
+                <tr>
+                    <td>Backup Repository</td>
+                    <td>
+                    {% if object.backup_repository %}
+                    <a href="{% url 'extras:gitrepository' slug=object.backup_repository.slug %}">{{ object.backup_repository }}</a>
+                    {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Backup Path Template</td>
+                    <td><pre>{{ object.backup_path_template }}</pre></td>
+                </tr>
+                <tr>
+                    <td>Intended Repository</td>
+                    <td>
+                    {% if object.intended_repository %}
+                    <a href="{% url 'extras:gitrepository' slug=object.intended_repository.slug %}">{{ object.intended_repository }}</a>
+                    {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Intended Path Template</td>
+                    <td><pre>{{ object.intended_path_template }}</pre></td>
+                </tr>
+                <tr>
+                    <td>Jinja Repository</td>
+                    <td>
+                    {% if object.jinja_repository %}
+                    <a href="{% url 'extras:gitrepository' slug=object.jinja_repository.slug %}">{{ object.jinja_repository }}</a>
+                    {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Jinja Path Template</td>
+                    <td><pre>{{ object.jinja_path_template }}</pre></td>
+                </tr>
+                <tr>
+                    <td>Backup Test Connectivity</td>
+                    <td>
+                        {% if object.backup_test_connectivity %}
+                            <span class="text-success">
+                                <i class="mdi mdi-check-bold"></i>
+                            </span>
+                        {% else %}
+                            <span class="text-danger">
+                                <i class="mdi mdi-close"></i>
+                            </span>
+                        {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td><a href="{{ object.get_url_to_filtered_device_list }}">Scope of Devices</td>
+                    <td><pre>{{ object.scope|render_json }}</pre></a></td>
+                </tr>
+                <tr>
+                    <td>SoT Aggregation Query</td>
+                    <td><pre>{{ object.sot_agg_query }}</pre></td>
+                </tr>
+        </div>
+    </table>
+</div>
+        {% include 'inc/custom_fields_panel.html' %}
+        {% include 'inc/relationships_panel.html' %}
+    </div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/nautobot_golden_config/urls.py b/nautobot_golden_config/urls.py
index a03323c5..52d49fc6 100644
--- a/nautobot_golden_config/urls.py
+++ b/nautobot_golden_config/urls.py
@@ -1,37 +1,155 @@
 """Django urlpatterns declaration for config compliance plugin."""
 from django.urls import path
 
-from . import views
+from nautobot.extras.views import ObjectChangeLogView
+from nautobot_golden_config import views, models
 
 app_name = "nautobot_golden_config"
 
 urlpatterns = [
-    path("home/", views.Home.as_view(), name="home"),
-    path("home/delete/", views.HomeBulkDeleteView.as_view(), name="home_bulk_delete"),
-    path("report/", views.ComplianceReport.as_view(), name="config_report"),
-    path("report/<str:device_name>", views.ComplianceDeviceReport.as_view(), name="device_report"),
+    path("golden/", views.GoldenConfigListView.as_view(), name="goldenconfig_list"),
+    path("golden/delete/", views.GoldenConfigBulkDeleteView.as_view(), name="goldenconfig_bulk_delete"),
+    path("config-compliance/", views.ConfigComplianceListView.as_view(), name="configcompliance_list"),
+    path("config-compliance/delete/", views.ConfigComplianceBulkDeleteView.as_view(), name="compliance_bulk_delete"),
+    path("config-compliance/overview/", views.ConfigComplianceOverview.as_view(), name="configcompliance_report"),
+    path("config-compliance/<uuid:pk>", views.ConfigComplianceView.as_view(), name="configcompliance"),
     path(
-        "report/<str:device_name>/<str:compliance>/",
+        "config-compliance/devicedetail/<uuid:pk>",
+        views.ConfigComplianceDeviceView.as_view(),
+        name="configcompliance_devicedetail",
+    ),
+    path(
+        "config-compliance/<uuid:pk>/delete/",
+        views.ConfigComplianceDeleteView.as_view(),
+        name="configcompliance_delete",
+    ),
+    path(
+        "config-compliance/details/<uuid:pk>/<str:config_type>/",
+        views.ConfigComplianceDetails.as_view(),
+        name="configcompliance_details",
+    ),
+    path(
+        "config-compliance/filtered/<uuid:pk>/<str:compliance>/",
         views.ComplianceDeviceFilteredReport.as_view(),
-        name="device_filter_report",
+        name="configcompliance_filter_report",
+    ),
+    path(
+        "config-compliance/<uuid:pk>/changelog/",
+        ObjectChangeLogView.as_view(),
+        name="configcompliance_changelog",
+        kwargs={"model": models.ConfigCompliance},
+    ),
+    path("compliance-rule/", views.ComplianceRuleListView.as_view(), name="compliancerule_list"),
+    path("compliance-rule/add/", views.ComplianceRuleEditView.as_view(), name="compliancerule_add"),
+    path(
+        "compliance-rule/import/",
+        views.ComplianceRuleBulkImportView.as_view(),
+        name="compliancerule_import",
     ),
-    path("report/delete/", views.ComplianceBulkDeleteView.as_view(), name="compliance_bulk_delete"),
-    path("overview-report/", views.ComplianceOverviewReport.as_view(), name="compliance_overview_report"),
-    path("config-details/<str:device_name>/<str:config_type>/", views.ConfigDetails.as_view(), name="config_details"),
-    path("compliance-features/", views.ComplianceFeatureView.as_view(), name="compliancefeature_list"),
-    path("compliance-features/add/", views.ComplianceFeatureEditView.as_view(), name="compliancefeature_add"),
     path(
-        "compliance-features/delete/",
+        "compliance-rule/delete/",
+        views.ComplianceRuleBulkDeleteView.as_view(),
+        name="compliancerule_bulk_delete",
+    ),
+    path("compliance-rule/<uuid:pk>/", views.ComplianceRuleView.as_view(), name="compliancerule"),
+    path("compliance-rule/edit/", views.ConfigReplaceBulkEditView.as_view(), name="compliancerule_bulk_edit"),
+    path("compliance-rule/<uuid:pk>/edit/", views.ComplianceRuleEditView.as_view(), name="compliancerule_edit"),
+    path(
+        "compliance-rule/<uuid:pk>/delete/",
+        views.ComplianceRuleDeleteView.as_view(),
+        name="compliancerule_delete",
+    ),
+    path(
+        "compliance-rule/<uuid:pk>/changelog/",
+        ObjectChangeLogView.as_view(),
+        name="compliancerule_changelog",
+        kwargs={"model": models.ComplianceRule},
+    ),
+    path("compliance-feature/", views.ComplianceFeatureListView.as_view(), name="compliancefeature_list"),
+    path("compliance-feature/add/", views.ComplianceFeatureEditView.as_view(), name="compliancefeature_add"),
+    path(
+        "compliance-feature/delete/",
         views.ComplianceFeatureBulkDeleteView.as_view(),
         name="compliancefeature_bulk_delete",
     ),
+    path("compliance-feature/<uuid:pk>/", views.ComplianceFeatureView.as_view(), name="compliancefeature"),
+    path("compliance-feature/edit/", views.ComplianceFeatureBulkEditView.as_view(), name="compliancefeature_bulk_edit"),
     path(
-        "compliance-features/<uuid:pk>/edit/", views.ComplianceFeatureEditView.as_view(), name="compliancefeature_edit"
+        "compliance-feature/<uuid:pk>/edit/", views.ComplianceFeatureEditView.as_view(), name="compliancefeature_edit"
     ),
     path(
-        "compliance-features/<uuid:pk>/delete/",
+        "compliance-feature/<uuid:pk>/delete/",
         views.ComplianceFeatureDeleteView.as_view(),
         name="compliancefeature_delete",
     ),
-    path("settings/<uuid:pk>/edit/", views.GoldenConfigSettingsEditView.as_view(), name="goldenconfigsettings_edit"),
+    path(
+        "compliance-feature/<uuid:pk>/changelog/",
+        ObjectChangeLogView.as_view(),
+        name="compliancefeature_changelog",
+        kwargs={"model": models.ComplianceFeature},
+    ),
+    path("setting/", views.GoldenConfigSettingView.as_view(), name="goldenconfigsetting"),
+    path("setting/edit/", views.GoldenConfigSettingEditView.as_view(), name="goldenconfigsetting_edit"),
+    path(
+        "setting/changelog/",
+        ObjectChangeLogView.as_view(),
+        name="goldenconfigsetting_changelog",
+        kwargs={"model": models.GoldenConfigSetting},
+    ),
+    path("config-remove/", views.ConfigRemoveListView.as_view(), name="configremove_list"),
+    path("config-remove/add/", views.ConfigRemoveEditView.as_view(), name="configremove_add"),
+    path(
+        "config-remove/import/",
+        views.ConfigRemoveBulkImportView.as_view(),
+        name="configremove_import",
+    ),
+    path(
+        "config-remove/edit/",
+        views.ConfigRemoveBulkEditView.as_view(),
+        name="configremove_bulk_edit",
+    ),
+    path(
+        "config-remove/delete/",
+        views.ConfigRemoveBulkDeleteView.as_view(),
+        name="configremove_bulk_delete",
+    ),
+    path(
+        "config-remove/<uuid:pk>/delete/",
+        views.ConfigRemoveDeleteView.as_view(),
+        name="configremove_delete",
+    ),
+    path("config-remove/<uuid:pk>/", views.ConfigRemoveView.as_view(), name="configremove"),
+    path(
+        "config-remove/<uuid:pk>/edit/",
+        views.ConfigRemoveEditView.as_view(),
+        name="configremove_edit",
+    ),
+    path(
+        "config-remove/<uuid:pk>/changelog/",
+        ObjectChangeLogView.as_view(),
+        name="configremove_changelog",
+        kwargs={"model": models.ConfigRemove},
+    ),
+    path("config-replace/", views.ConfigReplaceListView.as_view(), name="configreplace_list"),
+    path("config-replace/add/", views.ConfigReplaceEditView.as_view(), name="configreplace_add"),
+    path("config-replace/import/", views.ConfigReplaceBulkImportView.as_view(), name="configreplace_import"),
+    path("config-replace/edit/", views.ConfigReplaceBulkEditView.as_view(), name="configreplace_bulk_edit"),
+    path(
+        "config-replace/delete/",
+        views.ConfigReplaceBulkDeleteView.as_view(),
+        name="configreplace_bulk_delete",
+    ),
+    path("config-replace/<uuid:pk>/", views.ConfigReplaceView.as_view(), name="configreplace"),
+    path("config-replace/<uuid:pk>/edit/", views.ConfigReplaceEditView.as_view(), name="configreplace_edit"),
+    path(
+        "config-replace/<uuid:pk>/delete/",
+        views.ConfigReplaceDeleteView.as_view(),
+        name="configreplace_delete",
+    ),
+    path(
+        "config-replace/<uuid:pk>/changelog/",
+        ObjectChangeLogView.as_view(),
+        name="configreplace_changelog",
+        kwargs={"model": models.ConfigReplace},
+    ),
 ]
diff --git a/nautobot_golden_config/utilities/constant.py b/nautobot_golden_config/utilities/constant.py
index eddc4705..13b5e83f 100644
--- a/nautobot_golden_config/utilities/constant.py
+++ b/nautobot_golden_config/utilities/constant.py
@@ -14,5 +14,3 @@
     "backup": ENABLE_BACKUP,
     "sotagg": ENABLE_SOTAGG,
 }
-
-ALLOWED_OS = PLUGIN_CFG["allowed_os"]
diff --git a/nautobot_golden_config/utilities/git.py b/nautobot_golden_config/utilities/git.py
index 7fd831bb..489e062e 100644
--- a/nautobot_golden_config/utilities/git.py
+++ b/nautobot_golden_config/utilities/git.py
@@ -21,8 +21,14 @@ def __init__(self, obj):
         self.path = obj.filesystem_path
         self.url = obj.remote_url
         self.token = obj._token
+        self.token_user = obj.username
         if self.token and self.token not in self.url:
-            self.url = re.sub("//", f"//{self.token}:x-oauth-basic@", self.url)
+            # Some Git Providers require a user as well as a token.
+            if self.token_user:
+                self.url = re.sub("//", f"//{self.token_user}:{self.token}@", self.url)
+            else:
+                # Github only requires the token.
+                self.url = re.sub("//", f"//{self.token}@", self.url)
 
         self.branch = obj.branch
         self.obj = obj
diff --git a/nautobot_golden_config/utilities/graphql.py b/nautobot_golden_config/utilities/graphql.py
index fdfe240a..e315cd61 100644
--- a/nautobot_golden_config/utilities/graphql.py
+++ b/nautobot_golden_config/utilities/graphql.py
@@ -7,8 +7,7 @@
 from graphql import get_default_backend
 from graphql.error import GraphQLSyntaxError
 
-from nautobot_golden_config.models import GoldenConfigSettings
-from .constant import PLUGIN_CFG
+from nautobot_golden_config.utilities.constant import PLUGIN_CFG
 
 LOGGER = logging.getLogger(__name__)
 
@@ -20,10 +19,11 @@ def graph_ql_query(request, device, query):
     schema = graphene_settings.SCHEMA
 
     LOGGER.debug("GraphQL - set query variable to device.")
-    variables = {"device": device}
+    variables = {"device_id": str(device.pk)}
     try:
         LOGGER.debug("GraphQL - test query: `%s`", str(query))
         document = backend.document_from_string(schema, query)
+
     except GraphQLSyntaxError as error:
         LOGGER.warning("GraphQL - test query Failed: `%s`", str(query))
         return (400, {"error": str(error)})
@@ -35,9 +35,7 @@ def graph_ql_query(request, device, query):
         return (400, result.to_dict())
     data = result.data
 
-    global_settings = GoldenConfigSettings.objects.get(id="aaaaaaaa-0000-0000-0000-000000000001")
-    if global_settings.shorten_sot_query is True:
-        data = data["devices"][0]
+    data = data.get("device", {})
 
     if PLUGIN_CFG.get("sot_agg_transposer"):
         LOGGER.debug("GraphQL - tansform data with function: `%s`", str(PLUGIN_CFG.get("sot_agg_transposer")))
diff --git a/nautobot_golden_config/utilities/helper.py b/nautobot_golden_config/utilities/helper.py
index 0eba96e1..7ef5bbd2 100644
--- a/nautobot_golden_config/utilities/helper.py
+++ b/nautobot_golden_config/utilities/helper.py
@@ -5,10 +5,13 @@
 from jinja2.exceptions import TemplateError, TemplateSyntaxError
 
 from nornir_nautobot.exceptions import NornirNautobotException
+from nornir_nautobot.plugins.tasks.dispatcher import _DEFAULT_DRIVERS_MAPPING
 from nautobot.dcim.filters import DeviceFilterSet
-from nautobot.dcim.models import Device, Platform
+from nautobot.dcim.models import Device
+
+from nautobot_golden_config import models
+from nautobot_golden_config.utilities.constant import PLUGIN_CFG
 
-from .constant import ALLOWED_OS
 
 FIELDS = {
     "platform",
@@ -25,7 +28,7 @@
 }
 
 
-def get_allowed_os(data=None):
+def get_job_filter(data=None):
     """Helper function to return a the filterable list of OS's based on platform.slug and a specific custom value."""
     if not data:
         data = {}
@@ -33,25 +36,21 @@ def get_allowed_os(data=None):
     for field in FIELDS:
         if data.get(field):
             query[f"{field}_id"] = data[field].values_list("pk", flat=True)
-
     # Handle case where object is from single device run all.
     if data.get("device") and isinstance(data["device"], Device):
         query.update({"id": [str(data["device"].pk)]})
     elif data.get("device"):
         query.update({"id": data["device"].values_list("pk", flat=True)})
 
-    if "all" in ALLOWED_OS:
-        _allowed_os = Platform.objects.values_list("slug", flat=True)
-    else:
-        _allowed_os = ALLOWED_OS
-    return DeviceFilterSet(data=query, queryset=Device.objects.filter(platform__slug__in=_allowed_os)).qs
+    base_qs = models.GoldenConfigSetting.objects.first().get_queryset()
+    return DeviceFilterSet(data=query, queryset=base_qs).qs
 
 
-def get_allowed_os_from_nested():
-    """Helper method to filter out only in scope OS's."""
-    if "all" in ALLOWED_OS:
-        return {"device__platform__slug__in": Platform.objects.values_list("slug", flat=True)}
-    return {"device__platform__slug__in": ALLOWED_OS}
+def get_dispatcher():
+    """Helper method to load the dispatcher from nautobot nornir or config if defined."""
+    if PLUGIN_CFG.get("dispatcher_mapping"):
+        return PLUGIN_CFG["dispatcher_mapping"]
+    return _DEFAULT_DRIVERS_MAPPING
 
 
 def null_to_empty(val):
diff --git a/nautobot_golden_config/views.py b/nautobot_golden_config/views.py
index 46d7264f..9ee2daa6 100644
--- a/nautobot_golden_config/views.py
+++ b/nautobot_golden_config/views.py
@@ -1,4 +1,5 @@
 """Django views for Nautobot Golden Configuration."""
+from datetime import datetime
 
 import base64
 import io
@@ -11,9 +12,10 @@
 import numpy as np
 
 from django.contrib import messages
-from django.db.models import F, Q
-from django.db.models import Subquery, OuterRef, Count, FloatField, ExpressionWrapper, ProtectedError
+from django.db.models import F, Q, Max
+from django.db.models import Count, FloatField, ExpressionWrapper, ProtectedError
 from django.shortcuts import render, redirect
+from django_pivot.pivot import pivot
 
 from nautobot.dcim.models import Device
 from nautobot.core.views import generic
@@ -21,143 +23,71 @@
 from nautobot.utilities.error_handlers import handle_protectederror
 from nautobot.utilities.views import ContentTypePermissionRequiredMixin
 
-from .filters import ComplianceFeatureFilter, ConfigComplianceFilter, GoldenConfigurationFilter
-from .forms import (
-    ComplianceFeatureForm,
-    ComplianceFeatureFilterForm,
-    ConfigComplianceFilterForm,
-    GoldenConfigSettingsFeatureForm,
-    GoldenConfigurationFilterForm,
-)
-from .models import ComplianceFeature, ConfigCompliance, GoldenConfigSettings, GoldenConfiguration
-from .tables import (
-    ComplianceFeatureTable,
-    ConfigComplianceGlobalFeatureTable,
-    ConfigComplianceTable,
-    ConfigComplianceDeleteTable,
-    GoldenConfigurationTable,
-)
-from .utilities.constant import PLUGIN_CFG, ENABLE_COMPLIANCE, CONFIG_FEATURES
-from .utilities.helper import get_allowed_os_from_nested
-from .utilities.graphql import graph_ql_query
+from nautobot_golden_config import filters, forms, models, tables
+
+from nautobot_golden_config.utilities.constant import PLUGIN_CFG, ENABLE_COMPLIANCE, CONFIG_FEATURES
+from nautobot_golden_config.utilities.graphql import graph_ql_query
 
 LOGGER = logging.getLogger(__name__)
 
 GREEN = "#D5E8D4"
 RED = "#F8CECC"
 
+#
+# GoldenConfig
+#
+
 
-class Home(generic.ObjectListView):
+class GoldenConfigListView(generic.ObjectListView):
     """View for displaying the configuration management status for backup, intended, diff, and SoT Agg."""
 
-    table = GoldenConfigurationTable
-    filterset = GoldenConfigurationFilter
-    filterset_form = GoldenConfigurationFilterForm
-    queryset = GoldenConfiguration.objects.filter(**get_allowed_os_from_nested()).order_by("device__name")
-    template_name = "nautobot_golden_config/home.html"
+    table = tables.GoldenConfigTable
+    filterset = filters.GoldenConfigFilter
+    filterset_form = forms.GoldenConfigFilterForm
+    queryset = models.GoldenConfig.objects.all()
+    template_name = "nautobot_golden_config/goldenconfig_list.html"
 
     def extra_context(self):
         """Boilerplace code to modify data before returning."""
         return CONFIG_FEATURES
 
 
-class HomeBulkDeleteView(generic.BulkDeleteView):
+class GoldenConfigBulkDeleteView(generic.BulkDeleteView):
     """Standard view for bulk deletion of data."""
 
-    queryset = GoldenConfiguration.objects.filter(**get_allowed_os_from_nested()).order_by("device__name")
-    table = GoldenConfigurationTable
-    filterset = GoldenConfigurationFilter
-
-
-class ConfigDetails(ContentTypePermissionRequiredMixin, generic.View):
-    """View for the single configuration or diff of a single."""
-
-    def get_required_permission(self):
-        """Manually set permission when not tied to a model for config details."""
-        return "nautobot_golden_config.view_goldenconfiguration"
-
-    def get(self, request, device_name, config_type):
-        """Read request into a view of a single device."""
-        device = Device.objects.get(name=device_name)
-        config_details = GoldenConfiguration.objects.filter(device=device).first()
-        structure_format = "json"
-        if config_type == "backup":
-            output = config_details.backup_config
-        elif config_type == "intended":
-            output = config_details.intended_config
-        elif config_type == "compliance":
-            output = config_details.compliance_config
-            backup_date = str(config_details.backup_last_success_date.strftime("%b %d %Y"))
-            intended_date = str(config_details.intended_last_success_date.strftime("%b %d %Y"))
-            first_occurence = output.index("@@")
-            second_occurence = output.index("@@", first_occurence)
-            # This is logic to match diff2html's expected input.
-            output = (
-                "--- Backup File - "
-                + backup_date
-                + "\n+++ Intended File - "
-                + intended_date
-                + "\n"
-                + output[first_occurence:second_occurence]
-                + "@@"
-                + output[second_occurence + 2 :]
-            )
-        elif config_type == "sotagg":
-            if request.GET.get("format") in ["json", "yaml"]:
-                structure_format = request.GET.get("format")
-
-            global_settings = GoldenConfigSettings.objects.get(id="aaaaaaaa-0000-0000-0000-000000000001")
-            _, output = graph_ql_query(request, device, global_settings.sot_agg_query)
-
-            if structure_format == "yaml":
-                output = yaml.dump(output, default_flow_style=False)
-            else:
-                output = json.dumps(output, indent=4)
+    queryset = models.GoldenConfig.objects.all()
+    table = tables.GoldenConfigTable
+    filterset = filters.GoldenConfigFilter
 
-        template_name = "nautobot_golden_config/config_details.html"
-        if request.GET.get("modal") == "true":
-            template_name = "nautobot_golden_config/config_details_modal.html"
 
-        return render(
-            request,
-            template_name,
-            {"output": output, "device_name": device_name, "config_type": config_type, "format": structure_format},
-        )
+#
+# ConfigCompliance
+#
 
 
-class ComplianceReport(generic.ObjectListView):
+class ConfigComplianceListView(generic.ObjectListView):
     """Django View for visualizing the compliance report."""
 
-    filterset = ConfigComplianceFilter
-    filterset_form = ConfigComplianceFilterForm
-    queryset = ConfigCompliance.objects.filter(**get_allowed_os_from_nested())
+    filterset = filters.ConfigComplianceFilter
+    filterset_form = forms.ConfigComplianceFilterForm
+    queryset = models.ConfigCompliance.objects.all().order_by("device__name")
     template_name = "nautobot_golden_config/compliance_report.html"
-    table = ConfigComplianceTable
-
-    def extra_context(self):
-        """Boilerplate code to modify before returning data."""
-        return {"compliance": ENABLE_COMPLIANCE}
+    table = tables.ConfigComplianceTable
 
     def alter_queryset(self, request):
         """Build actual runtime queryset as the build time queryset provides no information."""
-        # Current implementation of for feature in ConfigCompliance.objects.values_list(), to always show all
-        # features, however this may or may not be desirable in the future. To modify, change to
-        # self.queryset.values_list()
-        return (
-            self.queryset.annotate(
-                **{
-                    feature: Subquery(
-                        self.queryset.filter(device=OuterRef("device_id"), feature=feature).values("compliance")
-                    )
-                    for feature in ConfigCompliance.objects.values_list("feature", flat=True)
-                    .distinct()
-                    .order_by("feature")
-                }
-            )
-            .distinct(*list(ConfigCompliance.objects.values_list("feature", flat=True).distinct()) + ["device__name"])
-            .order_by("device__name")
+        return pivot(
+            self.queryset,
+            ["device", "device__name"],
+            "rule__feature__slug",
+            "compliance_int",
+            aggregation=Max,
         )
 
+    def extra_context(self):
+        """Boilerplate code to modify before returning data."""
+        return {"compliance": ENABLE_COMPLIANCE}
+
     def queryset_to_csv(self):
         """Export queryset of objects as comma-separated value (CSV)."""
 
@@ -169,7 +99,7 @@ def conver_to_str(val):
             return "N/A"
 
         csv_data = []
-        headers = sorted(list(ConfigCompliance.objects.values_list("feature", flat=True).distinct()))
+        headers = sorted(list(models.ConfigCompliance.objects.values_list("feature", flat=True).distinct()))
         csv_data.append(",".join(list(["Device name"] + headers)))
         for obj in self.alter_queryset(None).values():
             # From all of the unique fields, obtain the columns, using list comprehension, add values per column,
@@ -181,16 +111,28 @@ def conver_to_str(val):
         return "\n".join(csv_data)
 
 
-class ComplianceBulkDeleteView(generic.BulkDeleteView):
+class ConfigComplianceView(generic.ObjectView):
+    """View for a device's specific configuration compliance feature."""
+
+    queryset = models.ConfigCompliance.objects.all()
+
+    def get_extra_context(self, request, instance):
+        """Add extra data to detail view for Nautobot."""
+        return {}
+
+
+class ConfigComplianceBulkDeleteView(generic.BulkDeleteView):
     """View for deleting one or more OnboardingTasks."""
 
-    queryset = ConfigCompliance.objects.filter(**get_allowed_os_from_nested()).order_by("device__name")
-    default_return_url = "plugins:nautobot_golden_config:config_report"
-    table = ConfigComplianceDeleteTable
-    filterset = ConfigComplianceFilter
+    queryset = models.ConfigCompliance.objects.all()
+    table = tables.ConfigComplianceDeleteTable
+    filterset = filters.ConfigComplianceFilter
 
     def post(self, request, **kwargs):
         """Delete instances based on post request data."""
+        # This is a deviation from standard Nautobot. Since the config compliance is pivot'd, the actual
+        # pk is based on the device, this crux of the change is to get all actual config changes based on
+        # the incoming device pk's.
         model = self.queryset.model
 
         # Are we deleting *all* objects in the queryset or just a selected subset?
@@ -204,14 +146,15 @@ def post(self, request, **kwargs):
 
         form_cls = self.get_form()
 
-        obj_to_del = [item[0] for item in ConfigCompliance.objects.filter(pk__in=pk_list).values_list("device")]
+        # The difference between nautobot core is the creation and usage of obj_to_del
+        obj_to_del = [item[0] for item in self.queryset.filter(device__pk__in=pk_list).values_list("id")]
         if "_confirm" in request.POST:
             form = form_cls(request.POST)
             if form.is_valid():
                 LOGGER.debug("Form validation was successful")
 
                 # Delete objects
-                queryset = ConfigCompliance.objects.filter(device__in=obj_to_del)
+                queryset = self.queryset.filter(pk__in=pk_list)
                 try:
                     deleted_count = queryset.delete()[1][model._meta.label]
                 except ProtectedError as error:
@@ -227,43 +170,58 @@ def post(self, request, **kwargs):
             LOGGER.debug("Form validation failed")
 
         else:
-            form = form_cls(initial={"pk": pk_list, "return_url": self.get_return_url(request)})
+            form = form_cls(
+                initial={
+                    "pk": obj_to_del,
+                    "return_url": self.get_return_url(request),
+                }
+            )
 
-        table = self.table(ConfigCompliance.objects.filter(device__in=obj_to_del), orderable=False)
+        # Retrieve objects being deleted
+        table = self.table(self.queryset.filter(pk__in=obj_to_del), orderable=False)
         if not table.rows:
-            messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
+            messages.warning(
+                request,
+                "No {} were selected for deletion.".format(model._meta.verbose_name_plural),
+            )
             return redirect(self.get_return_url(request))
 
-        return render(
-            request,
-            self.template_name,
-            {
-                "form": form,
-                "obj_type_plural": model._meta.verbose_name_plural,
-                "table": table,
-                "return_url": self.get_return_url(request),
-            },
-        )
+        context = {
+            "form": form,
+            "obj_type_plural": model._meta.verbose_name_plural,
+            "table": table,
+            "return_url": self.get_return_url(request),
+        }
+        context.update(self.extra_context())
+        return render(request, self.template_name, context)
 
 
-class ComplianceDeviceReport(ContentTypePermissionRequiredMixin, generic.View):
-    """View for the single device detailed information."""
+class ConfigComplianceDeleteView(generic.ObjectDeleteView):
+    """View for deleting compliance rules."""
+
+    queryset = models.ConfigCompliance.objects.all()
+
+
+# ConfigCompliance Non-Standards
+
+
+class ConfigComplianceDeviceView(ContentTypePermissionRequiredMixin, generic.View):
+    """View for individual device detailed information."""
 
     def get_required_permission(self):
         """Manually set permission when not tied to a model for device report."""
         return "nautobot_golden_config.view_configcompliance"
 
-    def get(self, request, device_name):
+    def get(self, request, pk):  # pylint: disable=invalid-name
         """Read request into a view of a single device."""
-        device = Device.objects.get(name=device_name)
-        compliance_details = (
-            ConfigCompliance.objects.filter(device=device).filter(**get_allowed_os_from_nested()).order_by("feature")
-        )
-        config_details = {"compliance_details": compliance_details, "device_name": device_name}
+        device = Device.objects.get(pk=pk)
+        compliance_details = models.ConfigCompliance.objects.filter(device=device)
+
+        config_details = {"compliance_details": compliance_details, "device": device}
 
         return render(
             request,
-            "nautobot_golden_config/device_report.html",
+            "nautobot_golden_config/compliance_device_report.html",
             config_details,
         )
 
@@ -275,34 +233,95 @@ def get_required_permission(self):
         """Manually set permission when not tied to a model for filtered report."""
         return "nautobot_golden_config.view_configcompliance"
 
-    def get(self, request, device_name, compliance):
+    def get(self, request, pk, compliance):  # pylint: disable=invalid-name
         """Read request into a view of a single device."""
-        device = Device.objects.get(name=device_name)
+        device = Device.objects.get(pk=pk)
+        compliance_details = models.ConfigCompliance.objects.filter(device=device)
+
         if compliance == "compliant":
-            compliance_details = (
-                ConfigCompliance.objects.filter(device=device)
-                .filter(**get_allowed_os_from_nested())
-                .order_by("feature")
-            )
             compliance_details = compliance_details.filter(compliance=True)
         else:
-            compliance_details = (
-                ConfigCompliance.objects.filter(device=device)
-                .filter(**get_allowed_os_from_nested())
-                .order_by("feature")
-            )
             compliance_details = compliance_details.filter(compliance=False)
 
-        config_details = {"compliance_details": compliance_details, "device_name": device_name}
-
+        config_details = {"compliance_details": compliance_details, "device": device}
         return render(
             request,
-            "nautobot_golden_config/device_report.html",
+            "nautobot_golden_config/compliance_device_report.html",
             config_details,
         )
 
 
-class GlobalReportHelper(ContentTypePermissionRequiredMixin, generic.View):
+class ConfigComplianceDetails(ContentTypePermissionRequiredMixin, generic.View):
+    """View for the single configuration or diff of a single."""
+
+    def get_required_permission(self):
+        """Manually set permission when not tied to a model for config details."""
+        return "nautobot_golden_config.view_goldenconfig"
+
+    def get(self, request, pk, config_type):  # pylint: disable=invalid-name,too-many-branches
+        """Read request into a view of a single device."""
+        device = Device.objects.get(pk=pk)
+        config_details = models.GoldenConfig.objects.filter(device=device).first()
+        structure_format = "json"
+        if not config_details:
+            output = ""
+        elif config_type == "backup":
+            output = config_details.backup_config
+        elif config_type == "intended":
+            output = config_details.intended_config
+        elif config_type == "compliance":
+            output = config_details.compliance_config
+            if config_details.backup_last_success_date:
+                backup_date = str(config_details.backup_last_success_date.strftime("%b %d %Y"))
+            else:
+                backup_date = datetime.now().strftime("%b %d %Y")
+            if config_details.intended_last_success_date:
+                intended_date = str(config_details.intended_last_success_date.strftime("%b %d %Y"))
+            else:
+                intended_date = datetime.now().strftime("%b %d %Y")
+            first_occurence = output.index("@@")
+            second_occurence = output.index("@@", first_occurence)
+            # This is logic to match diff2html's expected input.
+            output = (
+                "--- Backup File - "
+                + backup_date
+                + "\n+++ Intended File - "
+                + intended_date
+                + "\n"
+                + output[first_occurence:second_occurence]
+                + "@@"
+                + output[second_occurence + 2 :]
+            )
+        elif config_type == "sotagg":
+            if request.GET.get("format") in ["json", "yaml"]:
+                structure_format = request.GET.get("format")
+
+            global_settings = models.GoldenConfigSetting.objects.first()
+            _, output = graph_ql_query(request, device, global_settings.sot_agg_query)
+
+            if structure_format == "yaml":
+                output = yaml.dump(json.loads(json.dumps(output)), default_flow_style=False)
+            else:
+                output = json.dumps(output, indent=4)
+
+        template_name = "nautobot_golden_config/configcompliancedetails.html"
+        if request.GET.get("modal") == "true":
+            template_name = "nautobot_golden_config/configcompliancedetails_modal.html"
+
+        return render(
+            request,
+            template_name,
+            {
+                "output": output,
+                "device_name": device.name,
+                "config_type": config_type,
+                "format": structure_format,
+                "device": device,
+            },
+        )
+
+
+class ConfigComplianceOverviewOverviewHelper(ContentTypePermissionRequiredMixin, generic.View):
     """Customized overview view reports aggregation and filterset."""
 
     def get_required_permission(self):
@@ -343,7 +362,7 @@ def plot_visual(aggr):
     @staticmethod
     def plot_barchart_visual(qs):  # pylint: disable=too-many-locals
         """Construct report visual from queryset."""
-        labels = [item["feature"] for item in qs]
+        labels = [item["rule__feature__slug"] for item in qs]
 
         compliant = [item["compliant"] for item in qs]
         non_compliant = [item["non_compliant"] for item in qs]
@@ -410,21 +429,20 @@ def calculate_aggr_percentage(aggr):
         return aggr
 
 
-class ComplianceOverviewReport(generic.ObjectListView):
+class ConfigComplianceOverview(generic.ObjectListView):
     """View for executive report on configuration compliance."""
 
-    filterset = ConfigComplianceFilter
-    filterset_form = ConfigComplianceFilterForm
-    table = ConfigComplianceGlobalFeatureTable
-    template_name = "nautobot_golden_config/overview_report.html"
+    filterset = filters.ConfigComplianceFilter
+    filterset_form = forms.ConfigComplianceFilterForm
+    table = tables.ConfigComplianceGlobalFeatureTable
+    template_name = "nautobot_golden_config/compliance_overview_report.html"
     kind = "Features"
     queryset = (
-        ConfigCompliance.objects.values("feature")
-        .filter(**get_allowed_os_from_nested())
+        models.ConfigCompliance.objects.values("rule__feature__slug")
         .annotate(
-            count=Count("feature"),
-            compliant=Count("feature", filter=Q(compliance=True)),
-            non_compliant=Count("feature", filter=~Q(compliance=True)),
+            count=Count("rule__feature__slug"),
+            compliant=Count("rule__feature__slug", filter=Q(compliance=True)),
+            non_compliant=Count("rule__feature__slug", filter=~Q(compliance=True)),
             comp_percent=ExpressionWrapper(100 * F("compliant") / F("count"), output_field=FloatField()),
         )
         .order_by("-comp_percent")
@@ -439,11 +457,11 @@ def setup(self, request, *args, **kwargs):
         device_aggr, feature_aggr = self.get_global_aggr(request)
         feature_qs = self.filterset(request.GET, self.queryset).qs
         self.extra_content = {
-            "bar_chart": GlobalReportHelper.plot_barchart_visual(feature_qs),
+            "bar_chart": ConfigComplianceOverviewOverviewHelper.plot_barchart_visual(feature_qs),
             "device_aggr": device_aggr,
-            "device_visual": GlobalReportHelper.plot_visual(device_aggr),
+            "device_visual": ConfigComplianceOverviewOverviewHelper.plot_visual(device_aggr),
             "feature_aggr": feature_aggr,
-            "feature_visual": GlobalReportHelper.plot_visual(feature_aggr),
+            "feature_visual": ConfigComplianceOverviewOverviewHelper.plot_visual(feature_aggr),
         }
 
     def get_global_aggr(self, request):
@@ -453,7 +471,7 @@ def get_global_aggr(self, request):
             device_aggr: device global report dict
             feature_aggr: feature global report dict
         """
-        main_qs = ConfigCompliance.objects.filter(**get_allowed_os_from_nested())
+        main_qs = models.ConfigCompliance.objects
 
         device_aggr, feature_aggr = {}, {}
         if self.filterset is not None:
@@ -464,12 +482,12 @@ def get_global_aggr(self, request):
                 .aggregate(total=Count("device", distinct=True), compliants=Count("compliant", filter=Q(compliant=0)))
             )
             feature_aggr = self.filterset(request.GET, main_qs).qs.aggregate(
-                total=Count("feature"), compliants=Count("feature", filter=Q(compliance=True))
+                total=Count("rule"), compliants=Count("rule", filter=Q(compliance=True))
             )
 
         return (
-            GlobalReportHelper.calculate_aggr_percentage(device_aggr),
-            GlobalReportHelper.calculate_aggr_percentage(feature_aggr),
+            ConfigComplianceOverviewOverviewHelper.calculate_aggr_percentage(device_aggr),
+            ConfigComplianceOverviewOverviewHelper.calculate_aggr_percentage(feature_aggr),
         )
 
     def extra_context(self):
@@ -503,7 +521,7 @@ def queryset_to_csv(self):
         )
         csv_data.append(",".join([]))
 
-        qs = self.queryset.values("feature", "count", "compliant", "non_compliant", "comp_percent")
+        qs = self.queryset.values("rule", "count", "compliant", "non_compliant", "comp_percent")
         csv_data.append(",".join(["Total" if item == "count" else item.capitalize() for item in qs[0].keys()]))
         for obj in qs:
             csv_data.append(
@@ -513,38 +531,289 @@ def queryset_to_csv(self):
         return "\n".join(csv_data)
 
 
-class ComplianceFeatureView(generic.ObjectListView):
-    """View for managing the config compliance feature definition."""
+#
+# ComplianceFeature
+#
 
-    table = ComplianceFeatureTable
-    filterset = ComplianceFeatureFilter
-    filterset_form = ComplianceFeatureFilterForm
-    queryset = ComplianceFeature.objects.all().order_by("platform", "name")
-    template_name = "nautobot_golden_config/compliance_features.html"
+
+class ComplianceFeatureListView(generic.ObjectListView):
+    """View for managing the config compliance rule definition."""
+
+    table = tables.ComplianceFeatureTable
+    filterset = filters.ComplianceFeatureFilter
+    filterset_form = forms.ComplianceFeatureFilterForm
+    queryset = models.ComplianceFeature.objects.all()
+    # TODO: Get import working
+    action_buttons = ("add", "export")
+
+
+class ComplianceFeatureView(generic.ObjectView):
+    """View for single ComplianceFeature instance."""
+
+    queryset = models.ComplianceFeature.objects.all()
+
+    def get_extra_context(self, request, instance):
+        """Add extra data to detail view for Nautobot."""
+        return {}
+
+
+class ComplianceFeatureBulkImportView(generic.BulkImportView):
+    """View for bulk import of ComplianceFeature."""
+
+    queryset = models.ComplianceFeature.objects.all()
+    model_form = forms.ComplianceFeatureCSVForm
+    table = tables.ComplianceFeatureTable
 
 
 class ComplianceFeatureEditView(generic.ObjectEditView):
-    """View for managing compliance features."""
+    """View for managing compliance rules."""
 
-    queryset = ComplianceFeature.objects.all()
-    model_form = ComplianceFeatureForm
+    queryset = models.ComplianceFeature.objects.all()
+    model_form = forms.ComplianceFeatureForm
 
 
 class ComplianceFeatureDeleteView(generic.ObjectDeleteView):
-    """View for deleting compliance features."""
+    """View for deleting compliance rules."""
 
-    queryset = ComplianceFeature.objects.all()
+    queryset = models.ComplianceFeature.objects.all()
 
 
 class ComplianceFeatureBulkDeleteView(generic.BulkDeleteView):
-    """View for bulk deleting compliance features."""
+    """View for bulk deleting compliance rules."""
+
+    queryset = models.ComplianceFeature.objects.all()
+    table = tables.ComplianceFeatureTable
+
+
+class ComplianceFeatureBulkEditView(generic.BulkEditView):
+    """View for bulk deleting ComplianceFeature instance."""
+
+    queryset = models.ComplianceFeature.objects.all()
+    filterset = filters.ComplianceFeatureFilter
+    table = tables.ComplianceFeatureTable
+    form = forms.ComplianceFeatureBulkEditForm
+
+
+#
+# ComplianceRule
+#
+
+
+class ComplianceRuleListView(generic.ObjectListView):
+    """View for managing the config compliance rule definition."""
+
+    table = tables.ComplianceRuleTable
+    filterset = filters.ComplianceRuleFilter
+    filterset_form = forms.ComplianceRuleFilterForm
+    queryset = models.ComplianceRule.objects.all()
+    # TODO: Get import working
+    action_buttons = ("add", "export")
+
+
+class ComplianceRuleView(generic.ObjectView):
+    """View for single ComplianceRule instance."""
+
+    queryset = models.ComplianceRule.objects.all()
+
+    def get_extra_context(self, request, instance):
+        """Add extra data to detail view for Nautobot."""
+        return {}
+
+
+class ComplianceRuleBulkImportView(generic.BulkImportView):
+    """View for bulk import of ComplianceRule."""
+
+    queryset = models.ComplianceRule.objects.all()
+    model_form = forms.ComplianceRuleCSVForm
+    table = tables.ComplianceRuleTable
+
+
+class ComplianceRuleEditView(generic.ObjectEditView):
+    """View for managing compliance rules."""
+
+    queryset = models.ComplianceRule.objects.all()
+    model_form = forms.ComplianceRuleForm
+
+
+class ComplianceRuleDeleteView(generic.ObjectDeleteView):
+    """View for deleting compliance rules."""
+
+    queryset = models.ComplianceRule.objects.all()
+
+
+class ComplianceRuleBulkDeleteView(generic.BulkDeleteView):
+    """View for bulk deleting compliance rules."""
+
+    queryset = models.ComplianceRule.objects.all()
+    table = tables.ComplianceRuleTable
+
+
+class ComplianceRuleBulkEditView(generic.BulkEditView):
+    """View for bulk deleting ComplianceRule instance."""
+
+    queryset = models.ComplianceRule.objects.all()
+    filterset = filters.ComplianceRuleFilter
+    table = tables.ComplianceRuleTable
+    form = forms.ComplianceRuleBulkEditForm
+
+
+#
+# GoldenConfigSetting
+#
+
+
+class GoldenConfigSettingView(generic.ObjectView):
+    """View for the only GoldenConfigSetting instance."""
+
+    queryset = models.GoldenConfigSetting.objects.all()
 
-    queryset = ComplianceFeature.objects.all()
-    table = ComplianceFeatureTable
+    def get(self, request, *args, **kwargs):
+        """Override the get parameter to get the first instance to enforce singleton pattern."""
+        instance = self.queryset.first()
+
+        return render(
+            request,
+            self.get_template_name(),
+            {
+                "object": instance,
+                **self.get_extra_context(request, instance),
+            },
+        )
+
+    def get_extra_context(self, request, instance):
+        """Add extra data to detail view for Nautobot."""
+        return {}
 
 
-class GoldenConfigSettingsEditView(generic.ObjectEditView):
+class GoldenConfigSettingEditView(generic.ObjectEditView):
     """View for editing the Global configurations."""
 
-    queryset = GoldenConfigSettings.objects.filter(id="aaaaaaaa-0000-0000-0000-000000000001")
-    model_form = GoldenConfigSettingsFeatureForm
+    queryset = models.GoldenConfigSetting.objects.all()
+    model_form = forms.GoldenConfigSettingFeatureForm
+    default_return_url = "plugins:nautobot_golden_config:goldenconfigsetting"
+
+    def get_object(self, kwargs):
+        """Override method to get first object to enforce the singleton pattern."""
+        return self.queryset.first()
+
+
+#
+# ConfigRemove
+#
+
+
+class ConfigRemoveListView(generic.ObjectListView):
+    """View to display the current Line Removals."""
+
+    queryset = models.ConfigRemove.objects.all()
+    table = tables.ConfigRemoveTable
+    filterset = filters.ConfigRemoveFilter
+    filterset_form = forms.ConfigRemoveFeatureFilterForm
+
+
+class ConfigRemoveView(generic.ObjectView):
+    """View for single ConfigRemove instance."""
+
+    queryset = models.ConfigRemove.objects.all()
+
+    def get_extra_context(self, request, instance):
+        """Add extra data to detail view for Nautobot."""
+        return {}
+
+
+class ConfigRemoveBulkImportView(generic.BulkImportView):
+    """View for bulk import of ConfigRemove."""
+
+    queryset = models.ConfigRemove.objects.all()
+    model_form = forms.ConfigRemoveCSVForm
+    table = tables.ConfigRemoveTable
+
+
+class ConfigRemoveBulkEditView(generic.BulkEditView):
+    """View for bulk deleting ConfigRemove instances."""
+
+    queryset = models.ConfigRemove.objects.all()
+    filterset = filters.ConfigRemoveFilter
+    table = tables.ConfigRemoveTable
+    form = forms.ConfigRemoveBulkEditForm
+
+
+class ConfigRemoveEditView(generic.ObjectEditView):
+    """View for editing the current Line Removals."""
+
+    queryset = models.ConfigRemove.objects.all()
+    model_form = forms.ConfigRemoveForm
+
+
+class ConfigRemoveDeleteView(generic.ObjectDeleteView):
+    """View for deleting a ConfigRemove instance."""
+
+    queryset = models.ConfigRemove.objects.all()
+
+
+class ConfigRemoveBulkDeleteView(generic.BulkDeleteView):
+    """View for bulk deleting Line Removals."""
+
+    queryset = models.ConfigRemove.objects.all()
+    table = tables.ConfigRemoveTable
+
+
+#
+# ConfigReplace
+#
+
+
+class ConfigReplaceListView(generic.ObjectListView):
+    """View for displaying the current Line Replacements."""
+
+    queryset = models.ConfigReplace.objects.all()
+    table = tables.ConfigReplaceTable
+    filterset = filters.ConfigReplaceFilter
+    filterset_form = forms.ConfigReplaceFeatureFilterForm
+
+
+class ConfigReplaceView(generic.ObjectView):
+    """View for single ConfigReplace instance."""
+
+    queryset = models.ConfigReplace.objects.all()
+
+    def get_extra_context(self, request, instance):
+        """Add extra data to detail view for Nautobot."""
+        return {}
+
+
+class ConfigReplaceEditView(generic.ObjectEditView):
+    """View for editing the current Line Replacements."""
+
+    queryset = models.ConfigReplace.objects.all()
+    model_form = forms.ConfigReplaceForm
+
+
+class ConfigReplaceBulkDeleteView(generic.BulkDeleteView):
+    """View for bulk deleting Line Replacements."""
+
+    queryset = models.ConfigReplace.objects.all()
+    table = tables.ConfigReplaceTable
+
+
+class ConfigReplaceBulkImportView(generic.BulkImportView):
+    """View for bulk import of ConfigReplace."""
+
+    queryset = models.ConfigReplace.objects.all()
+    model_form = forms.ConfigReplaceCSVForm
+    table = tables.ConfigReplaceTable
+
+
+class ConfigReplaceDeleteView(generic.ObjectDeleteView):
+    """View for deleting a ConfigReplace instance."""
+
+    queryset = models.ConfigReplace.objects.all()
+
+
+class ConfigReplaceBulkEditView(generic.BulkEditView):
+    """View for bulk deleting ConfigReplace instances."""
+
+    queryset = models.ConfigReplace.objects.all()
+    filterset = filters.ConfigReplaceFilter
+    table = tables.ConfigReplaceTable
+    form = forms.ConfigReplaceBulkEditForm
diff --git a/poetry.lock b/poetry.lock
index 51075736..57288db5 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -16,28 +16,30 @@ python-versions = "*"
 
 [[package]]
 name = "asgiref"
-version = "3.3.1"
+version = "3.3.4"
 description = "ASGI specs, helper code, and adapters"
 category = "main"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
+
+[package.dependencies]
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
 
 [package.extras]
-tests = ["pytest", "pytest-asyncio"]
+tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
 
 [[package]]
 name = "astroid"
-version = "2.4.2"
+version = "2.5.6"
 description = "An abstract syntax tree for Python with inference support."
-category = "dev"
+category = "main"
 optional = false
-python-versions = ">=3.5"
+python-versions = "~=3.6"
 
 [package.dependencies]
-lazy-object-proxy = ">=1.4.0,<1.5.0"
-six = ">=1.12,<2.0"
+lazy-object-proxy = ">=1.4.0"
 typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""}
-wrapt = ">=1.11,<2.0"
+wrapt = ">=1.11,<1.13"
 
 [[package]]
 name = "attrs"
@@ -136,7 +138,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "ciscoconfparse"
-version = "1.5.29"
+version = "1.5.30"
 description = "Parse, Audit, Query, Build, and Modify Cisco IOS-style configurations"
 category = "main"
 optional = false
@@ -190,7 +192,7 @@ jinja2 = "*"
 
 [[package]]
 name = "cryptography"
-version = "3.4.6"
+version = "3.4.7"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 category = "main"
 optional = false
@@ -226,9 +228,17 @@ category = "main"
 optional = false
 python-versions = ">=3.6, <3.7"
 
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+description = "XML bomb protection for Python stdlib modules"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
 [[package]]
 name = "django"
-version = "3.1.7"
+version = "3.1.8"
 description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
 category = "main"
 optional = false
@@ -293,7 +303,7 @@ django-appconf = "*"
 
 [[package]]
 name = "django-debug-toolbar"
-version = "3.2"
+version = "3.2.1"
 description = "A configurable set of panels that display various debug information about the current request/response."
 category = "main"
 optional = false
@@ -335,15 +345,16 @@ Django = ">=1.11"
 django-js-asset = "*"
 
 [[package]]
-name = "django-pglocks"
-version = "1.0.4"
-description = "django_pglocks provides useful context managers for advisory locks for PostgreSQL."
+name = "django-pivot"
+version = "1.8.1"
+description = "Create pivot tables and histograms from ORM querysets"
 category = "main"
 optional = false
 python-versions = "*"
 
 [package.dependencies]
-six = ">=1.0.0"
+django = ">=1.10"
+six = "*"
 
 [[package]]
 name = "django-prometheus"
@@ -356,9 +367,21 @@ python-versions = "*"
 [package.dependencies]
 prometheus-client = ">=0.7"
 
+[[package]]
+name = "django-redis"
+version = "4.12.1"
+description = "Full featured redis cache backend for Django."
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+Django = ">=2.2"
+redis = ">=3.0.0"
+
 [[package]]
 name = "django-rq"
-version = "2.4.0"
+version = "2.4.1"
 description = "An app that provides django integration for RQ (Redis Queue)"
 category = "main"
 optional = false
@@ -370,7 +393,7 @@ redis = ">=3"
 rq = ">=1.2"
 
 [package.extras]
-sentry = ["raven (>=6.1.0)"]
+Sentry = ["raven (>=6.1.0)"]
 testing = ["mock (>=2.0.0)"]
 
 [[package]]
@@ -400,7 +423,7 @@ Django = ">=1.11"
 
 [[package]]
 name = "django-timezone-field"
-version = "4.1.1"
+version = "4.1.2"
 description = "A Django app providing database and form fields for pytz timezone objects."
 category = "main"
 optional = false
@@ -413,9 +436,27 @@ pytz = "*"
 [package.extras]
 rest_framework = ["djangorestframework (>=3.0.0)"]
 
+[[package]]
+name = "django-webserver"
+version = "1.2.0"
+description = "Django management commands for production webservers"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+Django = "*"
+
+[package.extras]
+gunicorn = ["gunicorn"]
+pyuwsgi = ["pyuwsgi"]
+test = ["pytest", "mock"]
+uvicorn = ["uvicorn (>0.6)"]
+waitress = ["waitress"]
+
 [[package]]
 name = "djangorestframework"
-version = "3.12.2"
+version = "3.12.4"
 description = "Web APIs for Django, made easy."
 category = "main"
 optional = false
@@ -463,17 +504,17 @@ validation = ["swagger-spec-validator (>=2.1.0)"]
 
 [[package]]
 name = "flake8"
-version = "3.8.4"
+version = "3.9.1"
 description = "the modular source code checker: pep8 pyflakes and co"
 category = "dev"
 optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
 
 [package.dependencies]
 importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 mccabe = ">=0.6.0,<0.7.0"
-pycodestyle = ">=2.6.0a1,<2.7.0"
-pyflakes = ">=2.2.0,<2.3.0"
+pycodestyle = ">=2.7.0,<2.8.0"
+pyflakes = ">=2.3.0,<2.4.0"
 
 [[package]]
 name = "funcy"
@@ -493,25 +534,26 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "gitdb"
-version = "4.0.5"
+version = "4.0.7"
 description = "Git Object Database"
 category = "main"
 optional = false
 python-versions = ">=3.4"
 
 [package.dependencies]
-smmap = ">=3.0.1,<4"
+smmap = ">=3.0.1,<5"
 
 [[package]]
 name = "gitpython"
-version = "3.1.13"
+version = "3.1.15"
 description = "Python Git Library"
 category = "main"
 optional = false
-python-versions = ">=3.4"
+python-versions = ">=3.5"
 
 [package.dependencies]
 gitdb = ">=4.0.1,<5"
+typing-extensions = ">=3.7.4.0"
 
 [[package]]
 name = "graphene"
@@ -584,20 +626,6 @@ graphql-core = ">=2.2,<3"
 promise = ">=2.2,<3"
 six = ">=1.12"
 
-[[package]]
-name = "gunicorn"
-version = "20.0.4"
-description = "WSGI HTTP Server for UNIX"
-category = "main"
-optional = false
-python-versions = ">=3.4"
-
-[package.extras]
-eventlet = ["eventlet (>=0.9.7)"]
-gevent = ["gevent (>=0.13)"]
-setproctitle = ["setproctitle"]
-tornado = ["tornado (>=0.2)"]
-
 [[package]]
 name = "idna"
 version = "2.10"
@@ -624,7 +652,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake
 
 [[package]]
 name = "importlib-resources"
-version = "5.1.0"
+version = "5.1.2"
 description = "Read resources from Python packages"
 category = "main"
 optional = false
@@ -635,7 +663,7 @@ zipp = {version = ">=0.4", markers = "python_version < \"3.8\""}
 
 [package.extras]
 docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-black (>=0.3.7)", "pytest-mypy"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-black (>=0.3.7)", "pytest-mypy"]
 
 [[package]]
 name = "inflection"
@@ -655,7 +683,7 @@ python-versions = "*"
 
 [[package]]
 name = "isort"
-version = "5.7.0"
+version = "5.8.0"
 description = "A Python utility / library to sort Python imports."
 category = "dev"
 optional = false
@@ -708,16 +736,16 @@ format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator
 
 [[package]]
 name = "junos-eznc"
-version = "2.5.4"
+version = "2.6.0"
 description = "Junos 'EZ' automation for non-programmers"
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [package.dependencies]
 jinja2 = ">=2.7.1"
 lxml = ">=3.2.4"
-ncclient = ">=0.6.3"
+ncclient = "0.6.9"
 netaddr = "*"
 paramiko = ">=1.15.2"
 pyparsing = "*"
@@ -738,15 +766,15 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "lazy-object-proxy"
-version = "1.4.3"
+version = "1.6.0"
 description = "A fast and thorough lazy object proxy."
-category = "dev"
+category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 
 [[package]]
 name = "lxml"
-version = "4.6.2"
+version = "4.6.3"
 description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
 category = "main"
 optional = false
@@ -760,7 +788,7 @@ source = ["Cython (>=0.29.7)"]
 
 [[package]]
 name = "markdown"
-version = "3.3.3"
+version = "3.3.4"
 description = "Python implementation of Markdown."
 category = "main"
 optional = false
@@ -838,52 +866,53 @@ textfsm = "*"
 
 [[package]]
 name = "nautobot"
-version = "1.0.0a1"
+version = "1.0.0"
 description = "Source of truth and network automation platform."
 category = "main"
 optional = false
 python-versions = ">=3.6,<4.0"
 
 [package.dependencies]
-Django = ">=3.1.7,<3.2.0"
+Django = ">=3.1.8,<3.2.0"
 django-cacheops = ">=5.1,<5.2"
 django-cors-headers = ">=3.7.0,<3.8.0"
 django-cryptography = ">=1.0,<1.1"
-django-debug-toolbar = ">=3.2,<3.3"
 django-filter = ">=2.4.0,<2.5.0"
 django-mptt = ">=0.11.0,<0.12.0"
-django-pglocks = ">=1.0.4,<1.1.0"
 django-prometheus = ">=2.1.0,<2.2.0"
-django-rq = ">=2.4.0,<3.0.0"
+django-redis = ">=4.12.1,<4.13.0"
+django-rq = ">=2.4.1,<2.5.0"
 django-tables2 = ">=2.3.4,<2.4.0"
 django-taggit = ">=1.3.0,<1.4.0"
-django-timezone-field = ">=4.1.1,<4.2.0"
-djangorestframework = ">=3.12.2,<3.13.0"
+django-timezone-field = ">=4.1.2,<4.2.0"
+django-webserver = ">=1.2.0,<1.3.0"
+djangorestframework = ">=3.12.4,<3.13.0"
 drf-yasg = {version = ">=1.20.0,<1.21.0", extras = ["validation"]}
-GitPython = ">=3.1.13,<3.2.0"
+GitPython = ">=3.1.15,<3.2.0"
 graphene-django = ">=2.15.0,<2.16.0"
-gunicorn = ">=20.0.4,<20.1.0"
 importlib-metadata = {version = ">=3.4.0,<3.5.0", markers = "python_version < \"3.8\""}
 Jinja2 = ">=2.11.3,<2.12.0"
-Markdown = ">=3.3.3,<3.4.0"
+Markdown = ">=3.3.4,<3.4.0"
 netaddr = ">=0.8.0,<0.9.0"
 Pillow = ">=8.1.0,<8.2.0"
 psycopg2-binary = ">=2.8.6,<2.9.0"
 pycryptodome = ">=3.10.1,<3.11.0"
+pyuwsgi = ">=2.0.19.1.post0,<2.1.0.0"
 PyYAML = ">=5.4.1,<5.5.0"
+social-auth-app-django = ">=4.0.0,<5.0.0"
 svgwrite = ">=1.4.1,<1.5.0"
 
 [[package]]
 name = "nautobot-plugin-nornir"
-version = "0.9.0"
+version = "0.9.1"
 description = "Nautobot Nornir plugin."
 category = "main"
 optional = false
 python-versions = ">=3.6,<4.0"
 
 [package.dependencies]
-nautobot = ">=1.0.0-alpha.1,<2.0.0"
-nornir-nautobot = ">=1.0.0,<2.0.0"
+nautobot = ">=1.0.0,<2.0.0"
+nornir-nautobot = ">=2.0.0,<3.0.0"
 
 [[package]]
 name = "ncclient"
@@ -911,7 +940,7 @@ importlib-resources = {version = "*", markers = "python_version < \"3.7\""}
 
 [[package]]
 name = "netmiko"
-version = "3.3.3"
+version = "3.4.0"
 description = "Multi-vendor library to simplify Paramiko SSH connections to network devices"
 category = "main"
 optional = false
@@ -928,9 +957,17 @@ tenacity = "*"
 [package.extras]
 test = ["pyyaml (>=5.1.2)", "pytest (>=5.1.2)"]
 
+[[package]]
+name = "netutils"
+version = "0.1.2"
+description = "Common helper functions useful in network automation."
+category = "main"
+optional = false
+python-versions = ">=3.6,<4.0"
+
 [[package]]
 name = "nornir"
-version = "3.0.0"
+version = "3.1.0"
 description = "Pluggable multi-threaded framework with inventory management to help operate collections of devices"
 category = "main"
 optional = false
@@ -947,7 +984,7 @@ docs = ["jupyter (>=1,<2)", "nbsphinx (>=0.5,<0.6)", "pygments (>=2,<3)", "sphin
 
 [[package]]
 name = "nornir-jinja2"
-version = "0.1.1"
+version = "0.1.2"
 description = "Jinja2 plugins for nornir"
 category = "main"
 optional = false
@@ -955,11 +992,11 @@ python-versions = ">=3.6,<4.0"
 
 [package.dependencies]
 jinja2 = ">=2.11.2,<3.0.0"
-nornir = ">=3.0.0b1,<3.1.0"
+nornir = ">=3,<4"
 
 [[package]]
 name = "nornir-napalm"
-version = "0.1.1"
+version = "0.1.2"
 description = "NAPALM's plugins for nornir"
 category = "main"
 optional = false
@@ -967,17 +1004,18 @@ python-versions = ">=3.6,<4.0"
 
 [package.dependencies]
 napalm = ">=3,<4"
-nornir = ">=3.0.0b1,<3.1.0"
+nornir = ">=3,<4"
 
 [[package]]
 name = "nornir-nautobot"
-version = "1.0.0"
+version = "2.0.1"
 description = "Nornir Nautobot"
 category = "main"
 optional = false
 python-versions = ">=3.6,<4.0"
 
 [package.dependencies]
+netutils = ">=0.1.2,<0.2.0"
 nornir = ">=3.0.0,<4.0.0"
 nornir-jinja2 = ">=0.1.1,<0.2.0"
 nornir-napalm = ">=0.1.1,<0.2.0"
@@ -999,7 +1037,7 @@ netmiko = ">=3.1.0,<4.0.0"
 
 [[package]]
 name = "nornir-utils"
-version = "0.1.1"
+version = "0.1.2"
 description = "Collection of plugins and functions for nornir that don't require external dependencies"
 category = "main"
 optional = false
@@ -1007,21 +1045,18 @@ python-versions = ">=3.6,<4.0"
 
 [package.dependencies]
 colorama = ">=0.4.3,<0.5.0"
-nornir = ">=3.0.0b1,<3.1.0"
+nornir = ">=3,<4"
 
 [[package]]
 name = "ntc-templates"
-version = "1.6.0"
-description = "Package to return structured data from the output of network devices."
+version = "2.0.0"
+description = "TextFSM Templates for Network Devices, and Python wrapper for TextFSM's CliTable."
 category = "main"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6,<4.0"
 
 [package.dependencies]
-textfsm = ">=1.1.0"
-
-[package.extras]
-dev = ["pytest", "pyyaml", "black", "yamllint", "ruamel.yaml"]
+textfsm = ">=1.1.0,<2.0.0"
 
 [[package]]
 name = "numpy"
@@ -1031,6 +1066,19 @@ category = "main"
 optional = false
 python-versions = ">=3.6"
 
+[[package]]
+name = "oauthlib"
+version = "3.1.0"
+description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.extras]
+rsa = ["cryptography"]
+signals = ["blinker"]
+signedtoken = ["cryptography", "pyjwt (>=1.0.0)"]
+
 [[package]]
 name = "packaging"
 version = "20.9"
@@ -1093,7 +1141,7 @@ python-versions = ">=2.6"
 
 [[package]]
 name = "pillow"
-version = "8.1.0"
+version = "8.1.2"
 description = "Python Imaging Library (Fork)"
 category = "main"
 optional = false
@@ -1101,11 +1149,11 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "prometheus-client"
-version = "0.9.0"
+version = "0.10.1"
 description = "Python client for the Prometheus monitoring system."
 category = "main"
 optional = false
-python-versions = "*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [package.extras]
 twisted = ["twisted"]
@@ -1134,7 +1182,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
 
 [[package]]
 name = "pycodestyle"
-version = "2.6.0"
+version = "2.7.0"
 description = "Python style guide checker"
 category = "dev"
 optional = false
@@ -1184,30 +1232,47 @@ test = ["coverage", "mock"]
 
 [[package]]
 name = "pyflakes"
-version = "2.2.0"
+version = "2.3.1"
 description = "passive checker of Python programs"
 category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
+[[package]]
+name = "pyjwt"
+version = "2.0.1"
+description = "JSON Web Token implementation in Python"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+crypto = ["cryptography (>=3.3.1,<4.0.0)"]
+dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4.0.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"]
+
 [[package]]
 name = "pylint"
-version = "2.6.0"
+version = "2.7.4"
 description = "python code static checker"
 category = "dev"
 optional = false
-python-versions = ">=3.5.*"
+python-versions = "~=3.6"
 
 [package.dependencies]
-astroid = ">=2.4.0,<=2.5"
+astroid = ">=2.5.2,<2.7"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 isort = ">=4.2.5,<6"
 mccabe = ">=0.6,<0.7"
 toml = ">=0.7.1"
 
+[package.extras]
+docs = ["sphinx (==3.5.1)", "python-docs-theme (==2020.12)"]
+
 [[package]]
 name = "pylint-django"
-version = "2.4.2"
+version = "2.4.3"
 description = "A Pylint plugin to help Pylint understand the Django web framework"
 category = "dev"
 optional = false
@@ -1218,7 +1283,7 @@ pylint = ">=2.0"
 pylint-plugin-utils = ">=0.5"
 
 [package.extras]
-for_tests = ["Faker (<=5.0.1)", "coverage", "django-tables2", "factory-boy", "pytest"]
+for_tests = ["django-tables2", "factory-boy", "coverage", "pytest"]
 with_django = ["django"]
 
 [[package]]
@@ -1297,6 +1362,21 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 [package.dependencies]
 six = ">=1.5"
 
+[[package]]
+name = "python3-openid"
+version = "3.2.0"
+description = "OpenID support for modern servers and consumers."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+defusedxml = "*"
+
+[package.extras]
+mysql = ["mysql-connector-python"]
+postgresql = ["psycopg2"]
+
 [[package]]
 name = "pytz"
 version = "2021.1"
@@ -1305,6 +1385,14 @@ category = "main"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "pyuwsgi"
+version = "2.0.19.1.post0"
+description = "The uWSGI server"
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "pyyaml"
 version = "5.4.1"
@@ -1326,7 +1414,7 @@ hiredis = ["hiredis (>=0.1.3)"]
 
 [[package]]
 name = "regex"
-version = "2020.11.13"
+version = "2021.4.4"
 description = "Alternative regular expression module, to replace re."
 category = "dev"
 optional = false
@@ -1350,9 +1438,24 @@ urllib3 = ">=1.21.1,<1.27"
 security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
 socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
 
+[[package]]
+name = "requests-oauthlib"
+version = "1.3.0"
+description = "OAuthlib authentication support for Requests."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+oauthlib = ">=3.0.0"
+requests = ">=2.0.0"
+
+[package.extras]
+rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
+
 [[package]]
 name = "rq"
-version = "1.7.0"
+version = "1.8.0"
 description = "RQ is a simple, lightweight, library for creating background jobs, and processing them."
 category = "main"
 optional = false
@@ -1364,14 +1467,14 @@ redis = ">=3.5.0"
 
 [[package]]
 name = "ruamel.yaml"
-version = "0.16.12"
+version = "0.16.13"
 description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
 category = "main"
 optional = false
 python-versions = "*"
 
 [package.dependencies]
-"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.9\""}
+"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""}
 
 [package.extras]
 docs = ["ryd"]
@@ -1406,7 +1509,7 @@ paramiko = "*"
 
 [[package]]
 name = "singledispatch"
-version = "3.6.0"
+version = "3.6.1"
 description = "Backport functools.singledispatch from Python 3.4 to Python 2.6-3.3."
 category = "main"
 optional = false
@@ -1417,7 +1520,7 @@ six = "*"
 
 [package.extras]
 docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "unittest2"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "unittest2"]
 
 [[package]]
 name = "six"
@@ -1429,11 +1532,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "smmap"
-version = "3.0.5"
+version = "4.0.0"
 description = "A pure Python implementation of a sliding window memory map manager"
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.5"
 
 [[package]]
 name = "snowballstemmer"
@@ -1443,6 +1546,42 @@ category = "dev"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "social-auth-app-django"
+version = "4.0.0"
+description = "Python Social Authentication, Django integration."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+six = "*"
+social-auth-core = ">=3.3.0"
+
+[[package]]
+name = "social-auth-core"
+version = "4.1.0"
+description = "Python social authentication made simple."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+cryptography = ">=1.4"
+defusedxml = ">=0.5.0rc1"
+oauthlib = ">=1.0.3"
+PyJWT = ">=2.0.0"
+python3-openid = ">=3.0.10"
+requests = ">=2.9.1"
+requests-oauthlib = ">=0.6.1"
+
+[package.extras]
+all = ["python-jose (>=3.0.0)", "python3-saml (>=1.2.1)", "cryptography (>=2.1.1)"]
+allpy3 = ["python-jose (>=3.0.0)", "python3-saml (>=1.2.1)", "cryptography (>=2.1.1)"]
+azuread = ["cryptography (>=2.1.1)"]
+openidconnect = ["python-jose (>=3.0.0)"]
+saml = ["python3-saml (>=1.2.1)"]
+
 [[package]]
 name = "sqlparse"
 version = "0.4.1"
@@ -1486,7 +1625,7 @@ six = "*"
 
 [[package]]
 name = "tenacity"
-version = "6.3.1"
+version = "7.0.0"
 description = "Retry code until it succeeds"
 category = "main"
 optional = false
@@ -1528,7 +1667,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "transitions"
-version = "0.8.7"
+version = "0.8.8"
 description = "A lightweight, object-oriented Python state machine implementation with many extensions."
 category = "main"
 optional = false
@@ -1543,9 +1682,9 @@ test = ["pytest"]
 
 [[package]]
 name = "typed-ast"
-version = "1.4.2"
+version = "1.4.3"
 description = "a fork of Python 2 and 3 ast modules with type comment support"
-category = "dev"
+category = "main"
 optional = false
 python-versions = "*"
 
@@ -1567,22 +1706,22 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "urllib3"
-version = "1.26.3"
+version = "1.26.4"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
 
 [package.extras]
-brotli = ["brotlipy (>=0.6.0)"]
 secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
 socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+brotli = ["brotlipy (>=0.6.0)"]
 
 [[package]]
 name = "wrapt"
 version = "1.12.1"
 description = "Module for decorators, wrappers and monkey patching."
-category = "dev"
+category = "main"
 optional = false
 python-versions = "*"
 
@@ -1599,20 +1738,20 @@ pyyaml = "*"
 
 [[package]]
 name = "zipp"
-version = "3.4.0"
+version = "3.4.1"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "main"
 optional = false
 python-versions = ">=3.6"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
 
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6"
-content-hash = "0de421ce10bfa8116d0b8c36b9dc6211b5d16fc05c5a64dbd81b1850f4cb051e"
+content-hash = "2929dfe99a74d47e53d6c94fbadcfbfdf98473dd3d9e2bae64390152c17d6e8c"
 
 [metadata.files]
 aniso8601 = [
@@ -1624,12 +1763,12 @@ appdirs = [
     {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
 ]
 asgiref = [
-    {file = "asgiref-3.3.1-py3-none-any.whl", hash = "sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17"},
-    {file = "asgiref-3.3.1.tar.gz", hash = "sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"},
+    {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"},
+    {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"},
 ]
 astroid = [
-    {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"},
-    {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"},
+    {file = "astroid-2.5.6-py3-none-any.whl", hash = "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e"},
+    {file = "astroid-2.5.6.tar.gz", hash = "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"},
 ]
 attrs = [
     {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
@@ -1672,24 +1811,36 @@ cffi = [
     {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"},
     {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"},
     {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55"},
     {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"},
     {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"},
     {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"},
     {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"},
     {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"},
     {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc"},
     {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"},
     {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"},
     {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"},
     {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"},
     {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"},
     {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76"},
     {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"},
     {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"},
     {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"},
     {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"},
     {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"},
     {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7"},
     {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"},
     {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"},
     {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"},
@@ -1699,8 +1850,8 @@ chardet = [
     {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
 ]
 ciscoconfparse = [
-    {file = "ciscoconfparse-1.5.29-py3-none-any.whl", hash = "sha256:a31a5583539bb3f260c4602f98dae1383f4010e5e74b930332c59e1ec31df63b"},
-    {file = "ciscoconfparse-1.5.29.tar.gz", hash = "sha256:8fb27bfcc7b97c974f68d3b73b01e349f1377171ec0d33b1e996d0a392224a7e"},
+    {file = "ciscoconfparse-1.5.30-py3-none-any.whl", hash = "sha256:1b7b98c0b1b1cb05fadc9c27711d79d4faee9cf4770a72b5798d7167bb97ccc2"},
+    {file = "ciscoconfparse-1.5.30.tar.gz", hash = "sha256:e3c9ad5ff7563c6d3b4542d2826f4b07feb0e46ea5c698886ab8c4d55cc28815"},
 ]
 click = [
     {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
@@ -1719,18 +1870,18 @@ coreschema = [
     {file = "coreschema-0.0.4.tar.gz", hash = "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607"},
 ]
 cryptography = [
-    {file = "cryptography-3.4.6-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799"},
-    {file = "cryptography-3.4.6-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7"},
-    {file = "cryptography-3.4.6-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3"},
-    {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b"},
-    {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964"},
-    {file = "cryptography-3.4.6-cp36-abi3-win32.whl", hash = "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2"},
-    {file = "cryptography-3.4.6-cp36-abi3-win_amd64.whl", hash = "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0"},
-    {file = "cryptography-3.4.6-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b"},
-    {file = "cryptography-3.4.6-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df"},
-    {file = "cryptography-3.4.6-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336"},
-    {file = "cryptography-3.4.6-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724"},
-    {file = "cryptography-3.4.6.tar.gz", hash = "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87"},
+    {file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"},
+    {file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"},
+    {file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"},
+    {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"},
+    {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"},
+    {file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"},
+    {file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"},
+    {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"},
+    {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"},
+    {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"},
+    {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"},
+    {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"},
 ]
 cycler = [
     {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"},
@@ -1740,9 +1891,13 @@ dataclasses = [
     {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"},
     {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"},
 ]
+defusedxml = [
+    {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
+    {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
+]
 django = [
-    {file = "Django-3.1.7-py3-none-any.whl", hash = "sha256:baf099db36ad31f970775d0be5587cc58a6256a6771a44eb795b554d45f211b8"},
-    {file = "Django-3.1.7.tar.gz", hash = "sha256:32ce792ee9b6a0cbbec340123e229ac9f765dff8c2a4ae9247a14b2ba3a365a7"},
+    {file = "Django-3.1.8-py3-none-any.whl", hash = "sha256:c348b3ddc452bf4b62361f0752f71a339140c777ebea3cdaaaa8fdb7f417a862"},
+    {file = "Django-3.1.8.tar.gz", hash = "sha256:f8393103e15ec2d2d313ccbb95a3f1da092f9f58d74ac1c61ca2ac0436ae1eac"},
 ]
 django-appconf = [
     {file = "django-appconf-1.0.4.tar.gz", hash = "sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"},
@@ -1761,8 +1916,8 @@ django-cryptography = [
     {file = "django_cryptography-1.0-py3-none-any.whl", hash = "sha256:0a99980b1cee7cc5e52f9b20b322620fea7cc124d770273e7bd285b20fd9d222"},
 ]
 django-debug-toolbar = [
-    {file = "django-debug-toolbar-3.2.tar.gz", hash = "sha256:84e2607d900dbd571df0a2acf380b47c088efb787dce9805aefeb407341961d2"},
-    {file = "django_debug_toolbar-3.2-py3-none-any.whl", hash = "sha256:9e5a25d0c965f7e686f6a8ba23613ca9ca30184daa26487706d4829f5cfb697a"},
+    {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"},
+    {file = "django_debug_toolbar-3.2.1-py3-none-any.whl", hash = "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"},
 ]
 django-filter = [
     {file = "django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06"},
@@ -1776,16 +1931,21 @@ django-mptt = [
     {file = "django-mptt-0.11.0.tar.gz", hash = "sha256:dfdb3af75ad27cdd4458b0544ec8574174f2b90f99bc2cafab6a15b4bc1895a8"},
     {file = "django_mptt-0.11.0-py2.py3-none-any.whl", hash = "sha256:90eb236eb4f1a92124bd7c37852bbe09c0d21158477cc237556d59842a91c509"},
 ]
-django-pglocks = [
-    {file = "django-pglocks-1.0.4.tar.gz", hash = "sha256:3c47c66fbfbd268ef46269673a0516a039539b0972b8ed2ec9cfee44c4b65523"},
+django-pivot = [
+    {file = "django-pivot-1.8.1.tar.gz", hash = "sha256:7184d3e3f5e96003150428bea106a9963f49f0431fa56f93595316c9b42bcca6"},
+    {file = "django_pivot-1.8.1-py3-none-any.whl", hash = "sha256:9bf83b2b61d4dc95c01e5b7a595ee223c5c1f08a4590733673a306b1513174d4"},
 ]
 django-prometheus = [
     {file = "django-prometheus-2.1.0.tar.gz", hash = "sha256:dd3f8da1399140fbef5c00d1526a23d1ade286b144281c325f8e409a781643f2"},
     {file = "django_prometheus-2.1.0-py2.py3-none-any.whl", hash = "sha256:c338d6efde1ca336e90c540b5e87afe9287d7bcc82d651a778f302b0be17a933"},
 ]
+django-redis = [
+    {file = "django-redis-4.12.1.tar.gz", hash = "sha256:306589c7021e6468b2656edc89f62b8ba67e8d5a1c8877e2688042263daa7a63"},
+    {file = "django_redis-4.12.1-py3-none-any.whl", hash = "sha256:1133b26b75baa3664164c3f44b9d5d133d1b8de45d94d79f38d1adc5b1d502e5"},
+]
 django-rq = [
-    {file = "django-rq-2.4.0.tar.gz", hash = "sha256:3851f4eb65358bf13cd179156ae695a8a76e18249f8d1c4b89c0c7efac7e1f6d"},
-    {file = "django_rq-2.4.0-py2.py3-none-any.whl", hash = "sha256:9dd86d57f35593b2d44f4be9f5cb078313e770cf1a451b910c473394b6a5ff8c"},
+    {file = "django-rq-2.4.1.tar.gz", hash = "sha256:f09059ab37403a47c7933bca396fabb7f3058732d132462eade5333bc4bcac5f"},
+    {file = "django_rq-2.4.1-py2.py3-none-any.whl", hash = "sha256:23981f83c537178cbbf730f192c13e99cede2204e9d917b1c1c80c42215dd227"},
 ]
 django-tables2 = [
     {file = "django-tables2-2.3.4.tar.gz", hash = "sha256:50ccadbd13740a996d8a4d4f144ef80134745cd0b5ec278061537e341f5ef7a2"},
@@ -1796,12 +1956,16 @@ django-taggit = [
     {file = "django_taggit-1.3.0-py3-none-any.whl", hash = "sha256:609b0223d8a652f3fae088b7fd29f294fdadaca2d7931d45c27d6c59b02fdf31"},
 ]
 django-timezone-field = [
-    {file = "django-timezone-field-4.1.1.tar.gz", hash = "sha256:b5b587aabed8db66eb3453691522164915c1aa1b326d8ddeadc8832a8580faeb"},
-    {file = "django_timezone_field-4.1.1-py3-none-any.whl", hash = "sha256:068dc2c9b11c2230e126f511a515609d46f8cc49278b293e7536be07997fe892"},
+    {file = "django-timezone-field-4.1.2.tar.gz", hash = "sha256:cffac62452d060e365938aa9c9f7b72d70d8b26b9c60243bce227b35abd1b9df"},
+    {file = "django_timezone_field-4.1.2-py3-none-any.whl", hash = "sha256:897c06e40b619cf5731a30d6c156886a7c64cba3a90364832148da7ef32ccf36"},
+]
+django-webserver = [
+    {file = "django-webserver-1.2.0.tar.gz", hash = "sha256:c976979d15b5ff9a212f7904d3b779e22219aebb4857860fcaf20e4e40f1da40"},
+    {file = "django_webserver-1.2.0-py2.py3-none-any.whl", hash = "sha256:09200631f266484b9e944e38e92681d6e9aa7d90d089a5c86d5fb08fddad84fe"},
 ]
 djangorestframework = [
-    {file = "djangorestframework-3.12.2-py3-none-any.whl", hash = "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7"},
-    {file = "djangorestframework-3.12.2.tar.gz", hash = "sha256:0898182b4737a7b584a2c73735d89816343369f259fea932d90dc78e35d8ac33"},
+    {file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"},
+    {file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"},
 ]
 dnspython = [
     {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"},
@@ -1812,8 +1976,8 @@ drf-yasg = [
     {file = "drf_yasg-1.20.0-py2.py3-none-any.whl", hash = "sha256:8b72e5b1875931a8d11af407be3a9a5ba8776541492947a0df5bafda6b7f8267"},
 ]
 flake8 = [
-    {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"},
-    {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"},
+    {file = "flake8-3.9.1-py2.py3-none-any.whl", hash = "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"},
+    {file = "flake8-3.9.1.tar.gz", hash = "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378"},
 ]
 funcy = [
     {file = "funcy-1.15-py2.py3-none-any.whl", hash = "sha256:c247c3d085e03a89974e0c9e8331e9a79db3768a263556ba896d6c92d665bba2"},
@@ -1823,12 +1987,12 @@ future = [
     {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
 ]
 gitdb = [
-    {file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"},
-    {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"},
+    {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"},
+    {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"},
 ]
 gitpython = [
-    {file = "GitPython-3.1.13-py3-none-any.whl", hash = "sha256:c5347c81d232d9b8e7f47b68a83e5dc92e7952127133c5f2df9133f2c75a1b29"},
-    {file = "GitPython-3.1.13.tar.gz", hash = "sha256:8621a7e777e276a5ec838b59280ba5272dd144a18169c36c903d8b38b99f750a"},
+    {file = "GitPython-3.1.15-py3-none-any.whl", hash = "sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867"},
+    {file = "GitPython-3.1.15.tar.gz", hash = "sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e"},
 ]
 graphene = [
     {file = "graphene-2.1.8-py2.py3-none-any.whl", hash = "sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896"},
@@ -1846,10 +2010,6 @@ graphql-relay = [
     {file = "graphql-relay-2.0.1.tar.gz", hash = "sha256:870b6b5304123a38a0b215a79eace021acce5a466bf40cd39fa18cb8528afabb"},
     {file = "graphql_relay-2.0.1-py3-none-any.whl", hash = "sha256:ac514cb86db9a43014d7e73511d521137ac12cf0101b2eaa5f0a3da2e10d913d"},
 ]
-gunicorn = [
-    {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
-    {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
-]
 idna = [
     {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
     {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
@@ -1859,8 +2019,8 @@ importlib-metadata = [
     {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"},
 ]
 importlib-resources = [
-    {file = "importlib_resources-5.1.0-py3-none-any.whl", hash = "sha256:885b8eae589179f661c909d699a546cf10d83692553e34dca1bf5eb06f7f6217"},
-    {file = "importlib_resources-5.1.0.tar.gz", hash = "sha256:bfdad047bce441405a49cf8eb48ddce5e56c696e185f59147a8b79e75e9e6380"},
+    {file = "importlib_resources-5.1.2-py3-none-any.whl", hash = "sha256:ebab3efe74d83b04d6bf5cd9a17f0c5c93e60fb60f30c90f56265fce4682a469"},
+    {file = "importlib_resources-5.1.2.tar.gz", hash = "sha256:642586fc4740bd1cad7690f836b3321309402b20b332529f25617ff18e8e1370"},
 ]
 inflection = [
     {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"},
@@ -1872,8 +2032,8 @@ invoke = [
     {file = "invoke-1.5.0.tar.gz", hash = "sha256:f0c560075b5fb29ba14dad44a7185514e94970d1b9d57dcd3723bec5fed92650"},
 ]
 isort = [
-    {file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"},
-    {file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"},
+    {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"},
+    {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"},
 ]
 itypes = [
     {file = "itypes-1.2.0-py2.py3-none-any.whl", hash = "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6"},
@@ -1888,8 +2048,8 @@ jsonschema = [
     {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"},
 ]
 junos-eznc = [
-    {file = "junos-eznc-2.5.4.tar.gz", hash = "sha256:bf036d0af9ee5c5e4f517cb5fc902fe891fa120e18f459805862c53d4a97193a"},
-    {file = "junos_eznc-2.5.4-py2.py3-none-any.whl", hash = "sha256:e05c36d56d8b8d13b1fb3bb763828bb3ee80fa1dcadc3a6762e8e2568504676d"},
+    {file = "junos-eznc-2.6.0.tar.gz", hash = "sha256:761a85a1485f8dc41b983ae7296889c7a78394b78d2de68f0831b7efa5f6ca34"},
+    {file = "junos_eznc-2.6.0-py2.py3-none-any.whl", hash = "sha256:598c3569b21f362f0bad626ad3756f9f8a3f4d507a0ff37ff76e33b3ed73859e"},
 ]
 kiwisolver = [
     {file = "kiwisolver-1.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd34fbbfbc40628200730bc1febe30631347103fc8d3d4fa012c21ab9c11eca9"},
@@ -1926,70 +2086,80 @@ kiwisolver = [
     {file = "kiwisolver-1.3.1.tar.gz", hash = "sha256:950a199911a8d94683a6b10321f9345d5a3a8433ec58b217ace979e18f16e248"},
 ]
 lazy-object-proxy = [
-    {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
-    {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
-    {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"},
-    {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"},
-    {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"},
-    {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"},
-    {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"},
-    {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"},
-    {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"},
-    {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"},
-    {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"},
-    {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"},
-    {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"},
-    {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"},
-    {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"},
-    {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"},
-    {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"},
-    {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"},
-    {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"},
-    {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"},
-    {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"},
+    {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"},
+    {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"},
+    {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"},
+    {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"},
+    {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"},
+    {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"},
+    {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"},
+    {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"},
+    {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"},
+    {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"},
+    {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"},
+    {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"},
+    {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"},
+    {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"},
+    {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"},
+    {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"},
+    {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"},
+    {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"},
+    {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"},
+    {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"},
+    {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"},
+    {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"},
 ]
 lxml = [
-    {file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f"},
-    {file = "lxml-4.6.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d"},
-    {file = "lxml-4.6.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:68a5d77e440df94011214b7db907ec8f19e439507a70c958f750c18d88f995d2"},
-    {file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e"},
-    {file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:69a63f83e88138ab7642d8f61418cf3180a4d8cd13995df87725cb8b893e950e"},
-    {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:42ebca24ba2a21065fb546f3e6bd0c58c3fe9ac298f3a320147029a4850f51a2"},
-    {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe"},
-    {file = "lxml-4.6.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388"},
-    {file = "lxml-4.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80"},
-    {file = "lxml-4.6.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b"},
-    {file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8"},
-    {file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780"},
-    {file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af"},
-    {file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37"},
-    {file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98"},
-    {file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d"},
-    {file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf"},
-    {file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939"},
-    {file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e"},
-    {file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711"},
-    {file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089"},
-    {file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01"},
-    {file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2"},
-    {file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc"},
-    {file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d"},
-    {file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3"},
-    {file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644"},
-    {file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308"},
-    {file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505"},
-    {file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a"},
-    {file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931"},
-    {file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03"},
-    {file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7e9eac1e526386df7c70ef253b792a0a12dd86d833b1d329e038c7a235dfceb5"},
-    {file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7ee8af0b9f7de635c61cdd5b8534b76c52cd03536f29f51151b377f76e214a1a"},
-    {file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:2e6fd1b8acd005bd71e6c94f30c055594bbd0aa02ef51a22bbfa961ab63b2d75"},
-    {file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf"},
-    {file = "lxml-4.6.2.tar.gz", hash = "sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc"},
+    {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"},
+    {file = "lxml-4.6.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"},
+    {file = "lxml-4.6.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"},
+    {file = "lxml-4.6.3-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"},
+    {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"},
+    {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"},
+    {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"},
+    {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"},
+    {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"},
+    {file = "lxml-4.6.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354"},
+    {file = "lxml-4.6.3-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16"},
+    {file = "lxml-4.6.3-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"},
+    {file = "lxml-4.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"},
+    {file = "lxml-4.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"},
+    {file = "lxml-4.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"},
+    {file = "lxml-4.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"},
+    {file = "lxml-4.6.3-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24"},
+    {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"},
+    {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617"},
+    {file = "lxml-4.6.3-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"},
+    {file = "lxml-4.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"},
+    {file = "lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"},
+    {file = "lxml-4.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"},
+    {file = "lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"},
+    {file = "lxml-4.6.3-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96"},
+    {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"},
+    {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92"},
+    {file = "lxml-4.6.3-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"},
+    {file = "lxml-4.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"},
+    {file = "lxml-4.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"},
+    {file = "lxml-4.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"},
+    {file = "lxml-4.6.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"},
+    {file = "lxml-4.6.3-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e"},
+    {file = "lxml-4.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"},
+    {file = "lxml-4.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae"},
+    {file = "lxml-4.6.3-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"},
+    {file = "lxml-4.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"},
+    {file = "lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"},
+    {file = "lxml-4.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"},
+    {file = "lxml-4.6.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"},
+    {file = "lxml-4.6.3-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59"},
+    {file = "lxml-4.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"},
+    {file = "lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a"},
+    {file = "lxml-4.6.3-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"},
+    {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"},
+    {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"},
 ]
 markdown = [
-    {file = "Markdown-3.3.3-py3-none-any.whl", hash = "sha256:c109c15b7dc20a9ac454c9e6025927d44460b85bd039da028d85e2b6d0bcc328"},
-    {file = "Markdown-3.3.3.tar.gz", hash = "sha256:5d9f2b5ca24bc4c7a390d22323ca4bad200368612b5aaa7796babf971d2b2f18"},
+    {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},
+    {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"},
 ]
 markupsafe = [
     {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
@@ -2085,12 +2255,12 @@ napalm = [
     {file = "napalm-3.2.0.tar.gz", hash = "sha256:6e69a505f5d4a678c42f048d13153afabf10b7fec72868e3bfb53ff4089206fc"},
 ]
 nautobot = [
-    {file = "nautobot-1.0.0a1-py3-none-any.whl", hash = "sha256:7e035e22ba8a2c35576e9c7e9e03eb5bf3ffa846d7391bccba4fe618e7f0a330"},
-    {file = "nautobot-1.0.0a1.tar.gz", hash = "sha256:ead0be9f36873326d9e639b8db497e67c694b06fe7d14be28977f7d916f1eea6"},
+    {file = "nautobot-1.0.0-py3-none-any.whl", hash = "sha256:f6cf363330f4dc24b3545c5484eb113dc1a82e7b20deff9b57f3c5e783dff066"},
+    {file = "nautobot-1.0.0.tar.gz", hash = "sha256:1fcdc8ffa6eaf3ad9c8fa3f8ec5ff437a1df44ffcd6003abee209c91d8f77efd"},
 ]
 nautobot-plugin-nornir = [
-    {file = "nautobot-plugin-nornir-0.9.0.tar.gz", hash = "sha256:f435bc4f08b7c957e8c01506281454633895a1af4de4fc4bd7867b6f003c0e7b"},
-    {file = "nautobot_plugin_nornir-0.9.0-py3-none-any.whl", hash = "sha256:a18dcbe62910d5762b7e3f5c1ed552b2e338d56dd8e0b41a75ae392ff97cf5d3"},
+    {file = "nautobot-plugin-nornir-0.9.1.tar.gz", hash = "sha256:18746be41104c1eaeed1124ce7b014eadfb10e68634bf3abbc011267f00662c2"},
+    {file = "nautobot_plugin_nornir-0.9.1-py3-none-any.whl", hash = "sha256:4a2dbdfc3eab76e7da43e4c7324b2f0daa8473359b4d3f468de34c5875cef7d2"},
 ]
 ncclient = [
     {file = "ncclient-0.6.9.tar.gz", hash = "sha256:0112f2ad41fb658f52446d870853a63691d69299c73c7351c520d38dbd8dc0c4"},
@@ -2100,36 +2270,40 @@ netaddr = [
     {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"},
 ]
 netmiko = [
-    {file = "netmiko-3.3.3-py3-none-any.whl", hash = "sha256:be165b81930aa779a2a34f4437af9f9807553030d2e3ef8da002314fb6b1366d"},
-    {file = "netmiko-3.3.3.tar.gz", hash = "sha256:ead6ea70b46307cce03d121d4da96fd862f76619474d6e962d1019560bb4c5a1"},
+    {file = "netmiko-3.4.0-py3-none-any.whl", hash = "sha256:b66f25717db3609878f83c85604349dd40a0ab494d8eafd817dcde8388131136"},
+    {file = "netmiko-3.4.0.tar.gz", hash = "sha256:acadb9dd97864ee848e2032f1f0e301c7b31e7a4153757d98f5c8ba1b9614993"},
+]
+netutils = [
+    {file = "netutils-0.1.2-py3-none-any.whl", hash = "sha256:a381c6f0715b1af818be1e4003a88aa5d2c66ef48c951db4c27dacaeea1a3d50"},
+    {file = "netutils-0.1.2.tar.gz", hash = "sha256:fdf9d809f5ee2f72a6a179d4f5a0ef4503a2836aa9a0695979aab847bc09a30b"},
 ]
 nornir = [
-    {file = "nornir-3.0.0-py3-none-any.whl", hash = "sha256:a8f77a7677850e7632ea9ae35a86f9b09aae353d836bd3e42faae53f17f8b328"},
-    {file = "nornir-3.0.0.tar.gz", hash = "sha256:f87dec4d18d46102969272986bf4d7fab8cef212e2ad54a039f8e249f3e4c975"},
+    {file = "nornir-3.1.0-py3-none-any.whl", hash = "sha256:0eed0ca73122ce8f4b90a560ada79d81174845a8a5320972292417eb19efa587"},
+    {file = "nornir-3.1.0.tar.gz", hash = "sha256:f96b0e2b8983045eef5345de5a6e61198ed2658a0e42049e4bd9a127b2bae034"},
 ]
 nornir-jinja2 = [
-    {file = "nornir_jinja2-0.1.1-py3-none-any.whl", hash = "sha256:6ae0e47eca1e084dc16ed4ffa788d19853637f0b629722e4eed29ca64f91c9b3"},
-    {file = "nornir_jinja2-0.1.1.tar.gz", hash = "sha256:ee4a90b38b1a7d25a272f52bb383e12c20568a0228406dae0e5262edfd4b3998"},
+    {file = "nornir_jinja2-0.1.2-py3-none-any.whl", hash = "sha256:9a1c9e8ef2b3d72966f68e415a77c10b0ef5f4d7dbc730acd0bc78d57d4ba716"},
+    {file = "nornir_jinja2-0.1.2.tar.gz", hash = "sha256:83520fa59076bfebe17cbacf54de6040eb7daefe564d2dd7563c13785140fa93"},
 ]
 nornir-napalm = [
-    {file = "nornir_napalm-0.1.1-py3-none-any.whl", hash = "sha256:f0def9ebedd99563cb95a7d390498b934106e1d8e2121dab4f132da02a75fd5d"},
-    {file = "nornir_napalm-0.1.1.tar.gz", hash = "sha256:92b861c5aaf429b40bb9c0a4b24ed471e4f9f3251fa49919200e0d502aad3563"},
+    {file = "nornir_napalm-0.1.2-py3-none-any.whl", hash = "sha256:313986bbb16eeaf3a80daf61f6a97e93ecebc47d0c586a2eb856ea91edc2cc1d"},
+    {file = "nornir_napalm-0.1.2.tar.gz", hash = "sha256:be7808a990242987500a65701edb626197c5d0b87f35d9eb5da7ce7e4d60fdd5"},
 ]
 nornir-nautobot = [
-    {file = "nornir-nautobot-1.0.0.tar.gz", hash = "sha256:2f253d926e84791322ea8582aa133a80852373fbe3961404f428e0d3b3783ea9"},
-    {file = "nornir_nautobot-1.0.0-py3-none-any.whl", hash = "sha256:16d672badc08d4fd246359b26f6760e1cfb898b8ab9f5fb8b7980335f63d7527"},
+    {file = "nornir-nautobot-2.0.1.tar.gz", hash = "sha256:3771a80bcd4df318bb0f36602a2d41eccaaa1f9daa6a316c3702619ad90d3b72"},
+    {file = "nornir_nautobot-2.0.1-py3-none-any.whl", hash = "sha256:551b1a82652786dd6f63bedc2f4407d029b3c5cbec88031f8e5697eefcf5dba0"},
 ]
 nornir-netmiko = [
     {file = "nornir_netmiko-0.1.1-py3-none-any.whl", hash = "sha256:c6eadb81f6f3b2f0c27bae151cc62673303f9d085ec3c773ecdc98f20ef30f91"},
     {file = "nornir_netmiko-0.1.1.tar.gz", hash = "sha256:fc41ded40923d23c6155b92c6749170629b4cc2649c340d24bff9f49315836c6"},
 ]
 nornir-utils = [
-    {file = "nornir_utils-0.1.1-py3-none-any.whl", hash = "sha256:21e05774e27ae4d81cf48fd915951260e19b5da3494fc1c74e9ec0fa9742644e"},
-    {file = "nornir_utils-0.1.1.tar.gz", hash = "sha256:636a76a3bdad1a3a618189a01c5425e22cb65d8f361227d7d5a4ca92ed47b0d1"},
+    {file = "nornir_utils-0.1.2-py3-none-any.whl", hash = "sha256:98915e781d4135e424fe1d4617dc231ada9a7c732c8e51e408f53fd3d37a13b0"},
+    {file = "nornir_utils-0.1.2.tar.gz", hash = "sha256:23ae95c4805b0ce8a5ed32935f3f86027e5701175e7740ab8b3a79946c5d90b2"},
 ]
 ntc-templates = [
-    {file = "ntc_templates-1.6.0-py3-none-any.whl", hash = "sha256:a45ca5bf20d8b4585d63f732f3f605761e20279734dab76d1960c082fd6f2417"},
-    {file = "ntc_templates-1.6.0.tar.gz", hash = "sha256:a4420beee9cc14797d945b51bffd6e0126913b2dbc672d2e0a1530d3fec5b28d"},
+    {file = "ntc_templates-2.0.0-py3-none-any.whl", hash = "sha256:6617f36aaa842179e94d8b8e6527e652baf4a18a5b2f94b26b6505e5722fbc95"},
+    {file = "ntc_templates-2.0.0.tar.gz", hash = "sha256:32d3b371dfe5aecd4c36f56184f109f1f75e4768e6087d234c0371cbefe82bcd"},
 ]
 numpy = [
     {file = "numpy-1.19.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff"},
@@ -2167,6 +2341,10 @@ numpy = [
     {file = "numpy-1.19.5-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73"},
     {file = "numpy-1.19.5.zip", hash = "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4"},
 ]
+oauthlib = [
+    {file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"},
+    {file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"},
+]
 packaging = [
     {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
     {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
@@ -2188,42 +2366,43 @@ pbr = [
     {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"},
 ]
 pillow = [
-    {file = "Pillow-8.1.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a"},
-    {file = "Pillow-8.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2"},
-    {file = "Pillow-8.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174"},
-    {file = "Pillow-8.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded"},
-    {file = "Pillow-8.1.0-cp36-cp36m-win32.whl", hash = "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d"},
-    {file = "Pillow-8.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d"},
-    {file = "Pillow-8.1.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234"},
-    {file = "Pillow-8.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8"},
-    {file = "Pillow-8.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17"},
-    {file = "Pillow-8.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7"},
-    {file = "Pillow-8.1.0-cp37-cp37m-win32.whl", hash = "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e"},
-    {file = "Pillow-8.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b"},
-    {file = "Pillow-8.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0"},
-    {file = "Pillow-8.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a"},
-    {file = "Pillow-8.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d"},
-    {file = "Pillow-8.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae"},
-    {file = "Pillow-8.1.0-cp38-cp38-win32.whl", hash = "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59"},
-    {file = "Pillow-8.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c"},
-    {file = "Pillow-8.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6"},
-    {file = "Pillow-8.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378"},
-    {file = "Pillow-8.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7"},
-    {file = "Pillow-8.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0"},
-    {file = "Pillow-8.1.0-cp39-cp39-win32.whl", hash = "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b"},
-    {file = "Pillow-8.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865"},
-    {file = "Pillow-8.1.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9"},
-    {file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913"},
-    {file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206"},
-    {file = "Pillow-8.1.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9"},
-    {file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032"},
-    {file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820"},
-    {file = "Pillow-8.1.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5"},
-    {file = "Pillow-8.1.0.tar.gz", hash = "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba"},
+    {file = "Pillow-8.1.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:5cf03b9534aca63b192856aa601c68d0764810857786ea5da652581f3a44c2b0"},
+    {file = "Pillow-8.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f91b50ad88048d795c0ad004abbe1390aa1882073b1dca10bfd55d0b8cf18ec5"},
+    {file = "Pillow-8.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5762ebb4436f46b566fc6351d67a9b5386b5e5de4e58fdaa18a1c83e0e20f1a8"},
+    {file = "Pillow-8.1.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e2cd8ac157c1e5ae88b6dd790648ee5d2777e76f1e5c7d184eaddb2938594f34"},
+    {file = "Pillow-8.1.2-cp36-cp36m-win32.whl", hash = "sha256:72027ebf682abc9bafd93b43edc44279f641e8996fb2945104471419113cfc71"},
+    {file = "Pillow-8.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d1d6bca39bb6dd94fba23cdb3eeaea5e30c7717c5343004d900e2a63b132c341"},
+    {file = "Pillow-8.1.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:90882c6f084ef68b71bba190209a734bf90abb82ab5e8f64444c71d5974008c6"},
+    {file = "Pillow-8.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:89e4c757a91b8c55d97c91fa09c69b3677c227b942fa749e9a66eef602f59c28"},
+    {file = "Pillow-8.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8c4e32218c764bc27fe49b7328195579581aa419920edcc321c4cb877c65258d"},
+    {file = "Pillow-8.1.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a01da2c266d9868c4f91a9c6faf47a251f23b9a862dce81d2ff583135206f5be"},
+    {file = "Pillow-8.1.2-cp37-cp37m-win32.whl", hash = "sha256:30d33a1a6400132e6f521640dd3f64578ac9bfb79a619416d7e8802b4ce1dd55"},
+    {file = "Pillow-8.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:71b01ee69e7df527439d7752a2ce8fb89e19a32df484a308eca3e81f673d3a03"},
+    {file = "Pillow-8.1.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:5a2d957eb4aba9d48170b8fe6538ec1fbc2119ffe6373782c03d8acad3323f2e"},
+    {file = "Pillow-8.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:87f42c976f91ca2fc21a3293e25bd3cd895918597db1b95b93cbd949f7d019ce"},
+    {file = "Pillow-8.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:15306d71a1e96d7e271fd2a0737038b5a92ca2978d2e38b6ced7966583e3d5af"},
+    {file = "Pillow-8.1.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:71f31ee4df3d5e0b366dd362007740106d3210fb6a56ec4b581a5324ba254f06"},
+    {file = "Pillow-8.1.2-cp38-cp38-win32.whl", hash = "sha256:98afcac3205d31ab6a10c5006b0cf040d0026a68ec051edd3517b776c1d78b09"},
+    {file = "Pillow-8.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:328240f7dddf77783e72d5ed79899a6b48bc6681f8d1f6001f55933cb4905060"},
+    {file = "Pillow-8.1.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bead24c0ae3f1f6afcb915a057943ccf65fc755d11a1410a909c1fefb6c06ad1"},
+    {file = "Pillow-8.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81b3716cc9744ffdf76b39afb6247eae754186838cedad0b0ac63b2571253fe6"},
+    {file = "Pillow-8.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:63cd413ac52ee3f67057223d363f4f82ce966e64906aea046daf46695e3c8238"},
+    {file = "Pillow-8.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8565355a29655b28fdc2c666fd9a3890fe5edc6639d128814fafecfae2d70910"},
+    {file = "Pillow-8.1.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1940fc4d361f9cc7e558d6f56ff38d7351b53052fd7911f4b60cd7bc091ea3b1"},
+    {file = "Pillow-8.1.2-cp39-cp39-win32.whl", hash = "sha256:46c2bcf8e1e75d154e78417b3e3c64e96def738c2a25435e74909e127a8cba5e"},
+    {file = "Pillow-8.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:aeab4cd016e11e7aa5cfc49dcff8e51561fa64818a0be86efa82c7038e9369d0"},
+    {file = "Pillow-8.1.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:74cd9aa648ed6dd25e572453eb09b08817a1e3d9f8d1bd4d8403d99e42ea790b"},
+    {file = "Pillow-8.1.2-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:e5739ae63636a52b706a0facec77b2b58e485637e1638202556156e424a02dc2"},
+    {file = "Pillow-8.1.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:903293320efe2466c1ab3509a33d6b866dc850cfd0c5d9cc92632014cec185fb"},
+    {file = "Pillow-8.1.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:5daba2b40782c1c5157a788ec4454067c6616f5a0c1b70e26ac326a880c2d328"},
+    {file = "Pillow-8.1.2-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:1f93f2fe211f1ef75e6f589327f4d4f8545d5c8e826231b042b483d8383e8a7c"},
+    {file = "Pillow-8.1.2-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:6efac40344d8f668b6c4533ae02a48d52fd852ef0654cc6f19f6ac146399c733"},
+    {file = "Pillow-8.1.2-pp37-pypy37_pp73-win32.whl", hash = "sha256:f36c3ff63d6fc509ce599a2f5b0d0732189eed653420e7294c039d342c6e204a"},
+    {file = "Pillow-8.1.2.tar.gz", hash = "sha256:b07c660e014852d98a00a91adfbe25033898a9d90a8f39beb2437d22a203fc44"},
 ]
 prometheus-client = [
-    {file = "prometheus_client-0.9.0-py2.py3-none-any.whl", hash = "sha256:b08c34c328e1bf5961f0b4352668e6c8f145b4a087e09b7296ef62cbe4693d35"},
-    {file = "prometheus_client-0.9.0.tar.gz", hash = "sha256:9da7b32f02439d8c04f7777021c304ed51d9ec180604700c1ba72a4d44dceb03"},
+    {file = "prometheus_client-0.10.1-py2.py3-none-any.whl", hash = "sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa"},
+    {file = "prometheus_client-0.10.1.tar.gz", hash = "sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d"},
 ]
 promise = [
     {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
@@ -2266,8 +2445,8 @@ psycopg2-binary = [
     {file = "psycopg2_binary-2.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6"},
 ]
 pycodestyle = [
-    {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
-    {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
+    {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
+    {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
 ]
 pycparser = [
     {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
@@ -2313,16 +2492,20 @@ pyeapi = [
     {file = "pyeapi-0.8.4.tar.gz", hash = "sha256:c33ad1eadd8ebac75f63488df9412081ce0b024c9e1da12a37196a5c60427c54"},
 ]
 pyflakes = [
-    {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
-    {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
+    {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
+    {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
+]
+pyjwt = [
+    {file = "PyJWT-2.0.1-py3-none-any.whl", hash = "sha256:b70b15f89dc69b993d8a8d32c299032d5355c82f9b5b7e851d1a6d706dffe847"},
+    {file = "PyJWT-2.0.1.tar.gz", hash = "sha256:a5c70a06e1f33d81ef25eecd50d50bd30e34de1ca8b2b9fa3fe0daaabcf69bf7"},
 ]
 pylint = [
-    {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"},
-    {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"},
+    {file = "pylint-2.7.4-py3-none-any.whl", hash = "sha256:209d712ec870a0182df034ae19f347e725c1e615b2269519ab58a35b3fcbbe7a"},
+    {file = "pylint-2.7.4.tar.gz", hash = "sha256:bd38914c7731cdc518634a8d3c5585951302b6e2b6de60fbb3f7a0220e21eeee"},
 ]
 pylint-django = [
-    {file = "pylint-django-2.4.2.tar.gz", hash = "sha256:f8d77f7da47a7019cda5cb669c214f03033208f9e945094661299d2637c0da06"},
-    {file = "pylint_django-2.4.2-py3-none-any.whl", hash = "sha256:355dddb25ef07dbdb77a818b0860ada722aab654c24da34aab916ec26d6390ba"},
+    {file = "pylint-django-2.4.3.tar.gz", hash = "sha256:a5a4515209a6237d1d390a4a307d53f53baaf4f058ecf4bb556c775d208f6b0d"},
+    {file = "pylint_django-2.4.3-py3-none-any.whl", hash = "sha256:dc5ed27bb7662d73444ccd15a0b3964ed6ced6cc2712b85db616102062d2ec35"},
 ]
 pylint-plugin-utils = [
     {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"},
@@ -2367,10 +2550,30 @@ python-dateutil = [
     {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
     {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
 ]
+python3-openid = [
+    {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"},
+    {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"},
+]
 pytz = [
     {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
     {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
 ]
+pyuwsgi = [
+    {file = "pyuwsgi-2.0.19.1.post0-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a425f562f382a097ca49df26b70d47d12f0cf0abf233610f3f58b1f7f780355e"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:56ecda11e873b2eb937b33d2999766322eebfa82ee5b26a2196a335c4e786186"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:297d1d0b8c472374b12eda7f17a9f5de67cf516612e42b71a7636afb9d1e3974"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp35-cp35m-macosx_10_13_intel.whl", hash = "sha256:5439f0f3ef5d6bf1622f341662d04c1d92b88889db40b295419e5fe75a7c7d45"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:890e7e863cb61c8369b6bcfa5d6f323753aaeec2cfaba16741f119c79b964aa7"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:bddeb8df77010d0f842068765a0b3155fdcfd847f14bc1ba89fc7e37914a13d2"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:90e4235020048456ad867aefc383cdf5528b7f6e327555ceec579c428a828759"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:94d4287b155aa789ce4b6f671c981f7d6c58fc3113330e2f29ac7926cb854645"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:66a9751f28abf348e0ddccadc4ded47623f2d35cf9609c87b57909d55a4cdc15"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:285e263a9094389f13cfdefd033a4e99fbed3ad120dba9ac5093846cc03ac5ab"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:dac4a04dc0f69d641dba984e83214d2c2cc098496c5d5585e7d3f4e7a9190f84"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ac79dead0685beab5ecfe0926426849a44c5572528f89bb17f6ecf5eb561024e"},
+    {file = "pyuwsgi-2.0.19.1.post0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:149675b2e020b0e833e8b871a545751ca346cbfed85c8fd2b320a01d40dc3d8f"},
+    {file = "pyuwsgi-2.0.19.1.post0.tar.gz", hash = "sha256:0bd14517398f494d828d77a9bf72b5a6cbef0112e1cc05e9a0080fa8828ccfa0"},
+]
 pyyaml = [
     {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
     {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
@@ -2378,18 +2581,26 @@ pyyaml = [
     {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
     {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
     {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
     {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
     {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
     {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
     {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
     {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
     {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
     {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
     {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
     {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
     {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
     {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
     {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
     {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
     {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
     {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
@@ -2399,59 +2610,64 @@ redis = [
     {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
 ]
 regex = [
-    {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"},
-    {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"},
-    {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"},
-    {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"},
-    {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"},
-    {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"},
-    {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"},
-    {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"},
-    {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"},
-    {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"},
-    {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"},
-    {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"},
-    {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"},
+    {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"},
+    {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"},
+    {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"},
+    {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"},
+    {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"},
+    {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"},
+    {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"},
+    {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"},
+    {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"},
+    {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"},
+    {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"},
+    {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"},
+    {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"},
+    {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"},
+    {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"},
+    {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"},
+    {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"},
+    {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"},
+    {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"},
+    {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"},
+    {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"},
+    {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"},
+    {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"},
+    {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"},
+    {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"},
+    {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"},
+    {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"},
+    {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"},
+    {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"},
+    {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"},
+    {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"},
+    {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"},
+    {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"},
+    {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"},
+    {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"},
+    {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"},
+    {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"},
+    {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"},
+    {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"},
+    {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"},
+    {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"},
 ]
 requests = [
     {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
     {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
 ]
+requests-oauthlib = [
+    {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"},
+    {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
+    {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
+]
 rq = [
-    {file = "rq-1.7.0-py2.py3-none-any.whl", hash = "sha256:e0435db8e8afd2f83fd17262722d02a4455403a8a6344670186497474d74ce3a"},
-    {file = "rq-1.7.0.tar.gz", hash = "sha256:0291603f0c5bb0ef18363f6b325ae76076c1d79bb918e052aac1f8da2297dbfb"},
+    {file = "rq-1.8.0-py2.py3-none-any.whl", hash = "sha256:d861f1e794b595ae83f7b919d0c56d7102bc18f4f7b71395446d60d70344577c"},
+    {file = "rq-1.8.0.tar.gz", hash = "sha256:2a253385bec82f0e723ad25fb547d3892900a94b3c3acc53514c4bd74897cb69"},
 ]
 "ruamel.yaml" = [
-    {file = "ruamel.yaml-0.16.12-py2.py3-none-any.whl", hash = "sha256:012b9470a0ea06e4e44e99e7920277edf6b46eee0232a04487ea73a7386340a5"},
-    {file = "ruamel.yaml-0.16.12.tar.gz", hash = "sha256:076cc0bc34f1966d920a49f18b52b6ad559fbe656a0748e3535cf7b3f29ebf9e"},
+    {file = "ruamel.yaml-0.16.13-py2.py3-none-any.whl", hash = "sha256:64b06e7873eb8e1125525ecef7345447d786368cadca92a7cd9b59eae62e95a3"},
+    {file = "ruamel.yaml-0.16.13.tar.gz", hash = "sha256:bb48c514222702878759a05af96f4b7ecdba9b33cd4efcf25c86b882cef3a942"},
 ]
 "ruamel.yaml.clib" = [
     {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc"},
@@ -2495,21 +2711,30 @@ scp = [
     {file = "scp-0.13.3.tar.gz", hash = "sha256:8bd748293d7362073169b96ce4b8c4f93bcc62cfc5f7e1d949e01e406a025bd4"},
 ]
 singledispatch = [
-    {file = "singledispatch-3.6.0-py2.py3-none-any.whl", hash = "sha256:ca602b0a8389aa93e3b183f57f29edc6af202bef718c2ec53a38ef25ccc5867e"},
-    {file = "singledispatch-3.6.0.tar.gz", hash = "sha256:c4ad3947f0cd35d3d99231c37190d2ee7bcbc271c2be6e1e3f4465e73a99511c"},
+    {file = "singledispatch-3.6.1-py2.py3-none-any.whl", hash = "sha256:85c97f94c8957fa4e6dab113156c182fb346d56d059af78aad710bced15f16fb"},
+    {file = "singledispatch-3.6.1.tar.gz", hash = "sha256:58b46ce1cc4d43af0aac3ac9a047bdb0f44e05f0b2fa2eec755863331700c865"},
 ]
 six = [
     {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
     {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
 ]
 smmap = [
-    {file = "smmap-3.0.5-py2.py3-none-any.whl", hash = "sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714"},
-    {file = "smmap-3.0.5.tar.gz", hash = "sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50"},
+    {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"},
+    {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"},
 ]
 snowballstemmer = [
     {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
     {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
 ]
+social-auth-app-django = [
+    {file = "social-auth-app-django-4.0.0.tar.gz", hash = "sha256:2c69e57df0b30c9c1823519c5f1992cbe4f3f98fdc7d95c840e091a752708840"},
+    {file = "social_auth_app_django-4.0.0-py2-none-any.whl", hash = "sha256:df5212370bd250108987c4748419a1a1d0cec750878856c2644c36aaa0fd3e58"},
+    {file = "social_auth_app_django-4.0.0-py3-none-any.whl", hash = "sha256:567ad0e028311541d7dfed51d3bf2c60440a6fd236d5d4d06c5a618b3d6c57c5"},
+]
+social-auth-core = [
+    {file = "social-auth-core-4.1.0.tar.gz", hash = "sha256:5ab43b3b15dce5f059db69cc3082c216574739f0edbc98629c8c6e8769c67eb4"},
+    {file = "social_auth_core-4.1.0-py3-none-any.whl", hash = "sha256:983b53167ac56e7ba4909db555602a6e7a98c97ca47183bb222eb85ba627bf2b"},
+]
 sqlparse = [
     {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"},
     {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"},
@@ -2527,8 +2752,8 @@ swagger-spec-validator = [
     {file = "swagger_spec_validator-2.7.3-py2.py3-none-any.whl", hash = "sha256:d1514ec7e3c058c701f27cc74f85ceb876d6418c9db57786b9c54085ed5e29eb"},
 ]
 tenacity = [
-    {file = "tenacity-6.3.1-py2.py3-none-any.whl", hash = "sha256:baed357d9f35ec64264d8a4bbf004c35058fad8795c5b0d8a7dc77ecdcbb8f39"},
-    {file = "tenacity-6.3.1.tar.gz", hash = "sha256:e14d191fb0a309b563904bbc336582efe2037de437e543b38da749769b544d7f"},
+    {file = "tenacity-7.0.0-py2.py3-none-any.whl", hash = "sha256:a0ce48587271515db7d3a5e700df9ae69cce98c4b57c23a4886da15243603dd8"},
+    {file = "tenacity-7.0.0.tar.gz", hash = "sha256:5bd16ef5d3b985647fe28dfa6f695d343aa26479a04e8792b9d3c8f49e361ae1"},
 ]
 text-unidecode = [
     {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
@@ -2542,40 +2767,40 @@ toml = [
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 ]
 transitions = [
-    {file = "transitions-0.8.7-py2.py3-none-any.whl", hash = "sha256:626229df01ea91dc0b28491bf5af7cc0aba6c2aa7289658e55ea77288258fa49"},
-    {file = "transitions-0.8.7.tar.gz", hash = "sha256:8c60ec0828cd037820726283cad5d4d77a5e31514e058b51250420e9873e9bc7"},
+    {file = "transitions-0.8.8-py2.py3-none-any.whl", hash = "sha256:f35efa070fbdf9a0f3f093b19f1258068786af75786a8cbcc884444f3d1a66d4"},
+    {file = "transitions-0.8.8.tar.gz", hash = "sha256:e7a86b31a161a76133f189b3ae9dad2755a80ea4c1e0eee1805648d021fb677d"},
 ]
 typed-ast = [
-    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
-    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
-    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
-    {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
-    {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
-    {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
-    {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
-    {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
-    {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
-    {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
-    {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
-    {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
-    {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
-    {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
-    {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
-    {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
-    {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
-    {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
+    {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
+    {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
+    {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
+    {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
+    {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
+    {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
+    {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
+    {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
+    {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
+    {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
+    {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
+    {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
+    {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
 ]
 typing-extensions = [
     {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
@@ -2587,8 +2812,8 @@ uritemplate = [
     {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"},
 ]
 urllib3 = [
-    {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"},
-    {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"},
+    {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"},
+    {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"},
 ]
 wrapt = [
     {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"},
@@ -2597,6 +2822,6 @@ yamlordereddictloader = [
     {file = "yamlordereddictloader-0.4.0.tar.gz", hash = "sha256:7f30f0b99ea3f877f7cb340c570921fa9d639b7f69cba18be051e27f8de2080e"},
 ]
 zipp = [
-    {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"},
-    {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"},
+    {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"},
+    {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"},
 ]
diff --git a/pyproject.toml b/pyproject.toml
index 5c08bb3f..256efb19 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "nautobot-golden-config"
-version = "0.9.0"
+version = "0.9.2"
 description = "A plugin for configuration on nautobot"
 authors = ["Network to Code, LLC", "<opensource@networktocode.com>"]
 
@@ -21,13 +21,16 @@ packages = [
 [tool.poetry.dependencies]
 python = "^3.6"
 matplotlib = "^3.3.2"
-nautobot = "^1.0.0-alpha.1"
-nautobot-plugin-nornir = "^0.9.0"
+nautobot = "~1.0"
+nautobot-plugin-nornir = "^0.9.1"
+django-debug-toolbar = "^3.2.1"
+astroid = "^2.5.5"
+django-pivot = "^1.8.1"
 
 [tool.poetry.dev-dependencies]
 black = "^20.8b1"
 bandit = "^1.6.2"
-pylint = "^2.5.2"
+pylint = "2.7.4"
 pylint-django = "^2.0.15"
 pydocstyle = "^5.0.2"
 flake8 = "^3.8.4"
diff --git a/tasks.py b/tasks.py
index 076c8478..74552c80 100644
--- a/tasks.py
+++ b/tasks.py
@@ -4,7 +4,7 @@
 from invoke import task
 
 PYTHON_VER = os.getenv("PYTHON_VER", "3.7")
-NAUTOBOT_VER = os.getenv("NAUTOBOT_VER", "v1.0.0a1")
+NAUTOBOT_VER = os.getenv("NAUTOBOT_VER", "1.0.1")
 NAUTOBOT_SRC_URL = os.getenv("NAUTOBOT_SRC_URL", f"https://github.com/nautobot/nautobot/archive/{NAUTOBOT_VER}.tar.gz")
 
 # Name of the docker image/container
@@ -241,7 +241,7 @@ def unittest(context, nautobot_ver=NAUTOBOT_VER, python_ver=PYTHON_VER):
     DEFAULT_ENV[NAUTOBOT_VER] = nautobot_ver
     DEFAULT_ENV[PYTHON_VER] = python_ver
 
-    docker = f"{COMPOSE_COMMAND} run nautobot"
+    docker = f"{COMPOSE_COMMAND} run --entrypoint='' nautobot "
     context.run(
         f'{docker} sh -c "nautobot-server test nautobot_golden_config"',
         env=DEFAULT_ENV,
@@ -261,7 +261,7 @@ def pylint(context, nautobot_ver=NAUTOBOT_VER, python_ver=PYTHON_VER):
     DEFAULT_ENV[NAUTOBOT_VER] = nautobot_ver
     DEFAULT_ENV[PYTHON_VER] = python_ver
 
-    docker = f"{COMPOSE_COMMAND} run nautobot"
+    docker = f"{COMPOSE_COMMAND} run --entrypoint='' nautobot "
     # We exclude the /migrations/ directory since it is autogenerated code
     context.run(
         f"{docker} sh -c \"cd /source && find . -name '*.py' -not -path '*/migrations/*' | "
@@ -283,7 +283,7 @@ def black(context, nautobot_ver=NAUTOBOT_VER, python_ver=PYTHON_VER):
     DEFAULT_ENV[NAUTOBOT_VER] = nautobot_ver
     DEFAULT_ENV[PYTHON_VER] = python_ver
 
-    docker = f"{COMPOSE_COMMAND} run nautobot"
+    docker = f"{COMPOSE_COMMAND} run --entrypoint='' nautobot "
     context.run(
         f'{docker} sh -c "cd /source && black --check --diff ."',
         env=DEFAULT_ENV,
@@ -303,7 +303,7 @@ def pydocstyle(context, nautobot_ver=NAUTOBOT_VER, python_ver=PYTHON_VER):
     DEFAULT_ENV[NAUTOBOT_VER] = nautobot_ver
     DEFAULT_ENV[PYTHON_VER] = python_ver
 
-    docker = f"{COMPOSE_COMMAND} run nautobot"
+    docker = f"{COMPOSE_COMMAND} run --entrypoint='' nautobot "
     # We exclude the /migrations/ directory since it is autogenerated code
     context.run(
         f"{docker} sh -c \"cd /source && find . -name '*.py' -not -path '*/migrations/*' | xargs pydocstyle\"",
@@ -324,7 +324,7 @@ def bandit(context, nautobot_ver=NAUTOBOT_VER, python_ver=PYTHON_VER):
     DEFAULT_ENV[NAUTOBOT_VER] = nautobot_ver
     DEFAULT_ENV[PYTHON_VER] = python_ver
 
-    docker = f"{COMPOSE_COMMAND} run nautobot"
+    docker = f"{COMPOSE_COMMAND} run --entrypoint='' nautobot "
     context.run(
         f'{docker} sh -c "cd /source && bandit --recursive ./ --configfile .bandit.yml"',
         env=DEFAULT_ENV,


Test patch:
diff --git a/nautobot_golden_config/tests/test_api.py b/nautobot_golden_config/tests/test_api.py
index ae904067..deefcd21 100644
--- a/nautobot_golden_config/tests/test_api.py
+++ b/nautobot_golden_config/tests/test_api.py
@@ -1,28 +1,22 @@
 """Unit tests for nautobot_golden_config."""
 from django.contrib.auth import get_user_model
-from django.test import TestCase
+
 from django.urls import reverse
 from rest_framework import status
-from rest_framework.test import APIClient
 
-from nautobot.users.models import Token
+from nautobot.utilities.testing import APITestCase
+
 
 User = get_user_model()
 
 
-class PlaceholderAPITest(TestCase):
+class GoldenConfigAPITest(APITestCase):  # pylint: disable=too-many-ancestors
     """Test the ConfigCompliance API."""
 
-    def setUp(self):
-        """Create a superuser and token for API calls."""
-        self.user = User.objects.create(username="testuser", is_superuser=True)
-        self.token = Token.objects.create(user=self.user)
-        self.client = APIClient()
-        self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")
-
-    def test_placeholder(self):
+    def test_device_list(self):
         """Verify that devices can be listed."""
         url = reverse("dcim-api:device-list")
-        response = self.client.get(url)
+        self.add_permissions("dcim.view_device")
+        response = self.client.get(url, **self.header)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.data["count"], 0)
diff --git a/nautobot_golden_config/tests/test_graphql.py b/nautobot_golden_config/tests/test_graphql.py
new file mode 100644
index 00000000..6724258f
--- /dev/null
+++ b/nautobot_golden_config/tests/test_graphql.py
@@ -0,0 +1,376 @@
+"""Golden Configuration Plugin GraphQL Testing."""
+
+import uuid
+
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+from django.test.client import RequestFactory
+
+from graphql import get_default_backend
+from graphene_django.settings import graphene_settings
+
+from nautobot.dcim.models import Platform, Site, Device, Manufacturer, DeviceRole, DeviceType
+from nautobot.extras.models import GitRepository
+
+from nautobot_golden_config.models import (
+    ComplianceFeature,
+    ComplianceRule,
+    ConfigCompliance,
+    GoldenConfig,
+    GoldenConfigSetting,
+    ConfigRemove,
+    ConfigReplace,
+)
+
+# Use the proper swappable User model
+User = get_user_model()
+
+GIT_DATA = [
+    {
+        "created": "2021-02-22",
+        "last_updated": "2021-02-23T03:32:46.414Z",
+        "_custom_field_data": {},
+        "name": "backups",
+        "slug": "backups",
+        "remote_url": "https://github.com/nautobot/demo-gc-backups",
+        "branch": "main",
+        "current_head": "c87710902da71e24c1b308a5ac12e33292726e4e",
+        "username": "",
+        "provided_contents": ["nautobot_golden_config.backupconfigs"],
+    },
+    {
+        "created": "2021-02-22",
+        "last_updated": "2021-02-23T03:32:46.868Z",
+        "_custom_field_data": {},
+        "name": "configs",
+        "slug": "configs",
+        "remote_url": "https://github.com/nautobot/demo-gc-generated-configs",
+        "branch": "main",
+        "current_head": "e975bbf3054778bf3f2d971e1b8d100a371b417e",
+        "username": "",
+        "provided_contents": ["nautobot_golden_config.intendedconfigs"],
+    },
+    {
+        "created": "2021-02-22",
+        "last_updated": "2021-02-22T05:01:21.863Z",
+        "_custom_field_data": {},
+        "name": "data",
+        "slug": "data",
+        "remote_url": "https://github.com/nautobot/demo-git-datasource",
+        "branch": "main",
+        "current_head": "f18b081ed8ca28fd7c4a8a3e46ef9cf909e29a57",
+        "username": "",
+        "provided_contents": ["extras.configcontext"],
+    },
+    {
+        "created": "2021-02-22",
+        "last_updated": "2021-02-22T05:01:32.046Z",
+        "_custom_field_data": {},
+        "name": "templates",
+        "slug": "templates",
+        "remote_url": "https://github.com/nautobot/demo-gc-templates",
+        "branch": "main",
+        "current_head": "f62171f19e4c743669120363779340b1b188b0dd",
+        "username": "",
+        "provided_contents": ["nautobot_golden_config.jinjatemplate"],
+    },
+]
+
+
+class TestGraphQLQuery(TestCase):  # pylint: disable=too-many-instance-attributes
+    """Test GraphQL Queries for Golden Config Plugin."""
+
+    def setUp(self):
+        """Setup request and create test data to validate GraphQL."""
+        super().setUp()
+        self.user = User.objects.create(username="Super User", is_active=True, is_superuser=True)
+
+        # Initialize fake request that will be required to execute GraphQL query
+        self.request = RequestFactory().request(SERVER_NAME="WebRequestContext")
+        self.request.id = uuid.uuid4()
+        self.request.user = self.user
+
+        self.backend = get_default_backend()
+        self.schema = graphene_settings.SCHEMA
+
+        manufacturer = Manufacturer.objects.create(name="Manufacturer 1", slug="manufacturer-1")
+        self.devicetype = DeviceType.objects.create(
+            manufacturer=manufacturer, model="Device Type 1", slug="device-type-1"
+        )
+        self.devicerole1 = DeviceRole.objects.create(name="Device Role 1", slug="device-role-1")
+
+        self.site1 = Site.objects.create(name="Site-1", slug="site-1", asn=65000)
+        self.platform1 = Platform.objects.create(
+            name="Platform1",
+            slug="platform1",
+        )
+
+        self.device1 = Device.objects.create(
+            name="Device 1",
+            device_type=self.devicetype,
+            device_role=self.devicerole1,
+            platform=self.platform1,
+            site=self.site1,
+            comments="First Device",
+        )
+
+        for item in GIT_DATA:
+            git_obj = GitRepository.objects.create(**item)
+            git_obj.save()
+
+        GoldenConfigSetting.objects.update(
+            backup_repository=GitRepository.objects.get(
+                provided_contents__contains="nautobot_golden_config.backupconfigs"
+            ),
+            backup_path_template="test/backup",
+            intended_repository=GitRepository.objects.get(
+                provided_contents__contains="nautobot_golden_config.intendedconfigs"
+            ),
+            intended_path_template="test/intended",
+            jinja_repository=GitRepository.objects.get(
+                provided_contents__contains="nautobot_golden_config.jinjatemplate"
+            ),
+            jinja_path_template="{{jinja_path}}",
+            backup_test_connectivity=True,
+            scope={"platform": ["platform1"]},
+            sot_agg_query="{test_model}",
+        )
+
+        self.feature1 = ComplianceFeature.objects.create(
+            name="aaa",
+            description="Test Desc",
+        )
+
+        self.rule1 = ComplianceRule.objects.create(
+            feature=self.feature1,
+            platform=self.platform1,
+            description="Test Desc",
+            config_ordered=True,
+            match_config="aaa ",
+        )
+
+        ConfigCompliance.objects.create(
+            device=self.device1,
+            rule=self.rule1,
+            compliance=True,
+            actual="aaa test",
+            intended="aaa test",
+            missing="",
+            extra="",
+            ordered=False,
+        )
+
+        GoldenConfig.objects.create(
+            device=self.device1,
+            backup_config="interface Eth1/1\ndescription test",
+            intended_config="interface Ethernet1/1\ndescription test",
+            compliance_config="interface Ethernet1/1\ndescription test",
+        )
+
+        ConfigRemove.objects.create(
+            name="Test Removal", platform=self.platform1, description="Test Desc", regex="^.Test.*"
+        )
+
+        ConfigReplace.objects.create(
+            name="Test Replace",
+            platform=self.platform1,
+            description="Test Desc",
+            regex="username\\s+(\\S+)",
+            replace="<redacted>",
+        )
+
+    def execute_query(self, query, variables=None):
+        """Function to execute a GraphQL query."""
+        document = self.backend.document_from_string(self.schema, query)
+        if variables:
+            return document.execute(context_value=self.request, variable_values=variables)
+        return document.execute(context_value=self.request)
+
+    def test_query_config_compliance(self):
+        """Test GraphQL Config Compliance Model."""
+        query = """
+            query {
+                config_compliances {
+                    device {
+                        name
+                    }
+                    rule {
+                      feature {
+                        name
+                      }
+                    }
+                    compliance
+                    actual
+                    intended
+                    missing
+                    extra
+                    ordered
+                    }
+                }
+        """
+        response_data = {
+            "config_compliances": [
+                {
+                    "actual": "aaa test",
+                    "compliance": True,
+                    "device": {"name": "Device 1"},
+                    "extra": "",
+                    "rule": {"feature": {"name": "aaa"}},
+                    "intended": "aaa test",
+                    "missing": "",
+                    "ordered": False,
+                }
+            ]
+        }
+        result = self.execute_query(query)
+        self.assertEqual(result.data, response_data)
+        self.assertEqual(len(result.data["config_compliances"]), 1)
+
+    def test_query_golden_config(self):
+        """Test Configuration management Model."""
+        query = """
+            query {
+                golden_configs {
+                    device {
+                        name
+                    }
+                    backup_config
+                    intended_config
+                    compliance_config
+                    }
+                }
+        """
+        # Need to figure out how to execute a mock Job.
+        response_data = {
+            "golden_configs": [
+                {
+                    "device": {"name": "Device 1"},
+                    "backup_config": "interface Eth1/1\ndescription test",
+                    "intended_config": "interface Ethernet1/1\ndescription test",
+                    "compliance_config": "interface Ethernet1/1\ndescription test",
+                }
+            ]
+        }
+        result = self.execute_query(query)
+        self.assertEqual(result.data, response_data)
+        self.assertEqual(len(result.data["golden_configs"]), 1)
+
+    def test_query_compliance_rule(self):
+        """Test Configuration Compliance Details Model."""
+        query = """
+            query {
+                compliance_rules {
+                    feature {
+                      name
+                    }
+                    platform {
+                        name
+                    }
+                    description
+                    config_ordered
+                    match_config
+                }
+            }
+        """
+        response_data = {
+            "compliance_rules": [
+                {
+                    "feature": {"name": "aaa"},
+                    "platform": {"name": "Platform1"},
+                    "description": "Test Desc",
+                    "config_ordered": True,
+                    "match_config": "aaa ",
+                }
+            ]
+        }
+        result = self.execute_query(query)
+        self.assertEqual(result.data, response_data)
+        self.assertEqual(len(result.data["compliance_rules"]), 1)
+
+    def test_query_golden_config_setting(self):
+        """Test GraphQL Golden Config Settings Model."""
+        query = """
+            query {
+                golden_config_settings {
+                    backup_path_template
+                    intended_path_template
+                    jinja_path_template
+                    backup_test_connectivity
+                    sot_agg_query
+                }
+            }
+        """
+        response_data = {
+            "golden_config_settings": [
+                {
+                    "backup_path_template": "test/backup",
+                    "intended_path_template": "test/intended",
+                    "jinja_path_template": "{{jinja_path}}",
+                    "backup_test_connectivity": True,
+                    "sot_agg_query": "{test_model}",
+                }
+            ]
+        }
+        result = self.execute_query(query)
+        self.assertEqual(result.data, response_data)
+        self.assertEqual(len(result.data["golden_config_settings"]), 1)
+
+    def test_query_line_removal(self):
+        """Test Regex Line Removal."""
+        query = """
+            query {
+                config_removes {
+                    name
+                    platform {
+                        name
+                    }
+                    description
+                    regex
+                }
+            }
+        """
+
+        response_data = {
+            "config_removes": [
+                {
+                    "name": "Test Removal",
+                    "platform": {"name": "Platform1"},
+                    "description": "Test Desc",
+                    "regex": "^.Test.*",
+                }
+            ]
+        }
+        result = self.execute_query(query)
+        self.assertEqual(len(result.data["config_removes"]), 1)
+        self.assertEqual(result.data, response_data)
+
+    def test_query_line_replace(self):
+        """Test Line Replacement Model."""
+        query = """
+            query {
+                config_replaces {
+                    name
+                    platform {
+                        name
+                    }
+                    description
+                    regex
+                    replace
+                }
+            }
+        """
+
+        response_data = {
+            "config_replaces": [
+                {
+                    "name": "Test Replace",
+                    "platform": {"name": "Platform1"},
+                    "description": "Test Desc",
+                    "regex": "username\\s+(\\S+)",
+                    "replace": "<redacted>",
+                }
+            ]
+        }
+        result = self.execute_query(query)
+        self.assertEqual(len(result.data["config_replaces"]), 1)
+        self.assertEqual(result.data, response_data)
diff --git a/nautobot_golden_config/tests/test_models.py b/nautobot_golden_config/tests/test_models.py
new file mode 100644
index 00000000..d99407b2
--- /dev/null
+++ b/nautobot_golden_config/tests/test_models.py
@@ -0,0 +1,118 @@
+"""Unit tests for nautobot_golden_config models."""
+
+from django.test import TestCase
+from django.core.exceptions import ValidationError
+
+from nautobot.dcim.models import Platform
+
+from nautobot_golden_config.models import (
+    GoldenConfigSetting,
+    ConfigRemove,
+    ConfigReplace,
+)
+
+
+class ConfigComplianceModelTestCase(TestCase):
+    """Test ConfigCompliance Model."""
+
+
+class GoldenConfigTestCase(TestCase):
+    """Test GoldenConfig Model."""
+
+
+class ComplianceRuleTestCase(TestCase):
+    """Test ComplianceRule Model."""
+
+
+class GoldenConfigSettingModelTestCase(TestCase):
+    """Test GoldenConfigSetting Model."""
+
+    def setUp(self):
+        """Get the golden config settings with the only allowed id."""
+        self.global_settings = GoldenConfigSetting.objects.first()
+
+    def test_bad_graphql_query(self):
+        """Invalid graphql query."""
+        self.global_settings.sot_agg_query = 'devices(name:"ams-edge-01")'
+        with self.assertRaises(ValidationError):
+            self.global_settings.clean()
+
+    def test_good_graphql_query_invalid_starts_with(self):
+        """Valid graphql query, however invalid in the usage with golden config plugin."""
+        self.global_settings.sot_agg_query = '{devices(name:"ams-edge-01"){id}}'
+        with self.assertRaises(ValidationError) as error:
+            self.global_settings.clean()
+        self.assertEqual(error.exception.message, "The GraphQL query must start with exactly `query ($device_id: ID!)`")
+
+    def test_good_graphql_query_validate_starts_with(self):
+        """Ensure clean() method returns None when valid query is sent through."""
+        self.global_settings.sot_agg_query = "query ($device_id: ID!) {device(id: $device_id) {id}}"
+        self.assertEqual(self.global_settings.clean(), None)
+
+
+class ConfigRemoveModelTestCase(TestCase):
+    """Test ConfigRemove Model."""
+
+    def setUp(self):
+        """Setup Object."""
+        self.platform = Platform.objects.create(slug="cisco_ios")
+        self.line_removal = ConfigRemove.objects.create(
+            name="foo", platform=self.platform, description="foo bar", regex="^Back.*"
+        )
+
+    def test_add_line_removal_entry(self):
+        """Test Add Object."""
+        self.assertEqual(self.line_removal.name, "foo")
+        self.assertEqual(self.line_removal.description, "foo bar")
+        self.assertEqual(self.line_removal.regex, "^Back.*")
+
+    def test_edit_line_removal_entry(self):
+        """Test Edit Object."""
+        new_name = "Line Remove"
+        new_desc = "Testing Remove Running Config Line"
+        new_regex = "^Running.*"
+        self.line_removal.name = new_name
+        self.line_removal.description = new_desc
+        self.line_removal.regex = new_regex
+        self.line_removal.save()
+
+        self.assertEqual(self.line_removal.name, new_name)
+        self.assertEqual(self.line_removal.description, new_desc)
+        self.assertEqual(self.line_removal.regex, new_regex)
+
+
+class ConfigReplaceModelTestCase(TestCase):
+    """Test ConfigReplace Model."""
+
+    def setUp(self):
+        """Setup Object."""
+        self.platform = Platform.objects.create(slug="cisco_ios")
+        self.line_replace = ConfigReplace.objects.create(
+            name="foo",
+            platform=self.platform,
+            description="foo bar",
+            regex=r"username(\S+)",
+            replace="<redacted>",
+        )
+
+    def test_add_line_replace_entry(self):
+        """Test Add Object."""
+        self.assertEqual(self.line_replace.name, "foo")
+        self.assertEqual(self.line_replace.description, "foo bar")
+        self.assertEqual(self.line_replace.regex, r"username(\S+)")
+        self.assertEqual(self.line_replace.replace, "<redacted>")
+
+    def test_edit_line_replace_entry(self):
+        """Test Edit Object."""
+        new_name = "Line Replacement"
+        new_desc = "Testing Replacing Config Line"
+        new_regex = r"password(\S+)"
+        self.line_replace.name = new_name
+        self.line_replace.description = new_desc
+        self.line_replace.regex = new_regex
+        self.line_replace.save()
+
+        self.assertEqual(self.line_replace.name, new_name)
+        self.assertEqual(self.line_replace.description, new_desc)
+        self.assertEqual(self.line_replace.regex, new_regex)
+        self.assertEqual(self.line_replace.replace, "<redacted>")
diff --git a/nautobot_golden_config/tests/test_nornir_plays/__init__.py b/nautobot_golden_config/tests/test_nornir_plays/__init__.py
new file mode 100644
index 00000000..875f3c49
--- /dev/null
+++ b/nautobot_golden_config/tests/test_nornir_plays/__init__.py
@@ -0,0 +1,1 @@
+"""Unit tests for nautobot_golden_config nornir_plays."""
diff --git a/nautobot_golden_config/tests/test_nornir_plays/test_config_compliance.py b/nautobot_golden_config/tests/test_nornir_plays/test_config_compliance.py
new file mode 100644
index 00000000..c1511fee
--- /dev/null
+++ b/nautobot_golden_config/tests/test_nornir_plays/test_config_compliance.py
@@ -0,0 +1,23 @@
+"""Unit tests for nautobot_golden_config nornir compliance."""
+
+import unittest
+from unittest.mock import patch, Mock
+from nautobot_golden_config.nornir_plays.config_compliance import get_features
+
+
+class ConfigComplianceTest(unittest.TestCase):
+    """Test Nornir Compliance Task."""
+
+    @patch("nautobot_golden_config.nornir_plays.config_compliance.ComplianceRule", autospec=True)
+    def test_get_features(self, mock_compliance_rule):
+        """Test proper return when Features are returned."""
+        features = {"config_ordered": "test_ordered", "match_config": "aaa\nsnmp\n"}
+        mock_obj = Mock(**features)
+        mock_obj.name = "test_name"
+        mock_obj.platform = Mock(slug="test_slug")
+        mock_compliance_rule.objects.all.return_value = [mock_obj]
+        features = get_features()
+        mock_compliance_rule.objects.all.assert_called_once()
+        self.assertEqual(
+            features, {"test_slug": [{"obj": mock_obj, "ordered": "test_ordered", "section": ["aaa", "snmp"]}]}
+        )
diff --git a/nautobot_golden_config/tests/test_utilities/__init__.py b/nautobot_golden_config/tests/test_utilities/__init__.py
new file mode 100644
index 00000000..2b6ce678
--- /dev/null
+++ b/nautobot_golden_config/tests/test_utilities/__init__.py
@@ -0,0 +1,1 @@
+"""Unit tests for nautobot_golden_config utilities."""
diff --git a/nautobot_golden_config/tests/test_utilities/test_git.py b/nautobot_golden_config/tests/test_utilities/test_git.py
new file mode 100644
index 00000000..f9c9ec35
--- /dev/null
+++ b/nautobot_golden_config/tests/test_utilities/test_git.py
@@ -0,0 +1,33 @@
+"""Unit tests for nautobot_golden_config utilities git."""
+
+import unittest
+from unittest.mock import patch, Mock
+from nautobot_golden_config.utilities.git import GitRepo
+
+
+class GitRepoTest(unittest.TestCase):
+    """Test Git Utility."""
+
+    def setUp(self):
+        """Setup a reusable mock object to pass into GitRepo."""
+        mock_obj = Mock()
+        mock_obj.filesystem_path = "/fake/path"
+        mock_obj.remote_url = "/fake/remote"
+        mock_obj._token = "fake token"  # pylint: disable=protected-access
+        self.mock_obj = mock_obj
+
+    @patch("nautobot_golden_config.utilities.git.Repo", autospec=True)
+    def test_gitrepo_path_noexist(self, mock_repo):
+        """Test Repo is not called when path isn't valid, ensure clone is called."""
+        GitRepo(self.mock_obj)
+        mock_repo.assert_not_called()
+        mock_repo.clone_from.assert_called_with("/fake/remote", to_path="/fake/path")
+
+    @patch("nautobot_golden_config.utilities.git.os")
+    @patch("nautobot_golden_config.utilities.git.Repo", autospec=True)
+    def test_gitrepo_path_exist(self, mock_repo, mock_os):
+        """Test Repo is not called when path is valid, ensure Repo is called."""
+        mock_os.path.isdir.return_value = True
+        GitRepo(self.mock_obj)
+        mock_repo.assert_called_once()
+        mock_repo.assert_called_with(path="/fake/path")
diff --git a/nautobot_golden_config/tests/test_utilities/test_graphql.py b/nautobot_golden_config/tests/test_utilities/test_graphql.py
new file mode 100644
index 00000000..db3844c0
--- /dev/null
+++ b/nautobot_golden_config/tests/test_utilities/test_graphql.py
@@ -0,0 +1,29 @@
+"""Unit tests for nautobot_golden_config utilities graphql."""
+
+from unittest.mock import patch
+from unittest import skip
+
+from nautobot.utilities.testing import TestCase
+from nautobot.dcim.models import Device
+from nautobot_golden_config.utilities.graphql import graph_ql_query
+
+# pylint: disable=no-self-use
+
+
+class GraphQLTest(TestCase):
+    """Test for the GraphQL Queries."""
+
+    @skip("Update to accomodate uuid vs device")
+    @patch("nautobot_golden_config.utilities.graphql.graph_ql_query")
+    def test_graph_ql_query(self, mock):
+        """Make sure graph_ql_query is called correctly."""
+        mock("request", Device.objects.get(name="ams-edge-01"), '{devices(name:"ams-edge-01"){id}}')
+        mock.assert_called_with("request", Device.objects.get(name="ams-edge-01"), '{devices(name:"ams-edge-01"){id}}')
+
+    @skip("Update to accomodate uuid vs device")
+    def test_bad_graph_ql_query_syntax(self):
+        """Ensure invalid GraphQL query results with Error."""
+        result = graph_ql_query("request", Device.objects.get(name="ams-edge-01"), "not valid query")
+        self.assertEqual(result[0], 400)
+        self.assertTrue(result[1]["error"])
+        self.assertRegex(result[1].get("error"), r"Syntax Error GraphQL.*")
diff --git a/nautobot_golden_config/tests/test_utilities/test_helpers.py b/nautobot_golden_config/tests/test_utilities/test_helpers.py
new file mode 100644
index 00000000..9304bf3f
--- /dev/null
+++ b/nautobot_golden_config/tests/test_utilities/test_helpers.py
@@ -0,0 +1,53 @@
+"""Unit tests for nautobot_golden_config utilities helpers."""
+
+import unittest
+from unittest.mock import patch, Mock
+from nornir_nautobot.exceptions import NornirNautobotException
+from jinja2.exceptions import TemplateError
+from nautobot_golden_config.utilities.helper import (
+    null_to_empty,
+    check_jinja_template,
+)
+
+# pylint: disable=no-self-use
+
+
+class HelpersTest(unittest.TestCase):
+    """Test Helper Functions."""
+
+    def test_null_to_empty_null(self):
+        """Ensure None returns with empty string."""
+        result = null_to_empty(None)
+        self.assertEqual(result, "")
+
+    def test_null_to_empty_val(self):
+        """Ensure if not None input is returned."""
+        result = null_to_empty("test")
+        self.assertEqual(result, "test")
+
+    def test_check_jinja_template_success(self):
+        """Simple success test to return template."""
+        worker = check_jinja_template("obj", "logger", "fake-template-name")
+        self.assertEqual(worker, "fake-template-name")
+
+    def test_check_jinja_template_exceptions_undefined(self):
+        """Use fake obj key to cause UndefinedError from Jinja2 Template."""
+        log_mock = Mock()
+        with self.assertRaises(NornirNautobotException):
+            check_jinja_template("test-obj", log_mock, "{{ obj.fake }}")
+
+    def test_check_jinja_template_exceptions_syntaxerror(self):
+        """Use invalid templating to cause TemplateSyntaxError from Jinja2 Template."""
+        log_mock = Mock()
+        with self.assertRaises(NornirNautobotException):
+            check_jinja_template("test-obj", log_mock, "{{ obj.fake }")
+
+    @patch("nautobot_golden_config.utilities.helper.Template")
+    def test_check_jinja_template_exceptions_templateerror(self, template_mock):
+        """Cause issue to cause TemplateError form Jinja2 Template."""
+        log_mock = Mock()
+        with self.assertRaises(NornirNautobotException):
+            template_mock.side_effect = TemplateError
+            template_render = check_jinja_template("test-obj", log_mock, "template")
+            self.assertEqual(template_render, TemplateError)
+            template_mock.assert_called_once()