Repository: CiscoDevNet/virl2-client (language: python)

Issue:
SIMPLE-6429 fix potentially mishandled data structures



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index dc3604f..6f5eaae 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -6,9 +6,11 @@ on:
 
   # Triggers the workflow on push or pull request events but only for the master branch
   push:
-    branches: [ master ]
+    branches:
+      - master
+      - dev
   pull_request:
-    branches: [ master ]
+    branches: [master]
 
   # Allows you to run this workflow manually from the Actions tab
   workflow_dispatch:
@@ -19,7 +21,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.7, 3.8, 3.9]
+        python-version: [3.8, 3.9]
 
     steps:
       - uses: actions/checkout@v2
@@ -30,9 +32,7 @@ jobs:
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          # pip install flake8 pytest
-          # if [ -f tests/requirements.txt ]; then pip install -r tests/requirements.txt; fi
-          pip install -r tests/requirements.txt
+          if [ -f tests/requirements.txt ]; then pip install -r tests/requirements.txt; fi
       - name: Lint with flake8
         run: |
           # stop the build if there are Python syntax errors or undefined names
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4b7fb14..f12e74a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,14 +3,14 @@
 #
 
 repos:
-- repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.1.0
-  hooks:
-    - id: trailing-whitespace
-    - id: end-of-file-fixer
-    - id: check-added-large-files
-- repo: https://github.com/psf/black
-  rev: 22.1.0
-  hooks:
-    - id: black
-      language_version: python3
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.1.0
+    hooks:
+      - id: trailing-whitespace
+      - id: end-of-file-fixer
+      - id: check-added-large-files
+  - repo: https://github.com/psf/black
+    rev: 22.3.0
+    hooks:
+      - id: black
+        language_version: python3
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..876f22d
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,20 @@
+# Changes
+
+Lists the changes in the package as they relate to controller API changes
+or functional changes in the package API.  This was started with 2.4.1.
+
+## Version 2.4.1
+
+### import_lab()
+
+This PCL function imports an existing topology from a string.  With the new
+schema and the use of UUIDs which was introduced in 2.3, the export format of
+labs has also changed:
+
+- interfaces are now stored with the nodes and not in a global list anymore
+- specific attributes of nodes/interfaces are not stored in a "data" dictionary
+  anymore but with the node/interface itself
+
+Importing / exporting such a lab will use the new representation.  Going
+forward, the backward compatibility will be removed with 2.5.  At the moment,
+deprecation warnings will be printed in these cases.
diff --git a/Makefile b/Makefile
index 21d0982..3b24aba 100644
--- a/Makefile
+++ b/Makefile
@@ -1,13 +1,14 @@
 
 .phony: export diff
 
+# https://github.com/python-poetry/poetry/issues/3472
 # https://github.com/python-poetry/poetry/issues/3160
 # when resolved, we should be able to run with hashes
 tests/requirements.txt: poetry.lock
 	poetry export --format=requirements.txt --dev --without-hashes --output=$@
 
 clean:
-	rm -rf dist virl2_client.egg-info .built .pytest_cache
+	rm -rf dist virl2_client.egg-info .built .pytest_cache .coverage coverage.xml
 	find . -depth -type f -name '*.pyc' -exec rm {} \; || true
 	find . -depth -type d -name '__pycache__' -exec rmdir {} \; || true
 	cd docs && make clean
diff --git a/README.md b/README.md
index 62807e0..e1fa3e9 100644
--- a/README.md
+++ b/README.md
@@ -2,23 +2,30 @@
 
 # VIRL 2 Client Library
 
+> **Note:** The product has been renamed from *VIRL* to *Cisco Modeling Labs* /
+> CML 2.  References to VIRL still exist in the product documentation and within
+> code or examples.
+>
+> The name of the package itself has not been changed.  Throughout the
+> documentation it is referred to as "virl2_client",  "Python Client Library" or
+> "PCL".
+
 ## Introduction
 
-This is the client library for the Cisco VIRL 2 Network Simulation Platform
+This is the client library for the *Cisco Modeling Labs* Platform
 (`virl2_client`). It provides a Python package to programmatically create,
-edit, delete and control network simulations on a VIRL 2 controller.
+edit, delete and control network simulations on a CML 2 controller.
 
-It is a pure Python implementation that requires Python3. We've tested and
+It is a pure Python implementation that requires Python 3. We've tested and
 written the package with Python 3.8.10.
 
-The **status** of this package can be considered **Beta**. We're not aware of
-any major issues at the time of release. However, since this is the first
-release of the package, bugs might exist. Both in the package as well as in
-the API implementation on the controller.
+The status of the package can be considered **stable**.  Issues with the
+software should be raised via the [GitHub issue
+tracker](https://github.com/CiscoDevNet/virl2-client/issues).
 
 ## Use Case Description
 
-The client library provides a convenient interface to control the lifecycle of
+The client library provides a convenient interface to control the life-cycle of
 a network simulation. This can be used for automation scripts directly in
 Python but also for third party integrations / plugins which need to integrate
 with a simulated network. Examples already existing are an [Ansible
@@ -26,18 +33,27 @@ plugin](https://github.com/CiscoDevNet/ansible-virl).
 
 ## Installation
 
-The package comes in form of a wheel that is downloadable from the VIRL 2
-controller. The package can be installed either from PyPi using
+The package comes in form of a wheel that is downloadable from the CML
+2 controller. The package can be installed either from PyPI using
 
     pip3 install virl2_client
 
-or, alternatively, the version that is bundled with the VIRL 2 controller can
+If you want to interact with devices via the client library, you need to
+also install the pyATS library. This can be achieved in one go using
+
+```
+pip3 install "virl2_client[pyats]"
+```
+
+Note that this does *not* pull in the full pyATS package... See below how that is achieved.
+
+or, alternatively, the version that is bundled with the CML 2 controller can
 be downloaded to the local filesystem and then directly installed via
 
     pip3 install ./virl2_client-*.whl
 
 The bundled version is available on the index site of the docs when viewed
-directly on the VIRL 2 controller.
+directly on the CML 2 controller.
 
 Ensure to replace use the correct file name, replacing the wildcard with the
 proper version/build information. For example
@@ -46,29 +62,39 @@ proper version/build information. For example
 
 We recommend the use of a virtual environment for installation.
 
-If you want to interact with devices via the client library, you need to
-install pyATS library.
+If you require the full version of the pyATS library including things like Genie
+then you need to do this in a subsequent step like shown here:
+
+    pip3 install "pyats[full]"
 
-    pip3 install pyats
+> **IMPORTANT**: The version of the Python client library  must be compatible
+> with the version of the controller.  If you are running an older controller
+> version then it's likely that the latest client library version from PyPI can
+> **not** be used.  In this case, you need to either use the version available
+> from the controller itself or by specifying a version constraint.
+>
+> Example: When on a controller version 2.2.x, then you'd need to install with
+> `pip3 install "virl2-client<2.3.0"`. This will ensure that the version
+> installed is compatible with 2.2.x.
 
 ## Usage
 
-The package itself is fairly well documented using docstrings. In addition, the
+The package itself is fairly well documented using *docstrings*. In addition, the
 documentation is available in HTML format on the controller itself, via the
 "Tools -> Client Library" menu.
 
 ## Compatibility
 
-This package and the used API is specific to VIRL 2. It is not
+This package and the used API is specific to CML 2. It is not
 backwards compatible with VIRL 1.x and therefore can not be used with VIRL
 1.x. If you are looking for a convenient tool to interface with the VIRL 1 API
-then the [VIRL Utils tool](https://github.com/CiscoDevNet/virlutils) is
+then the [CML Utils tool](https://github.com/CiscoDevNet/virlutils) is
 recommended.
 
 ## Known Issues
 
-There are no known issues at this point. See the comment in the *Introduction*
-section.
+There are no major known issues at this point. See the comment in the *Introduction*
+section.  Also, see the *Issues* section in GitHub to learn about known issues or raise new ones, if needed.  Also see [CHANGES](CHANGES.md).
 
 ## Getting Help
 
@@ -89,6 +115,6 @@ details.
 
 ## References
 
-This package is part of the VIRL 2 Network Simulation platform. For details, go
-to https://developer.cisco.com/modeling-labs. Additional documentation for the
-product is available at https://developer.cisco.com/docs/modeling-labs
+This package is part of the CML 2 Network Simulation platform. For details, go
+to <https://developer.cisco.com/modeling-labs>. Additional documentation for the
+product is available at <https://developer.cisco.com/docs/modeling-labs>
diff --git a/docs/Makefile b/docs/Makefile
index b755c18..eac712e 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -28,6 +28,6 @@ clean:
 
 buildapi:
 	rm -rf $(SOURCEDIR)/api/*
-	sphinx-apidoc --templatedir template -fMT ../virl2_client -o $(SOURCEDIR)/api
+	sphinx-apidoc --templatedir template -fMT -o $(SOURCEDIR)/api ../virl2_client
 	@echo "Auto-generation of API documentation finished. " \
 	      "The generated files are in '$(SOURCEDIR)/api/'"
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 5aa1220..a50d804 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -46,7 +46,11 @@
 # ones.
 # See this reference here:
 # https://brendanhasz.github.io/2019/01/05/sphinx.html
-extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"]
+extensions = [
+    "sphinx.ext.autodoc",
+    # "autoapi.extension",
+    "sphinx.ext.napoleon",
+]
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ["_templates"]
@@ -65,7 +69,7 @@
 #
 # This is also used if you do content translation via gettext catalogs.
 # Usually you set "language" from the command line for these cases.
-language = None
+language = "en"
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
@@ -74,7 +78,7 @@
 exclude_patterns = ["_build", "source/api/virl2_client.rst"]
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = None
+pygments_style = "material"
 
 
 # -- Options for HTML output -------------------------------------------------
@@ -93,7 +97,7 @@
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ["_static"]
+# html_static_path = ["_static"]
 
 # Custom sidebar templates, must be a dictionary that maps document names
 # to template names.
@@ -184,3 +188,31 @@
 
 # A list of files that should not be packed into the epub file.
 epub_exclude_files = ["search.html"]
+
+# https://sphinx-hxl.readthedocs.io/en/stable/usage/extensions/autodoc.html
+autodoc_default_options = {
+    # 'members': 'var1, var2',
+    # 'member-order': 'bysource',
+    "member-order": "groupwise",
+    "special-members": "__init__",
+    # "ignore-module-all": None,
+    # 'undoc-members': None,
+    # 'exclude-members': '__weakref__'
+}
+
+# If we have the PCL documented with type hints, then this might
+# be the preferred way to generate the documentation.
+# This requires sphinx-autoapi to be installed.
+# https://sphinx-autoapi.readthedocs.io/en/latest/index.html
+# Also, moving to Markdown might be an option going forward, removing
+# the need for ReST...
+# https://docs.readthedocs.io/en/stable/intro/getting-started-with-sphinx.html#using-markdown-with-sphinx
+# https://myst-parser.readthedocs.io/en/latest/index.html
+# rschmied July 2022
+#
+# autoapi_modules = {"virl2_client": {"output": "autogenerated"}}
+# autoapi_dirs = [
+#     "../../virl2_client",
+# ]
+# autoapi_root = "api"
+# autodoc_typehints = "description"
diff --git a/docs/source/examples.rst b/docs/source/examples.rst
index 9d59758..ceb01db 100644
--- a/docs/source/examples.rst
+++ b/docs/source/examples.rst
@@ -65,7 +65,7 @@ Stopping all the labs
 
 This snippet loops over all labs and stops them::
 
-    for lab in client_library.all_labs():
+    for lab in client.all_labs():
         lab.stop()
 
 Getting all lab names
@@ -73,7 +73,7 @@ Getting all lab names
 
 Get a list of all the lab names the user owns::
 
-    all_labs_names = [lab.name for lab in client_library.all_labs()]
+    all_labs_names = [lab.title for lab in client.all_labs()]
 
 Stopping all labs of a User
 ---------------------------
@@ -81,12 +81,12 @@ Stopping all labs of a User
 The following code loops over all labs the user owns, stops the lab,
 wipes the lab and then removes the lab from the controller::
 
-    lab_list = client_library.get_lab_list()
+    lab_list = client.get_lab_list()
     for lab_id in lab_list:
-        lab = client_library.join_existing_lab(lab_id)
+        lab = client.join_existing_lab(lab_id)
         lab.stop()
         lab.wipe()
-        client_library.remove_lab(lab_id)
+        client.remove_lab(lab_id)
 
 
 Uploading an image disk file
@@ -128,7 +128,7 @@ and is interactive::
 
     # this assumes that there's exactly one lab with this title
     our_lab = client.find_labs_by_title('my_lab')[0]
-    xr_node = our_lab.get_node_by_label('pe2')
+    iosv_node = our_lab.get_node_by_label('iosv-0')
 
     # open the Netmiko connection via the terminal server
     # (SSH to the controller connects to the terminal server)
@@ -141,7 +141,7 @@ and is interactive::
     c.write_channel('\r')
 
     # open the connection to the console
-    c.write_channel(f'open /{our_lab.id}/{xr_node.id}/0\r')
+    c.write_channel(f'open /{our_lab.title}/{iosv_node.label}/0\r')
 
     # router login
     # this makes an assumption that it's required to login
@@ -149,8 +149,8 @@ and is interactive::
     c.write_channel(LAB_USERNAME + '\r')
     c.write_channel(LAB_PASSWORD + '\r')
 
-    # switch to Cisco XR mode
-    netmiko.redispatch(c, device_type='cisco_xr')
+    # switch to Cisco IOS mode
+    netmiko.redispatch(c, device_type='cisco_ios')
     c.find_prompt()
 
     # get the list of interfaces
@@ -158,15 +158,19 @@ and is interactive::
     print(result)
 
     # create the keys
+    c.write_channel('enable\r')
+    c.write_channel('configure terminal\r')
     result = c.send_command('crypto key generate rsa',
-                            expect_string='How many bits in the modul us \[2048\]\: ')
+                            expect_string='How many bits in the modulus \[512\]\: ')
     print(result)
 
     # send the key length
     c.write_channel('2048\n')
 
     # retrieve the result
-    result = c.send_command('show crypto key mypubkey rsa')
+    c.write_channel('exit\r')
+    c.write_channel('disable\r')
+    result = c.send_command_timing('show crypto key mypubkey rsa', last_read=2.0)
     print(result)
 
 
@@ -379,7 +383,7 @@ should be removed::
     print()
     lnum = 0
     while lnum < 1 or lnum > i:
-        lnum = input("enter link number to condition (1-{}): ".format(i))
+        lnum = input("enter link number to condition (1-{}): ".format(i-1))
         try:
             lnum = int(lnum)
         except ValueError:
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 00ef1f7..9376369 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -1,11 +1,11 @@
 .. virl2_client documentation master file, created by
 
-VIRL :sup:`2` API Client Documentation
-=======================================
+CML 2 API Client Documentation
+==============================
 
-This is the VIRL :sup:`2` client library (`virl2_client`). It provides a Python
-package to programmatically create, edit, delete and control network simulations
-on a VIRL :sup:`2` controller.
+This is the CML 2 client library (`virl2_client`). It provides a Python package
+to programmatically create, edit, delete and control network simulations on a
+CML 2 controller.
 
 .. toctree::
     :maxdepth: 2
@@ -23,8 +23,9 @@ on a VIRL :sup:`2` controller.
 
     `Download the client library (Linux/Win/macOS) </client>`_
 
-    **Note** that the above link *only* works when the documentation is viewed
-    on the VIRL :sup:`2` controller.
+    .. note::
+        That the above link *only* works when the documentation is viewed
+        on the CML 2 controller.
 
 Indices and tables
 ==================
diff --git a/docs/source/intro.rst b/docs/source/intro.rst
index 0324abf..06df8db 100644
--- a/docs/source/intro.rst
+++ b/docs/source/intro.rst
@@ -1,45 +1,80 @@
 Introduction
 =============
 
-This is the client library for the Cisco VIRL :sup:`2` Network Simulation Platform
-(``virl2_client``). It provides a Python package to programmatically create, edit,
-delete and control network simulations on a VIRL :sup:`2` controller.
+.. note::
+    The product has been renamed from *VIRL* to *Cisco Modeling Labs* /
+    CML 2. References to VIRL still exist in the product documentation and within
+    code or examples.
 
-It is a pure Python implementation that requires Python3. We've written and tested
-the package with Python 3.6.8.
+    The name of the package itself has not been changed. Throughout the
+    documentation it is referred to as "virl2_client", "Python Client Library"
+    or "PCL".
 
-The **status** of this package can be considered **Beta**. We're not aware of
-any major issues at the time of release. However, since this is the first
-release of the package, bugs might exist. Both in the package as well as in the
-API implementation on the controller.
+This is the client library for the Cisco Modeling Labs Platform
+(``virl2_client``). It provides a Python package to programmatically create,
+edit, delete and control network simulations on a CML 2 controller.
+
+It is a pure Python implementation that requires Python 3. We've written and
+tested the package with Python 3.8.10.
+
+The status of the package can be considered **stable**.  Issues with the software
+should be raised via the `GitHub issue tracker <https://github.com/CiscoDevNet/virl2-client/issues>`_.
 
 Use Case Description
 *********************
 
 The client library provides a convenient interface to control the lifecycle of a
-network simulation running on VIRL2. This can be used for automation scripts
-directly in Python but also for third party integrations / plugins which need to
-integrate with a simulated network. Examples already existing are an `Ansible
-plugin <https://github.com/CiscoDevNet/ansible-virl/>`_.
+network simulation. This can be used for automation scripts directly in Python
+but also for third party integrations / plugins which need to integrate with a
+simulated network. Examples already existing are an `Ansible plugin
+<https://github.com/CiscoDevNet/ansible-virl/>`_.
 
 Installation
 *************
 
-The package comes in form of a wheel that is downloadable from the VIRL
-controller. The package can be installed either from PyPi using::
+The package comes in form of a wheel that is downloadable from the CML 2
+controller. The package can be installed either from PyPI using::
+
+    pip3 install virl2_client
 
-    pip install virl2_client
+If you want to interact with devices via the client library, you need to
+also install the pyATS library. This can be achieved in one go using::
 
-or, alternatively, the version that is bundled with the VIRL :sup:`2` controller
-can be downloaded to the local filesystem and then directly installed via::
+    pip3 install "virl2_client[pyats]"
 
-    pip install ./virl2_client-*.whl
+Note that this does *not* pull in the full pyATS package... See below how that is achieved.
+
+Alternatively, the version that is bundled with the CML 2 controller can be
+downloaded to the local filesystem and then directly installed via::
+
+    pip3 install ./virl2_client-*.whl
 
 The bundled version is available on the index site of the docs when
-viewed directly on the VIRL :sup:`2` controller.
+viewed directly on the CML 2 controller.
+
+Ensure to replace use the correct file name, replacing the wildcard with the
+proper version/build information. For example::
+
+    pip3 install virl2_client-2.0.0b10-py3-none-any.whl
 
 We recommend the use of a virtual environment for installation.
 
+If you require the full version of the pyATS library including things like Genie
+then you need to do this in a subsequent step like shown here::
+
+    pip3 install "pyats[full]"
+
+.. warning::
+    The version of the Python client library must be compatible with the version
+    of the controller.  If you are running an older controller version then it's
+    likely that the latest client library version from PyPI can **not** be used.
+    In this case, you need to either use the version available from the
+    controller itself or by specifying a version constraint.
+
+    Example: When on a controller version 2.2.x, then you'd need to install with
+    ``pip3 install "virl2-client<2.3.0"``. This will ensure that the version
+    installed is compatible with 2.2.x.
+
 Usage
 ******
 
@@ -54,16 +89,17 @@ and the ``Lab`` :class:`virl2_client.models.lab.Lab` class.
 Compatibility
 **************
 
-This package and the used API is specific to VIRL :sup:`2`. It is not backwards
-compatible with VIRL 1.x and therefore can not be used with VIRL 1.x. If you
-are looking for a convenient tool to interface with the VIRL 1 API then the
-`VIRL Utils tool <https://github.com/CiscoDevNet/virlutils>`_ is recommended.
+This package and the used API is specific to CML 2. It is not backwards
+compatible with VIRL 1.x and therefore can not be used with VIRL 1.x. If you are
+looking for a convenient tool to interface with the VIRL 1 API then the `VIRL
+Utils tool <https://github.com/CiscoDevNet/virlutils>`_ is recommended.
 
 Known Issues
 *************
 
-There are no known issues at this point. See the comment in the *Introduction*
-section.
+There are no major known issues at this point. See the comment in the
+*Introduction* section. Also, see the *Issues* section in GitHub to learn about
+known issues or raise new ones, if needed.  Also see ``CHANGES.md``.
 
 Getting Help
 *************
@@ -76,7 +112,7 @@ Getting Involved
 
 We welcome contributions. Whether you fixed a bug, added a new feature or
 corrected a typo, all contributions are welcome. General instructions on how to
-contribute can be found in the CONTRIBUTING file.
+contribute can be found in the ``CONTRIBUTING`` file.
 
 Licensing Info
 ***************
@@ -87,6 +123,6 @@ details.
 References
 ***********
 
-This package is part of the VIRL :sup:`2` Network Simulation platform.
+This package is part of the CML 2 Network Simulation platform.
 For details, go to `<https://developer.cisco.com/modeling-labs>`_.  Or to
 `<https://developer.cisco.com/docs/virl2-client/>`_.
diff --git a/examples/licensing.py b/examples/licensing.py
index 91cccff..d911c1d 100644
--- a/examples/licensing.py
+++ b/examples/licensing.py
@@ -107,14 +107,16 @@
     "features": [
         {
             "name": "CML - Enterprise License",
-            "description": "Cisco Modeling Labs - Enterprise License with 20 nodes capacity included",
+            "description": "Cisco Modeling Labs - Enterprise License with 20 nodes \
+                capacity included",
             "in_use": 1,
             "status": "IN_COMPLIANCE",
             "version": "1.0"
         },
         {
             "name": "CML \u2013 Expansion Nodes",
-            "description": "Cisco Modeling Labs - Expansion node capacity for CML Enterprise Servers",
+            "description": "Cisco Modeling Labs - Expansion node capacity for CML \
+                Enterprise Servers",
             "in_use": 50,
             "status": "IN_COMPLIANCE",
             "version": "1.0"
@@ -129,9 +131,11 @@
 
 [
     {
-        "id": "regid.2019-10.com.cisco.CML_ENT_BASE,1.0_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
+        "id": "regid.2019-10.com.cisco.CML_ENT_BASE,\
+            1.0_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
         "name": "CML - Enterprise License",
-        "description": "Cisco Modeling Labs - Enterprise License with 20 nodes capacity included",
+        "description": "Cisco Modeling Labs - Enterprise License with 20 nodes \
+            capacity included",
         "in_use": 1,
         "status": "IN_COMPLIANCE",
         "version": "1.0",
@@ -139,9 +143,11 @@
         "max": 1
     },
     {
-        "id": "regid.2019-10.com.cisco.CML_NODE_COUNT,1.0_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
+        "id": "regid.2019-10.com.cisco.CML_NODE_COUNT,\
+            1.0_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
         "name": "CML \u2013 Expansion Nodes",
-        "description": "Cisco Modeling Labs - Expansion node capacity for CML Enterprise Servers",
+        "description": "Cisco Modeling Labs - Expansion node capacity for \
+            CML Enterprise Servers",
         "in_use": 50,
         "status": "IN_COMPLIANCE",
         "version": "1.0",
diff --git a/examples/sample.py b/examples/sample.py
index 92fa1d1..32e0b39 100644
--- a/examples/sample.py
+++ b/examples/sample.py
@@ -51,7 +51,9 @@
 cl.is_system_ready(wait=True)
 
 # set transport if needed - also proxy can be set if needed
-# cl.licensing.licensing.set_transport(ssms=ssms, proxy_server="172.16.1.100", proxy_port=8888)
+# cl.licensing.licensing.set_transport(
+#     ssms=ssms, proxy_server="172.16.1.100", proxy_port=8888
+# )
 cl.licensing.set_transport(ssms=SSMS)
 cl.licensing.install_certificate(cert=CERT)
 # 'register_wait' method waits max 45s for registration status to become COMPLETED
diff --git a/poetry.lock b/poetry.lock
index c395da5..4f62863 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -17,12 +17,10 @@ python-versions = ">=3.6"
 [package.dependencies]
 aiosignal = ">=1.1.2"
 async-timeout = ">=4.0.0a3,<5.0"
-asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""}
 attrs = ">=17.3.0"
 charset-normalizer = ">=2.0,<3.0"
 frozenlist = ">=1.1.1"
 multidict = ">=4.5,<7.0"
-typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
 yarl = ">=1.0,<2.0"
 
 [package.extras]
@@ -55,6 +53,14 @@ python-versions = ">=3.6"
 [package.dependencies]
 frozenlist = ">=1.1.0"
 
+[[package]]
+name = "alabaster"
+version = "0.7.12"
+description = "A configurable sidebar-enabled Sphinx theme"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
 [[package]]
 name = "async-lru"
 version = "1.0.3"
@@ -71,20 +77,9 @@ category = "main"
 optional = true
 python-versions = ">=3.6"
 
-[package.dependencies]
-typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
-
-[[package]]
-name = "asynctest"
-version = "0.13.0"
-description = "Enhance the standard unittest package with features for testing asyncio libraries"
-category = "main"
-optional = true
-python-versions = ">=3.5"
-
 [[package]]
 name = "atomicwrites"
-version = "1.4.0"
+version = "1.4.1"
 description = "Atomic file writes."
 category = "dev"
 optional = false
@@ -104,9 +99,20 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
 tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
 tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
 
+[[package]]
+name = "babel"
+version = "2.10.3"
+description = "Internationalization utilities"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+pytz = ">=2015.7"
+
 [[package]]
 name = "certifi"
-version = "2022.5.18.1"
+version = "2022.6.15"
 description = "Python package for providing Mozilla's CA Bundle."
 category = "main"
 optional = false
@@ -114,7 +120,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "cffi"
-version = "1.15.0"
+version = "1.15.1"
 description = "Foreign Function Interface for Python calling C code."
 category = "main"
 optional = true
@@ -141,26 +147,26 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "charset-normalizer"
-version = "2.0.12"
+version = "2.1.0"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 category = "main"
 optional = false
-python-versions = ">=3.5.0"
+python-versions = ">=3.6.0"
 
 [package.extras]
 unicode_backport = ["unicodedata2"]
 
 [[package]]
 name = "colorama"
-version = "0.4.4"
+version = "0.4.5"
 description = "Cross-platform colored terminal text."
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "cryptography"
-version = "37.0.2"
+version = "37.0.4"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 category = "main"
 optional = true
@@ -190,7 +196,7 @@ graph = ["objgraph (>=1.7.2)"]
 
 [[package]]
 name = "distlib"
-version = "0.3.4"
+version = "0.3.5"
 description = "Distribution utilities"
 category = "dev"
 optional = false
@@ -204,6 +210,14 @@ category = "main"
 optional = true
 python-versions = ">=3.6"
 
+[[package]]
+name = "docutils"
+version = "0.17.1"
+description = "Docutils -- Python Documentation Utilities"
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
 [[package]]
 name = "filelock"
 version = "3.7.1"
@@ -225,7 +239,6 @@ optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
-importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""}
 mccabe = ">=0.6.0,<0.7.0"
 pycodestyle = ">=2.8.0,<2.9.0"
 pyflakes = ">=2.4.0,<2.5.0"
@@ -259,7 +272,6 @@ python-versions = ">=3.6"
 
 [package.dependencies]
 gitdb = ">=4.0.1,<5"
-typing-extensions = {version = ">=3.7.4.0", markers = "python_version < \"3.8\""}
 
 [[package]]
 name = "identify"
@@ -280,21 +292,29 @@ category = "main"
 optional = false
 python-versions = ">=3.5"
 
+[[package]]
+name = "imagesize"
+version = "1.4.1"
+description = "Getting image size from png/jpeg/jpeg2000/gif file"
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
 [[package]]
 name = "importlib-metadata"
-version = "4.2.0"
+version = "4.12.0"
 description = "Read metadata from Python packages"
 category = "main"
-optional = false
-python-versions = ">=3.6"
+optional = true
+python-versions = ">=3.7"
 
 [package.dependencies]
-typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
 zipp = ">=0.5"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
+perf = ["ipython"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
 
 [[package]]
 name = "iniconfig"
@@ -355,11 +375,11 @@ python-versions = ">=3.7"
 
 [[package]]
 name = "nodeenv"
-version = "1.6.0"
+version = "1.7.0"
 description = "Node.js virtual environment builder"
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
 
 [[package]]
 name = "packaging"
@@ -400,9 +420,6 @@ category = "dev"
 optional = false
 python-versions = ">=3.6"
 
-[package.dependencies]
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
-
 [package.extras]
 dev = ["pre-commit", "tox"]
 testing = ["pytest", "pytest-benchmark"]
@@ -418,7 +435,6 @@ python-versions = ">=3.7"
 [package.dependencies]
 cfgv = ">=2.0.0"
 identify = ">=1.0.0"
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 nodeenv = ">=0.11.1"
 pyyaml = ">=5.1"
 toml = "*"
@@ -433,7 +449,6 @@ optional = true
 python-versions = ">=3.7"
 
 [package.dependencies]
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 wcwidth = "*"
 
 [package.extras]
@@ -460,7 +475,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "pyats"
-version = "22.5"
+version = "22.6"
 description = "pyATS - Python Automation Test System"
 category = "main"
 optional = true
@@ -468,29 +483,29 @@ python-versions = ">=3.5"
 
 [package.dependencies]
 packaging = ">=20.0"
-"pyats.aereport" = ">=22.5.0,<22.6.0"
-"pyats.aetest" = ">=22.5.0,<22.6.0"
-"pyats.async" = ">=22.5.0,<22.6.0"
-"pyats.connections" = ">=22.5.0,<22.6.0"
-"pyats.datastructures" = ">=22.5.0,<22.6.0"
-"pyats.easypy" = ">=22.5.0,<22.6.0"
-"pyats.kleenex" = ">=22.5.0,<22.6.0"
-"pyats.log" = ">=22.5.0,<22.6.0"
-"pyats.reporter" = ">=22.5.0,<22.6.0"
-"pyats.results" = ">=22.5.0,<22.6.0"
-"pyats.tcl" = ">=22.5.0,<22.6.0"
-"pyats.topology" = ">=22.5.0,<22.6.0"
-"pyats.utils" = ">=22.5.0,<22.6.0"
+"pyats.aereport" = ">=22.6.0,<22.7.0"
+"pyats.aetest" = ">=22.6.0,<22.7.0"
+"pyats.async" = ">=22.6.0,<22.7.0"
+"pyats.connections" = ">=22.6.0,<22.7.0"
+"pyats.datastructures" = ">=22.6.0,<22.7.0"
+"pyats.easypy" = ">=22.6.0,<22.7.0"
+"pyats.kleenex" = ">=22.6.0,<22.7.0"
+"pyats.log" = ">=22.6.0,<22.7.0"
+"pyats.reporter" = ">=22.6.0,<22.7.0"
+"pyats.results" = ">=22.6.0,<22.7.0"
+"pyats.tcl" = ">=22.6.0,<22.7.0"
+"pyats.topology" = ">=22.6.0,<22.7.0"
+"pyats.utils" = ">=22.6.0,<22.7.0"
 
 [package.extras]
-full = ["cookiecutter", "genie (>=22.5.0,<22.6.0)", "pyats.robot (>=22.5.0,<22.6.0)", "genie.libs.robot (>=22.5.0,<22.6.0)", "genie.telemetry (>=22.5.0,<22.6.0)", "genie.trafficgen (>=22.5.0,<22.6.0)", "pyats.contrib (>=22.5.0,<22.6.0)"]
-library = ["genie (>=22.5.0,<22.6.0)"]
-robot = ["pyats.robot (>=22.5.0,<22.6.0)", "genie.libs.robot (>=22.5.0,<22.6.0)"]
+full = ["cookiecutter", "genie (>=22.6.0,<22.7.0)", "pyats.robot (>=22.6.0,<22.7.0)", "genie.libs.robot (>=22.6.0,<22.7.0)", "genie.telemetry (>=22.6.0,<22.7.0)", "genie.trafficgen (>=22.6.0,<22.7.0)", "pyats.contrib (>=22.6.0,<22.7.0)"]
+library = ["genie (>=22.6.0,<22.7.0)"]
+robot = ["pyats.robot (>=22.6.0,<22.7.0)", "genie.libs.robot (>=22.6.0,<22.7.0)"]
 template = ["cookiecutter"]
 
 [[package]]
 name = "pyats.aereport"
-version = "22.5"
+version = "22.6"
 description = "pyATS AEreport: Result Collection and Reporting"
 category = "main"
 optional = true
@@ -500,15 +515,15 @@ python-versions = ">=3.5"
 jinja2 = "*"
 junit-xml = "*"
 psutil = "*"
-"pyats.log" = ">=22.5.0,<22.6.0"
-"pyats.results" = ">=22.5.0,<22.6.0"
+"pyats.log" = ">=22.6.0,<22.7.0"
+"pyats.results" = ">=22.6.0,<22.7.0"
 
 [package.extras]
 dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.aetest"
-version = "22.5"
+version = "22.6"
 description = "pyATS AEtest: Testscript Engine"
 category = "main"
 optional = true
@@ -516,11 +531,11 @@ python-versions = ">=3.5"
 
 [package.dependencies]
 jinja2 = "*"
-"pyats.aereport" = ">=22.5.0,<22.6.0"
-"pyats.datastructures" = ">=22.5.0,<22.6.0"
-"pyats.log" = ">=22.5.0,<22.6.0"
-"pyats.results" = ">=22.5.0,<22.6.0"
-"pyats.utils" = ">=22.5.0,<22.6.0"
+"pyats.aereport" = ">=22.6.0,<22.7.0"
+"pyats.datastructures" = ">=22.6.0,<22.7.0"
+"pyats.log" = ">=22.6.0,<22.7.0"
+"pyats.results" = ">=22.6.0,<22.7.0"
+"pyats.utils" = ">=22.6.0,<22.7.0"
 pyyaml = "*"
 
 [package.extras]
@@ -528,37 +543,37 @@ dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.async"
-version = "22.5"
+version = "22.6"
 description = "pyATS Async: Asynchronous Execution of Codes"
 category = "main"
 optional = true
 python-versions = ">=3.5"
 
 [package.dependencies]
-"pyats.log" = ">=22.5.0,<22.6.0"
+"pyats.log" = ">=22.6.0,<22.7.0"
 
 [package.extras]
 dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.connections"
-version = "22.5"
+version = "22.6"
 description = "pyATS Connection: Device Connection Handling & Base Classes"
 category = "main"
 optional = true
 python-versions = ">=3.5"
 
 [package.dependencies]
-"pyats.async" = ">=22.5.0,<22.6.0"
-"pyats.datastructures" = ">=22.5.0,<22.6.0"
-unicon = ">=22.5.0,<22.6.0"
+"pyats.async" = ">=22.6.0,<22.7.0"
+"pyats.datastructures" = ">=22.6.0,<22.7.0"
+unicon = ">=22.6.0,<22.7.0"
 
 [package.extras]
 dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.datastructures"
-version = "22.5"
+version = "22.6"
 description = "pyATS Datastructures: Extended Datastructures for Grownups"
 category = "main"
 optional = true
@@ -569,7 +584,7 @@ dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.easypy"
-version = "22.5"
+version = "22.6"
 description = "pyATS Easypy: launcher and runtime environment"
 category = "main"
 optional = true
@@ -579,20 +594,20 @@ python-versions = ">=3.5"
 distro = "*"
 jinja2 = "*"
 psutil = "*"
-"pyats.aereport" = ">=22.5.0,<22.6.0"
-"pyats.datastructures" = ">=22.5.0,<22.6.0"
-"pyats.kleenex" = ">=22.5.0,<22.6.0"
-"pyats.log" = ">=22.5.0,<22.6.0"
-"pyats.results" = ">=22.5.0,<22.6.0"
-"pyats.topology" = ">=22.5.0,<22.6.0"
-"pyats.utils" = ">=22.5.0,<22.6.0"
+"pyats.aereport" = ">=22.6.0,<22.7.0"
+"pyats.datastructures" = ">=22.6.0,<22.7.0"
+"pyats.kleenex" = ">=22.6.0,<22.7.0"
+"pyats.log" = ">=22.6.0,<22.7.0"
+"pyats.results" = ">=22.6.0,<22.7.0"
+"pyats.topology" = ">=22.6.0,<22.7.0"
+"pyats.utils" = ">=22.6.0,<22.7.0"
 
 [package.extras]
 dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.kleenex"
-version = "22.5"
+version = "22.6"
 description = "pyATS Kleenex: Testbed Preparation, Clean & Finalization"
 category = "main"
 optional = true
@@ -600,12 +615,12 @@ python-versions = ">=3.5"
 
 [package.dependencies]
 distro = "*"
-"pyats.aetest" = ">=22.5.0,<22.6.0"
-"pyats.async" = ">=22.5.0,<22.6.0"
-"pyats.datastructures" = ">=22.5.0,<22.6.0"
-"pyats.log" = ">=22.5.0,<22.6.0"
-"pyats.topology" = ">=22.5.0,<22.6.0"
-"pyats.utils" = ">=22.5.0,<22.6.0"
+"pyats.aetest" = ">=22.6.0,<22.7.0"
+"pyats.async" = ">=22.6.0,<22.7.0"
+"pyats.datastructures" = ">=22.6.0,<22.7.0"
+"pyats.log" = ">=22.6.0,<22.7.0"
+"pyats.topology" = ">=22.6.0,<22.7.0"
+"pyats.utils" = ">=22.6.0,<22.7.0"
 requests = "*"
 
 [package.extras]
@@ -613,7 +628,7 @@ dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.log"
-version = "22.5"
+version = "22.6"
 description = "pyATS Log: Logging Format and Utilities"
 category = "main"
 optional = true
@@ -624,9 +639,9 @@ aiofiles = ">=0.6.0"
 aiohttp = {version = "<4.0", markers = "python_version >= \"3.6\""}
 aiohttp-swagger = ">=1.0.15"
 async-lru = ">=1.0.2"
-chardet = ">=3.0.4"
+chardet = ">=3.0.4,<5.0.0"
 jinja2 = "*"
-"pyats.datastructures" = ">=22.5.0,<22.6.0"
+"pyats.datastructures" = ">=22.6.0,<22.7.0"
 python-engineio = ">=3.13.0,<4.0.0"
 python-socketio = ">=4.2.0,<5.0.0"
 pyyaml = "*"
@@ -636,7 +651,7 @@ dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.reporter"
-version = "22.5"
+version = "22.6"
 description = "pyATS Reporter: Result Collection and Reporting"
 category = "main"
 optional = true
@@ -644,10 +659,10 @@ python-versions = ">=3.5"
 
 [package.dependencies]
 gitpython = "<=3.1.18"
-"pyats.aereport" = ">=22.5.0,<22.6.0"
-"pyats.log" = ">=22.5.0,<22.6.0"
-"pyats.results" = ">=22.5.0,<22.6.0"
-"pyats.utils" = ">=22.5.0,<22.6.0"
+"pyats.aereport" = ">=22.6.0,<22.7.0"
+"pyats.log" = ">=22.6.0,<22.7.0"
+"pyats.results" = ">=22.6.0,<22.7.0"
+"pyats.utils" = ">=22.6.0,<22.7.0"
 pyyaml = "*"
 
 [package.extras]
@@ -655,7 +670,7 @@ dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.results"
-version = "22.5"
+version = "22.6"
 description = "pyATS Results: Representing Results using Objects"
 category = "main"
 optional = true
@@ -666,31 +681,31 @@ dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.tcl"
-version = "22.5"
+version = "22.6"
 description = "pyATS Tcl: Tcl Integration and Objects"
 category = "main"
 optional = true
 python-versions = ">=3.5"
 
 [package.dependencies]
-"pyats.datastructures" = ">=22.5.0,<22.6.0"
-"pyats.log" = ">=22.5.0,<22.6.0"
+"pyats.datastructures" = ">=22.6.0,<22.7.0"
+"pyats.log" = ">=22.6.0,<22.7.0"
 
 [package.extras]
 dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.topology"
-version = "22.5"
+version = "22.6"
 description = "pyATS Topology: Topology Objects and Testbed YAMLs"
 category = "main"
 optional = true
 python-versions = ">=3.5"
 
 [package.dependencies]
-"pyats.connections" = ">=22.5.0,<22.6.0"
-"pyats.datastructures" = ">=22.5.0,<22.6.0"
-"pyats.utils" = ">=22.5.0,<22.6.0"
+"pyats.connections" = ">=22.6.0,<22.7.0"
+"pyats.datastructures" = ">=22.6.0,<22.7.0"
+"pyats.utils" = ">=22.6.0,<22.7.0"
 pyyaml = "*"
 yamllint = "*"
 
@@ -699,7 +714,7 @@ dev = ["sphinx", "sphinx-rtd-theme"]
 
 [[package]]
 name = "pyats.utils"
-version = "22.5"
+version = "22.6"
 description = "pyATS Utils: Utilities Module"
 category = "main"
 optional = true
@@ -708,8 +723,8 @@ python-versions = ">=3.5"
 [package.dependencies]
 cryptography = "*"
 distro = "*"
-"pyats.datastructures" = ">=22.5.0,<22.6.0"
-"pyats.topology" = ">=22.5.0,<22.6.0"
+"pyats.datastructures" = ">=22.6.0,<22.7.0"
+"pyats.topology" = ">=22.6.0,<22.7.0"
 
 [package.extras]
 dev = ["sphinx", "sphinx-rtd-theme", "requests-mock"]
@@ -738,6 +753,14 @@ category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
+[[package]]
+name = "pygments"
+version = "2.12.0"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
 [[package]]
 name = "pyparsing"
 version = "3.0.9"
@@ -761,7 +784,6 @@ python-versions = ">=3.7"
 atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
 attrs = ">=19.2.0"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
 iniconfig = "*"
 packaging = "*"
 pluggy = ">=0.12,<2.0"
@@ -802,6 +824,14 @@ six = ">=1.9.0"
 asyncio_client = ["aiohttp (>=3.4)", "websockets (>=7.0)"]
 client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
 
+[[package]]
+name = "pytz"
+version = "2022.1"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = true
+python-versions = "*"
+
 [[package]]
 name = "pyyaml"
 version = "6.0"
@@ -812,7 +842,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "requests"
-version = "2.28.0"
+version = "2.28.1"
 description = "Python HTTP for Humans."
 category = "main"
 optional = false
@@ -820,13 +850,13 @@ python-versions = ">=3.7, <4"
 
 [package.dependencies]
 certifi = ">=2017.4.17"
-charset-normalizer = ">=2.0.0,<2.1.0"
+charset-normalizer = ">=2,<3"
 idna = ">=2.5,<4"
 urllib3 = ">=1.21.1,<1.27"
 
 [package.extras]
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "requests-mock"
@@ -886,6 +916,132 @@ category = "main"
 optional = true
 python-versions = ">=3.6"
 
+[[package]]
+name = "snowballstemmer"
+version = "2.2.0"
+description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "sphinx"
+version = "5.0.2"
+description = "Python documentation generator"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+alabaster = ">=0.7,<0.8"
+babel = ">=1.3"
+colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""}
+docutils = ">=0.14,<0.19"
+imagesize = "*"
+importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
+Jinja2 = ">=2.3"
+packaging = "*"
+Pygments = ">=2.0"
+requests = ">=2.5.0"
+snowballstemmer = ">=1.1"
+sphinxcontrib-applehelp = "*"
+sphinxcontrib-devhelp = "*"
+sphinxcontrib-htmlhelp = ">=2.0.0"
+sphinxcontrib-jsmath = "*"
+sphinxcontrib-qthelp = "*"
+sphinxcontrib-serializinghtml = ">=1.1.5"
+
+[package.extras]
+docs = ["sphinxcontrib-websupport"]
+lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.950)", "docutils-stubs", "types-typed-ast", "types-requests"]
+test = ["pytest (>=4.6)", "html5lib", "cython", "typed-ast"]
+
+[[package]]
+name = "sphinx-rtd-theme"
+version = "1.0.0"
+description = "Read the Docs theme for Sphinx"
+category = "main"
+optional = true
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+
+[package.dependencies]
+docutils = "<0.18"
+sphinx = ">=1.6"
+
+[package.extras]
+dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"]
+
+[[package]]
+name = "sphinxcontrib-applehelp"
+version = "1.0.2"
+description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["flake8", "mypy", "docutils-stubs"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-devhelp"
+version = "1.0.2"
+description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["flake8", "mypy", "docutils-stubs"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-htmlhelp"
+version = "2.0.0"
+description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.extras]
+lint = ["flake8", "mypy", "docutils-stubs"]
+test = ["pytest", "html5lib"]
+
+[[package]]
+name = "sphinxcontrib-jsmath"
+version = "1.0.1"
+description = "A sphinx extension which renders display math in HTML via JavaScript"
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[package.extras]
+test = ["pytest", "flake8", "mypy"]
+
+[[package]]
+name = "sphinxcontrib-qthelp"
+version = "1.0.3"
+description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["flake8", "mypy", "docutils-stubs"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-serializinghtml"
+version = "1.1.5"
+description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["flake8", "mypy", "docutils-stubs"]
+test = ["pytest"]
+
 [[package]]
 name = "toml"
 version = "0.10.2"
@@ -902,17 +1058,9 @@ category = "dev"
 optional = false
 python-versions = ">=3.7"
 
-[[package]]
-name = "typing-extensions"
-version = "4.2.0"
-description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
 [[package]]
 name = "unicon"
-version = "22.5"
+version = "22.6"
 description = "Unicon Connection Library"
 category = "main"
 optional = true
@@ -921,7 +1069,7 @@ python-versions = ">=3.5"
 [package.dependencies]
 dill = "*"
 pyyaml = "*"
-"unicon.plugins" = ">=22.5.0,<22.6.0"
+"unicon.plugins" = ">=22.6.0,<22.7.0"
 
 [package.extras]
 dev = ["cisco-distutils", "coverage", "restview", "sphinx", "sphinxcontrib-napoleon", "sphinxcontrib-mockautodoc", "sphinx-rtd-theme"]
@@ -930,7 +1078,7 @@ robot = ["robotframework"]
 
 [[package]]
 name = "unicon.plugins"
-version = "22.5"
+version = "22.6"
 description = "Unicon Connection Library Plugins"
 category = "main"
 optional = true
@@ -939,18 +1087,18 @@ python-versions = "*"
 [package.dependencies]
 PrettyTable = "*"
 pyyaml = "*"
-unicon = ">=22.5.0,<22.6.0"
+unicon = ">=22.6.0,<22.7.0"
 
 [package.extras]
 dev = ["setuptools", "pip", "wheel", "coverage", "restview", "sphinx", "sphinxcontrib-napoleon", "sphinxcontrib-mockautodoc", "sphinx-rtd-theme"]
 
 [[package]]
 name = "urllib3"
-version = "1.26.9"
+version = "1.26.10"
 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"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
 
 [package.extras]
 brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
@@ -959,7 +1107,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 
 [[package]]
 name = "virtualenv"
-version = "20.14.1"
+version = "20.15.1"
 description = "Virtual Python Environment builder"
 category = "dev"
 optional = false
@@ -968,7 +1116,6 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
 [package.dependencies]
 distlib = ">=0.3.1,<1"
 filelock = ">=3.2,<4"
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
 platformdirs = ">=2,<3"
 six = ">=1.9.0,<2"
 
@@ -986,11 +1133,11 @@ python-versions = "*"
 
 [[package]]
 name = "yamllint"
-version = "1.26.3"
+version = "1.27.1"
 description = "A linter for YAML files."
 category = "main"
 optional = true
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 
 [package.dependencies]
 pathspec = ">=0.5.3"
@@ -1007,27 +1154,27 @@ python-versions = ">=3.6"
 [package.dependencies]
 idna = ">=2.0"
 multidict = ">=4.0"
-typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
 
 [[package]]
 name = "zipp"
-version = "3.8.0"
+version = "3.8.1"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "main"
-optional = false
+optional = true
 python-versions = ">=3.7"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
+docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
 
 [extras]
+docs = ["sphinx", "sphinx_rtd_theme"]
 pyats = ["pyats"]
 
 [metadata]
 lock-version = "1.1"
-python-versions = "^3.7.0"
-content-hash = "a450ae6d1f9e26d51a2593749d0d73bc1195870de5387bf4288d731a2653e5eb"
+python-versions = "^3.8.0"
+content-hash = "e5e27f2732c9bed9efdac857aeb5c4779461a9d4c522c3fe11648a31526a2ce9"
 
 [metadata.files]
 aiofiles = [
@@ -1116,6 +1263,10 @@ aiosignal = [
     {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
     {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
 ]
+alabaster = [
+    {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
+    {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
+]
 async-lru = [
     {file = "async-lru-1.0.3.tar.gz", hash = "sha256:c2cb9b2915eb14e6cf3e717154b40f715bf90e596d73623677affd0d1fbcd32a"},
     {file = "async_lru-1.0.3-py3-none-any.whl", hash = "sha256:ea692c303feb6211ff260d230dae1583636f13e05c9ae616eada77855b7f415c"},
@@ -1124,74 +1275,14 @@ async-timeout = [
     {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
     {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
 ]
-asynctest = [
-    {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
-    {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
-]
-atomicwrites = [
-    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
-    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
-]
+atomicwrites = []
 attrs = [
     {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
     {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
 ]
-certifi = [
-    {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"},
-    {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"},
-]
-cffi = [
-    {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
-    {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
-    {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
-    {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
-    {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
-    {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
-    {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
-    {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
-    {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
-    {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
-    {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
-    {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
-    {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
-    {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
-    {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
-    {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
-    {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
-    {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
-    {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
-    {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
-    {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
-    {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
-    {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
-    {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
-    {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
-]
+babel = []
+certifi = []
+cffi = []
 cfgv = [
     {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
     {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
@@ -1200,50 +1291,22 @@ chardet = [
     {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
     {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
 ]
-charset-normalizer = [
-    {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
-    {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
-]
-colorama = [
-    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
-    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
-]
-cryptography = [
-    {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"},
-    {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"},
-    {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"},
-    {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"},
-    {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"},
-    {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"},
-    {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"},
-    {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"},
-    {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"},
-    {file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"},
-    {file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"},
-    {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"},
-    {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"},
-    {file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"},
-    {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"},
-    {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"},
-    {file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"},
-    {file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"},
-    {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"},
-    {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"},
-    {file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"},
-    {file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"},
-]
+charset-normalizer = []
+colorama = []
+cryptography = []
 dill = [
     {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"},
     {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"},
 ]
-distlib = [
-    {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
-    {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
-]
+distlib = []
 distro = [
     {file = "distro-1.7.0-py3-none-any.whl", hash = "sha256:d596311d707e692c2160c37807f83e3820c5d539d5a83e87cfb6babd8ba3a06b"},
     {file = "distro-1.7.0.tar.gz", hash = "sha256:151aeccf60c216402932b52e40ee477a939f8d58898927378a02abbe852c1c39"},
 ]
+docutils = [
+    {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
+    {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
+]
 filelock = [
     {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
     {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
@@ -1329,10 +1392,8 @@ idna = [
     {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
     {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
 ]
-importlib-metadata = [
-    {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"},
-    {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"},
-]
+imagesize = []
+importlib-metadata = []
 iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
@@ -1451,10 +1512,7 @@ multidict = [
     {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
     {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
 ]
-nodeenv = [
-    {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
-    {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
-]
+nodeenv = []
 packaging = [
     {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
     {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
@@ -1471,10 +1529,7 @@ pluggy = [
     {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
     {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
 ]
-pre-commit = [
-    {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"},
-    {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"},
-]
+pre-commit = []
 prettytable = [
     {file = "prettytable-3.3.0-py3-none-any.whl", hash = "sha256:d1c34d72ea2c0ffd6ce5958e71c428eb21a3d40bf3133afe319b24aeed5af407"},
     {file = "prettytable-3.3.0.tar.gz", hash = "sha256:118eb54fd2794049b810893653b20952349df6d3bc1764e7facd8a18064fa9b0"},
@@ -1517,244 +1572,20 @@ py = [
     {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
     {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
 ]
-pyats = [
-    {file = "pyats-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8ce5378e238311368e3496fe9ba7f50f4d210a5b478c386ccc1fdd62445f36dc"},
-    {file = "pyats-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:58e1ab885c2f6bf62ddece22c989abbad387d8132492b77daf8181089895fe0c"},
-    {file = "pyats-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79be2562c8556416ac56979470925e7445db9ef264e70ea718877ceacfb49023"},
-    {file = "pyats-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:efa4e50965525b210758c9f3f1dd99a02e1002bc56cb47292641506aa0ec6db9"},
-    {file = "pyats-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:619c652e68af3517bcb0f247839eb41cb28c2f09b946242854464664dc57c61a"},
-    {file = "pyats-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:7df1b2071059e990838f2119508dd8d57d88d52871d014a81d8621c3ee35fe02"},
-    {file = "pyats-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:35cb0e2bc6fccc4371d63a5c592bb575fe471e0d894d9c90646e167e950f493f"},
-    {file = "pyats-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:5a38eee487e629dc0e77293bc5d7e651010f567ac8d26505343f6ce002aa399a"},
-    {file = "pyats-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:f48d2abb81f577873d8b98614a0e1c58cbe3f1e4aae5f9c5ddfec66b5d4ef369"},
-    {file = "pyats-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97741ab74258ae4df97dc3da425189f1780e1314d39acd308fa20bd0f492af2a"},
-    {file = "pyats-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:44073818b462ce1f20c19756bdc69794bba6fcc3574ac5cc7759694094947365"},
-    {file = "pyats-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:06d4881d6bb4f9461ff2e083cded64f322c54c03f5c44c1babbd7029e7c681a9"},
-    {file = "pyats-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:29de48766b52bd5f9ad22c813b005fc31e905f3258c5f78f6514390e0603f9a3"},
-    {file = "pyats-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b5a0c758719a63a74cdb3f35b3f4d32deeb151646f1f520d2a41a01f56f639e2"},
-    {file = "pyats-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a4ad2c702c47c3353dfdd52696a29efb778c4a9f4eef978ce1d97f748cc2ad40"},
-]
-"pyats.aereport" = [
-    {file = "pyats.aereport-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:40d1c0b0908a6960cc889a8e91a1c7018758f578c063e3bbc5c35de9c29cde34"},
-    {file = "pyats.aereport-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:867ddae7a9c8a265b38cbeb59579dd187cc137704e7d4fa0992050d5d5bb2a4b"},
-    {file = "pyats.aereport-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a60580737c555066732d8db5169d7f246ab6949e83a66f3dd989a3b133c7293a"},
-    {file = "pyats.aereport-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:1029b088f576fc9d658a08b9948d8de3d7aa6d44cfb2bf7d79d263b9f61d106d"},
-    {file = "pyats.aereport-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8694e33b55e88af9c499f62f4ea2d41e2ab53fde0b38d7bd0a6341c663209158"},
-    {file = "pyats.aereport-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:661ad2d4a267cc3f0ff1456701ccf90648b986afe72d14afbafe456730651e2b"},
-    {file = "pyats.aereport-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eb14fb4aa6683cab6292986e4d93033d147836d4702f7906139d39f57c3cfbf5"},
-    {file = "pyats.aereport-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:f473949287418b9e541b5240b59bddd801ae20643be5d96345a3299e7375a316"},
-    {file = "pyats.aereport-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:0cec1a92f07a00192043cdcb19591d0d6261c356029c964fc45c7fa748e01208"},
-    {file = "pyats.aereport-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44010de99673185fa1f5540fb86a6276a11960eee8117527e7a23c6b130c6ff6"},
-    {file = "pyats.aereport-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:00e08da100975d2c9c48288fe5d326337fe55cc8b817a98e053c21bc232c102f"},
-    {file = "pyats.aereport-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:f6413a0e3e65d54521bf1ca31e0c73e0536f77d732224ff462d6011b918168b0"},
-    {file = "pyats.aereport-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:1d34d45f8d8e625f9f7131f4d5caf3cb4cf9fdfe8690b62f114cd4b63e47c991"},
-    {file = "pyats.aereport-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33f9118d25bf83cacc30a234b46b472fecab99f6f51b37946331cefc7e75695c"},
-    {file = "pyats.aereport-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f3437692430b2de35fb1930faca624f18159e8fc5cd508a5d3305095f71f839"},
-]
-"pyats.aetest" = [
-    {file = "pyats.aetest-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:dd25c3a968a7d341b9b4c66bbf8971561443e8c2807081b071bc78acb1325068"},
-    {file = "pyats.aetest-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:2c445b3649ac8f745f2e409f5f8330a3f1ec0dc9f2b8338965e5a5998a2d41e8"},
-    {file = "pyats.aetest-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01f6b1055529ffbc1c2203be686f2bf4322ce1741379a94a4a09a0f3e04f5ba"},
-    {file = "pyats.aetest-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:dd7d45faa5e967e1d2a9e89dcddfb961518da769b71547e270bbe77236e66e0a"},
-    {file = "pyats.aetest-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ec373e72ebd0642ca7abf300b834a2dc963a08187e07fe74a8f971bb3e925dd6"},
-    {file = "pyats.aetest-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:6ef9a7e2c004133a9ba5826ad6fe4e8c96c977afa6154774685db2b6fe658c78"},
-    {file = "pyats.aetest-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0835a61eb67b8ed96e6f7ee075c5c5befd700f0d3fccee07e9943d4709c07434"},
-    {file = "pyats.aetest-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:1840c7c426071466b78ee1f16b95607e44d38cd900b02a68151fa3a9eae5c721"},
-    {file = "pyats.aetest-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:7e3f31de0586b9e5754c7fff5ad02794b88f2a39c3a8adae9833945d17cd4775"},
-    {file = "pyats.aetest-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6b93c03003eecf2f543a5829266f8d46007691d54a6b6ee6c059d685a2463f20"},
-    {file = "pyats.aetest-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:67571ebc19e9e80c05f79547c4bbffbbb6e5b2bf6e5edf9b276045e0320b0071"},
-    {file = "pyats.aetest-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:78c62dcb3d8c9e1cf7019760753a20163895f2e6a340fd793f7cb9550736bccf"},
-    {file = "pyats.aetest-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:281bd231b5126044f9d702e4b07079d2c2ca6db7f70b5e4267341b830f0b250b"},
-    {file = "pyats.aetest-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57ed249f9612e98e22df7fb0956921e786dbe44cd1291b149e5767c9257e741f"},
-    {file = "pyats.aetest-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:13df4f56420dfe8fbdba11dac21bcfa70d4c60045e4ac12d4775595eb708d1cc"},
-]
-"pyats.async" = [
-    {file = "pyats.async-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ae2fbee12782f4a7526e27be6d8be03c2c0c6ac886dd1a29da24500b379fc425"},
-    {file = "pyats.async-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:eaca832c5d263f2cc770f800ecdae1036854af953c45c89737a590afba77e4d2"},
-    {file = "pyats.async-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29424e0233fa684ac7bea72cade8ce276d3e7deb1d56e9e49da62d6b23836d43"},
-    {file = "pyats.async-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:e1f15986f615d67d22fedc313fa299ebd51a15881e97785faca1ef99e5a8e12e"},
-    {file = "pyats.async-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0cd521d53311080c2d8c31a4a540828cb43f720f60118f218a01aedb7d49fa6"},
-    {file = "pyats.async-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:9ad2aa0f045d0c9d91c676cb0aaf2cf27d2091c503ca6753cdc46195477c7893"},
-    {file = "pyats.async-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8fff2b4019275c3fa1bdf30b0bb813ff6d9c2c0e88dae44303e60f184f4f4e4b"},
-    {file = "pyats.async-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:e2231cde8f406d1e4a512f336b6f2fdc8e2a19556d01ff359dd5f96f75493c12"},
-    {file = "pyats.async-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:31bfa8f47cc7b3d7535579644c36624774bdb4e4b071f7cbe2cdca24128ece8d"},
-    {file = "pyats.async-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:552443f148fb38d3ba5a8154bb609a55b116192f322468f4983e6878537b4de7"},
-    {file = "pyats.async-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4e524417b2ae9ee6ea9d974a6d11b1f95a90d78e7e2f41f4453a42a4fc9be93e"},
-    {file = "pyats.async-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:88a365fac4cfd96cb3b7bb307f9fdc6a64a1aace49e3aaa43657a1f668542ac2"},
-    {file = "pyats.async-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:2ddc59c1af509a8a793b736d0a7559b54fae72b187f845bfa5c93255824526d0"},
-    {file = "pyats.async-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:640459c6e643bf9d7447b1b7909eb9ead880e1c6b81d21f9e04e6885ed920e83"},
-    {file = "pyats.async-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6ec1c76625c75329f87000f86f025f1053d0383e49c959a2b2e06dc7927fe2ea"},
-]
-"pyats.connections" = [
-    {file = "pyats.connections-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:55518297b426688825457ae8a5e5b5c2e7747f22d0bb8efad2d2dcc0409b9389"},
-    {file = "pyats.connections-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:72cae2794640147ee8abb91e1eadafcd98c1953c1d3d0ba7cccebd2bd35a028d"},
-    {file = "pyats.connections-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c26173fafc68d0a8325af57230fca6d9e39dee6765db0b604f2f34b2872eb319"},
-    {file = "pyats.connections-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:acd85ca0c8137b3209cbc9257b73fa384f98de1415e7d63f01ed2fbd12beed06"},
-    {file = "pyats.connections-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0a79ad536b90fd3e629c76aadf83bde866fc265360aa07dd9294773f4f6dca2a"},
-    {file = "pyats.connections-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:ec777c27529480450310eaa833f976f69d64bb6ea4e72cb66823d5acec371be7"},
-    {file = "pyats.connections-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5cf3d35b996cb8be56df2835126d24bcb9ba0757ab2666eba509d86a483d70d7"},
-    {file = "pyats.connections-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:da13b3c85b35841b0b89a880f39af1e24b9959ed5c537baadc3a97b5f7e4e4ef"},
-    {file = "pyats.connections-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:019cc33c6ce4184b1604d74d6d840463adb9869d249587ef1cd752b8725952ae"},
-    {file = "pyats.connections-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1792732fc9a6dfa8ad25d1fa855fdd0beb4825f7b04e574a47fd3f792f144afc"},
-    {file = "pyats.connections-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:425f64d9f93b647a1868014db949d024ffebb797af65c89d9bccf247057f709c"},
-    {file = "pyats.connections-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:356960a556213bb4d004d306ffa5b8691b294d57599be0ada7a56d38fb124572"},
-    {file = "pyats.connections-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:8040de362470a76e0d086728b4a3d236a79d9fc734091394496776d06d1ab8ea"},
-    {file = "pyats.connections-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4c44587c9faa1aa8930488d27eb21089c958a1dd456f375561541b1c2813231"},
-    {file = "pyats.connections-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cc9805053b626d889da7926d84a67dc86e0666f09303e8c81575492847941985"},
-]
-"pyats.datastructures" = [
-    {file = "pyats.datastructures-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9261dc061ec6a449d43da336916305d3584a751270c8fb216b462843010df0e2"},
-    {file = "pyats.datastructures-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:5dc91d2a52388ee6553e23a6f4f4d7202c35f7a77028baf7d8cfea397dfbdcf6"},
-    {file = "pyats.datastructures-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a52273252719141ab4fbe7a8ac3710b24b79e05651d777a6b7dec32bae3abe53"},
-    {file = "pyats.datastructures-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:3052d7b4d54d8ff20bce009d0e00c7b53546a4ffdc6a0ecce6d308ef77673a95"},
-    {file = "pyats.datastructures-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:9c3171f308261f503261c5a50a373a5961f33fc9fb3fc4c9721b37c7de184b98"},
-    {file = "pyats.datastructures-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:27ce8a85f0ccc7a5ea4c70e8d3883cde2402b81e30bbdf39a0dd3ab3d3ed1019"},
-    {file = "pyats.datastructures-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:de01689241b0742d31e5bd5fd6ab83dfcb0f0b1d23cfa625dbbbbc774551980d"},
-    {file = "pyats.datastructures-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:6ea9191a32c32b61dd9fb6d244983e8cd35bc184ad0e25d0d5e861db17c0b153"},
-    {file = "pyats.datastructures-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:a6a5d32fffd3cf4ceb57337a6aefe3a0ef53465794e75b32915f4451bcc4b2c5"},
-    {file = "pyats.datastructures-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2bc666a0043c4a39561638f30071aaa49aaf697059e7357e12dbd78ebf955511"},
-    {file = "pyats.datastructures-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:276977e8841c12e78711aff91dc0af1b91dda7eca8da9e163e77022d09ca08bb"},
-    {file = "pyats.datastructures-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:13c7f4f198a1aa235e3603979e427ad8fc5d6eb082df9388f5c6e2215f1e17f2"},
-    {file = "pyats.datastructures-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:e52011143dc4752225d8fe9560474e5d52bc630c3db0d54043e9c5e08095cc52"},
-    {file = "pyats.datastructures-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:248aed9b07d48521a24bcfb988dfac3780e2471e62c252d36cae1b2c34af708b"},
-    {file = "pyats.datastructures-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7940866eabd68fcb1b765d68d04a11751896d757d8b208c2db28b6a6d9d40520"},
-]
-"pyats.easypy" = [
-    {file = "pyats.easypy-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:f4bf2ad91256cdf3fedfc3762988828c197649de94aa567799879f64e3370946"},
-    {file = "pyats.easypy-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:82c94128a7fbb16f45fa99016be3f63439b329db7e3e8dd056b040b4d20fb1af"},
-    {file = "pyats.easypy-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cbf67fbafc31b941068ad6cccdde5ace701f6916c35a708d90f8225a6cb33108"},
-    {file = "pyats.easypy-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:c3726d0672535c35446da287586bc123a98d0cffdfd4c3d672cf8e8b91429848"},
-    {file = "pyats.easypy-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:bb79b7628e508c92f47803460695fa7caae52ab0e95d8bfdafeb681011c7c985"},
-    {file = "pyats.easypy-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:2c0dfd6fe7441f182f9e6aa374282bd54ba0c48ffd33aa286f6abaa209002be2"},
-    {file = "pyats.easypy-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dbd4f3d1e969da0462848d7421edba38401c225921db6639391120497efc21f4"},
-    {file = "pyats.easypy-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0582f247216520e133fe6488770d8b465da317a64d5468467aae8fc54eadc81"},
-    {file = "pyats.easypy-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:c55f8d7d76237839391443cca7fa037238819b0f5523fa91e82112b991b6ced3"},
-    {file = "pyats.easypy-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1c0d2c8f07b4e20780653f1b050ae330234917fe2103f60dd208a8b9c5017244"},
-    {file = "pyats.easypy-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9a0eaa14adba7001435ed4d571a4988c2df21946501764be31cea16963eab760"},
-    {file = "pyats.easypy-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:583fec9c5d9af34aaefa6f8dd73b4252a9c7798bbd453728442ca9ed4ebab1f0"},
-    {file = "pyats.easypy-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:cfd9badcbccb5274d0b24fb9bf8f80f44374a14a61d5dd569c98e09a4d2de3fb"},
-    {file = "pyats.easypy-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e1d30581c7691f7ab8598864d05888bbaa6618de6fd48d9c2fb046895f310d23"},
-    {file = "pyats.easypy-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d3eb62e2fddf1d448e59ab98d8c22eafbba7b92da3c89498097e82a9a40c315f"},
-]
-"pyats.kleenex" = [
-    {file = "pyats.kleenex-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6f92b143d53bd6b35f2e0a958bca9260df8fefdcb055effa9fcf18c4422aecc2"},
-    {file = "pyats.kleenex-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:db7c2f0494ef9f2c76ef2d3e6bff31202da60e04c042b9b8deabac0fe516f6e9"},
-    {file = "pyats.kleenex-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80574b5bc27005bfb35dad481d9af9fb6adcdbc7455f100bafa2f7f571df9562"},
-    {file = "pyats.kleenex-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:7b386741c95b14c7f5385ab83fd769a2b6b8d9b1ab48589bf91063437295d95a"},
-    {file = "pyats.kleenex-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:4a02502bc9fb38ad60d34535c897d67ea9fd454e67a0c652bac984906f46a0c4"},
-    {file = "pyats.kleenex-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:4c3e7909dd52fcfb067a5648fc0dc526cb8b4ba012544e49116f9e5c289f491d"},
-    {file = "pyats.kleenex-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:9e1e70486b89f06b0ce03cd23197fb9c31c306e055d270fcdcd635c335ede6c4"},
-    {file = "pyats.kleenex-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:5b5f905f706c5ae2dce8a18e4c04c42466d5281c1146eaad35e85bd6bd1a9ad7"},
-    {file = "pyats.kleenex-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:c1fe97bf45b8ca3223bc74c93da982a7d6f3db549cb48950c64e13c40f122588"},
-    {file = "pyats.kleenex-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5171216ee9647f5a24f4fa2263db98a82daedb3d2555e531bded1c3e3aedfe8"},
-    {file = "pyats.kleenex-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:74447c7781dc7995d8d4d9a1240a8e897cb0fcffbacc10648d825917f88eb7b1"},
-    {file = "pyats.kleenex-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:82f9284fabf511c549f724964ff54fee6a0f42be8278ae6cb9b7bec8fc141026"},
-    {file = "pyats.kleenex-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:044192724a881bb229f65f3b59549e73d0f7eda4824b77e9f7e0b33a08dac55b"},
-    {file = "pyats.kleenex-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c23916098f7d6fe597f1a092ad68d89ddc2f2471776e2876588f364077bfa618"},
-    {file = "pyats.kleenex-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8c81966b01aadd4b02a55a50181f7503cc8032ccabc0e73e4498eeb847918e94"},
-]
-"pyats.log" = [
-    {file = "pyats.log-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e42bd7dde78b8b23e14273a32ee26ba6dbf90d92ebaff941ee56acdc6e416e73"},
-    {file = "pyats.log-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:abf88c552a086aa7d6effcd4770070421173dedcf5cc061263feaae4cab25160"},
-    {file = "pyats.log-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:302b3d5ad2217a10ac71511eb8071750b5bcd88aabec02839e7930a3786eaaa5"},
-    {file = "pyats.log-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6f0d7fe19a6bff24af5c00aa3bf58f6d4e47a4de8d77e2f729c31a27db307b0f"},
-    {file = "pyats.log-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ea00d0307e08c958cfa10fb4ff0eb485a420895bad302dd55706413acb595eb3"},
-    {file = "pyats.log-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:91991cac0ecc4ec0b124632558e4ee2aad70b36c50c25901a9feaa4a49855adb"},
-    {file = "pyats.log-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:58d9a73baa6f2c0d4a149f43dd1381b91407c5fe5aec877b2e208051183800ce"},
-    {file = "pyats.log-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:56dae5205a73f5cd911faa8c04bdabfaa6d289e85c46e7d061d9e9f91568c246"},
-    {file = "pyats.log-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:9852d2667e13f37938206e14b599274d96d3a2758dc4f131d86cc05b1f9858d8"},
-    {file = "pyats.log-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3db825948da8be757fce038ec0a27668cc7bb67105de44574ea6d8a22c70f5b0"},
-    {file = "pyats.log-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c64ec9553fcaca5b31de70d7d488a6208cd1aa6c4d886f33bd765d45c00df771"},
-    {file = "pyats.log-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:df2dea2e49afdf27ec8b8e569b5438897872d075c0991e2e9643bab7514922e1"},
-    {file = "pyats.log-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:0918bea3fc756cba52ff7d8be31868080162074e08dbaaed220f4309a723b262"},
-    {file = "pyats.log-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d571645feb60f5ead31841df1a52c4c2c70b3d5dba7a562117d6e958cf72e6ce"},
-    {file = "pyats.log-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8d7b32e8bb7f15a3c98997217581177aa4a2eb1f8891834e16a760637800a1b"},
-]
-"pyats.reporter" = [
-    {file = "pyats.reporter-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:13e66b80f754587d48c55b2f070eb5b5ef3933f7d9e47e3e544882c9501bc694"},
-    {file = "pyats.reporter-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:db8692ce17a989f66fde3bd90383f2f6bfe3446600e49e22a20276b874b1ee30"},
-    {file = "pyats.reporter-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:94a6ac13d09c8d80efc0a164036c7ef42857849a2154e4d6e05cfec00c0e3d2c"},
-    {file = "pyats.reporter-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:25e0088a2735b99c201f9be65f81b54f11b2cb2144607f1ab23f19d07bb9b8dc"},
-    {file = "pyats.reporter-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:2d3f39b4da7701ea35f3e754d2e56b99dd90183e8e4cd4019baefc26ef2f4748"},
-    {file = "pyats.reporter-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:0c945be3fd78f517d333c153080517353c3fb5974de9859cbd2e757cb13d55a7"},
-    {file = "pyats.reporter-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c347d8757eeea792dc42f5d3b2ed23d2b12000fbd53c8fbe4bd8e16ab3ac1276"},
-    {file = "pyats.reporter-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0317b60692d46ca75093e76611e8ac1aaf4eb5d947c7085779ca08f80656aab4"},
-    {file = "pyats.reporter-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2d7084ee1e7de197051a16ba89565c1dfba5c5d157f661a1e414091ded8cabb6"},
-    {file = "pyats.reporter-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5251937297af97abe6997d93ce4196bcffa2a4f7f3ac3385ac3d2432f8cfc742"},
-    {file = "pyats.reporter-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e0d7350e7efb60f0bb9b0388e1f884285b838291d2cf711746b48f847b96c4d0"},
-    {file = "pyats.reporter-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:e1e3c7c39c3220613cc8747ecc1a7a2c4cf5e5cfa7d7a46a380cef00a5279817"},
-    {file = "pyats.reporter-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:1bbd33832e518946989fb5f40eaeef323da313a7b60b39ed97718f0a0f728e45"},
-    {file = "pyats.reporter-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e5677f6f36a9855ed408a31dd9fddc87b2fbe1182f62a283ed20f7f99e127bc"},
-    {file = "pyats.reporter-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8514965827ffe3012a88ed4608253a0028a3e32b537bcd4b25c09548de0abdad"},
-]
-"pyats.results" = [
-    {file = "pyats.results-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:99df166c78ccefbe1d4a980067ea1219aabd1732fefe6fa1cc95430a48540892"},
-    {file = "pyats.results-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:cbed7d1ba0fa556d74d55ed01d8c92198521494e9baa4355519f31de13e7000d"},
-    {file = "pyats.results-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9959acef26a3edebb5dd94a420d92f05ada97401d22112f011e41e857931413"},
-    {file = "pyats.results-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:02eacf68773e1e9c789ef4969a228015f84225a90fcb3299ae8f3b3fa25c2479"},
-    {file = "pyats.results-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:1e2d194cd05f5c50ed1a2ead36b74f22837273033af3bcaa1b7274a4d6210567"},
-    {file = "pyats.results-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:c837c20b5326fd30fffd002c889c90a1057d63620657c265bc15edaab3c932be"},
-    {file = "pyats.results-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:061cae2b55ff3654f655fbe0de47d5e70c57d4b55f2eed00e88c5b539fc36b6c"},
-    {file = "pyats.results-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:74466181d44ecaf6900519f153b6d45f24af9024a4476a79e89860fc3e4215a3"},
-    {file = "pyats.results-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:88674730bf192fbb4e99d56922aac9b0cdea96a52c6337a1183efe90414eba6f"},
-    {file = "pyats.results-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4372425bafcfe8ce9130153967d4a82a40698c3b8ff88709b5b4dcf9489bf432"},
-    {file = "pyats.results-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0d43940054eb4fcf8c35c9b7f738df87faebbbc83b6700197182ffdf0acc5506"},
-    {file = "pyats.results-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:ceee990b96c6d69fb6005d4eb342824b0b68b43f437102c9a1763268f5c16a95"},
-    {file = "pyats.results-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:03f3291150c7bbbc6eb7b5446cd680be16b7a73ecae7382f5a3719cf772c1748"},
-    {file = "pyats.results-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4d94205c50af50c0c9978d1a5b1996dbc03b61436d0639eadcdb6322617bb0c"},
-    {file = "pyats.results-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d6ed36d4eec9e610a09fb0bda69ce14ae965c7e93cabc9e4f21c747357f5168d"},
-]
-"pyats.tcl" = [
-    {file = "pyats.tcl-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b8ff0d6220c441a824382b300979c04d5891bd11ad23579e6e24d155d7159fa"},
-    {file = "pyats.tcl-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:cd0cab6fa82ddf064a70f6b2b8706ae1172c0ed590d628db49d90184a61f1450"},
-    {file = "pyats.tcl-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8b4344c0af3f486a7b6b2af84fd946e2a8e871bffc3532bcf37fbdf65e7ffd4e"},
-    {file = "pyats.tcl-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:692a848936dc092b4dbfcfb6a5b5ee1962b3784d0436da6190c7e51be8ccc968"},
-    {file = "pyats.tcl-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5cefb816fa2975fac72f3d2aff46596d2357932d7bae5b99a217fb300474baca"},
-    {file = "pyats.tcl-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:2d46942b60191a0ebedf4dbbe98f998b66abb09e7aa62b53662df2b7a57af78a"},
-    {file = "pyats.tcl-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:94addd864977f8010ffef56cd5a2ade0ea56ec1f790a037760c4cf0abd4b1a71"},
-    {file = "pyats.tcl-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:af055076689e7de71f0be3de35ae525737485673090b9b15746b920f8e588fbe"},
-    {file = "pyats.tcl-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:889e8432656c808fc872e2fd5a624844e4f2874ea8f82b71dc3a7befc6f07eaa"},
-    {file = "pyats.tcl-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c5952380a56ca3b49513f784685327251703f92cb6acd1cd376b0edbdd696fe0"},
-    {file = "pyats.tcl-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9856be73c0f085994b86baa2b1c38132ccec4619263d7f9f382de3ca8362f74f"},
-    {file = "pyats.tcl-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:6d1bcb92dada804a8e4680fc11ca2b7aa12b9010c4ab5f5bc57432b412978d86"},
-    {file = "pyats.tcl-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:0d6f07ca6db9263aca76078530ca076888b3c249c46f2c86ccb4006a03da652f"},
-    {file = "pyats.tcl-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8cae942f9ded43651eb26a70e06151f31d313cd3e83ac3b40501832b3debb1d"},
-    {file = "pyats.tcl-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:10b4e487c2083c457d7d35bffa75b74495176763d1917f6c0cf3107f264c01b0"},
-]
-"pyats.topology" = [
-    {file = "pyats.topology-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e2b9b783f616ec8d8b479faf8eceed2714d72ebf67406aa5318a852e27c3428c"},
-    {file = "pyats.topology-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:e565926bc5881bd49efa0a663bff134bf7752e3f1e4bab94860a9781f06ab408"},
-    {file = "pyats.topology-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:994fccda7819485c803ea7cc744bd28d155b969374319f1c6942f4e6fc1f6398"},
-    {file = "pyats.topology-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6945d06c55eb415cdbd2cb967bffd9a257b66dab7819e62f1fe4b8b70cf353ee"},
-    {file = "pyats.topology-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:db2cb06701cb33b7e77d06ff03352d72cd6dfcd54cd91000dcce1a8519c19fc4"},
-    {file = "pyats.topology-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:4ce1ed2f8c77b3006e7f9b9573a81005e930c34d5c069be3d0b0329c5633c357"},
-    {file = "pyats.topology-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dd1085c0c6364d545f1891f7ce1375379ed4233f09d6befb81e10d815f3f6f66"},
-    {file = "pyats.topology-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:cacf3e23568edcc79aaddcb92505880daf0d20319bb3acdbca259ef1075c4ed8"},
-    {file = "pyats.topology-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:dd15410d5ea429591451cd4fa343e632ed6c62b01a92c2a76c0541d32cc0b77e"},
-    {file = "pyats.topology-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b8b394071556318a2a374a87cb754081de31dbeeb1c2e735d81686212c6766b6"},
-    {file = "pyats.topology-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ed3b3a40c47b9c9d7ced2872b6252d79c9bf1c42fd12931d98b1ca53bcdd8d10"},
-    {file = "pyats.topology-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:f22acffc2ced9d01bea5dc715bdd1ef5dbce51f7ff7c19073eb2982c8038e979"},
-    {file = "pyats.topology-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:db14c4ca8f54ac4e3bcac666a1565eb206e1a57274c19c00d0df451db54a5ccc"},
-    {file = "pyats.topology-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6977e406709e79f05f2a43a39623e00f42138b6d5a3dea82109a4e8fc2139b45"},
-    {file = "pyats.topology-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3ded8f1dbabf52e2f5c354c7a88ff79f33e515267ba6a7d11b5e0084bb491703"},
-]
-"pyats.utils" = [
-    {file = "pyats.utils-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:63542f4f11d8efae61b45a6c8ceb78a277826a05bc5858158d7fc4748952173a"},
-    {file = "pyats.utils-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:3004f4bcc4973712a6e300217446fb3a033e8a3a5ff5cd428d5749ea6af46421"},
-    {file = "pyats.utils-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4aaa44ab5c252b39ee22a5611a94c21f0e6234cab2c8d7b0596ddca28ca425e9"},
-    {file = "pyats.utils-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:07b5171c189909dab9d97f023f2902701e76daf099c0ec62e4c19a75f9dd954a"},
-    {file = "pyats.utils-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:66c03ca033fc33199523cdecbb08f7fa8dd9bbf0cd120756a9c66c020bf52603"},
-    {file = "pyats.utils-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:56380e6470c86f63145eec1057d72bb08a9aca28a662c59e55fffedc4a02ab04"},
-    {file = "pyats.utils-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6fb321ccfebb4991d8ccd9f60d41ed43a2caf9d0b4b3b650ed57feb9bbbb290e"},
-    {file = "pyats.utils-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:30c536e4711f665aabf55dcc1045bb64b553cf9ce4fa2a05362de2078ce8282e"},
-    {file = "pyats.utils-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:4f6e279efb4080eaeeda1e9faa26e8570c6de87e4a8773f25f4cbed07521b9eb"},
-    {file = "pyats.utils-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e5fa0d0a6bf28b0d39504abad212ed97124d526cea040a20bd3532f38ae5c7ac"},
-    {file = "pyats.utils-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:010ba9bb04097f6cbd1c49dbc23d5191d405e08d0974049e714be8c12b95b97f"},
-    {file = "pyats.utils-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:ab6a7218385c0156d362251f8e01014a7448df97d0d08a3514d0efe7f2cd2cf9"},
-    {file = "pyats.utils-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:eb3fbe2db928fd4ee4e6d9a0aad43de5f5745dfc8de2a0cba3c0e1100c18a6db"},
-    {file = "pyats.utils-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:27fcefd5fab5f7e81e37170b21448173bd564dad884f4f98b49321406ea1381e"},
-    {file = "pyats.utils-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b464939e0decbfdb2e8c2bb59aaeafb5f678c6fcd9dc6e35d778bbf2f100d73b"},
-]
+pyats = []
+"pyats.aereport" = []
+"pyats.aetest" = []
+"pyats.async" = []
+"pyats.connections" = []
+"pyats.datastructures" = []
+"pyats.easypy" = []
+"pyats.kleenex" = []
+"pyats.log" = []
+"pyats.reporter" = []
+"pyats.results" = []
+"pyats.tcl" = []
+"pyats.topology" = []
+"pyats.utils" = []
 pycodestyle = [
     {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
     {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
@@ -1767,6 +1598,10 @@ pyflakes = [
     {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
     {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
 ]
+pygments = [
+    {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"},
+    {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"},
+]
 pyparsing = [
     {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
     {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
@@ -1783,6 +1618,10 @@ python-socketio = [
     {file = "python-socketio-4.6.1.tar.gz", hash = "sha256:cd1f5aa492c1eb2be77838e837a495f117e17f686029ebc03d62c09e33f4fa10"},
     {file = "python_socketio-4.6.1-py2.py3-none-any.whl", hash = "sha256:5a21da53fdbdc6bb6c8071f40e13d100e0b279ad997681c2492478e06f370523"},
 ]
+pytz = [
+    {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
+    {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
+]
 pyyaml = [
     {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
     {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
@@ -1818,10 +1657,7 @@ pyyaml = [
     {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
     {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
 ]
-requests = [
-    {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"},
-    {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"},
-]
+requests = []
 requests-mock = [
     {file = "requests-mock-1.9.3.tar.gz", hash = "sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba"},
     {file = "requests_mock-1.9.3-py2.py3-none-any.whl", hash = "sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970"},
@@ -1842,6 +1678,39 @@ smmap = [
     {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
     {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
 ]
+snowballstemmer = [
+    {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
+    {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
+]
+sphinx = []
+sphinx-rtd-theme = [
+    {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"},
+    {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"},
+]
+sphinxcontrib-applehelp = [
+    {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
+    {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
+]
+sphinxcontrib-devhelp = [
+    {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
+    {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
+]
+sphinxcontrib-htmlhelp = [
+    {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"},
+    {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"},
+]
+sphinxcontrib-jsmath = [
+    {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
+    {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
+]
+sphinxcontrib-qthelp = [
+    {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
+    {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
+]
+sphinxcontrib-serializinghtml = [
+    {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
+    {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
+]
 toml = [
     {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -1850,45 +1719,15 @@ tomli = [
     {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
 ]
-typing-extensions = [
-    {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
-    {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
-]
-unicon = [
-    {file = "unicon-22.5-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:70794d5a63a03d4f933fce239a33bd49866d39755ea4690fd2115e6032e80007"},
-    {file = "unicon-22.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:405f9b6855aacfe8ed137f23066a6aa8cf510e4fa4dae6daccb1258b25d38395"},
-    {file = "unicon-22.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c02ee0736010c812cd671198536510b08f0f22260c1c0682b407301b65b338de"},
-    {file = "unicon-22.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:be971f98ec220fcd1f801e9244002b8f72a3effa4fe1f5d51a300a3482a5ab1b"},
-    {file = "unicon-22.5-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:efc7c254b58cf4a9ad725583c0424dbdb476d04c09e45c453942fc79549380a2"},
-    {file = "unicon-22.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:fd1bd86d11f284e1d750e843aa550b01bcea4f1023a6676b17b3ce0ab12c0906"},
-    {file = "unicon-22.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2325ff7fa0bb618fcd0a5d6632f77277d47d191c2d46c5a0ecdc61a07fc3bfa9"},
-    {file = "unicon-22.5-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:7e615766c2092c285200d10b2a16d334242c3a8a91c525b87d9719eb812d14fd"},
-    {file = "unicon-22.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:10eb7d5b5728a9cfaf1dfd967fee0ad7f198376cc4fca0f3d37c8eb78ae86069"},
-    {file = "unicon-22.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c1ec8cfdc8dc7334f285180803c6000d156c5ba23c5262a26f2ce3702fc94e2b"},
-    {file = "unicon-22.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c202c12db64a365cbb2f16327fa9d5e33ecb3f8361afe31460d559bcd21e0981"},
-    {file = "unicon-22.5-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7122fc0941157ea9b6dfb5d676e935c415f97db4af909f689d34c918d6eed265"},
-    {file = "unicon-22.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:caf313c680dc9da0381da314a4ff85f2d50cfa2e35211a00d2766832e34af095"},
-    {file = "unicon-22.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9064b12c1b11fe678b2e3fd17f9e2612192566cb3ae37ccfb6adf6564a66550e"},
-    {file = "unicon-22.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a22047a307d32bcd78e4b3f2b83feae97e21dd6507712b8d19fea5358c27994f"},
-]
-"unicon.plugins" = [
-    {file = "unicon.plugins-22.5-py3-none-any.whl", hash = "sha256:6777d82858bce25cd150efd3689bd47b20b33734527529841e0407261326b668"},
-]
-urllib3 = [
-    {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
-    {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
-]
-virtualenv = [
-    {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"},
-    {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"},
-]
+unicon = []
+"unicon.plugins" = []
+urllib3 = []
+virtualenv = []
 wcwidth = [
     {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
     {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
 ]
-yamllint = [
-    {file = "yamllint-1.26.3.tar.gz", hash = "sha256:3934dcde484374596d6b52d8db412929a169f6d9e52e20f9ade5bf3523d9b96e"},
-]
+yamllint = []
 yarl = [
     {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},
     {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"},
@@ -1963,7 +1802,4 @@ yarl = [
     {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"},
     {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},
 ]
-zipp = [
-    {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"},
-    {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
-]
+zipp = []
diff --git a/pyproject.toml b/pyproject.toml
index e3cefc3..b10ea38 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "virl2_client"
-version = "2.4.0"
+version = "2.4.1"
 description = "VIRL2 Client Library"
 authors = ["Simon Knight <simknigh@cisco.com>", "Ralph Schmieder <rschmied@cisco.com>"]
 license = "Apache-2.0"
@@ -20,15 +20,22 @@ classifiers = [
 ]
 
 [tool.poetry.dependencies]
-python = "^3.7.0"
+python = "^3.8.0"
 requests = "^2"
 requests-toolbelt = "^0.9.1"
 urllib3 = "^1.26.9"
 
-# optional pyats package
+# optional pyATS package
 # (this pulls in a lot more dependencies)
+# it does not pull in genie, need to specify extras=["full"] for pyATS.
+# pyats = {extras=["full"], version = "^22.4", optional = true}
 pyats = {version = "^22.4", optional = true}
 
+# needed to build documentation, technically, this is a dev-dependency
+# https://github.com/python-poetry/poetry/pull/606 -- no support
+sphinx_rtd_theme = {version="^1", optional = true}
+sphinx =  {version="^5", optional = true}
+
 [tool.poetry.dev-dependencies]
 flake8 = "^4.0.1"
 pre-commit = "^2.17.0"
@@ -38,6 +45,7 @@ responses = "^0.20.0"
 
 [tool.poetry.extras]
 pyats = ["pyats"]
+docs = ["sphinx", "sphinx_rtd_theme"]
 
 [build-system]
 requires = ["poetry>=1.1.8"]
diff --git a/virl2_client/__init__.py b/virl2_client/__init__.py
index e1d71c0..eda8c4b 100644
--- a/virl2_client/__init__.py
+++ b/virl2_client/__init__.py
@@ -18,8 +18,17 @@
 # limitations under the License.
 #
 
-# flake8: noqa: F401
-
 from .exceptions import InterfaceNotFound, LabNotFound, LinkNotFound, NodeNotFound
 from .virl2_client import ClientConfig, ClientLibrary, InitializationError
 from .models.authentication import Context
+
+__all__ = [
+    "InterfaceNotFound",
+    "LabNotFound",
+    "LinkNotFound",
+    "NodeNotFound",
+    "ClientConfig",
+    "ClientLibrary",
+    "InitializationError",
+    "Context",
+]
diff --git a/virl2_client/exceptions.py b/virl2_client/exceptions.py
index ba47822..b2c5d04 100644
--- a/virl2_client/exceptions.py
+++ b/virl2_client/exceptions.py
@@ -19,17 +19,37 @@
 #
 
 
-class NodeNotFound(Exception):
+class VirlException(Exception):
     pass
 
 
-class LinkNotFound(Exception):
+class InitializationError(VirlException):
     pass
 
 
-class InterfaceNotFound(Exception):
+class ElementAlreadyExists(VirlException, FileExistsError):
     pass
 
 
-class LabNotFound(Exception):
+class ElementNotFound(VirlException, KeyError):
+    pass
+
+
+class NodeNotFound(ElementNotFound):
+    pass
+
+
+class LinkNotFound(ElementNotFound):
+    pass
+
+
+class InterfaceNotFound(ElementNotFound):
+    pass
+
+
+class LabNotFound(ElementNotFound):
+    pass
+
+
+class DesynchronizedError(VirlException):
     pass
diff --git a/virl2_client/models/authentication.py b/virl2_client/models/authentication.py
index 1f2aa5a..08c1c8c 100644
--- a/virl2_client/models/authentication.py
+++ b/virl2_client/models/authentication.py
@@ -22,14 +22,15 @@
 from urllib.parse import urljoin, urlparse
 
 import requests
+import requests.auth
 
-logger = logging.getLogger(__name__)
+_LOGGER = logging.getLogger(__name__)
 
 
 class TokenAuth(requests.auth.AuthBase):
     """
     Inspired by:
-    http://docs.python-requests.org/en/v2.9.1/user/authentication/?highlight=AuthBase#new-forms-of-authentication
+    http://requests.readthedocs.io/en/v2.9.1/user/authentication/?highlight=AuthBase#new-forms-of-authentication
     """
 
     def __init__(self, client_library):
@@ -47,14 +48,14 @@ def handle_401_unauthorized(self, resp, **kwargs):  # pylint: disable=W0613
         # As almost every library call uses "raise_for_status()"
         if resp.status_code != 401:
             if not resp.ok:
-                logger.error("API Error: %s", resp.text)
+                _LOGGER.error("API Error: %s", resp.text)
             return resp
 
         # reset existing token:
         self.token = None
         # repeat last request that has failed
         token = self.authenticate()
-        logger.warning("re-auth called on 401 unauthorized")
+        _LOGGER.warning("re-auth called on 401 unauthorized")
         request = resp.request.copy()
         request.headers["Authorization"] = "Bearer {}".format(token)
         request.deregister_hook("response", self.handle_401_unauthorized)
@@ -70,9 +71,9 @@ def authenticate(self):
         )  # pylint: disable=W0212
         parsed_url = urlparse(url)
         if parsed_url.port is not None and parsed_url.port != 443:
-            logger.warning("Not using SSL port of 443: %d", parsed_url.port)
+            _LOGGER.warning("Not using SSL port of 443: %d", parsed_url.port)
         if parsed_url.scheme != "https":
-            logger.warning("Not using https scheme: %s", parsed_url.scheme)
+            _LOGGER.warning("Not using https scheme: %s", parsed_url.scheme)
         data = {
             "username": self.client_library.username,
             "password": self.client_library.password,
diff --git a/virl2_client/models/cl_pyats.py b/virl2_client/models/cl_pyats.py
index e85538b..132be30 100644
--- a/virl2_client/models/cl_pyats.py
+++ b/virl2_client/models/cl_pyats.py
@@ -19,9 +19,6 @@
 #
 
 import io
-import logging
-
-logger = logging.getLogger(__name__)
 
 
 class PyatsNotInstalled(Exception):
@@ -33,20 +30,19 @@ class PyatsDeviceNotFound(Exception):
 
 
 class ClPyats:
-    """Creates a pyATS object that can be used to run commands
-    against a device either in exec mode ``show version`` or in
-    configuration mode ``interface gi0/0\\\\nno shut``.
-
-    :param lab: The lab which should be used with pyATS
-    :type lab: models.Lab
-    :param hostname: Force hostname/ip and port for console terminal server
-    :type hostname: str
-    :raises PyatsNotInstalled: when pyATS can not be found
-    :raises PyatsDeviceNotFound: when the device can not be found
-    """
-
     def __init__(self, lab, hostname=None):
-        """Constructor method"""
+        """
+        Creates a pyATS object that can be used to run commands
+        against a device either in exec mode ``show version`` or in
+        configuration mode ``interface gi0/0 \\n no shut``.
+
+        :param lab: The lab which should be used with pyATS
+        :type lab: models.Lab
+        :param hostname: Force hostname/ip and port for console terminal server
+        :type hostname: str
+        :raises PyatsNotInstalled: when pyATS can not be found
+        :raises PyatsDeviceNotFound: when the device can not be found
+        """
         self._pyats_installed = False
         self._lab = lab
         self._hostname = hostname
@@ -65,7 +61,8 @@ def _check_pyats_installed(self):
             raise PyatsNotInstalled
 
     def sync_testbed(self, username, password):
-        """Syncs the the testbed from the server. Note that this
+        """
+        Syncs the testbed from the server. Note that this
         will always fetch the latest topology data from the server.
 
         :param username: the username that will be inserted into
@@ -85,7 +82,8 @@ def sync_testbed(self, username, password):
         self._testbed = data
 
     def run_command(self, node_label, command):
-        """Run a command on the device in exec mode
+        """
+        Run a command on the device in `exec` mode.
 
         :param node_label: the label / title of the device
         :type node_label: str
@@ -108,8 +106,9 @@ def run_command(self, node_label, command):
         return pyats_device.execute(command, log_stdout=False)
 
     def run_config_command(self, node_label, command):
-        """Run a command on the device in configure mode. pyATS
-        handles the change into configure mode automatically.
+        """
+        Run a command on the device in `configure` mode. pyATS
+        handles the change into `configure` mode automatically.
 
         :param node_label: the label / title of the device
         :type node_label: str
@@ -132,6 +131,6 @@ def run_config_command(self, node_label, command):
         return pyats_device.configure(command, log_stdout=False)
 
     def cleanup(self):
-        """cleans up the pyATS connections"""
+        """Cleans up the pyATS connections."""
         for pyats_device in self._connections:
             pyats_device.destroy()
diff --git a/virl2_client/models/groups.py b/virl2_client/models/groups.py
index 9e91ee6..3854dae 100644
--- a/virl2_client/models/groups.py
+++ b/virl2_client/models/groups.py
@@ -18,14 +18,8 @@
 # limitations under the License.
 #
 
-import logging
 
-
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.INFO)
-
-
-class GroupManagement(object):
+class GroupManagement:
     def __init__(self, context):
         self.ctx = context
 
@@ -38,7 +32,7 @@ def groups(self):
         Get the list of available groups.
 
         :return: list of group objects
-        :rtype:  list
+        :rtype: list
         """
         response = self.ctx.session.get(self.base_url)
         response.raise_for_status()
@@ -46,7 +40,7 @@ def groups(self):
 
     def get_group(self, group_id):
         """
-        Gets the info for specified group..
+        Gets info for the specified group.
 
         :param group_id: group uuid4
         :type group_id: str
diff --git a/virl2_client/models/interface.py b/virl2_client/models/interface.py
index 95db18c..c741855 100644
--- a/virl2_client/models/interface.py
+++ b/virl2_client/models/interface.py
@@ -19,29 +19,29 @@
 #
 
 import logging
+import warnings
 from functools import total_ordering
 
-logger = logging.getLogger(__name__)
+_LOGGER = logging.getLogger(__name__)
 
 
 @total_ordering
 class Interface:
-    """A VIRL2 network interface, part of a node.
-
-    :param iid: interface ID
-    :type iid: str
-    :param node: node object
-    :type node: models.Node
-    :param label: the label of the interface
-    :type label: str
-    :param slot: the slot of the interface
-    :type slot: int
-    :param iface_type: the type of the interface, defaults to "physical"
-    :type iface_type: str, optional
-    """
-
     def __init__(self, iid, node, label, slot, iface_type="physical"):
-        """Constructor method"""
+        """
+        A VIRL2 network interface, part of a node.
+
+        :param iid: interface ID
+        :type iid: str
+        :param node: node object
+        :type node: models.Node
+        :param label: the label of the interface
+        :type label: str
+        :param slot: the slot of the interface
+        :type slot: int
+        :param iface_type: the type of the interface, defaults to "physical"
+        :type iface_type: str
+        """
         self.id = iid
         self.node = node
         self.type = iface_type
@@ -57,10 +57,6 @@ def __init__(self, iid, node, label, slot, iface_type="physical"):
         }
         self.ip_snooped_info = {"mac_address": None, "ipv4": None, "ipv6": None}
 
-    @property
-    def is_physical(self):
-        return self.type == "physical"
-
     def __eq__(self, other):
         if not isinstance(other, Interface):
             return False
@@ -95,17 +91,46 @@ def lab_base_url(self):
     def _base_url(self):
         return self.lab_base_url + "/interfaces/{}".format(self.id)
 
-    def links(self):
+    @property
+    def physical(self):
+        """Whether the interface is physical."""
         self.node.lab.sync_topology_if_outdated()
-        return [lnk for lnk in self.node.lab.links() if self in lnk.interfaces]
+        return self.type == "physical"
 
-    def degree(self):
-        self.node.lab.sync_topology_if_outdated()
-        return len(self.links())
+    @property
+    def connected(self):
+        """Whether the interface is connected to a link."""
+        return self.link is not None
 
-    def is_connected(self):
+    @property
+    def state(self):
+        self.node.lab.sync_states_if_outdated()
+        return self._state
+
+    @property
+    def link(self):
+        """Is link if connected, otherwise None."""
         self.node.lab.sync_topology_if_outdated()
-        return self.degree() > 0
+        for link in self.node.lab.links():
+            if self in link.interfaces:
+                return link
+
+    @property
+    def peer_interface(self):
+        link = self.link
+        if link is None:
+            return None
+        interfaces = link.interfaces
+        if interfaces[0] is self:
+            return interfaces[1]
+        return interfaces[0]
+
+    @property
+    def peer_node(self):
+        peer_interface = self.peer_interface
+        if peer_interface is None:
+            return None
+        return peer_interface.node
 
     @property
     def readbytes(self):
@@ -142,31 +167,34 @@ def discovered_ipv6(self):
         self.node.lab.sync_l3_addresses_if_outdated()
         return self.ip_snooped_info["ipv6"]
 
-    def peer_interfaces(self):
-        self.node.lab.sync_topology_if_outdated()
-        ifaces = set()
-        for link in self.links():
-            if link.interface_a.id == self.id:
-                ifaces.add(link.interface_b)
-            else:
-                ifaces.add(link.interface_a)
-        return ifaces
+    @property
+    def is_physical(self):
+        warnings.warn("Deprecated, use .physical instead.", DeprecationWarning)
+        return self.physical
 
-    def peer_nodes(self):
-        self.node.lab.sync_topology_if_outdated()
-        return {iface.node for iface in self.peer_interfaces()}
+    def as_dict(self):
+        # TODO what should be here in 'data' key?
+        return {"id": self.id, "node": self.node.id, "data": self.id}
+
+    def get_link_to(self, other_interface):
+        """
+        Returns the link between this interface and another.
+
+        :param other_interface: the other interface
+        :type other_interface: models.Interface
+        :returns: A Link
+        :rtype: models.Link
+        """
+        link = self.link
+        return link if other_interface in link.interfaces else None
 
     def remove_on_server(self):
-        logger.info("Removing interface %s", self)
+        _LOGGER.info("Removing interface %s", self)
 
         url = self._base_url
         response = self.node.session.delete(url)
         response.raise_for_status()
 
-    def as_dict(self):
-        # TODO what should be here in 'data' key?
-        return {"id": self.id, "node": self.node.id, "data": self.id}
-
     def bring_up(self):
         url = self._base_url + "/state/start"
         response = self.session.put(url)
@@ -177,7 +205,25 @@ def shutdown(self):
         response = self.session.put(url)
         response.raise_for_status()
 
-    @property
-    def state(self):
-        self.node.lab.sync_states_if_outdated()
-        return self._state
+    def peer_interfaces(self):
+        warnings.warn("Deprecated, use .peer_interface instead.", DeprecationWarning)
+        return {self.peer_interface}
+
+    def peer_nodes(self):
+        warnings.warn("Deprecated, use .peer_node instead.", DeprecationWarning)
+        return {self.peer_node}
+
+    def links(self):
+        warnings.warn("Deprecated, use .link instead.", DeprecationWarning)
+        link = self.link
+        if link is None:
+            return []
+        return [link]
+
+    def degree(self):
+        warnings.warn("Deprecated, use .connected instead.", DeprecationWarning)
+        return int(self.connected)
+
+    def is_connected(self):
+        warnings.warn("Deprecated, use .connected instead.", DeprecationWarning)
+        return self.connected
diff --git a/virl2_client/models/lab.py b/virl2_client/models/lab.py
index c0be4ff..b78746c 100644
--- a/virl2_client/models/lab.py
+++ b/virl2_client/models/lab.py
@@ -20,45 +20,26 @@
 
 import json
 import logging
+import warnings
 import time
+from typing import Dict
 
 from .node import Node
 from .interface import Interface
 from .link import Link
-from ..exceptions import LabNotFound, LinkNotFound, NodeNotFound
+from ..exceptions import (
+    LabNotFound,
+    LinkNotFound,
+    NodeNotFound,
+    ElementAlreadyExists,
+    InterfaceNotFound,
+)
 from .cl_pyats import ClPyats
 
-logger = logging.getLogger(__name__)
+_LOGGER = logging.getLogger(__name__)
 
 
 class Lab:
-    """A VIRL2 lab network topology. Contains nodes, links and interfaces.
-    Initializes a Lab instance.
-
-    :param title: Name / title of the lab
-    :type title: str
-    :param lab_id: A lab ID
-    :type lab_id: str
-    :param context: The context of the ClientLibrary that holds the connection data to the server
-    :type context: Context
-    :param username: Username of the user to authenticate
-    :type username: str
-    :param password: Password of the user to authenticate
-    :type password: str
-    :param auto_sync: Should local changes sync to the server automatically
-    :type auto_sync: bool
-    :param auto_sync_interval: Interval to auto sync in seconds
-    :type auto_sync_interval: float
-    :param wait: Wait for convergence on backend
-    :type wait: bool
-    :param wait_max_iterations: Maximum number of tries or calls for convergence
-    :type wait_max_iterations: int
-    :param wait_time: Time to sleep between calls for convergence on backend
-    :type wait_time: int
-    :param hostname: Force hostname/ip and port for pyATS console terminal server
-    :type hostname: str
-    """
-
     def __init__(
         self,
         title,
@@ -73,7 +54,34 @@ def __init__(
         wait_time=5,
         hostname=None,
     ):
-        """Constructor method"""
+        """
+        A VIRL2 lab network topology. Contains nodes, links and interfaces.
+
+        :param title: Name / title of the lab
+        :type title: str
+        :param lab_id: A lab ID
+        :type lab_id: str
+        :param context: The context of the ClientLibrary that holds the connection data
+            to the server
+        :type context: Context
+        :param username: Username of the user to authenticate
+        :type username: str
+        :param password: Password of the user to authenticate
+        :type password: str
+        :param auto_sync: Should local changes sync to the server automatically
+        :type auto_sync: bool
+        :param auto_sync_interval: Interval to auto sync in seconds
+        :type auto_sync_interval: float
+        :param wait: Wait for convergence on backend
+        :type wait: bool
+        :param wait_max_iterations: Maximum number of tries or calls for convergence
+        :type wait_max_iterations: int
+        :param wait_time: Time to sleep between calls for convergence on backend
+        :type wait_time: int
+        :param hostname: Force hostname/ip and port for pyATS console terminal server
+        :type hostname: str
+        """
+
         self.username = username
         self.password = password
 
@@ -81,22 +89,22 @@ def __init__(
         self._description = ""
         self._notes = ""
         self._lab_id = lab_id
-        self._nodes = {}
         self._context = context
         self._owner = username
+        self._nodes: Dict[str, Node] = {}
         """
         Dictionary containing all nodes in the lab.
-        It maps node identifier to `virl2_client.models.Node`
+        It maps node identifier to `models.Node`
         """
-        self._links = {}
+        self._links: Dict[str, Link] = {}
         """
         Dictionary containing all links in the lab.
-        It maps link identifier to `virl2_client.models.Link`
+        It maps link identifier to `models.Link`
         """
-        self._interfaces = {}
+        self._interfaces: Dict[str, Interface] = {}
         """
         Dictionary containing all interfaces in the lab.
-        It maps interface identifier to `virl2_client.models.Interface`
+        It maps interface identifier to `models.Interface`
         """
         self.events = []
         self.pyats = ClPyats(self, hostname)
@@ -279,7 +287,7 @@ def nodes(self):
         Returns the list of nodes in the lab.
 
         :returns: A list of Node objects
-        :rtype: list
+        :rtype: List[Node]
         """
         self.sync_topology_if_outdated()
         return list(self._nodes.values())
@@ -289,7 +297,7 @@ def links(self):
         Returns the list of links in the lab.
 
         :returns: A list of Link objects
-        :rtype: list
+        :rtype: List[Link]
         """
         self.sync_topology_if_outdated()
         return list(self._links.values())
@@ -299,7 +307,7 @@ def interfaces(self):
         Returns the list of interfaces in the lab.
 
         :returns: A list of Interface objects
-        :rtype: list
+        :rtype: List[Interface]
         """
         self.sync_topology_if_outdated()
         return list(self._interfaces.values())
@@ -324,13 +332,13 @@ def statistics(self):
 
     def get_node_by_id(self, node_id):
         """
-        Returns the node identified by the node_id.
+        Returns the node identified by the ID.
 
-        :param node_id:
+        :param node_id: ID of the node to be returned
         :type node_id: str
         :returns: A Node object
         :rtype: models.Node
-        :raises KeyError: if node not found
+        :raises NodeNotFound: If node not found
         """
         self.sync_topology_if_outdated()
         try:
@@ -342,73 +350,109 @@ def get_node_by_label(self, label):
         """
         Returns the node identified by the label.
 
-        :param label:
+        :param label: label of the node to be returned
         :type label: str
         :returns: A Node object
         :rtype: models.Node
-        :raises NodeNotFound: if node not found
+        :raises NodeNotFound: If node not found
         """
         self.sync_topology_if_outdated()
         for node in self._nodes.values():
             if node.label == label:
                 return node
-        else:
-            raise NodeNotFound(label)
+        raise NodeNotFound(label)
 
-    def get_link_by_nodes(self, node1, node2):
+    def get_interface_by_id(self, interface_id):
         """
-        Returns the link identified by two nodes.
+        Returns the interface identified by the ID.
 
-        :param node1: node id of first node
-        :type node1: str
-        :param node2: node id of second node
-        :type node2: str
-        :returns: A Link object
-        :rtype: models.Link
-        :raises LinkNotFound: if link not found
+        :param interface_id: ID of the interface to be returned
+        :type interface_id: str
+        :returns: An Interface object
+        :rtype: models.Interface
+        :raises InterfaceNotFound: If interface not found
         """
         self.sync_topology_if_outdated()
-        for link in self.links():
-            link_node_pair = (link.interface_a.node, link.interface_b.node)
-
-            if (node1, node2) == link_node_pair:
-                return link
-            elif (node2, node1) == link_node_pair:
-                return link
-        else:
-            raise LinkNotFound()
+        try:
+            return self._interfaces[interface_id]
+        except KeyError:
+            raise InterfaceNotFound(interface_id)
 
-    def get_link_by_interfaces(self, iface1, iface2):
+    def get_link_by_id(self, link_id):
         """
-        Returns the link identified by two interfaces.
+        Returns the link identified by the ID.
 
-        :param iface1: node id of first node
-        :type iface1: str
-        :param iface2: node id of second node
-        :type iface2: str
+        :param link_id: ID of the interface to be returned
+        :type link_id: str
         :returns: A Link object
         :rtype: models.Link
-        :raises LinkNotFound: if link not found
+        :raises LinkNotFound: If interface not found
         """
         self.sync_topology_if_outdated()
-        for link in self.links():
-            link_iface_pair = (link.interface_a, link.interface_b)
+        try:
+            return self._links[link_id]
+        except KeyError:
+            raise LinkNotFound(link_id)
 
-            if (iface1, iface2) == link_iface_pair:
-                return link
-            elif (iface2, iface1) == link_iface_pair:
-                return link
-        else:
-            raise LinkNotFound()
+    @staticmethod
+    def get_link_by_nodes(node1, node2):
+        """
+        DEPRECATED
+
+        Returns ONE of the links identified by two node objects.
+
+        Deprecated. Use `Node.get_link_to` to get one link
+        or `Node.get_links_to` to get all links.
+
+        :param node1: the first node
+        :type node1: models.Node
+        :param node2: the second node
+        :type node2: models.Node
+        :returns: one of links between the nodes
+        :rtype: models.Link
+        :raises LinkNotFound: If no such link exists
+        """
+        warnings.warn(
+            "Deprecated, use Node.get_link_to or Node.get_links_to instead.",
+            DeprecationWarning,
+        )
+        if not (links := node1.get_links_to(node2)):
+            raise LinkNotFound
+        return links[0]
+
+    @staticmethod
+    def get_link_by_interfaces(iface1, iface2):
+        """
+        DEPRECATED
+
+        Returns the link identified by two interface objects.
+
+        Deprecated. Use `Interface.get_link_to` to get link.
+
+        :param iface1: the first interface
+        :type iface1: models.Interface
+        :param iface2: the second interface
+        :type iface2: models.Interface
+        :returns: the link between the interfaces
+        :rtype: models.Link
+        :raises LinkNotFound: If no such link exists
+        """
+        warnings.warn(
+            "Deprecated, use Interface.get_link_to instead.", DeprecationWarning
+        )
+        link = iface1.link
+        if iface2 in link.interfaces:
+            return link
+        raise LinkNotFound
 
     def find_nodes_by_tag(self, tag):
         """
-        Returns the node identified by the given tag.
+        Returns the nodes identified by the given tag.
 
-        :param tag:
+        :param tag: tag of the nodes to be returned
         :type tag: str
-        :returns: a list of tags
-        :rtype: list
+        :returns: a list of nodes
+        :rtype: List[Node]
         """
         self.sync_topology_if_outdated()
         return [node for node in self.nodes() if tag in node.tags()]
@@ -421,7 +465,7 @@ def create_node(
         y=0,
         wait=None,
         populate_interfaces=False,
-        **kwargs
+        **kwargs,
     ):
         """
         Creates a node in the lab with the given parameters.
@@ -430,13 +474,15 @@ def create_node(
         :type label: str
         :param node_definition: Node definition to use
         :type label: str
-        :param x: x co-ordinate
+        :param x: x coordinate
         :type x: int
-        :param y: y co-ordinate
+        :param y: y coordinate
         :type y: int
-        :param wait: Wait for convergence (if left at default, the lab wait property takes precedence)
+        :param wait: Wait for convergence (if left at default,
+            the lab wait property takes precedence)
         :type wait: bool
-        :param populate_interfaces: automatically create pre-defined number of interfaces on node creation
+        :param populate_interfaces: automatically create pre-defined number
+            of interfaces on node creation
         :returns: a Node object
         :rtype: models.Node
         """
@@ -485,9 +531,10 @@ def add_node_local(
         boot_disk_size=0,
         tags=None,
     ):
-        "Helper function to add a node to the client library."
+        """Helper function to add a node to the client library."""
         if tags is None:
-            # TODO: see if can deprecate now tags set automatically on server at creation
+            # TODO: see if can deprecate now tags set automatically
+            # on server at creation
             tags = []
         node = Node(
             self,
@@ -514,37 +561,42 @@ def remove_node(self, node, wait=None):
 
         :param node: the node
         :type node: Node
-        :param wait: Wait for convergence (if left at default, the lab wait property takes precedence)
+        :param wait: Wait for convergence (if left at default,
+            the lab wait property takes precedence)
         :type wait: bool
         """
         node.remove_on_server()
         for iface in node.interfaces():
-            for lnk in iface.links():
+            if iface.link is not None:
                 try:
-                    del self._links[lnk.id]
+                    del self._links[iface.link.id]
                 except KeyError:
-                    # element may already have been deleted on server, and removed locally due to auto-sync
+                    # element may already have been deleted on server,
+                    # and removed locally due to auto-sync
                     pass
             try:
                 del self._interfaces[iface.id]
             except KeyError:
-                # element may already have been deleted on server, and removed locally due to auto-sync
+                # element may already have been deleted on server,
+                # and removed locally due to auto-sync
                 pass
         try:
             del self._nodes[node.id]
         except KeyError:
-            # element may already have been deleted on server, and removed locally due to auto-sync
+            # element may already have been deleted on server,
+            # and removed locally due to auto-sync
             pass
 
         if self.need_to_wait(wait):
             self.wait_until_lab_converged()
-        logger.debug("%s node removed from lab %s", node.id, self._lab_id)
+        _LOGGER.debug("%s node removed from lab %s", node.id, self._lab_id)
 
     def remove_nodes(self, wait=None):
         """
         Remove all nodes from the lab.
 
-        :param wait: Wait for convergence (if left at default, the lab wait property takes precedence)
+        :param wait: Wait for convergence (if left at default,
+            the lab wait property takes precedence)
         :type wait: bool
         """
         # TODO: see if this is used - in testing?
@@ -553,7 +605,7 @@ def remove_nodes(self, wait=None):
 
         if self.need_to_wait(wait):
             self.wait_until_lab_converged()
-        logger.debug("all nodes removed from lab %s", self._lab_id)
+        _LOGGER.debug("all nodes removed from lab %s", self._lab_id)
 
     def remove_link(self, link, wait=None):
         """
@@ -561,19 +613,21 @@ def remove_link(self, link, wait=None):
 
         :param link: the link
         :type link: Link
-        :param wait: Wait for convergence (if left at default, the lab wait property takes precedence)
+        :param wait: Wait for convergence (if left at default,
+            the lab wait property takes precedence)
         :type wait: bool
         """
         link.remove_on_server()
         try:
             del self._links[link.id]
         except KeyError:
-            # element may already have been deleted on server, and removed locally due to auto-sync
+            # element may already have been deleted on server,
+            # and removed locally due to auto-sync
             pass
 
         if self.need_to_wait(wait):
             self.wait_until_lab_converged()
-        logger.debug("link %s removed from lab %s", link.id, self._lab_id)
+        _LOGGER.debug("link %s removed from lab %s", link.id, self._lab_id)
 
     def remove_interface(self, iface, wait=None):
         """
@@ -586,9 +640,9 @@ def remove_interface(self, iface, wait=None):
         :type wait: bool
         """
         iface.remove_on_server()
-        for lnk in iface.links():
+        if iface.link is not None:
             try:
-                del self._links[lnk.id]
+                del self._links[iface.link.id]
             except KeyError:
                 # element may already have been deleted on server, and removed
                 # locally due to auto-sync
@@ -602,7 +656,7 @@ def remove_interface(self, iface, wait=None):
 
         if self.need_to_wait(wait):
             self.wait_until_lab_converged()
-        logger.debug("interface %s removed from lab %s", iface.id, self._lab_id)
+        _LOGGER.debug("interface %s removed from lab %s", iface.id, self._lab_id)
 
     def create_link(self, i1, i2, wait=None):
         """
@@ -708,9 +762,16 @@ def create_interface_local(
             self._interfaces[iface_id].node = node
             self._interfaces[iface_id].label = label
             self._interfaces[iface_id].slot = slot
-            self._interfaces[iface_id].iface_type = iface_type
+            self._interfaces[iface_id].type = iface_type
         return self._interfaces[iface_id]
 
+    @staticmethod
+    def _get_element_from_data(data: dict, element: str) -> int:
+        try:
+            return int(data[element])
+        except (TypeError, KeyError):
+            return 0
+
     def sync_statistics(self):
         """Retrieve the simulation statistic data from the back end server."""
         url = self.lab_base_url + "/simulation_stats"
@@ -722,14 +783,8 @@ def sync_statistics(self):
         for node_id, node_data in node_statistics.items():
             # TODO: standardise so if shutdown, then these are set to 0 on
             # server side
-            try:
-                disk_read = int(node_data["block0_rd_bytes"])
-            except (TypeError, KeyError):
-                disk_read = 0
-            try:
-                disk_write = int(node_data["block0_wr_bytes"])
-            except (TypeError, KeyError):
-                disk_write = 0
+            disk_read = self._get_element_from_data(node_data, "block0_rd_bytes")
+            disk_write = self._get_element_from_data(node_data, "block0_wr_bytes")
 
             self._nodes[node_id].statistics = {
                 "cpu_usage": float(node_data.get("cpu_usage", 0)),
@@ -740,23 +795,10 @@ def sync_statistics(self):
         for link_id, link_data in link_statistics.items():
             # TODO: standardise so if shutdown, then these are set to 0 on
             # server side
-            try:
-                readbytes = int(link_data["readbytes"])
-            except (TypeError, KeyError):
-                readbytes = 0
-            try:
-                readpackets = int(link_data["readpackets"])
-            except (TypeError, KeyError):
-                readpackets = 0
-
-            try:
-                writebytes = int(link_data["writebytes"])
-            except (TypeError, KeyError):
-                writebytes = 0
-            try:
-                writepackets = int(link_data["writepackets"])
-            except (TypeError, KeyError):
-                writepackets = 0
+            readbytes = self._get_element_from_data(link_data, "readbytes")
+            readpackets = self._get_element_from_data(link_data, "readpackets")
+            writebytes = self._get_element_from_data(link_data, "writebytes")
+            writepackets = self._get_element_from_data(link_data, "writepackets")
 
             link = self._links[link_id]
             link.statistics = {
@@ -818,15 +860,15 @@ def wait_until_lab_converged(self, max_iterations=None, wait_time=None):
             self.wait_max_iterations if max_iterations is None else max_iterations
         )
         wait_time = self.wait_time if wait_time is None else wait_time
-        logger.info("Waiting for lab %s to converge", self._lab_id)
+        _LOGGER.info("Waiting for lab %s to converge", self._lab_id)
         for index in range(max_iter):
             converged = self.has_converged()
             if converged:
-                logger.info("Lab %s has booted", self._lab_id)
+                _LOGGER.info("Lab %s has booted", self._lab_id)
                 return
 
             if index % 10 == 0:
-                logging.info(
+                _LOGGER.info(
                     "Lab has not converged, attempt %s/%s, waiting...",
                     index,
                     max_iter,
@@ -837,7 +879,7 @@ def wait_until_lab_converged(self, max_iterations=None, wait_time=None):
             self.id,
             max_iter,
         )
-        logger.error(msg)
+        _LOGGER.error(msg)
         # after maximum retries are exceeded and lab has not converged
         # error must be raised - it makes no sense to just log info
         # and let client fail with something else if wait is explicitly
@@ -864,7 +906,7 @@ def start(self, wait=None):
         response.raise_for_status()
         if self.need_to_wait(wait):
             self.wait_until_lab_converged()
-        logger.debug("started lab: %s", self._lab_id)
+        _LOGGER.debug("started lab: %s", self._lab_id)
 
     def state(self):
         """
@@ -876,18 +918,16 @@ def state(self):
         url = self.lab_base_url + "/state"
         response = self.session.get(url)
         response.raise_for_status()
-        logger.debug("lab state: %s -> %s", self._lab_id, response.text)
+        _LOGGER.debug("lab state: %s -> %s", self._lab_id, response.text)
         return response.json()
 
     def is_active(self):
         """
         Returns whether the lab is started.
 
-        Deprecated since 2.4 (will be removed in 2.5)
         :returns: Whether the lab is started
         :rtype: bool
         """
-        logger.warning("Deprecated since 2.4 (will be removed in 2.5)")
         return self.state() == "STARTED"
 
     def details(self):
@@ -900,7 +940,7 @@ def details(self):
         url = self.lab_base_url
         response = self.session.get(url)
         response.raise_for_status()
-        logger.debug("lab state: %s -> %s", self._lab_id, response.text)
+        _LOGGER.debug("lab state: %s -> %s", self._lab_id, response.text)
         return response.json()
 
     def stop(self, wait=None):
@@ -916,7 +956,7 @@ def stop(self, wait=None):
         response.raise_for_status()
         if self.need_to_wait(wait):
             self.wait_until_lab_converged()
-        logger.debug("stopped lab: %s", self._lab_id)
+        _LOGGER.debug("stopped lab: %s", self._lab_id)
 
     def wipe(self, wait=None):
         """
@@ -931,7 +971,7 @@ def wipe(self, wait=None):
         response.raise_for_status()
         if self.need_to_wait(wait):
             self.wait_until_lab_converged()
-        logger.debug("wiped lab: %s", self._lab_id)
+        _LOGGER.debug("wiped lab: %s", self._lab_id)
 
     def remove(self):
         """
@@ -947,7 +987,7 @@ def remove(self):
         url = self.lab_base_url
         response = self.session.delete(url)
         response.raise_for_status()
-        logger.debug("removed lab: %s", response.text)
+        _LOGGER.debug("removed lab: %s", response.text)
 
     def sync_events(self):
         # TODO: return a boolean if events have changed since last run
@@ -969,7 +1009,12 @@ def build_configurations(self):
         # sync to get the updated configs
         self.sync_topology_if_outdated()
 
-    def sync(self, topology_only=True, with_node_configurations=False):
+    def sync(
+        self,
+        topology_only=True,
+        with_node_configurations=None,
+        exclude_configurations=False,
+    ):
         """
         Synchronize current lab applying the changes that
         were done in UI or in another ClientLibrary session::
@@ -978,18 +1023,29 @@ def sync(self, topology_only=True, with_node_configurations=False):
 
         :param topology_only: do not sync statistics and IP addresses
         :type topology_only: bool
-        :param with_node_configurations: also sync the configuration
-            of the nodes itself
+        :param with_node_configurations: DEPRECATED, does the opposite of
+            what is expected: disables syncing node configuration when True
         :type with_node_configurations: bool
+        :param exclude_configurations: disable syncing node configuration
+        :type exclude_configurations: bool
         """
-        self._sync_topology(with_node_configurations)
+
+        if with_node_configurations is not None:
+            warnings.warn(
+                'The argument "with_node_configurations" is deprecated, as it does '
+                "the opposite of what is expected. Use exclude_configurations instead.",
+                DeprecationWarning,
+            )
+            exclude_configurations = with_node_configurations
+
+        self._sync_topology(exclude_configurations)
 
         if not topology_only:
             self.sync_statistics()
             self.sync_layer3_addresses()
 
     def _sync_topology(self, exclude_configurations=False):
-        "Helper function to sync topologies from the backend server."
+        """Helper function to sync topologies from the backend server."""
         # TODO: check what happens if call twice
         url = self._context.base_url + "labs/{}".format(self._lab_id) + "/topology"
         params = {"exclude_configurations": exclude_configurations}
@@ -1016,54 +1072,63 @@ def _sync_topology(self, exclude_configurations=False):
     def import_lab(self, topology):
         self._import_lab(topology)
 
+        self._handle_import_nodes(topology)
+        self._handle_import_interfaces(topology)
+        self._handle_import_links(topology)
+
+    def _import_lab(self, topology):
+        """Replaces lab properties. Will raise KeyError if any property is missing."""
+        lab_dict = topology.get("lab")
+        if lab_dict is None:
+            warnings.warn(
+                "Labs created in older CML releases (schema version 0.0.5 or lower) "
+                "are deprecated.  Use labs with schema version 0.1.0 instead.",
+                DeprecationWarning,
+            )
+            self._title = topology["lab_title"]
+            self._description = topology["lab_description"]
+            self._notes = topology["lab_notes"]
+            self._owner = topology.get("lab_owner", self.username)
+        else:
+            self._title = lab_dict["title"]
+            self._description = lab_dict["description"]
+            self._notes = lab_dict["notes"]
+            self._owner = lab_dict.get("owner", self.username)
+
+    def _handle_import_nodes(self, topology):
         for node in topology["nodes"]:
             node_id = node["id"]
             if node_id in self._nodes:
-                raise Exception("Node already exists")
+                raise ElementAlreadyExists("Node already exists")
             self._import_node(node_id, node)
 
             if "interfaces" not in node:
-                # logger.warning("Deprecated since 2.4 (will be removed in 2.5)")
                 continue
 
             for iface in node["interfaces"]:
                 iface_id = iface["id"]
                 if iface_id in self._interfaces:
-                    raise Exception("Interface already exists")
+                    raise ElementAlreadyExists("Interface already exists")
                 self._import_interface(iface_id, node_id, iface)
 
+    def _handle_import_interfaces(self, topology):
         if "interfaces" in topology:
-            # logger.warning("Deprecated since 2.4 (will be removed in 2.5)")
             for iface in topology["interfaces"]:
                 iface_id = iface["id"]
                 node_id = iface["node"]
                 if iface_id in self._interfaces:
-                    raise Exception("Interface already exists")
+                    raise ElementAlreadyExists("Interface already exists")
                 self._import_interface(iface_id, node_id, iface)
 
+    def _handle_import_links(self, topology):
         for link in topology["links"]:
             link_id = link["id"]
             if link_id in self._links:
-                raise Exception("Link already exists")
+                raise ElementAlreadyExists("Link already exists")
             iface_a_id = link["interface_a"]
             iface_b_id = link["interface_b"]
             self._import_link(link_id, iface_b_id, iface_a_id)
 
-    def _import_lab(self, topology):
-        lab_dict = topology.get("lab")
-        if lab_dict is None:
-            logger.warning("Deprecated since 2.4 (will be removed in 2.5)")
-            self._title = topology["lab_title"]
-            self._description = topology["lab_description"]
-            self._notes = topology["lab_notes"]
-            self._owner = topology["lab_owner"]
-            # topology.get("lab_owner", self.username)
-        else:
-            self._title = lab_dict["title"]
-            self._description = lab_dict["description"]
-            self._notes = lab_dict["notes"]
-            self._owner = lab_dict.get("owner", self.username)
-
     def _import_link(self, link_id, iface_b_id, iface_a_id):
         iface_a = self._interfaces[iface_a_id]
         iface_b = self._interfaces[iface_b_id]
@@ -1071,7 +1136,6 @@ def _import_link(self, link_id, iface_b_id, iface_a_id):
 
     def _import_interface(self, iface_id, node_id, iface_data):
         if "data" in iface_data:
-            # logger.warning("Deprecated since 2.4 (will be removed in 2.5)")
             iface_data = iface_data["data"]
         label = iface_data["label"]
         slot = iface_data.get("slot")
@@ -1081,35 +1145,18 @@ def _import_interface(self, iface_id, node_id, iface_data):
 
     def _import_node(self, node_id, node_data):
         if "data" in node_data:
-            # logger.warning("Deprecated since 2.4 (will be removed in 2.5)")
             node_data = node_data["data"]
-        label = node_data["label"]
-        x = node_data["x"]
-        y = node_data["y"]
-        node_definition = node_data["node_definition"]
-        image_definition = node_data.get("image_definition", None)
-        ram = node_data["ram"]
-        cpus = node_data["cpus"]
-        cpu_limit = node_data.get("cpu_limit", 100)
-        data_volume = node_data["data_volume"]
-        boot_disk_size = node_data["boot_disk_size"]
-        tags = node_data["tags"]
-        config = node_data.get("configuration", "")
-        return self.add_node_local(
+        node = self.add_node_local(
             node_id,
-            label,
-            node_definition,
-            image_definition,
-            config,
-            x,
-            y,
-            ram,
-            cpus,
-            cpu_limit,
-            data_volume,
-            boot_disk_size,
-            tags,
+            node_data["label"],
+            node_data["node_definition"],
+            node_data.get("image_definition", None),
+            node_data.get("configuration", ""),
+            node_data["x"],
+            node_data["y"],
         )
+        node.update(node_data, "configuration" not in node_data)
+        return node
 
     def update_lab(self, topology, exclude_configurations):
         self._import_lab(topology)
@@ -1123,7 +1170,6 @@ def update_lab(self, topology, exclude_configurations):
         update_node_keys = set(node["id"] for node in topology["nodes"])
         update_link_keys = set(links["id"] for links in topology["links"])
         if "interfaces" in topology:
-            # logger.warning("Deprecated since 2.4 (will be removed in 2.5)")
             update_interface_keys = set(iface["id"] for iface in topology["interfaces"])
         else:
             update_interface_keys = set(
@@ -1137,44 +1183,55 @@ def update_lab(self, topology, exclude_configurations):
         removed_links = existing_link_keys - update_link_keys
         removed_interfaces = existing_interface_keys - update_interface_keys
 
+        self._remove_elements(removed_nodes, removed_links, removed_interfaces)
+
+        # new elements
+        new_nodes = update_node_keys - existing_node_keys
+        new_links = update_link_keys - existing_link_keys
+        new_interfaces = update_interface_keys - existing_interface_keys
+
+        self._add_elements(topology, new_nodes, new_links, new_interfaces)
+
+        # kept elements
+        kept_nodes = update_node_keys.intersection(existing_node_keys)
+        # kept_links = update_link_keys.intersection(existing_link_keys)
+        # kept_interfaces = update_interface_keys.intersection(existing_interface_keys)
+
+        self._update_elements(topology, kept_nodes, exclude_configurations)
+
+    def _remove_elements(self, removed_nodes, removed_links, removed_interfaces):
         for link_id in removed_links:
             link = self._links[link_id]
-            logger.warning("Removed link %s", link)
+            _LOGGER.info("Removed link %s", link)
             del self._links[link_id]
 
         for interface_id in removed_interfaces:
             interface = self._interfaces[interface_id]
-            logger.warning("Removed interface %s", interface)
+            _LOGGER.info("Removed interface %s", interface)
             del self._interfaces[interface_id]
 
         for node_id in removed_nodes:
             node = self._nodes[node_id]
-            logger.warning("Removed node %s", node)
+            _LOGGER.info("Removed node %s", node)
             del self._nodes[node_id]
 
-        # new elements
-        new_nodes = update_node_keys - existing_node_keys
-        new_links = update_link_keys - existing_link_keys
-        new_interfaces = update_interface_keys - existing_interface_keys
-
+    def _add_elements(self, topology, new_nodes, new_links, new_interfaces):
         for node in topology["nodes"]:
             node_id = node["id"]
             if node_id in new_nodes:
                 node = self._import_node(node_id, node)
-                logger.info("Added node %s", node)
+                _LOGGER.info("Added node %s", node)
 
             if "interfaces" in topology:
-                # logger.warning("Deprecated since 2.4 (will be removed in 2.5)")
                 continue
 
             for interface in node["interfaces"]:
                 interface_id = interface["id"]
                 if interface_id in new_interfaces:
                     interface = self._import_interface(interface_id, node_id, interface)
-                    logger.info("Added interface %s", interface)
+                    _LOGGER.info("Added interface %s", interface)
 
         if "interfaces" in topology:
-            # logger.warning("Deprecated since 2.4 (will be removed in 2.5)")
             for iface in topology["interfaces"]:
                 iface_id = iface["id"]
                 if iface_id in new_interfaces:
@@ -1186,13 +1243,9 @@ def update_lab(self, topology, exclude_configurations):
             iface_a_id = link_data["interface_a"]
             iface_b_id = link_data["interface_b"]
             link = self._import_link(link_id, iface_b_id, iface_a_id)
-            logger.info("Added link %s", link)
-
-        # kept elements
-        kept_nodes = update_node_keys.intersection(existing_node_keys)
-        # kept_links = update_link_keys.intersection(existing_link_keys)
-        # kept_interfaces = update_interface_keys.intersection(existing_interface_keys)
+            _LOGGER.info("Added link %s", link)
 
+    def _update_elements(self, topology, kept_nodes, exclude_configurations):
         for node_id in kept_nodes:
             node = self._find_node_in_topology(node_id, topology)
             lab_node = self._nodes[node_id]
diff --git a/virl2_client/models/licensing.py b/virl2_client/models/licensing.py
index 7ee0b56..b1aa962 100644
--- a/virl2_client/models/licensing.py
+++ b/virl2_client/models/licensing.py
@@ -22,14 +22,13 @@
 import logging
 
 
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.INFO)
+_LOGGER = logging.getLogger(__name__)
 
 DEFAULT_PROXY_SERVER = None
 DEFAULT_PROXY_PORT = None
 
 
-class Licensing(object):
+class Licensing:
 
     max_wait = 30
     wait_interval = 1.5
@@ -65,7 +64,7 @@ def renew_authorization(self):
         url = self.base_url + "/authorization/renew"
         response = self.ctx.session.put(url)
         response.raise_for_status()
-        logger.info("The agent has scheduled an authorization renewal.")
+        _LOGGER.info("The agent has scheduled an authorization renewal.")
         return response.status_code == 204
 
     def set_transport(self, ssms, proxy_server=None, proxy_port=None):
@@ -76,7 +75,7 @@ def set_transport(self, ssms, proxy_server=None, proxy_port=None):
         data = {"ssms": ssms, "proxy": {"server": proxy_server, "port": proxy_port}}
         response = self.ctx.session.put(url, json=data)
         response.raise_for_status()
-        logger.info("The transport configuration has been accepted. Config: %s.", data)
+        _LOGGER.info("The transport configuration has been accepted. Config: %s.", data)
         return response.status_code == 204
 
     def set_default_transport(self):
@@ -97,7 +96,7 @@ def set_product_license(self, product_license):
         url = self.base_url + "/product_license"
         response = self.ctx.session.put(url, json=product_license)
         response.raise_for_status()
-        logger.info("Product license was accepted by the agent.")
+        _LOGGER.info("Product license was accepted by the agent.")
         return response.status_code == 204
 
     def get_certificate(self):
@@ -108,7 +107,7 @@ def get_certificate(self):
         response = self.ctx.session.get(url)
         response.raise_for_status()
         if response:
-            logger.info("Certificate received.")
+            _LOGGER.info("Certificate received.")
             return response.json()
 
     def install_certificate(self, cert):
@@ -119,7 +118,7 @@ def install_certificate(self, cert):
         url = self.base_url + "/certificate"
         response = self.ctx.session.post(url, data=cert)
         response.raise_for_status()
-        logger.info("Certificate was accepted by the agent.")
+        _LOGGER.info("Certificate was accepted by the agent.")
         return response.status_code == 204
 
     def remove_certificate(self):
@@ -130,7 +129,7 @@ def remove_certificate(self):
         url = self.base_url + "/certificate"
         response = self.ctx.session.delete(url)
         response.raise_for_status()
-        logger.info("Certificate was removed.")
+        _LOGGER.info("Certificate was removed.")
         return response.status_code == 204
 
     def register(self, token, reregister=False):
@@ -142,7 +141,7 @@ def register(self, token, reregister=False):
             url, json={"token": token, "reregister": reregister}
         )
         response.raise_for_status()
-        logger.info("Registration request was accepted by the agent.")
+        _LOGGER.info("Registration request was accepted by the agent.")
         return response.status_code == 204
 
     def register_renew(self):
@@ -152,7 +151,7 @@ def register_renew(self):
         url = self.base_url + "/registration/renew"
         response = self.ctx.session.put(url)
         response.raise_for_status()
-        logger.info("The renewal request was accepted by the agent.")
+        _LOGGER.info("The renewal request was accepted by the agent.")
         return response.status_code == 204
 
     def register_wait(self, token, reregister=False):
@@ -173,14 +172,14 @@ def deregister(self):
         response = self.ctx.session.delete(url)
         response.raise_for_status()
         if response.status_code == 202:
-            logger.warning(
+            _LOGGER.warning(
                 "Deregistration has been completed on the Product Instance but was "
                 "unable to deregister from Smart Software Licensing due to a "
                 "communication timeout."
             )
             # TODO try to register again and unregister
         if response.status_code == 204:
-            logger.info(
+            _LOGGER.info(
                 "The Product Instance was successfully deregistered from Smart "
                 "Software Licensing."
             )
@@ -211,7 +210,7 @@ def reservation_mode(self, data):
         response = self.ctx.session.put(url, json=data)
         response.raise_for_status()
         msg = "enabled" if data else "disabled"
-        logger.info("The reservation mode has been %s.", msg)
+        _LOGGER.info("The reservation mode has been %s.", msg)
 
     def enable_reservation_mode(self):
         """
@@ -232,7 +231,7 @@ def request_reservation(self):
         url = self.base_url + "/reservation/request"
         response = self.ctx.session.post(url)
         response.raise_for_status()
-        logger.info("The reservation request code received.")
+        _LOGGER.info("The reservation request code received.")
         return response.json()
 
     def complete_reservation(self, authorization_code):
@@ -243,7 +242,7 @@ def complete_reservation(self, authorization_code):
         url = self.base_url + "/reservation/complete"
         response = self.ctx.session.post(url, json=authorization_code)
         response.raise_for_status()
-        logger.info("The confirmation code of completed reservation received.")
+        _LOGGER.info("The confirmation code of completed reservation received.")
         return response.json()
 
     def cancel_reservation(self):
@@ -253,7 +252,7 @@ def cancel_reservation(self):
         url = self.base_url + "/reservation/cancel"
         response = self.ctx.session.delete(url)
         response.raise_for_status()
-        logger.info("The reservation request has been cancelled.")
+        _LOGGER.info("The reservation request has been cancelled.")
         return response.status_code == 204
 
     def release_reservation(self):
@@ -264,7 +263,7 @@ def release_reservation(self):
         url = self.base_url + "/reservation/release"
         response = self.ctx.session.delete(url)
         response.raise_for_status()
-        logger.info("The return code of the released reservation received.")
+        _LOGGER.info("The return code of the released reservation received.")
         return response.json()
 
     def discard_reservation(self, data):
@@ -276,7 +275,7 @@ def discard_reservation(self, data):
         url = self.base_url + "/reservation/discard"
         response = self.ctx.session.post(url, json=data)
         response.raise_for_status()
-        logger.info(
+        _LOGGER.info(
             "The discard code for an already cancelled reservation request received."
         )
         return response.json()
@@ -288,7 +287,7 @@ def get_reservation_confirmation_code(self):
         url = self.base_url + "/reservation/confirmation_code"
         response = self.ctx.session.get(url)
         response.raise_for_status()
-        logger.info("The confirmation code of the completed reservation received.")
+        _LOGGER.info("The confirmation code of the completed reservation received.")
         return response.json()
 
     def delete_reservation_confirmation_code(self):
@@ -298,7 +297,7 @@ def delete_reservation_confirmation_code(self):
         url = self.base_url + "/reservation/confirmation_code"
         response = self.ctx.session.delete(url)
         response.raise_for_status()
-        logger.info("The confirmation code has been removed.")
+        _LOGGER.info("The confirmation code has been removed.")
         return response.status_code == 204
 
     def get_reservation_return_code(self):
@@ -308,7 +307,7 @@ def get_reservation_return_code(self):
         url = self.base_url + "/reservation/return_code"
         response = self.ctx.session.get(url)
         response.raise_for_status()
-        logger.info("The return code of the released reservation received.")
+        _LOGGER.info("The return code of the released reservation received.")
         return response.json()
 
     def delete_reservation_return_code(self):
@@ -318,7 +317,7 @@ def delete_reservation_return_code(self):
         url = self.base_url + "/reservation/return_code"
         response = self.ctx.session.delete(url)
         response.raise_for_status()
-        logger.info("The return code has been removed.")
+        _LOGGER.info("The return code has been removed.")
         return response.status_code == 204
 
     def wait_for_status(self, what, *target_status):
@@ -342,5 +341,5 @@ def wait_for_status(self, what, *target_status):
                     "Last status was {}".format(what, target_status, timeout, status)
                 )
             status = self.status()[what]["status"]
-            logger.debug("%s status: %s", what, status)
+            _LOGGER.debug("%s status: %s", what, status)
             count += 1
diff --git a/virl2_client/models/link.py b/virl2_client/models/link.py
index 0963d43..53d0a99 100644
--- a/virl2_client/models/link.py
+++ b/virl2_client/models/link.py
@@ -22,26 +22,25 @@
 import time
 from functools import total_ordering
 
-logger = logging.getLogger(__name__)
+_LOGGER = logging.getLogger(__name__)
 
 
 @total_ordering
 class Link:
-    """A VIRL2 network link between two nodes, connecting
-    to two interfaces on these nodes.
-
-    :param lab: the lab object
-    :type lab: modles.Lab
-    :param lid: the lab ID
-    :type lid: str
-    :param iface_a: the first interface of the link
-    :type iface_a: a models.Interface
-    :param iface_b: the second interface of a the link
-    :type iface_b: models.Interface
-    """
-
     def __init__(self, lab, lid, iface_a, iface_b):
-        """Constructor method"""
+        """
+        A VIRL2 network link between two nodes, connecting
+        to two interfaces on these nodes.
+
+        :param lab: the lab object
+        :type lab: models.Lab
+        :param lid: the link ID
+        :type lid: str
+        :param iface_a: the first interface of the link
+        :type iface_a: models.Interface
+        :param iface_b: the second interface of the link
+        :type iface_b: models.Interface
+        """
         self.id = lid
         self.interface_a = iface_a
         self.interface_b = iface_b
@@ -89,7 +88,7 @@ def __repr__(self):
             str(self.lab),
             self.id,
             self.interface_a,
-            self.interface_a,
+            self.interface_b,
         )
 
     def __eq__(self, other: object):
@@ -117,7 +116,7 @@ def node_b(self):
 
     @property
     def nodes(self):
-        """Return nodes this link connects"""
+        """Return nodes this link connects."""
         self.lab.sync_topology_if_outdated()
         return self.node_a, self.node_b
 
@@ -142,13 +141,13 @@ def base_url(self):
         return self.lab_base_url + "/links/{}".format(self.id)
 
     def remove_on_server(self):
-        logger.info("Removing link %s", self)
+        _LOGGER.info("Removing link %s", self)
         url = self.base_url
         response = self.session.delete(url)
         response.raise_for_status()
 
     def wait_until_converged(self, max_iterations=None, wait_time=None):
-        logger.info("Waiting for link %s to converge", self.id)
+        _LOGGER.info("Waiting for link %s to converge", self.id)
         max_iter = (
             self.lab.wait_max_iterations if max_iterations is None else max_iterations
         )
@@ -156,11 +155,11 @@ def wait_until_converged(self, max_iterations=None, wait_time=None):
         for index in range(max_iter):
             converged = self.has_converged()
             if converged:
-                logger.info("Link %s has booted", self.id)
+                _LOGGER.info("Link %s has converged", self.id)
                 return
 
             if index % 10 == 0:
-                logging.info(
+                _LOGGER.info(
                     "Link has not converged, attempt %s/%s, waiting...",
                     index,
                     max_iter,
@@ -171,7 +170,7 @@ def wait_until_converged(self, max_iterations=None, wait_time=None):
             self.id,
             max_iter,
         )
-        logger.error(msg)
+        _LOGGER.error(msg)
         # after maximum retries are exceeded and link has not converged
         # error must be raised - it makes no sense to just log info
         # and let client fail with something else if wait is explicitly
@@ -200,7 +199,8 @@ def stop(self, wait=None):
             self.wait_until_converged()
 
     def set_condition(self, bandwidth, latency, jitter, loss):
-        """set_condition applies conditioning to this link.
+        """
+        Applies conditioning to this link.
 
         :param bandwidth: desired bandwidth, 0-10000000 kbps
         :type bandwidth: int
@@ -222,7 +222,8 @@ def set_condition(self, bandwidth, latency, jitter, loss):
         response.raise_for_status()
 
     def get_condition(self):
-        """get_condition retrieves the current condition on this link.
+        """
+        Retrieves the current condition on this link.
         If there is no link condition specified, None is returned.
 
         :return: the applied link condition
@@ -241,15 +242,17 @@ def get_condition(self):
         return result
 
     def remove_condition(self):
-        """remove_condition removes link conditioning. If
-        there's no condition applied then this is a no-op.
+        """
+        Removes link conditioning.
+        If there's no condition applied then this is a no-op for the controller.
         """
         url = self.base_url + "/condition"
         response = self.session.delete(url)
         response.raise_for_status()
 
     def set_condition_by_name(self, name):
-        """set_condition_by_name is a convenience function to provide
+        """
+        A convenience function to provide
         some commonly used link condition settings for various link types.
 
         Inspired by:  https://github.com/tylertreat/comcast
@@ -287,12 +290,12 @@ def set_condition_by_name(self, name):
         }
 
         if name not in options.keys():
-            logger.error(
-                "unknown condition name '%s', known values: '%s'",
+            msg = "unknown condition name '{}', known values: '{}'".format(
                 name,
                 ", ".join(list(options.keys())),
             )
-            raise ValueError
+            _LOGGER.error(msg)
+            raise ValueError(msg)
 
         latency, bandwidth, loss = options[name]
         self.set_condition(bandwidth, latency, 0, loss)
diff --git a/virl2_client/models/node.py b/virl2_client/models/node.py
index 7cfcb14..34d0f9e 100644
--- a/virl2_client/models/node.py
+++ b/virl2_client/models/node.py
@@ -21,47 +21,14 @@
 import logging
 import time
 from functools import total_ordering
-from itertools import chain
 
 from ..exceptions import InterfaceNotFound
 
-logger = logging.getLogger(__name__)
-
-flatten = chain.from_iterable
+_LOGGER = logging.getLogger(__name__)
 
 
 @total_ordering
 class Node:
-    """A VIRL2 Node object. Typically a virtual machine representing a router, switch or server.
-
-    :param lab: the Lab this nodes belongs to
-    :type lab: models.Lab
-    :param nid: the Node ID
-    :type nid: str
-    :param node_definition: The node definition of this node
-    :type node_definition: str
-    :param image_definition: The image definition of this node
-    :type image_definition: str
-    :param config: The day0 configuration of this node
-    :type config: str
-    :param x: X coordinate on topology canvas
-    :type x: int
-    :param y: Y coordinate on topology canvas
-    :type y: int
-    :param ram: memory of node in MiB (if applicable)
-    :type ram: int
-    :param cpus: Amount of CPUs in this node (if applicable)
-    :type cpus: int
-    :param cpu_limit: CPU limit (default at 100%)
-    :type cpu_limit: int
-    :param data_volume: Size in GiB of 2nd HDD (if > 0)
-    :type data_volume: int
-    :param boot_disk_size: Size in GiB of boot disk (will expand to this size)
-    :type boot_disk_size: int
-    :param tags: List of tags List[str, str]
-    :type tags: list
-    """
-
     def __init__(
         self,
         lab,
@@ -79,7 +46,37 @@ def __init__(
         boot_disk_size,
         tags,
     ):
-        """Constructor method"""
+        """
+        A VIRL2 Node object. Typically a virtual machine representing a router,
+        switch or server.
+
+        :param lab: the Lab this nodes belongs to
+        :type lab: models.Lab
+        :param nid: the Node ID
+        :type nid: str
+        :param node_definition: The node definition of this node
+        :type node_definition: str
+        :param image_definition: The image definition of this node
+        :type image_definition: str
+        :param config: The day0 configuration of this node
+        :type config: str
+        :param x: X coordinate on topology canvas
+        :type x: int
+        :param y: Y coordinate on topology canvas
+        :type y: int
+        :param ram: memory of node in MiB (if applicable)
+        :type ram: int
+        :param cpus: Amount of CPUs in this node (if applicable)
+        :type cpus: int
+        :param cpu_limit: CPU limit (default at 100%)
+        :type cpu_limit: int
+        :param data_volume: Size in GiB of 2nd HDD (if > 0)
+        :type data_volume: int
+        :param boot_disk_size: Size in GiB of boot disk (will expand to this size)
+        :type boot_disk_size: int
+        :param tags: List of tags
+        :type tags: List[str, str]
+        """
         self.lab = lab
         self.id = nid
         self._label = label
@@ -96,6 +93,7 @@ def __init__(
         self._data_volume = data_volume
         self._boot_disk_size = boot_disk_size
         self._tags = tags
+        self._compute_id = None
 
         self.statistics = {"cpu_usage": 0, "disk_read": 0, "disk_write": 0}
 
@@ -108,22 +106,25 @@ def __str__(self):
         return "Node: {}".format(self._label)
 
     def __repr__(self):
-        return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format(
-            self.__class__.__name__,
-            str(self.lab),
-            self.id,
-            self._label,
-            self._node_definition,
-            self._image_definition,
-            self._config,
-            self._x,
-            self._y,
-            self._ram,
-            self._cpus,
-            self._cpu_limit,
-            self._data_volume,
-            self._boot_disk_size,
-            self._tags,
+        return (
+            "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, "
+            "{!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format(
+                self.__class__.__name__,
+                str(self.lab),
+                self.id,
+                self._label,
+                self._node_definition,
+                self._image_definition,
+                self._config,
+                self._x,
+                self._y,
+                self._ram,
+                self._cpus,
+                self._cpu_limit,
+                self._data_volume,
+                self._boot_disk_size,
+                self._tags,
+            )
         )
 
     def __eq__(self, other):
@@ -145,7 +146,7 @@ def interfaces(self):
 
     def physical_interfaces(self):
         self.lab.sync_topology_if_outdated()
-        return [iface for iface in self.interfaces() if iface.is_physical]
+        return [iface for iface in self.interfaces() if iface.physical]
 
     def create_interface(self, slot=None, wait=False):
         """
@@ -170,25 +171,30 @@ def next_available_interface(self):
         to connect to other nodes!
 
         :returns: an interface or None, if all existing ones are connected
-        :rtype: models.interface
+        :rtype: models.Interface
         """
         for iface in self.interfaces():
-            if not iface.is_connected() and iface.is_physical:
+            if not iface.connected and iface.physical:
                 return iface
         return None
 
     def peer_interfaces(self):
-        self.lab.sync_topology_if_outdated()
-        peer_ifaces = (iface.peer_interfaces() for iface in self.interfaces())
-        return list(set(flatten(peer_ifaces)))
+        peer_ifaces = {
+            iface.peer_interface for iface in self.interfaces() if iface is not None
+        }
+        return list(peer_ifaces)
 
     def peer_nodes(self):
-        self.lab.sync_topology_if_outdated()
-        return list(set(iface.node for iface in self.peer_interfaces()))
+        return list({iface.node for iface in self.peer_interfaces()})
 
     def links(self):
-        self.lab.sync_topology_if_outdated()
-        return list(set(flatten(iface.links() for iface in self.interfaces())))
+        """
+        :returns: list of links
+        :rtype: list[models.Link]
+        """
+        return list(
+            {link for iface in self.interfaces() if (link := iface.link) is not None}
+        )
 
     def degree(self):
         self.lab.sync_topology_if_outdated()
@@ -306,8 +312,13 @@ def node_definition(self, value):
         self._set_node_property("node_definition", value)
         self._image_definition = value
 
+    @property
+    def compute_id(self):
+        self.lab.sync_topology_if_outdated()
+        return self._compute_id
+
     def _set_node_property(self, key, val):
-        logger.info("Setting node property %s %s: %s", self, key, val)
+        _LOGGER.info("Setting node property %s %s: %s", self, key, val)
         node_url = "{}".format(self._base_url)
         response = self.session.patch(url=node_url, json={key: val})
         response.raise_for_status()
@@ -347,8 +358,36 @@ def get_interface_by_slot(self, slot):
                 return iface
         raise InterfaceNotFound("{}:{}".format(slot, self))
 
+    def get_links_to(self, other_node):
+        """
+        Returns all links between this node and another.
+
+        :param other_node: the other node
+        :type other_node: models.Node
+        :returns: a list of links
+        :rtype: list[models.Link]
+        """
+        links = []
+        for link in self.links():
+            if other_node in link.nodes:
+                links.append(link)
+        return links
+
+    def get_link_to(self, other_node):
+        """
+        Returns one link between this node and another.
+
+        :param other_node: the other node
+        :type other_node: models.Node
+        :returns: a link
+        :rtype: models.Link
+        """
+        for link in self.links():
+            if other_node in link.nodes:
+                return link
+
     def wait_until_converged(self, max_iterations=None, wait_time=None):
-        logger.info("Waiting for node %s to converge", self.id)
+        _LOGGER.info("Waiting for node %s to converge", self.id)
         max_iter = (
             self.lab.wait_max_iterations if max_iterations is None else max_iterations
         )
@@ -356,11 +395,11 @@ def wait_until_converged(self, max_iterations=None, wait_time=None):
         for index in range(max_iter):
             converged = self.has_converged()
             if converged:
-                logger.info("Node %s has converged", self.id)
+                _LOGGER.info("Node %s has converged", self.id)
                 return
 
             if index % 10 == 0:
-                logging.info(
+                _LOGGER.info(
                     "Node has not converged, attempt %s/%s, waiting...",
                     index,
                     max_iter,
@@ -371,7 +410,7 @@ def wait_until_converged(self, max_iterations=None, wait_time=None):
             self.id,
             max_iter,
         )
-        logger.error(msg)
+        _LOGGER.error(msg)
         # after maximum retries are exceeded and node has not converged
         # error must be raised - it makes no sense to just log info
         # and let client fail with something else if wait is explicitly
@@ -431,7 +470,7 @@ def vnc_key(self):
         return response.json()
 
     def remove_on_server(self):
-        logger.info("Removing node %s", self)
+        _LOGGER.info("Removing node %s", self)
         url = self._base_url
         response = self.session.delete(url)
         response.raise_for_status()
@@ -479,7 +518,8 @@ def sync_layer3_addresses(self):
         to work, the device has to be attached to the external network
         in bridge mode and must run DHCP to acquire an IP address.
         """
-        # TODO: can optimise the sync of l3 to only be for the node rather than whole lab
+        # TODO: can optimise the sync of l3 to only be for the node
+        # rather than whole lab
         url = self._base_url + "/layer3_addresses"
         response = self.session.get(url)
         response.raise_for_status()
@@ -503,7 +543,6 @@ def map_l3_addresses_to_interfaces(self, mapping):
 
     def update(self, node_data, exclude_configurations):
         if "data" in node_data:
-            # logger.warning("Deprecated since 2.4 (will be removed in 2.5)")
             node_data = node_data["data"]
         self._label = node_data["label"]
         self._x = node_data["x"]
@@ -516,6 +555,7 @@ def update(self, node_data, exclude_configurations):
         self._data_volume = node_data["data_volume"]
         self._boot_disk_size = node_data["boot_disk_size"]
         self._tags = node_data["tags"]
+        self._compute_id = node_data.get("compute_id")
         if not exclude_configurations:
             self._config = node_data.get("configuration")
 
diff --git a/virl2_client/models/node_image_definitions.py b/virl2_client/models/node_image_definitions.py
index d469fdb..6582738 100644
--- a/virl2_client/models/node_image_definitions.py
+++ b/virl2_client/models/node_image_definitions.py
@@ -18,24 +18,19 @@
 # limitations under the License.
 #
 
-import logging
 import os
 import time
 from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
 
-logger = logging.getLogger(__name__)
-
 
 class NodeImageDefinitions:
-    """
-    VIRL2 Definition classes to specify a node VM and associated disk images.
-
-    :param context: the authentication context to use
-    :type context: authentication.Context
-    """
-
     def __init__(self, context):
-        """Constructor method"""
+        """
+        VIRL2 Definition classes to specify a node VM and associated disk images.
+
+        :param context: the authentication context to use
+        :type context: authentication.Context
+        """
         self._context = context
 
     @property
@@ -184,7 +179,8 @@ def upload_image_file(self, filename, rename=None, chunk_size_mb=10):
         :type filename: str
         :param rename:  Optional filename to rename to
         :type rename: str
-        :param chunk_size_mb: Optional size of upload chunk (mb) (deprecated since 2.2.0)
+        :param chunk_size_mb: Optional size of upload chunk (mb)
+            (deprecated since 2.2.0)
         :type chunk_size_mb: int
         """
         url = self._base_url + "images/upload"
diff --git a/virl2_client/models/system.py b/virl2_client/models/system.py
index 26483ec..ee29c5c 100644
--- a/virl2_client/models/system.py
+++ b/virl2_client/models/system.py
@@ -18,14 +18,8 @@
 # limitations under the License.
 #
 
-import logging
 
-
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.INFO)
-
-
-class SystemManagement(object):
+class SystemManagement:
     def __init__(self, context):
         self.ctx = context
 
diff --git a/virl2_client/models/users.py b/virl2_client/models/users.py
index 71d63d4..a186572 100644
--- a/virl2_client/models/users.py
+++ b/virl2_client/models/users.py
@@ -18,14 +18,8 @@
 # limitations under the License.
 #
 
-import logging
 
-
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.INFO)
-
-
-class UserManagement(object):
+class UserManagement:
     def __init__(self, context):
         self.ctx = context
 
diff --git a/virl2_client/utils.py b/virl2_client/utils.py
index 407b6b4..b530dd8 100644
--- a/virl2_client/utils.py
+++ b/virl2_client/utils.py
@@ -26,7 +26,7 @@ class TextFsmNotInstalled(Exception):
     pass
 
 
-logger = logging.getLogger(__name__)
+_LOGGER = logging.getLogger(__name__)
 
 
 class TextFsmTemplateHelper:
@@ -77,7 +77,7 @@ def parse_with_textfsm_template(template, cli_result):
     try:
         import textfsm
     except ImportError:
-        logger.warning("TextFSM not installed")
+        _LOGGER.warning("TextFSM not installed")
         raise TextFsmNotInstalled
 
     string_fh = io.StringIO(template)
@@ -125,7 +125,8 @@ def parse_line(line, keys, offsets):
         start = offsets[key]["start"]
         end = offsets[key]["end"]
         value = line[start:end]
-        # strip off whitespace as could be right padded if short entry relative to others in the column
+        # strip off whitespace as could be right padded if short entry relative
+        # to others in the column
         result[key] = value.rstrip()
     return result
 
diff --git a/virl2_client/virl2_client.py b/virl2_client/virl2_client.py
index 154e9e9..5a90a3d 100644
--- a/virl2_client/virl2_client.py
+++ b/virl2_client/virl2_client.py
@@ -23,17 +23,16 @@
 import os
 import re
 import time
-import typing
+from typing import NamedTuple, Optional, Dict, Union
 import urllib
 from functools import lru_cache
 from pathlib import Path
 from urllib.parse import urljoin, urlsplit, urlunsplit
 
-import pkg_resources
 import requests
 import urllib3
 
-from .exceptions import LabNotFound
+from .exceptions import LabNotFound, InitializationError
 from .models import (
     Context,
     Lab,
@@ -45,16 +44,11 @@
     GroupManagement,
 )
 
-logger = logging.getLogger(__name__)
+_LOGGER = logging.getLogger(__name__)
 cached = lru_cache(maxsize=None)  # cache results forever
 
 
-class InitializationError(Exception):
-    pass
-
-
-class Version(object):
-
+class Version:
     __slots__ = ("version_str", "major", "minor", "patch")
 
     def __init__(self, version_str: str):
@@ -129,16 +123,19 @@ def minor_or_patch_differs(self, other):
         return self.minor_differs(other) or self.patch_differs(other)
 
 
-class ClientConfig(typing.NamedTuple):
-    url: str = None
-    username: str = None
-    password: str = None
+class ClientConfig(NamedTuple):
+    """Stores client library configuration, which can be used to create
+    any number of identically configured instances of ClientLibrary."""
+
+    url: Optional[str] = None
+    username: Optional[str] = None
+    password: Optional[str] = None
     ssl_verify: bool = True
     allow_http: bool = False
     auto_sync: float = 1.0
     raise_for_auth_failure: bool = True
     convergence_wait_max_iter: int = 500
-    convergence_wait_time: typing.Union[int, float] = 5
+    convergence_wait_time: Union[int, float] = 5
 
     def make_client(self):
         client = ClientLibrary(
@@ -157,37 +154,10 @@ def make_client(self):
 
 
 class ClientLibrary:
-    """
-    Initializes a ClientLibrary instance. Note that ssl_verify can
-    also be a string that points to a cert (see class documentation).
-
-    :param url: URL of controller. It's also possible to pass the
-        URL via the ``VIRL2_URL`` environment variable. If no protocol
-        scheme is provided, "https:" is used.
-    :type url: str
-    :param username: Username of the user to authenticate
-    :type username: str
-    :param password: Password of the user to authenticate
-    :type password: str
-    :param ssl_verify: SSL controller certificate verification
-    :type ssl_verify: bool
-    :param raise_for_auth_failure: Raise an exception if unable
-        to connect to controller (use for scripting scenarios)
-    :type raise_for_auth_failure: bool
-    :param allow_http: If set, a https URL will not be enforced
-    :type allow_http: bool
-    :param convergence_wait_max_iter: maximum number of iterations to wait for
-        lab to converge
-    :type convergence_wait_max_iter: int
-    :param convergence_wait_time: wait interval in seconds to wait during one
-        iteration during lab convergence wait
-    :type convergence_wait_time: int
-    :raises InitializationError: If no URL is provided or
-        authentication fails or host can't be reached
-    """
+    """Python bindings for the REST API of a CML controller."""
 
     # current client version
-    VERSION = Version("2.4.0")
+    VERSION = Version("2.4.1")
     # list of Version objects
     INCOMPATIBLE_CONTROLLER_VERSIONS = [
         Version("2.0.0"),
@@ -211,40 +181,41 @@ def __init__(
         convergence_wait_max_iter=500,
         convergence_wait_time=5,
     ):
-        """Constructor method"""
+        """
+        Initializes a ClientLibrary instance. Note that ssl_verify can
+        also be a string that points to a cert (see class documentation).
+
+        :param url: URL of controller. It's also possible to pass the
+            URL via the ``VIRL2_URL`` environment variable. If no protocol
+            scheme is provided, "https:" is used.
+        :type url: str
+        :param username: Username of the user to authenticate
+        :type username: str
+        :param password: Password of the user to authenticate
+        :type password: str
+        :param ssl_verify: SSL controller certificate verification
+        :type ssl_verify: bool
+        :param raise_for_auth_failure: Raise an exception if unable
+            to connect to controller (use for scripting scenarios)
+        :type raise_for_auth_failure: bool
+        :param allow_http: If set, a https URL will not be enforced
+        :type allow_http: bool
+        :param convergence_wait_max_iter: maximum number of iterations to wait for
+            lab to converge
+        :type convergence_wait_max_iter: int
+        :param convergence_wait_time: wait interval in seconds to wait during one
+            iteration during lab convergence wait
+        :type convergence_wait_time: int
+        :raises InitializationError: If no URL is provided or
+            authentication fails or host can't be reached
+        """
 
         url = self._environ_get(url, "VIRL2_URL")
         if url is None or len(url) == 0:
             message = "no URL provided"
             raise InitializationError(message)
 
-        # prepare the URL
-        try:
-            url_parts = urlsplit(url, "https")
-        except ValueError:
-            message = "invalid URL / hostname"
-            raise InitializationError(message)
-
-        # https://docs.python.org/3/library/urllib.parse.html
-        # Following the syntax specifications in RFC 1808, urlparse recognizes
-        # a netloc only if it is properly introduced by ‘//’. Otherwise the
-        # input is presumed to be a relative URL and thus to start with
-        # a path component.
-        if len(url_parts.netloc) == 0:
-            try:
-                url_parts = urlsplit("//" + url, "https")
-            except ValueError:
-                message = "invalid URL / hostname"
-                raise InitializationError(message)
-
-        if not allow_http and url_parts.scheme == "http":
-            message = "invalid URL scheme (must be https)"
-            raise InitializationError(message)
-        if url_parts.scheme not in ("http", "https"):
-            message = "invalid URL scheme (should be https)"
-            raise InitializationError(message)
-        url = urlunsplit(url_parts)
-        base_url = urljoin(url, "api/v0/")
+        url, base_url = self._prepare_url(url, allow_http)
 
         # check environment for username
         username = self._environ_get(username, "VIRL2_USER")
@@ -263,9 +234,11 @@ def __init__(
         self._context = Context(base_url)
         """
         Within the Client Library context:
-        `requests.Session()` instance that can be used to send requests to the controller.
+        `requests.Session()` instance that can be used to send requests
+            to the controller.
         `uuid.uuid4()` instance to uniquely identify this client library session.
-        `base_url` stores the base URL."""
+        `base_url` stores the base URL.
+        """
 
         # http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification
 
@@ -274,19 +247,21 @@ def __init__(
         self.session.verify = ssl_verify
 
         if ssl_verify is False:
-            logger.warning("SSL Verification disabled")
+            _LOGGER.warning("SSL Verification disabled")
             urllib3.disable_warnings()
 
         # checks version from system_info against self.VERSION
         self.check_controller_version()
 
         self.session.auth = TokenAuth(self)
-        """
-        `auto_sync` automatically syncs data with the backend after a specific
-        time. The default expiry time is 1.0s. This time can be configured by
-        setting the `auto_sync_interval`."""
+        # Note: session.auth is defined in the requests module to be of type AuthBase,
+        #  which has no logout function; this TokenAuth function will, therefore,
+        #  not be visible to a type checker, causing warnings.
 
         self.auto_sync = True
+        """`auto_sync` automatically syncs data with the backend after a specific
+        time. The default expiry time is 1.0s. This time can be configured by
+        setting the `auto_sync_interval`."""
         self.auto_sync_interval = 1.0  # seconds
 
         self.convergence_wait_max_iter = convergence_wait_max_iter
@@ -297,7 +272,7 @@ def __init__(
 
         self.url = url
         self.raise_for_auth_failure = raise_for_auth_failure
-        self._labs = {}
+        self._labs: Dict[str, Lab] = {}
 
         self.licensing = Licensing(context=self._context)
         self.user_management = UserManagement(context=self._context)
@@ -310,7 +285,7 @@ def __init__(
             if raise_for_auth_failure:
                 raise
             else:
-                logger.warning(exc)
+                _LOGGER.warning(exc)
                 return
 
     def __repr__(self):
@@ -327,8 +302,38 @@ def __repr__(self):
     def __str__(self):
         return "{} URL: {}".format(self.__class__.__name__, self._context.base_url)
 
+    def _prepare_url(self, url, allow_http):
+        # prepare the URL
+        try:
+            url_parts = urlsplit(url, "https")
+        except ValueError:
+            message = "invalid URL / hostname"
+            raise InitializationError(message)
+
+        # https://docs.python.org/3/library/urllib.parse.html
+        # Following the syntax specifications in RFC 1808, urlparse recognizes
+        # a netloc only if it is properly introduced by ‘//’. Otherwise, the
+        # input is presumed to be a relative URL and thus to start with
+        # a path component.
+        if len(url_parts.netloc) == 0:
+            try:
+                url_parts = urlsplit("//" + url, "https")
+            except ValueError:
+                message = "invalid URL / hostname"
+                raise InitializationError(message)
+
+        if not allow_http and url_parts.scheme == "http":
+            message = "invalid URL scheme (must be https)"
+            raise InitializationError(message)
+        if url_parts.scheme not in ("http", "https"):
+            message = "invalid URL scheme (should be https)"
+            raise InitializationError(message)
+        url = urlunsplit(url_parts)
+        base_url = urljoin(url, "api/v0/")
+        return url, base_url
+
     def _make_test_auth_call(self):
-        # make a call to confirm auth works by using the "authok" endpoint
+        """Make a call to confirm auth works by using the "authok" endpoint."""
         try:
             url = self._base_url + "authok"
             response = self.session.get(url)
@@ -348,11 +353,11 @@ def _make_test_auth_call(self):
 
     @staticmethod
     def _environ_get(value, key, default=None):
-        # If the value is not set yet, fetch it from environment or return default
+        """If the value is not set yet, fetch it from environment or return default."""
         if value is None:
             value = os.environ.get(key)
             if value:
-                logger.info("Using value %s from environment")
+                _LOGGER.info("Using value %s from environment", key)
             else:
                 value = default
         return value
@@ -360,17 +365,17 @@ def _environ_get(value, key, default=None):
     @property
     def session(self):
         """
-        Returns the used Requests session object
+        Returns the used Requests session object.
 
         :returns: The Requests session object
-        :rtype: Requests.Session
+        :rtype: requests.Session
         """
         return self._context.session
 
     @property
     def _base_url(self):
         """
-        Returns the base URL to access the controller
+        Returns the base URL to access the controller.
 
         :returns: The base URL
         :rtype: str
@@ -381,7 +386,9 @@ def logout(self, clear_all_sessions=False):
         """
         Invalidate current token.
         """
+        # noinspection PyUnresolvedReferences
         return self.session.auth.logout(clear_all_sessions=clear_all_sessions)
+        # See session.auth assignment above for noinspection reasoning.
 
     def get_host(self):
         """
@@ -413,6 +420,7 @@ def check_controller_version(self, controller_version=None):
         raises exception if versions are incompatible or prints warning
         if client minor version is lower than controller minor version.
 
+        :type controller_version: str
         :rtype: None
         """
         controller_version = (
@@ -421,7 +429,7 @@ def check_controller_version(self, controller_version=None):
 
         # are we running against a test version?
         if controller_version == "testing":
-            logger.warning("testing version detected!")
+            _LOGGER.warning("testing version detected!")
             return
 
         controller_version = Version(controller_version)
@@ -441,7 +449,7 @@ def check_controller_version(self, controller_version=None):
         if self.VERSION.minor_differs(controller_version) and self.VERSION.minor_lt(
             controller_version
         ):
-            logger.warning(
+            _LOGGER.warning(
                 "Please ensure the client version is compatible with the controller"
                 " version. Client %s, controller %s.",
                 self.VERSION,
@@ -450,13 +458,13 @@ def check_controller_version(self, controller_version=None):
 
     def is_system_ready(self, wait=False, max_wait=60, sleep=5):
         """
-        is_system_ready reports whether the system is ready or not.
+        Reports whether the system is ready or not.
 
         :param wait: if this is true, the call will block until it's ready
         :type wait: bool
-        :param max_wait: wait for a maximum of 'max_wait' seconds
+        :param max_wait: maximum time to wait, in seconds
         :type wait: int
-        :param sleep: wait for 'sleep' seconds between tries
+        :param sleep: time to wait between tries, in seconds
         :type wait: int
         :returns: ready state
         :rtype: bool
@@ -479,13 +487,6 @@ def is_system_ready(self, wait=False, max_wait=60, sleep=5):
             loops -= 1
         return ready
 
-    def wait_for_lld_connected(self):
-        """
-        Waits until the controller has a compute node connected.
-        (Deprecated)
-        """
-        raise Exception("this is deprecated, use is_system_ready(wait=True), if needed")
-
     @staticmethod
     def is_virl_1x(path: Path):
         if path.suffix == ".virl":
@@ -512,7 +513,8 @@ def import_lab(self, topology, title=None, offline=False, virl_1x=False):
         :raises ValueError: if there's no lab ID in the API response
         :raises requests.exceptions.HTTPError: if there was a transport error
         """
-        # TODO: refactor to return the local lab, and sync it, if already exists in self._labs
+        # TODO: refactor to return the local lab, and sync it,
+        # if already exists in self._labs
         if offline:
             lab_id = "offline_lab"
         else:
@@ -564,31 +566,40 @@ def import_lab_from_path(self, path, title=None):
         topology_path = Path(path)
         if not topology_path.exists():
             message = "{} can not be found".format(path)
-            raise Exception(message)
+            raise FileNotFoundError(message)
 
         topology = topology_path.read_text()
         return self.import_lab(
             topology, title=title, virl_1x=self.is_virl_1x(topology_path)
         )
 
+    def get_sample_labs(self):
+        """
+        Returns a dictionary with information about all sample labs available on host.
+
+        :returns: A dictionary of sample lab information
+        :rtype: dict
+        """
+        url = urljoin(self._base_url, "sample/labs")
+        response = self.session.get(url)
+        response.raise_for_status()
+        return response.json()
+
     def import_sample_lab(self, title):
         """
         Imports a built-in sample lab.
 
-        :param title: Sample lab name with extension
+        :param title: sample lab name, as returned by get_sample_labs
         :type title: str
-        :returns: A Lab instance
+        :returns: a Lab instance
         :rtype: models.Lab
         """
-        topology_file_path = Path("import_export") / "SampleData" / title
-        topology = pkg_resources.resource_string(
-            "simple_common", topology_file_path.as_posix()
-        )
-        return self.import_lab(
-            topology=topology.decode(),
-            title=title,
-            virl_1x=self.is_virl_1x(topology_file_path),
-        )
+
+        url = urljoin(self._base_url, "sample/labs/" + title)
+        response = self.session.put(url)
+        response.raise_for_status()
+        lab_id = response.json()
+        return self.join_existing_lab(lab_id)
 
     def all_labs(self, show_all=False):
         """
@@ -649,6 +660,7 @@ def create_lab(self, title=None):
         response = self.session.post(url, params=params)
         response.raise_for_status()
         result = response.json()
+        # TODO: server generated title not loaded
         lab_id = result["id"]
         lab = Lab(
             title or lab_id,
@@ -691,7 +703,7 @@ def join_existing_lab(self, lab_id, sync_lab=True):
 
             lab = client_library.join_existing_lab("2e6a18")
 
-        :param lab_id: The lab ID to be removed.
+        :param lab_id: The lab ID to be joined.
         :type lab_id: str
         :param sync_lab: Synchronize changes.
         :type sync_lab: bool
@@ -717,19 +729,44 @@ def join_existing_lab(self, lab_id, sync_lab=True):
         return lab
 
     def get_diagnostics(self):
-        """Returns the controller diagnostic data as JSON
+        """
+        Returns the controller diagnostic data as JSON
 
         :returns: diagnostic data
-        :rtype: str
+        :rtype: dict
         """
         url = self._base_url + "diagnostics"
         response = self.session.get(url)
         response.raise_for_status()
         return response.json()
 
+    def get_system_health(self):
+        """
+        Returns the controller system health data as JSON
+
+        :returns: system health data
+        :rtype: dict
+        """
+        url = self._base_url + "system_health"
+        response = self.session.get(url)
+        response.raise_for_status()
+        return response.json()
+
+    def get_system_stats(self):
+        """
+        Returns the controller resource statistics as JSON
+
+        :returns: system resource statistics
+        :rtype: dict
+        """
+        url = self._base_url + "system_stats"
+        response = self.session.get(url)
+        response.raise_for_status()
+        return response.json()
+
     def find_labs_by_title(self, title):
         """
-        Return a list of labs which match the given title
+        Return a list of labs which match the given title.
 
         :param title: The title to search for
         :type title: str
@@ -762,7 +799,8 @@ def find_labs_by_title(self, title):
         return matched_labs
 
     def get_lab_list(self, show_all=False):
-        """Returns list of all lab IDs.
+        """
+        Returns list of all lab IDs.
 
         :param show_all: Whether to get only labs owned by the admin or all user labs
         :type show_all: bool


Test patch:
diff --git a/conftest.py b/conftest.py
index f7acc94..9ac7c4a 100644
--- a/conftest.py
+++ b/conftest.py
@@ -3,7 +3,7 @@
 #
 # This file is part of VIRL 2
 #
-# Copyright 2020-2021 Cisco Systems Inc.
+# Copyright 2020-2022 Cisco Systems Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -20,129 +20,24 @@
 
 import os
 import pytest
-import time
-import warnings
 
-from requests import HTTPError
-from urllib3.exceptions import InsecureRequestWarning
 
-from virl2_client import ClientConfig, ClientLibrary
-
-
-def pytest_addoption(parser):
-    # only add these options if the test is run inside the
-    # root virl2 repository itself and not from outside
+def pytest_addoption(parser, pluginmanager):
+    # Only add these options if the test is run inside the root virl2 repository
+    # itself and not from outside
     root_dir = parser.extra_info.get("rootdir", "")
-    if not root_dir.endswith("virl2_client"):
+    if not root_dir.endswith("virl2-client"):
         return
 
-    parser.addoption(
-        "--controller-url",
-        default="https://127.0.0.1",
-        metavar="VIRL2_URL",
-        help="The URL of a CML2 controller",
-    )
-
-    parser.addoption(
-        "--controller-username",
-        default=None,
-        metavar="VIRL2_USER",
-        help="The user on CML2 controller",
-    )
-
-    parser.addoption(
-        "--controller-password",
-        default=None,
-        metavar="VIRL2_PASS",
-        help="The user's password on CML2 controller",
-    )
-
-    parser.addoption(
-        "--controller-ssl-verify",
-        default=False,
-        nargs="?",
-        const=True,
-        metavar="CA_BUNDLE",
-        help="Verify certificate of CML2 controller",
-    )
-
-    parser.addoption(
-        "--controller-allow-http",
-        default=False,
-        action="store_true",
-        help="Allow HTTP for CML2 controller URL",
-    )
-
-
-@pytest.fixture
-def no_ssl_warnings():
-    with warnings.catch_warnings():
-        # We don't care about SSL connections to untrusted servers in tests:
-        warnings.simplefilter("ignore", InsecureRequestWarning)
-        yield
-
-
-def stop_wipe_and_remove_all_labs(client_library: ClientLibrary):
-    lab_list = client_library.get_lab_list()
-    for lab_id in lab_list:
-        lab = client_library.join_existing_lab(lab_id)
-        lab.stop()
-        lab.wipe()
-        client_library.remove_lab(lab_id)
-
-
-@pytest.fixture(scope="session")
-def client_config(request) -> ClientConfig:
-    return ClientConfig(
-        url=request.config.getoption("--controller-url"),
-        username=request.config.getoption("--controller-username"),
-        password=request.config.getoption("--controller-password"),
-        ssl_verify=request.config.getoption("--controller-ssl-verify"),
-        allow_http=request.config.getoption("--controller-allow-http"),
-        raise_for_auth_failure=True,
-    )
-
-
-def client_library_keep_labs_base(client_config: ClientConfig) -> ClientLibrary:
-    client_library = client_config.make_client()
-    for _ in range(5):
-        try:
-            client_library.is_system_ready()
-        except HTTPError as err:
-            if err.errno == 504:
-                # system still initialising, wait longer
-                time.sleep(2)
-
-    return client_library
-
-
-@pytest.fixture
-def client_library_keep_labs(
-    no_ssl_warnings, client_config: ClientConfig
-) -> ClientLibrary:
-    # for integration testing, the client library needs to connect to a mock simulator
-    # running via HTTP on a non SSL servr / non-standard port. We therefore need to
-    # set the allow_http to True. Otherwise the client library would enforce the HTTPS
-    # scheme and the tests would fail. This should never be required in the wild.
-    yield client_library_keep_labs_base(client_config)
-
-
-@pytest.fixture(scope="session")
-def client_library_session(client_config: ClientConfig) -> ClientLibrary:
-    """This client library has session lifetime"""
-    yield client_library_keep_labs_base(client_config)
-
-
-@pytest.fixture
-def client_library(client_library_keep_labs: ClientLibrary) -> ClientLibrary:
-    client_library = client_library_keep_labs
-    stop_wipe_and_remove_all_labs(client_library)
-    # Reset "current" lab:
-    client_library.lab = None
-    yield client_library
-    # tear down - delete labs from the tests
-    # TODO: see if these need updating now remove_all_labs doesnt stop the lab
-    stop_wipe_and_remove_all_labs(client_library)
+    # The pytest.ini file has the "asyncio_mode=auto" set... pytest-asyncio
+    # defaults to "strict" these days. However, if the option is set but the
+    # plugin is not installed this ini file option will cause a warning.  Adding
+    # the parser option here when the module is *not* installed will suppress
+    # the warning.
+    # Note: The virl2_client package by itself does *not* depend on
+    # pytest-asycnio
+    if pluginmanager.get_plugin("asyncio") is None:
+        parser.addini("asyncio_mode", "suppress the warning")
 
 
 @pytest.fixture(scope="function")
diff --git a/pytest.ini b/pytest.ini
index c1b9d5e..e6aaf71 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -3,27 +3,16 @@ addopts =
     --strict-markers
     --verbose
     --showlocals
-    --ignore=live_vis_v2
-    --ignore=tests/integration
+
 # the below configuration lines are needed to for
-# code coverage reporting and also control execution
-# of integration tests
-#    -m "not integration and not slow"
+# code coverage reporting
 #    --cov-branch
 #    --cov-report term-missing
 
-filterwarnings =
-    # https://github.com/zalando/connexion/issues/1136
-    ignore:Deprecated, use .can_read_body #2005:DeprecationWarning
-    ignore:Unverified HTTPS request is being made to host.*:urllib3.exceptions.InsecureRequestWarning
-
-markers =
-    integration: tests that communicate external servers that need to be running before firing the tests
-    slow: unit tests that take too long to run in a "regular" test run during development
-    nomock: unit tests that require an actual VM to run (in combination with integration tests)
-
 junit_family=xunit1
 
+asyncio_mode=auto
+
 # start pytest with --log-level=info
 # log_cli=True
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 324599d..b9fbbcc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -18,6 +18,7 @@
 # limitations under the License.
 #
 
+from pathlib import Path
 import pytest
 import requests
 
@@ -26,6 +27,8 @@
 
 CURRENT_VERSION = ClientLibrary.VERSION.version_str
 
+FAKE_HOST_API = "https://0.0.0.0/api/v0/"
+
 
 def client_library_patched_system_info(version):
     with patch.object(
@@ -63,3 +66,58 @@ def client_library_server_2_19_0():
 def mocked_session():
     with patch.object(requests, "Session", autospec=True) as session:
         yield session
+
+
+def resp_body_from_file(req, context):
+    """
+    A callback that returns the contents of a file based on the request.
+
+    :param req: The title to search for
+    :type req: An HTTP request (or proxy) object
+    :param context: unused but required context parameter
+    :type context: An HTTP response object
+    :returns: A string, the contents of a file that corresponds to a response body for the request.
+    :rtype: string
+    """
+    endpoint_parts = req.path.split("/")[3:]
+    filename = "not initialized"
+    if len(endpoint_parts) == 1:
+        filename = endpoint_parts[0] + ".json"
+    elif endpoint_parts[0] == "labs":
+        lab_id = endpoint_parts[1]
+        filename = "_".join(endpoint_parts[2:]) + "-" + lab_id + ".json"
+    file_path = Path("test_data", filename)
+    return file_path.read_text()
+
+
+@pytest.fixture
+def requests_mock_with_labs(requests_mock):
+    """
+    A test fixture that provides basic lab data with requests-mock so that unit tests can
+    call ``client.all_labs`` or ``client.join_existing_lab``.  The sample data includes
+    some runtime data, like node states and simulation_statistics.
+    """
+    requests_mock.get(
+        FAKE_HOST_API + "system_information",
+        json={"version": CURRENT_VERSION, "ready": True},
+    )
+    requests_mock.post(FAKE_HOST_API + "authenticate", json="BOGUS_TOKEN")
+    requests_mock.get(FAKE_HOST_API + "authok", text=None)
+    resp_from_files = (
+        "labs",
+        "populate_lab_tiles",
+        "labs/444a78d1-575c-4746-8469-696e580f17b6/topology",
+        "labs/444a78d1-575c-4746-8469-696e580f17b6/simulation_stats",
+        "labs/444a78d1-575c-4746-8469-696e580f17b6/layer3_addresses",
+        "labs/df76a038-076f-4744-85c0-b2e1daf1bc06/topology",
+        "labs/df76a038-076f-4744-85c0-b2e1daf1bc06/simulation_stats",
+        "labs/df76a038-076f-4744-85c0-b2e1daf1bc06/layer3_addresses",
+        "labs/3031b614-0e76-4450-9fe0-6b3be0bc0bd2/topology",
+        "labs/3031b614-0e76-4450-9fe0-6b3be0bc0bd2/simulation_stats",
+        "labs/3031b614-0e76-4450-9fe0-6b3be0bc0bd2/layer3_addresses",
+        "labs/863799a0-3d09-4af4-be26-cad997b6ab27/topology",
+        "labs/863799a0-3d09-4af4-be26-cad997b6ab27/simulation_stats",
+        "labs/863799a0-3d09-4af4-be26-cad997b6ab27/layer3_addresses",
+    )
+    for api in resp_from_files:
+        requests_mock.get(FAKE_HOST_API + api, text=resp_body_from_file)
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 063e6b0..48edbbd 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,22 +1,21 @@
-atomicwrites==1.4.0; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0"
+atomicwrites==1.4.1; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0"
 attrs==21.4.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
-certifi==2022.5.18.1; python_version >= "3.7" and python_version < "4"
+certifi==2022.6.15; python_version >= "3.7" and python_version < "4"
 cfgv==3.3.1; python_full_version >= "3.6.1" and python_version >= "3.7"
-charset-normalizer==2.0.12; python_version >= "3.7" and python_version < "4" and python_full_version >= "3.5.0"
-colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0"
-distlib==0.3.4; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
+charset-normalizer==2.1.0; python_version >= "3.7" and python_version < "4" and python_full_version >= "3.6.0"
+colorama==0.4.5; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0"
+distlib==0.3.5; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
 filelock==3.7.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
 flake8==4.0.1; python_version >= "3.6"
 identify==2.5.1; python_version >= "3.7"
 idna==3.3; python_version >= "3.7" and python_version < "4"
-importlib-metadata==4.2.0; python_version < "3.8" and python_version >= "3.7" and (python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version >= "3.7" and python_version < "3.8")
 iniconfig==1.1.1; python_version >= "3.7"
 mccabe==0.6.1; python_version >= "3.6"
-nodeenv==1.6.0; python_version >= "3.7"
+nodeenv==1.7.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.7.0" and python_version >= "3.7"
 packaging==21.3; python_version >= "3.7"
 platformdirs==2.5.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
 pluggy==1.0.0; python_version >= "3.7"
-pre-commit==2.19.0; python_version >= "3.7"
+pre-commit==2.20.0; python_version >= "3.7"
 py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
 pycodestyle==2.8.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
 pyflakes==2.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
@@ -25,12 +24,10 @@ pytest==7.1.2; python_version >= "3.7"
 pyyaml==6.0; python_version >= "3.7"
 requests-mock==1.9.3
 requests-toolbelt==0.9.1
-requests==2.28.0; python_version >= "3.7" and python_version < "4"
+requests==2.28.1; python_version >= "3.7" and python_version < "4"
 responses==0.20.0; python_version >= "3.7"
 six==1.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
 toml==0.10.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7"
 tomli==2.0.1; python_version >= "3.7"
-typing-extensions==4.2.0; python_version < "3.8" and python_version >= "3.7"
-urllib3==1.26.9; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4")
-virtualenv==20.14.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
-zipp==3.8.0; python_version < "3.8" and python_version >= "3.7"
+urllib3==1.26.10; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0" and python_version < "4")
+virtualenv==20.15.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
diff --git a/tests/test_client_library.py b/tests/test_client_library.py
index 8960231..549c456 100644
--- a/tests/test_client_library.py
+++ b/tests/test_client_library.py
@@ -83,7 +83,7 @@ def test_import_lab_from_path_virl_title(
     Lab.sync = Mock()
     new_title = "new_title"
     (tmp_path / "topology.virl").write_text("<?xml version='1.0' encoding='UTF-8'?>")
-    with patch.object(Lab, "sync", autospec=True) as sync_mock:
+    with patch.object(Lab, "sync", autospec=True):
         lab = cl.import_lab_from_path(
             path=(tmp_path / "topology.virl").as_posix(), title=new_title
         )
@@ -91,7 +91,7 @@ def test_import_lab_from_path_virl_title(
     assert lab.lab_base_url.startswith("https://0.0.0.0/fake_url/api/v0/labs/")
 
     cl.session.post.assert_called_once_with(
-        f"https://0.0.0.0/fake_url/api/v0/import/virl-1x?title={new_title}",
+        "https://0.0.0.0/fake_url/api/v0/import/virl-1x?title=" + new_title,
         data="<?xml version='1.0' encoding='UTF-8'?>",
     )
 
@@ -701,24 +701,24 @@ def test_import_lab_offline(
 
 
 def test_convergence_parametrization(client_library_server_current, mocked_session):
-    MAX_ITER = 2
-    WAIT_TIME = 1
+    max_iter = 2
+    max_time = 1
     cl = ClientLibrary(
         url="https://0.0.0.0/fake_url/",
         username="test",
         password="pa$$",
-        convergence_wait_max_iter=MAX_ITER,
-        convergence_wait_time=WAIT_TIME,
+        convergence_wait_max_iter=max_iter,
+        convergence_wait_time=max_time,
     )
     # check that passing of value from client to lab is working
     lab = cl.create_lab()
-    assert lab.wait_max_iterations == MAX_ITER
-    assert lab.wait_time == WAIT_TIME
+    assert lab.wait_max_iterations == max_iter
+    assert lab.wait_time == max_time
     with patch.object(Lab, "has_converged", return_value=False):
         with pytest.raises(RuntimeError) as err:
             lab.wait_until_lab_converged()
         assert (
-            "has not converged, maximum tries %s exceeded" % MAX_ITER
+            "has not converged, maximum tries %s exceeded" % max_iter
         ) in err.value.args[0]
 
         # try to override values on function
diff --git a/tests/test_client_library_labs.py b/tests/test_client_library_labs.py
index 91ffd60..40b46c7 100644
--- a/tests/test_client_library_labs.py
+++ b/tests/test_client_library_labs.py
@@ -25,7 +25,9 @@
 
 from virl2_client.exceptions import NodeNotFound
 from virl2_client.models import Interface, Lab
-from virl2_client.virl2_client import Context
+from virl2_client.virl2_client import ClientLibrary, Context
+
+FAKE_HOST = "https://0.0.0.0"
 
 
 def test_topology_creation_and_removal():
@@ -57,20 +59,20 @@ def test_topology_creation_and_removal():
     assert sorted(node_b.links()) == [lnk1, lnk2]
     assert node_c.links() == [lnk2]
 
-    assert i1.degree() == 1
-    assert i2.degree() == 1
-    assert i3.degree() == 1
-    assert i4.degree() == 1
+    assert i1.connected is True
+    assert i2.connected is True
+    assert i3.connected is True
+    assert i4.connected is True
 
-    assert i1.peer_interfaces() == {i2}
-    assert i2.peer_interfaces() == {i1}
-    assert i3.peer_interfaces() == {i4}
-    assert i4.peer_interfaces() == {i3}
+    assert i1.peer_interface is i2
+    assert i2.peer_interface is i1
+    assert i3.peer_interface is i4
+    assert i4.peer_interface is i3
 
-    assert i1.peer_nodes() == {node_b}
-    assert i2.peer_nodes() == {node_a}
-    assert i3.peer_nodes() == {node_c}
-    assert i4.peer_nodes() == {node_b}
+    assert i1.peer_node is node_b
+    assert i2.peer_node is node_a
+    assert i3.peer_node is node_c
+    assert i4.peer_node is node_b
 
     assert lnk1.nodes == (node_a, node_b)
     assert lnk1.interfaces == (i1, i2)
@@ -129,8 +131,10 @@ def test_create_node():
     node = lab.create_node("testnode", "server")
     assert node.node_definition == "server"
     assert node.label == "testnode"
+    assert node.compute_id is None  # None until we start the node.
 
 
+# If this ``requests_mock`` fixture is confusing, see https://requests-mock.readthedocs.io/en/latest/pytest.html.
 def test_create_link(requests_mock):
     requests_mock.post("mock://labs/1/nodes", json={"id": "n0"})
     requests_mock.post(
@@ -263,3 +267,23 @@ def test_connect_two_nodes(requests_mock):
         "writepackets": 0,
     }
     assert link.id == "l0"
+
+
+def test_join_existing_lab(change_test_dir, requests_mock_with_labs):
+    client = ClientLibrary(url=FAKE_HOST, username="test", password="pa$$")
+    lab = client.join_existing_lab("444a78d1-575c-4746-8469-696e580f17b6")
+    assert lab.title == "IOSv Feature Tests"
+    assert lab.statistics == {"nodes": 7, "links": 8, "interfaces": 24}
+
+
+def test_all_labs(
+    client_library_server_current, change_test_dir, requests_mock_with_labs
+):
+    client = ClientLibrary(url=FAKE_HOST, username="test", password="pa$$")
+    all_labs = client.all_labs()
+    assert len(all_labs) == 4
+    iosv_labs = client.find_labs_by_title("IOSv Feature Tests")
+    assert len(iosv_labs) == 1
+    lab: Lab = iosv_labs[0]
+    node = lab.get_node_by_label("csr1000v-0")
+    assert node.compute_id == "99c887f5-052e-4864-a583-49fa7c4b68a9"
diff --git a/tests/test_data/labs.json b/tests/test_data/labs.json
new file mode 100644
index 0000000..1f3fb9d
--- /dev/null
+++ b/tests/test_data/labs.json
@@ -0,0 +1,6 @@
+[
+  "444a78d1-575c-4746-8469-696e580f17b6",
+  "df76a038-076f-4744-85c0-b2e1daf1bc06",
+  "3031b614-0e76-4450-9fe0-6b3be0bc0bd2",
+  "863799a0-3d09-4af4-be26-cad997b6ab27"
+]
diff --git a/tests/test_data/layer3_addresses-444a78d1-575c-4746-8469-696e580f17b6.json b/tests/test_data/layer3_addresses-444a78d1-575c-4746-8469-696e580f17b6.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/test_data/layer3_addresses-444a78d1-575c-4746-8469-696e580f17b6.json
@@ -0,0 +1,1 @@
+{}
diff --git a/tests/test_data/layer3_addresses-863799a0-3d09-4af4-be26-cad997b6ab27.json b/tests/test_data/layer3_addresses-863799a0-3d09-4af4-be26-cad997b6ab27.json
new file mode 100644
index 0000000..abc2105
--- /dev/null
+++ b/tests/test_data/layer3_addresses-863799a0-3d09-4af4-be26-cad997b6ab27.json
@@ -0,0 +1,140 @@
+{
+  "e79a0eb7-5c10-4538-8776-b832df78ba8b": {
+    "name": "csr1000v-2",
+    "interfaces": {
+      "52:54:00:19:22:35": {
+        "id": "ffb9634f-3889-466c-9d77-cc60c0dfde0f",
+        "label": "GigabitEthernet1",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:1f:22:69": {
+        "id": "55e76e62-b5af-4875-a028-48a956257e72",
+        "label": "GigabitEthernet2",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:00:3d:28": {
+        "id": "10dfbc02-838b-4cde-9af3-5dfed2b3e369",
+        "label": "GigabitEthernet3",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:10:45:57": {
+        "id": "dcaa0e1c-6245-4320-8b21-32a57af15929",
+        "label": "GigabitEthernet4",
+        "ip4": [],
+        "ip6": []
+      }
+    }
+  },
+  "f4220194-d8b4-42ed-9967-226e1ed92ca0": {
+    "name": "alpine-0",
+    "interfaces": {
+      "52:54:00:0a:26:93": {
+        "id": "9ca58f2d-c96d-48bb-abde-0eb333d13a96",
+        "label": "eth0",
+        "ip4": [],
+        "ip6": []
+      }
+    }
+  },
+  "6d583887-a109-4df2-b7fe-45c27379a49d": {
+    "name": "iosv-0",
+    "interfaces": {
+      "52:54:00:06:8b:d6": {
+        "id": "7bd4984c-29df-4a42-b221-996dfdfc60bd",
+        "label": "GigabitEthernet0/0",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:0f:d1:ea": {
+        "id": "27975a0f-a63c-46c9-9987-6cfc4cafa2c6",
+        "label": "GigabitEthernet0/1",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:06:86:5c": {
+        "id": "fc2796d5-abd5-41e9-abe9-c471c45f3734",
+        "label": "GigabitEthernet0/2",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:0d:59:4b": {
+        "id": "12a5df49-ad47-4bba-8969-34d32db7bea3",
+        "label": "GigabitEthernet0/3",
+        "ip4": [],
+        "ip6": []
+      }
+    }
+  },
+  "90e9329f-7d20-4eb2-b396-06d0aa497ce9": {
+    "name": "csr1000v-0",
+    "interfaces": {
+      "52:54:00:0d:75:39": {
+        "id": "bd8ba744-c484-42c0-b096-93546787c681",
+        "label": "GigabitEthernet1",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:04:b8:87": {
+        "id": "e598f094-2cba-47aa-b704-2d3b1c562323",
+        "label": "GigabitEthernet2",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:17:61:cb": {
+        "id": "8f2aab3a-77e2-447f-9b63-31a991cc1b85",
+        "label": "GigabitEthernet3",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:00:24:81": {
+        "id": "35cf19ca-ca56-45a7-8295-efb0416af1ce",
+        "label": "GigabitEthernet4",
+        "ip4": [],
+        "ip6": []
+      }
+    }
+  },
+  "d9a0bb6a-dbdf-45e5-9222-18dc5fb4adf0": {
+    "name": "iosvl2-0",
+    "interfaces": {
+      "52:54:00:03:53:a1": {
+        "id": "e1622f5f-151a-4ee9-b090-7c87c775cae5",
+        "label": "GigabitEthernet0/0",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:16:39:d6": {
+        "id": "bdac665f-4c26-47dd-8ffb-6ba24553eba7",
+        "label": "GigabitEthernet0/1",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:0e:cb:5e": {
+        "id": "3b5c912c-208e-4a2f-92f1-5f4ae524c49d",
+        "label": "GigabitEthernet0/2",
+        "ip4": [],
+        "ip6": []
+      },
+      "52:54:00:18:17:d5": {
+        "id": "49542943-033c-4bd4-b36d-6a0a85a091e6",
+        "label": "GigabitEthernet0/3",
+        "ip4": [],
+        "ip6": []
+      }
+    }
+  },
+  "9c840733-ac3f-4195-8839-b2c0379bc719": {
+    "name": "ubuntu-0",
+    "interfaces": {
+      "52:54:00:1d:4b:bb": {
+        "id": "52bcdd81-9055-496f-823d-163937e39de2",
+        "label": "ens2",
+        "ip4": [],
+        "ip6": []
+      }
+    }
+  }
+}
diff --git a/tests/test_data/populate_lab_tiles.json b/tests/test_data/populate_lab_tiles.json
new file mode 100644
index 0000000..3957040
--- /dev/null
+++ b/tests/test_data/populate_lab_tiles.json
@@ -0,0 +1,1108 @@
+{
+  "lab_tiles": {
+    "444a78d1-575c-4746-8469-696e580f17b6": {
+      "state": "STOPPED",
+      "created": "2022-06-10T14:23:33+00:00",
+      "modified": "2022-06-10T14:23:33+00:00",
+      "lab_title": "IOSv Feature Tests",
+      "lab_description": "A sample lab for testing IOSv features",
+      "lab_notes": "# IOSv Feature Testing\n\n## Features\n\n- CDP\n- ping\n- OSPF single-area\n- NAT - static\n- HSRP\n- DHCP\n- Routed subinterface\n\n## Test Details\n\nFor more details on test procedure, see the [IOSv](https://developer.cisco.com/docs/modeling-labs/#!iosv) page in the [CML documentation](https://developer.cisco.com/docs/modeling-labs/).",
+      "owner": "640a1c81-f1e7-4159-8fa2-ad35d9a68586",
+      "owner_username": "jdoe",
+      "node_count": 7,
+      "link_count": 8,
+      "id": "444a78d1-575c-4746-8469-696e580f17b6",
+      "groups": [],
+      "topology": {
+        "nodes": [
+          {
+            "id": "99cda47a-ecb2-4d31-86c4-74e7a8201958",
+            "label": "iosv-0",
+            "x": -400,
+            "y": 0,
+            "node_definition": "iosv",
+            "image_definition": "",
+            "state": "STOPPED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "913e62a7-e096-4ed9-bb9f-03ae13106fc5",
+            "label": "iosvl2-0",
+            "x": -250,
+            "y": 0,
+            "node_definition": "iosvl2",
+            "image_definition": "",
+            "state": "STOPPED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "0f9565f7-4fa3-4312-8dda-1db183a55950",
+            "label": "csr1000v-0",
+            "x": -550,
+            "y": 0,
+            "node_definition": "csr1000v",
+            "image_definition": "",
+            "state": "STOPPED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "56c875d9-4f2a-4688-9fba-660716cff4cb",
+            "label": "iosv-1",
+            "x": -400,
+            "y": 150,
+            "node_definition": "iosv",
+            "image_definition": "",
+            "state": "STOPPED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "aa51eca6-ae81-40fc-a713-e1a168280d21",
+            "label": "alpine-0",
+            "x": -396,
+            "y": -164,
+            "node_definition": "alpine",
+            "image_definition": "",
+            "state": "STOPPED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "e5222bd8-52ff-4e1d-b6c9-89241132fb13",
+            "label": "alpine-1",
+            "x": -100,
+            "y": 0,
+            "node_definition": "alpine",
+            "image_definition": "",
+            "state": "STOPPED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "004c00c9-2606-485c-8ff9-d698e430fa6a",
+            "label": "ext-conn-0",
+            "x": -248,
+            "y": -166,
+            "node_definition": "external_connector",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          }
+        ],
+        "links": [
+          {
+            "id": "8f8980be-a12c-4d42-a319-08169a0cbef7",
+            "node_a": "99cda47a-ecb2-4d31-86c4-74e7a8201958",
+            "node_b": "0f9565f7-4fa3-4312-8dda-1db183a55950",
+            "state": "STOPPED"
+          },
+          {
+            "id": "4c029bda-8f54-42d5-908e-1638b736f957",
+            "node_a": "56c875d9-4f2a-4688-9fba-660716cff4cb",
+            "node_b": "99cda47a-ecb2-4d31-86c4-74e7a8201958",
+            "state": "STOPPED"
+          },
+          {
+            "id": "74df9890-4df6-4e61-8aa7-9459d6381a85",
+            "node_a": "99cda47a-ecb2-4d31-86c4-74e7a8201958",
+            "node_b": "913e62a7-e096-4ed9-bb9f-03ae13106fc5",
+            "state": "STOPPED"
+          },
+          {
+            "id": "fbfb78fa-d62d-448f-a5e9-1dacaa1504ad",
+            "node_a": "56c875d9-4f2a-4688-9fba-660716cff4cb",
+            "node_b": "913e62a7-e096-4ed9-bb9f-03ae13106fc5",
+            "state": "STOPPED"
+          },
+          {
+            "id": "0321b86a-50e6-4c6a-8151-60acfdd07775",
+            "node_a": "56c875d9-4f2a-4688-9fba-660716cff4cb",
+            "node_b": "0f9565f7-4fa3-4312-8dda-1db183a55950",
+            "state": "STOPPED"
+          },
+          {
+            "id": "4244119a-d0d0-486a-aec7-48b587e0b7dd",
+            "node_a": "99cda47a-ecb2-4d31-86c4-74e7a8201958",
+            "node_b": "aa51eca6-ae81-40fc-a713-e1a168280d21",
+            "state": "STOPPED"
+          },
+          {
+            "id": "6b92a334-4838-4f1c-b3f3-45c4baf00268",
+            "node_a": "e5222bd8-52ff-4e1d-b6c9-89241132fb13",
+            "node_b": "913e62a7-e096-4ed9-bb9f-03ae13106fc5",
+            "state": "STOPPED"
+          },
+          {
+            "id": "d344bab0-c445-49d7-a1ab-a9132a4bcf4d",
+            "node_a": "913e62a7-e096-4ed9-bb9f-03ae13106fc5",
+            "node_b": "004c00c9-2606-485c-8ff9-d698e430fa6a",
+            "state": "DEFINED_ON_CORE"
+          }
+        ]
+      }
+    },
+    "df76a038-076f-4744-85c0-b2e1daf1bc06": {
+      "state": "DEFINED_ON_CORE",
+      "created": "2022-06-30T03:46:10+00:00",
+      "modified": "2022-06-30T03:48:06+00:00",
+      "lab_title": "DevNet Sandbox SD-WAN",
+      "lab_description": "",
+      "lab_notes": "",
+      "owner": "640a1c81-f1e7-4159-8fa2-ad35d9a68586",
+      "owner_username": "jdoe",
+      "node_count": 19,
+      "link_count": 32,
+      "id": "df76a038-076f-4744-85c0-b2e1daf1bc06",
+      "groups": [],
+      "topology": {
+        "nodes": [
+          {
+            "id": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+            "label": "internet",
+            "x": 257,
+            "y": 339,
+            "node_definition": "csr1000v",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "929917c0-ce1a-4c08-a8b9-d260192918f1",
+            "label": "host1",
+            "x": 141,
+            "y": 570,
+            "node_definition": "ubuntu",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "ddff687e-fe77-44f7-93ec-11a2493931a3",
+            "label": "host2",
+            "x": 337,
+            "y": 564,
+            "node_definition": "ubuntu",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "db15e434-e9be-4c42-b998-1b08548daa5b",
+            "label": "vbond1",
+            "x": 190,
+            "y": 50,
+            "node_definition": "viptela-bond",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "9ea578b6-1f9d-47dd-8db8-5d13118cda7d",
+            "label": "vmanage1",
+            "x": 250,
+            "y": 0,
+            "node_definition": "viptela-manage",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "a651ed0b-fbe5-47ad-8c77-47f66764c6eb",
+            "label": "vsmart1",
+            "x": 327,
+            "y": 50,
+            "node_definition": "viptela-smart",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "4fd590a3-a359-41ec-91a5-ac2711138b7c",
+            "label": "vedge-hq",
+            "x": 148,
+            "y": 133,
+            "node_definition": "viptela-edge",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "073f07e9-fbde-4e0a-9f6a-ad39ac5e78b0",
+            "label": "vedge1",
+            "x": 143,
+            "y": 461,
+            "node_definition": "viptela-edge",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "b26badb4-0c3a-4069-aeba-7a9c5e56f300",
+            "label": "vedge2",
+            "x": 337,
+            "y": 458,
+            "node_definition": "viptela-edge",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34",
+            "label": "DMZ",
+            "x": 260,
+            "y": 132,
+            "node_definition": "unmanaged_switch",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "3d92ed02-98e0-41d8-ab9c-63dfbd063a48",
+            "label": "hq",
+            "x": 258,
+            "y": 249,
+            "node_definition": "csr1000v",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "3d3af2eb-9535-4dd4-9adc-cc7d0ae38245",
+            "label": "core",
+            "x": 150,
+            "y": 250,
+            "node_definition": "csr1000v",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "1dda713d-b300-4e6e-90e4-abc41d532e87",
+            "label": "hqdc",
+            "x": 50,
+            "y": 250,
+            "node_definition": "unmanaged_switch",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "6098d180-e37f-4277-a0f8-65fab6d99546",
+            "label": "server1",
+            "x": 50,
+            "y": 131,
+            "node_definition": "ubuntu",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "3d5ccdd5-0837-4f16-bfc4-9ffd13806ac8",
+            "label": "sp1",
+            "x": 552,
+            "y": 340,
+            "node_definition": "csr1000v",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "e4b81866-1132-43b0-bfe0-b2a3f85c16ad",
+            "label": "service1",
+            "x": 805,
+            "y": 338,
+            "node_definition": "ubuntu",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "dc7c06c1-1c91-4b43-b467-64bc11f83223",
+            "label": "spdc",
+            "x": 665,
+            "y": 339,
+            "node_definition": "unmanaged_switch",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "label": "sandbox-backend",
+            "x": 800,
+            "y": 200,
+            "node_definition": "unmanaged_switch",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "a8e87a75-d502-4b34-bd53-7a707ffa2452",
+            "label": "bridge-to-sandbox",
+            "x": 800,
+            "y": 50,
+            "node_definition": "external_connector",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          }
+        ],
+        "links": [
+          {
+            "id": "a14cda09-f07c-4f45-9339-56429b4101ed",
+            "node_a": "3d92ed02-98e0-41d8-ab9c-63dfbd063a48",
+            "node_b": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "15b9c0d0-529f-4e5c-b936-419d635ef33e",
+            "node_a": "3d92ed02-98e0-41d8-ab9c-63dfbd063a48",
+            "node_b": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "550e598d-ac81-46f6-82eb-70845cddb6c4",
+            "node_a": "3d92ed02-98e0-41d8-ab9c-63dfbd063a48",
+            "node_b": "3d3af2eb-9535-4dd4-9adc-cc7d0ae38245",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "eccb95d0-4805-4cbb-869d-c6f0cc3585e4",
+            "node_a": "1dda713d-b300-4e6e-90e4-abc41d532e87",
+            "node_b": "3d3af2eb-9535-4dd4-9adc-cc7d0ae38245",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "baa26cf8-6605-40cd-8cf1-26fe1dddcc4b",
+            "node_a": "6098d180-e37f-4277-a0f8-65fab6d99546",
+            "node_b": "1dda713d-b300-4e6e-90e4-abc41d532e87",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "c19c11c1-6b73-4bb6-99aa-7b528d2ea542",
+            "node_a": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+            "node_b": "3d5ccdd5-0837-4f16-bfc4-9ffd13806ac8",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "e0ffe483-3643-4e45-bdd2-636693c50d10",
+            "node_a": "3d5ccdd5-0837-4f16-bfc4-9ffd13806ac8",
+            "node_b": "dc7c06c1-1c91-4b43-b467-64bc11f83223",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "9dde85ed-2652-4aba-a993-7dce622e6f6b",
+            "node_a": "a651ed0b-fbe5-47ad-8c77-47f66764c6eb",
+            "node_b": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "5ac4717e-6c90-4d5d-a7f2-400df0cc4a10",
+            "node_a": "9ea578b6-1f9d-47dd-8db8-5d13118cda7d",
+            "node_b": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "81c82498-19d0-4f9d-adb2-a9e60804f126",
+            "node_a": "db15e434-e9be-4c42-b998-1b08548daa5b",
+            "node_b": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "40b3e18b-2aa8-4df1-8da0-8ab812959201",
+            "node_a": "4fd590a3-a359-41ec-91a5-ac2711138b7c",
+            "node_b": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "2cde1162-3ad7-4e18-b11d-0d59e86bff25",
+            "node_a": "3d3af2eb-9535-4dd4-9adc-cc7d0ae38245",
+            "node_b": "4fd590a3-a359-41ec-91a5-ac2711138b7c",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "eece855f-ef7c-416f-b663-55853fb3ecde",
+            "node_a": "dc7c06c1-1c91-4b43-b467-64bc11f83223",
+            "node_b": "e4b81866-1132-43b0-bfe0-b2a3f85c16ad",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "57fd4851-ea86-4184-9f89-8673cf9a3a11",
+            "node_a": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+            "node_b": "073f07e9-fbde-4e0a-9f6a-ad39ac5e78b0",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "b98f9abd-b103-463a-8b59-af7593b4c552",
+            "node_a": "073f07e9-fbde-4e0a-9f6a-ad39ac5e78b0",
+            "node_b": "929917c0-ce1a-4c08-a8b9-d260192918f1",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "aadc84d2-8391-432e-a8c8-bdec73620c3b",
+            "node_a": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+            "node_b": "b26badb4-0c3a-4069-aeba-7a9c5e56f300",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "9015e932-11da-49eb-8cad-0199b1ec8705",
+            "node_a": "b26badb4-0c3a-4069-aeba-7a9c5e56f300",
+            "node_b": "ddff687e-fe77-44f7-93ec-11a2493931a3",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "e82ae6d2-2931-4dda-b33d-6172a4fa3e98",
+            "node_a": "a8e87a75-d502-4b34-bd53-7a707ffa2452",
+            "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "3fb2273c-825c-482e-90a7-66f838e8989f",
+            "node_a": "db15e434-e9be-4c42-b998-1b08548daa5b",
+            "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "688fe683-6bd5-421a-879c-1dc0f960160e",
+            "node_a": "9ea578b6-1f9d-47dd-8db8-5d13118cda7d",
+            "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "cc9e0ca0-129d-4c40-86b5-22d8748a2bae",
+            "node_a": "a651ed0b-fbe5-47ad-8c77-47f66764c6eb",
+            "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "ae178622-d545-4a09-9010-e0bcf5feff45",
+            "node_a": "6098d180-e37f-4277-a0f8-65fab6d99546",
+            "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "517f6c20-1b79-49ca-aed3-a800c856a937",
+            "node_a": "4fd590a3-a359-41ec-91a5-ac2711138b7c",
+            "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "2b1c93fc-1062-4c5a-8e44-55d6d9f177d0",
+            "node_a": "3d3af2eb-9535-4dd4-9adc-cc7d0ae38245",
+            "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "5e1e0747-3dc1-4c3a-b76c-33be9539c0c7",
+            "node_a": "3d92ed02-98e0-41d8-ab9c-63dfbd063a48",
+            "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "8d55a4c5-0e55-4392-b7e1-ae4201d93797",
+            "node_a": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+            "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "1d5e0635-e938-4b86-aade-bcee9ca73775",
+            "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "node_b": "3d5ccdd5-0837-4f16-bfc4-9ffd13806ac8",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "54c3d1de-7d59-47e4-9c1b-3be6ad8b7f5f",
+            "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "node_b": "e4b81866-1132-43b0-bfe0-b2a3f85c16ad",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "b037c4b5-32d8-4fd6-9cd7-b822bd7bfe15",
+            "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "node_b": "073f07e9-fbde-4e0a-9f6a-ad39ac5e78b0",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "b44393b8-a725-4717-a473-5a0ec52e0eca",
+            "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "node_b": "929917c0-ce1a-4c08-a8b9-d260192918f1",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "eb6e7349-10e0-4694-952e-369f6648c659",
+            "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "node_b": "b26badb4-0c3a-4069-aeba-7a9c5e56f300",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "9c864f7d-5881-462b-8d0c-1280f08ccda2",
+            "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+            "node_b": "ddff687e-fe77-44f7-93ec-11a2493931a3",
+            "state": "DEFINED_ON_CORE"
+          }
+        ]
+      }
+    },
+    "3031b614-0e76-4450-9fe0-6b3be0bc0bd2": {
+      "state": "DEFINED_ON_CORE",
+      "created": "2022-06-30T17:46:46+00:00",
+      "modified": "2022-06-30T17:46:46+00:00",
+      "lab_title": "cml-viptela-images-testing",
+      "lab_description": "Lab for testing Viptela images.",
+      "lab_notes": "",
+      "owner": "640a1c81-f1e7-4159-8fa2-ad35d9a68586",
+      "owner_username": "jdoe",
+      "node_count": 15,
+      "link_count": 14,
+      "id": "3031b614-0e76-4450-9fe0-6b3be0bc0bd2",
+      "groups": [],
+      "topology": {
+        "nodes": [
+          {
+            "id": "302b1db3-5e14-45e4-9769-4094d912a6a2",
+            "label": "internet",
+            "x": -250,
+            "y": 0,
+            "node_definition": "csr1000v",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "dd48e270-c192-4cad-990e-83e3c0fe11d2",
+            "label": "hq-rtr1",
+            "x": 0,
+            "y": 0,
+            "node_definition": "csr1000v",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "c499b260-1679-4d61-b807-e5db80ffd9d0",
+            "label": "hq-dmz-lan",
+            "x": 250,
+            "y": 0,
+            "node_definition": "unmanaged_switch",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "f1f67f07-85bd-442b-840d-e5e9f93cd7e4",
+            "label": "hq-mgmt-br",
+            "x": 350,
+            "y": -300,
+            "node_definition": "external_connector",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "926f0f14-d1df-4320-8b32-5549e4465e4c",
+            "label": "internet-mgmt",
+            "x": -250,
+            "y": -150,
+            "node_definition": "external_connector",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "2ceda08f-04dd-4c98-9b88-f074427485ca",
+            "label": "OOB-mgmt-lan",
+            "x": 400,
+            "y": -250,
+            "node_definition": "iosvl2",
+            "image_definition": "iosvl2-2020",
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "99e83795-51d8-418a-97b8-a68f1c471598",
+            "label": "site2-vedge1",
+            "x": -100,
+            "y": 150,
+            "node_definition": "viptela-edge",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "4bfeed2c-a2ca-4b73-a974-974d32de93d8",
+            "label": "vsmart1",
+            "x": 400,
+            "y": 150,
+            "node_definition": "viptela-smart",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "54975a72-47a7-4052-980f-0af2bb8907cc",
+            "label": "vbond1",
+            "x": 400,
+            "y": 0,
+            "node_definition": "viptela-bond",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "0bd147bb-bfaf-4698-b8a2-978e975f7290",
+            "label": "site2-host1",
+            "x": -250,
+            "y": 400,
+            "node_definition": "alpine",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "3ea48ff8-ca14-498f-92ff-7c848923c1cc",
+            "label": "site1-vedge1",
+            "x": -500,
+            "y": 200,
+            "node_definition": "viptela-edge",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "c513c0e9-3ea2-4541-a7af-7020fb847b52",
+            "label": "site1-host1",
+            "x": -500,
+            "y": 400,
+            "node_definition": "alpine",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "61ef5f7b-b6bb-49f4-bd34-957e090d0556",
+            "label": "vmanage1",
+            "x": 400,
+            "y": -100,
+            "node_definition": "viptela-manage",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "1028ee8e-36ed-405e-b8b7-d3ecd5b96c69",
+            "label": "site3-vedge1",
+            "x": -50,
+            "y": 200,
+            "node_definition": "viptela-edge",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "f2ee4b01-13e0-4fc8-af86-f65ae1bef9e7",
+            "label": "site3-host1",
+            "x": -50,
+            "y": 400,
+            "node_definition": "alpine",
+            "image_definition": null,
+            "state": "DEFINED_ON_CORE",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          }
+        ],
+        "links": [
+          {
+            "id": "e16aaa90-b966-41a6-bc23-197590293b5b",
+            "node_a": "302b1db3-5e14-45e4-9769-4094d912a6a2",
+            "node_b": "926f0f14-d1df-4320-8b32-5549e4465e4c",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "cfae0bcb-4548-43f9-9a16-37009e869072",
+            "node_a": "99e83795-51d8-418a-97b8-a68f1c471598",
+            "node_b": "302b1db3-5e14-45e4-9769-4094d912a6a2",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "51f73528-bf37-49c8-a8e8-1ebec9a35fac",
+            "node_a": "3ea48ff8-ca14-498f-92ff-7c848923c1cc",
+            "node_b": "302b1db3-5e14-45e4-9769-4094d912a6a2",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "05c218f7-6e86-4e2d-9250-ad7d76d88c9f",
+            "node_a": "1028ee8e-36ed-405e-b8b7-d3ecd5b96c69",
+            "node_b": "302b1db3-5e14-45e4-9769-4094d912a6a2",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "b71fdcf0-6100-443e-9891-5343c7b03a27",
+            "node_a": "c513c0e9-3ea2-4541-a7af-7020fb847b52",
+            "node_b": "3ea48ff8-ca14-498f-92ff-7c848923c1cc",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "c8b07ed2-84ce-4655-9ac8-fa4806c3956b",
+            "node_a": "0bd147bb-bfaf-4698-b8a2-978e975f7290",
+            "node_b": "99e83795-51d8-418a-97b8-a68f1c471598",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "a0c48e21-3ae1-48f2-80a8-4d947b996a92",
+            "node_a": "f2ee4b01-13e0-4fc8-af86-f65ae1bef9e7",
+            "node_b": "1028ee8e-36ed-405e-b8b7-d3ecd5b96c69",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "a1094d59-9c4d-4a42-a6b5-99f0cbc23068",
+            "node_a": "302b1db3-5e14-45e4-9769-4094d912a6a2",
+            "node_b": "dd48e270-c192-4cad-990e-83e3c0fe11d2",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "72e8f1ee-2799-4cdd-b78e-02051edf2a3b",
+            "node_a": "dd48e270-c192-4cad-990e-83e3c0fe11d2",
+            "node_b": "c499b260-1679-4d61-b807-e5db80ffd9d0",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "00ab556f-39cc-4834-ae2f-fe2c176a53e0",
+            "node_a": "c499b260-1679-4d61-b807-e5db80ffd9d0",
+            "node_b": "4bfeed2c-a2ca-4b73-a974-974d32de93d8",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "bf3c7952-d3a8-444e-b748-63a35c0e9a03",
+            "node_a": "c499b260-1679-4d61-b807-e5db80ffd9d0",
+            "node_b": "54975a72-47a7-4052-980f-0af2bb8907cc",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "e24c5f0b-d2ff-4072-8557-44aaf5dd0ce7",
+            "node_a": "c499b260-1679-4d61-b807-e5db80ffd9d0",
+            "node_b": "61ef5f7b-b6bb-49f4-bd34-957e090d0556",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "5eb94dd6-b57e-42c0-9879-000f9ecc8f58",
+            "node_a": "61ef5f7b-b6bb-49f4-bd34-957e090d0556",
+            "node_b": "2ceda08f-04dd-4c98-9b88-f074427485ca",
+            "state": "DEFINED_ON_CORE"
+          },
+          {
+            "id": "ee2d8e09-a3c3-45e6-b796-79b08f7bfdab",
+            "node_a": "2ceda08f-04dd-4c98-9b88-f074427485ca",
+            "node_b": "f1f67f07-85bd-442b-840d-e5e9f93cd7e4",
+            "state": "DEFINED_ON_CORE"
+          }
+        ]
+      }
+    },
+    "863799a0-3d09-4af4-be26-cad997b6ab27": {
+      "state": "STARTED",
+      "created": "2022-08-05T04:55:23+00:00",
+      "modified": "2022-08-05T04:55:23+00:00",
+      "lab_title": "CSR 1000v Feature Tests",
+      "lab_description": "A sample lab for testing CSR 1000v features",
+      "lab_notes": "# CSR 1000v Feature Testing\n\n## Features\n\n- CDP\n- ping\n- OSPF single-area\n- NAT - static\n- HSRP\n- DHCP\n- Routed subinterface\n- Restconf\n- Netconf\n\n## Test Details\n\nFor more details on test procedure, see the [CSR 1000v](https://developer.cisco.com/docs/modeling-labs/#!csr-1000v) page in the [CML documentation](https://developer.cisco.com/docs/modeling-labs/).",
+      "owner": "640a1c81-f1e7-4159-8fa2-ad35d9a68586",
+      "owner_username": "jdoe",
+      "node_count": 6,
+      "link_count": 7,
+      "id": "863799a0-3d09-4af4-be26-cad997b6ab27",
+      "groups": [],
+      "topology": {
+        "nodes": [
+          {
+            "id": "e79a0eb7-5c10-4538-8776-b832df78ba8b",
+            "label": "csr1000v-2",
+            "x": -600,
+            "y": -50,
+            "node_definition": "csr1000v",
+            "image_definition": "",
+            "state": "BOOTED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "f4220194-d8b4-42ed-9967-226e1ed92ca0",
+            "label": "alpine-0",
+            "x": -600,
+            "y": -350,
+            "node_definition": "alpine",
+            "image_definition": "",
+            "state": "BOOTED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "6d583887-a109-4df2-b7fe-45c27379a49d",
+            "label": "iosv-0",
+            "x": -800,
+            "y": -250,
+            "node_definition": "iosv",
+            "image_definition": "",
+            "state": "BOOTED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "90e9329f-7d20-4eb2-b396-06d0aa497ce9",
+            "label": "csr1000v-0",
+            "x": -600,
+            "y": -250,
+            "node_definition": "csr1000v",
+            "image_definition": "",
+            "state": "BOOTED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "d9a0bb6a-dbdf-45e5-9222-18dc5fb4adf0",
+            "label": "iosvl2-0",
+            "x": -500,
+            "y": -150,
+            "node_definition": "iosvl2",
+            "image_definition": "",
+            "state": "BOOTED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          },
+          {
+            "id": "9c840733-ac3f-4195-8839-b2c0379bc719",
+            "label": "ubuntu-0",
+            "x": -350,
+            "y": -150,
+            "node_definition": "ubuntu",
+            "image_definition": "",
+            "state": "BOOTED",
+            "cpus": null,
+            "cpu_limit": null,
+            "ram": null,
+            "data_volume": null,
+            "boot_disk_size": null
+          }
+        ],
+        "links": [
+          {
+            "id": "e7869898-9a4a-40af-813b-f4fbe10f6546",
+            "node_a": "6d583887-a109-4df2-b7fe-45c27379a49d",
+            "node_b": "e79a0eb7-5c10-4538-8776-b832df78ba8b",
+            "state": "STARTED"
+          },
+          {
+            "id": "01fff06c-62de-4421-b335-499316576075",
+            "node_a": "e79a0eb7-5c10-4538-8776-b832df78ba8b",
+            "node_b": "d9a0bb6a-dbdf-45e5-9222-18dc5fb4adf0",
+            "state": "STARTED"
+          },
+          {
+            "id": "793acf56-3c20-43bb-a295-e1372491c722",
+            "node_a": "90e9329f-7d20-4eb2-b396-06d0aa497ce9",
+            "node_b": "d9a0bb6a-dbdf-45e5-9222-18dc5fb4adf0",
+            "state": "STARTED"
+          },
+          {
+            "id": "ffdc378e-3f24-425f-8435-9cfa30b5ccd5",
+            "node_a": "e79a0eb7-5c10-4538-8776-b832df78ba8b",
+            "node_b": "90e9329f-7d20-4eb2-b396-06d0aa497ce9",
+            "state": "STARTED"
+          },
+          {
+            "id": "02ab4e89-051c-4af3-98b1-e26f708b54ea",
+            "node_a": "6d583887-a109-4df2-b7fe-45c27379a49d",
+            "node_b": "90e9329f-7d20-4eb2-b396-06d0aa497ce9",
+            "state": "STARTED"
+          },
+          {
+            "id": "bbfac097-5b23-461b-9f7d-77c7738b8e72",
+            "node_a": "9c840733-ac3f-4195-8839-b2c0379bc719",
+            "node_b": "d9a0bb6a-dbdf-45e5-9222-18dc5fb4adf0",
+            "state": "STARTED"
+          },
+          {
+            "id": "ce004107-dcc6-484e-8afc-4df7d6c28651",
+            "node_a": "f4220194-d8b4-42ed-9967-226e1ed92ca0",
+            "node_b": "90e9329f-7d20-4eb2-b396-06d0aa497ce9",
+            "state": "STARTED"
+          }
+        ]
+      }
+    }
+  }
+}
diff --git a/tests/test_data/simulation_stats-444a78d1-575c-4746-8469-696e580f17b6.json b/tests/test_data/simulation_stats-444a78d1-575c-4746-8469-696e580f17b6.json
new file mode 100644
index 0000000..855d1ee
--- /dev/null
+++ b/tests/test_data/simulation_stats-444a78d1-575c-4746-8469-696e580f17b6.json
@@ -0,0 +1,54 @@
+{
+  "links": {
+    "6b92a334-4838-4f1c-b3f3-45c4baf00268": {
+      "readbytes": 19141,
+      "readpackets": 213,
+      "writebytes": 56261605,
+      "writepackets": 809613,
+      "drops": 0
+    },
+    "8f8980be-a12c-4d42-a319-08169a0cbef7": {
+      "readbytes": 12348085,
+      "readpackets": 121260,
+      "writebytes": 0,
+      "writepackets": 0,
+      "drops": 0
+    },
+    "4c029bda-8f54-42d5-908e-1638b736f957": {
+      "readbytes": 12651784,
+      "readpackets": 121852,
+      "writebytes": 12648331,
+      "writepackets": 121828,
+      "drops": 0
+    },
+    "74df9890-4df6-4e61-8aa7-9459d6381a85": {
+      "readbytes": 27189087,
+      "readpackets": 339406,
+      "writebytes": 75103635,
+      "writepackets": 1091449,
+      "drops": 0
+    },
+    "4244119a-d0d0-486a-aec7-48b587e0b7dd": {
+      "readbytes": 7187841,
+      "readpackets": 63761,
+      "writebytes": 26257806,
+      "writepackets": 76901,
+      "drops": 0
+    },
+    "fbfb78fa-d62d-448f-a5e9-1dacaa1504ad": {
+      "readbytes": 25981658,
+      "readpackets": 320370,
+      "writebytes": 76296651,
+      "writepackets": 1110403,
+      "drops": 0
+    },
+    "0321b86a-50e6-4c6a-8151-60acfdd07775": {
+      "readbytes": 12348758,
+      "readpackets": 121273,
+      "writebytes": 0,
+      "writepackets": 0,
+      "drops": 0
+    }
+  },
+  "nodes": {}
+}
diff --git a/tests/test_data/simulation_stats-863799a0-3d09-4af4-be26-cad997b6ab27.json b/tests/test_data/simulation_stats-863799a0-3d09-4af4-be26-cad997b6ab27.json
new file mode 100644
index 0000000..2049a3b
--- /dev/null
+++ b/tests/test_data/simulation_stats-863799a0-3d09-4af4-be26-cad997b6ab27.json
@@ -0,0 +1,115 @@
+{
+  "links": {
+    "e7869898-9a4a-40af-813b-f4fbe10f6546": {
+      "readbytes": 45142004,
+      "readpackets": 443346,
+      "writebytes": 0,
+      "writepackets": 0,
+      "drops": 0
+    },
+    "01fff06c-62de-4421-b335-499316576075": {
+      "readbytes": 0,
+      "readpackets": 0,
+      "writebytes": 209428658,
+      "writepackets": 3064111,
+      "drops": 0
+    },
+    "793acf56-3c20-43bb-a295-e1372491c722": {
+      "readbytes": 0,
+      "readpackets": 0,
+      "writebytes": 209424350,
+      "writepackets": 3064099,
+      "drops": 0
+    },
+    "02ab4e89-051c-4af3-98b1-e26f708b54ea": {
+      "readbytes": 45145043,
+      "readpackets": 443350,
+      "writebytes": 0,
+      "writepackets": 0,
+      "drops": 0
+    },
+    "bbfac097-5b23-461b-9f7d-77c7738b8e72": {
+      "readbytes": 10236702,
+      "readpackets": 31273,
+      "writebytes": 65351085,
+      "writepackets": 1000905,
+      "drops": 0
+    },
+    "ffdc378e-3f24-425f-8435-9cfa30b5ccd5": {
+      "readbytes": 0,
+      "readpackets": 0,
+      "writebytes": 0,
+      "writepackets": 0,
+      "drops": 0
+    },
+    "ce004107-dcc6-484e-8afc-4df7d6c28651": {
+      "readbytes": 96002774,
+      "readpackets": 281129,
+      "writebytes": 0,
+      "writepackets": 0,
+      "drops": 0
+    }
+  },
+  "nodes": {
+    "6d583887-a109-4df2-b7fe-45c27379a49d": {
+      "cpu_usage": 1.64,
+      "block0_wr_bytes": 1617408,
+      "block0_rd_bytes": 306721280,
+      "times": {
+        "QUEUED": 25,
+        "STARTED": 1966177,
+        "BOOTED": 1966003
+      }
+    },
+    "d9a0bb6a-dbdf-45e5-9222-18dc5fb4adf0": {
+      "cpu_usage": 2.71,
+      "block0_wr_bytes": 1185280,
+      "block0_rd_bytes": 491976192,
+      "times": {
+        "QUEUED": 52,
+        "STARTED": 1966150,
+        "BOOTED": 1965857
+      }
+    },
+    "9c840733-ac3f-4195-8839-b2c0379bc719": {
+      "cpu_usage": 0.15,
+      "block0_wr_bytes": 42074950144,
+      "block0_rd_bytes": 317299712,
+      "times": {
+        "QUEUED": 39,
+        "STARTED": 1966164,
+        "BOOTED": 1965991
+      }
+    },
+    "e79a0eb7-5c10-4538-8776-b832df78ba8b": {
+      "cpu_usage": 100.04,
+      "block0_wr_bytes": 178096128,
+      "block0_rd_bytes": 781876736,
+      "times": {
+        "QUEUED": 22,
+        "STARTED": 1966181,
+        "BOOTED": 1966031
+      }
+    },
+    "90e9329f-7d20-4eb2-b396-06d0aa497ce9": {
+      "cpu_usage": 100.18,
+      "block0_wr_bytes": 178636800,
+      "block0_rd_bytes": 812588544,
+      "times": {
+        "QUEUED": 49,
+        "STARTED": 1966154,
+        "BOOTED": 1966002
+      }
+    },
+    "f4220194-d8b4-42ed-9967-226e1ed92ca0": {
+      "cpu_usage": 2.02,
+      "block0_wr_bytes": 275878912,
+      "block0_rd_bytes": 46081024,
+      "times": {
+        "QUEUED": 35,
+        "STARTED": 1966167,
+        "BOOTED": 1966145
+      }
+    }
+  }
+}
diff --git a/tests/test_data/topology-3031b614-0e76-4450-9fe0-6b3be0bc0bd2.json b/tests/test_data/topology-3031b614-0e76-4450-9fe0-6b3be0bc0bd2.json
new file mode 100644
index 0000000..5d46ceb
--- /dev/null
+++ b/tests/test_data/topology-3031b614-0e76-4450-9fe0-6b3be0bc0bd2.json
@@ -0,0 +1,618 @@
+{
+  "lab": {
+    "description": "Lab for testing Viptela images.",
+    "notes": "",
+    "owner": "640a1c81-f1e7-4159-8fa2-ad35d9a68586",
+    "title": "cml-viptela-images-testing",
+    "version": "0.1.0"
+  },
+  "links": [
+    {
+      "id": "e16aaa90-b966-41a6-bc23-197590293b5b",
+      "interface_a": "9be9eb3f-c6eb-4106-9948-8328368241f2",
+      "interface_b": "62e25ca9-fab1-4ce8-bc68-0b97df65b6d3",
+      "label": "internet-GigabitEthernet1<->internet-mgmt-port",
+      "node_a": "302b1db3-5e14-45e4-9769-4094d912a6a2",
+      "node_b": "926f0f14-d1df-4320-8b32-5549e4465e4c"
+    },
+    {
+      "id": "cfae0bcb-4548-43f9-9a16-37009e869072",
+      "interface_a": "302e5e8f-94c9-4a4f-b873-bb1ad6d4d04f",
+      "interface_b": "37df3621-c790-4771-9b2d-b504e0c85aaa",
+      "label": "site2-vedge1-ge0/0<->internet-GigabitEthernet4",
+      "node_a": "99e83795-51d8-418a-97b8-a68f1c471598",
+      "node_b": "302b1db3-5e14-45e4-9769-4094d912a6a2"
+    },
+    {
+      "id": "51f73528-bf37-49c8-a8e8-1ebec9a35fac",
+      "interface_a": "9c6fd8d7-e89d-4f23-80ee-947567f75470",
+      "interface_b": "46151d0e-5943-4f0b-8317-eba4730aad13",
+      "label": "site1-vedge1-ge0/0<->internet-GigabitEthernet3",
+      "node_a": "3ea48ff8-ca14-498f-92ff-7c848923c1cc",
+      "node_b": "302b1db3-5e14-45e4-9769-4094d912a6a2"
+    },
+    {
+      "id": "05c218f7-6e86-4e2d-9250-ad7d76d88c9f",
+      "interface_a": "e961040f-f403-4af6-a15a-19c8c5ff082d",
+      "interface_b": "5f252735-9118-4f6b-b379-86c3de1b4611",
+      "label": "site3-vedge1-ge0/0<->internet-GigabitEthernet5",
+      "node_a": "1028ee8e-36ed-405e-b8b7-d3ecd5b96c69",
+      "node_b": "302b1db3-5e14-45e4-9769-4094d912a6a2"
+    },
+    {
+      "id": "b71fdcf0-6100-443e-9891-5343c7b03a27",
+      "interface_a": "9b7e51ae-bf14-44ce-91a9-3d95c8b0f939",
+      "interface_b": "899787a1-70a3-4e90-a36e-0e0df82e2721",
+      "label": "site1-host1-eth0<->site1-vedge1-ge0/1",
+      "node_a": "c513c0e9-3ea2-4541-a7af-7020fb847b52",
+      "node_b": "3ea48ff8-ca14-498f-92ff-7c848923c1cc"
+    },
+    {
+      "id": "c8b07ed2-84ce-4655-9ac8-fa4806c3956b",
+      "interface_a": "88dc9a84-d7d6-4635-b336-ea659608c34c",
+      "interface_b": "1c5437cf-c692-4fd3-a434-98ac92d3b6fa",
+      "label": "site2-host1-eth0<->site2-vedge1-ge0/1",
+      "node_a": "0bd147bb-bfaf-4698-b8a2-978e975f7290",
+      "node_b": "99e83795-51d8-418a-97b8-a68f1c471598"
+    },
+    {
+      "id": "a0c48e21-3ae1-48f2-80a8-4d947b996a92",
+      "interface_a": "570bdabe-c99e-4423-b94a-95420727f652",
+      "interface_b": "49d9ed96-309f-4d38-b131-634edfe8d5b8",
+      "label": "site3-host1-eth0<->site3-vedge1-ge0/1",
+      "node_a": "f2ee4b01-13e0-4fc8-af86-f65ae1bef9e7",
+      "node_b": "1028ee8e-36ed-405e-b8b7-d3ecd5b96c69"
+    },
+    {
+      "id": "a1094d59-9c4d-4a42-a6b5-99f0cbc23068",
+      "interface_a": "cd12202f-2fca-4363-8ae5-539751094324",
+      "interface_b": "9f784ee7-5468-4ccb-bb3e-5b31357b0417",
+      "label": "internet-GigabitEthernet2<->hq-rtr1-GigabitEthernet2",
+      "node_a": "302b1db3-5e14-45e4-9769-4094d912a6a2",
+      "node_b": "dd48e270-c192-4cad-990e-83e3c0fe11d2"
+    },
+    {
+      "id": "72e8f1ee-2799-4cdd-b78e-02051edf2a3b",
+      "interface_a": "94d2865b-66f7-4ecd-b786-897f571e7f57",
+      "interface_b": "41a63781-7b47-4670-9e51-c940d64f04d2",
+      "label": "hq-rtr1-GigabitEthernet3<->hq-dmz-lan-port0",
+      "node_a": "dd48e270-c192-4cad-990e-83e3c0fe11d2",
+      "node_b": "c499b260-1679-4d61-b807-e5db80ffd9d0"
+    },
+    {
+      "id": "00ab556f-39cc-4834-ae2f-fe2c176a53e0",
+      "interface_a": "30892501-56db-42e3-89b6-98ecf0b7e84c",
+      "interface_b": "c635d9b9-42f7-4c10-85dc-b9a6e477862a",
+      "label": "hq-dmz-lan-port3<->vsmart1-eth1",
+      "node_a": "c499b260-1679-4d61-b807-e5db80ffd9d0",
+      "node_b": "4bfeed2c-a2ca-4b73-a974-974d32de93d8"
+    },
+    {
+      "id": "bf3c7952-d3a8-444e-b748-63a35c0e9a03",
+      "interface_a": "24fd017b-4a37-42a0-adeb-1d67c481254f",
+      "interface_b": "926f250b-6f98-46bd-a467-11e08dcf85f8",
+      "label": "hq-dmz-lan-port2<->vbond1-ge0/0",
+      "node_a": "c499b260-1679-4d61-b807-e5db80ffd9d0",
+      "node_b": "54975a72-47a7-4052-980f-0af2bb8907cc"
+    },
+    {
+      "id": "e24c5f0b-d2ff-4072-8557-44aaf5dd0ce7",
+      "interface_a": "3b35d1cc-863e-4ec7-989e-32ae6a4853d0",
+      "interface_b": "4cc2fb90-b7db-43ca-b528-6229a3864a46",
+      "label": "hq-dmz-lan-port1<->vmanage1-eth1",
+      "node_a": "c499b260-1679-4d61-b807-e5db80ffd9d0",
+      "node_b": "61ef5f7b-b6bb-49f4-bd34-957e090d0556"
+    },
+    {
+      "id": "5eb94dd6-b57e-42c0-9879-000f9ecc8f58",
+      "interface_a": "90c84457-d55e-4387-b660-5a3d1cc0ed85",
+      "interface_b": "b3528aa6-298a-4ef6-b1ba-e79ecaff93ea",
+      "label": "vmanage1-eth0<->OOB-mgmt-lan-GigabitEthernet0/1",
+      "node_a": "61ef5f7b-b6bb-49f4-bd34-957e090d0556",
+      "node_b": "2ceda08f-04dd-4c98-9b88-f074427485ca"
+    },
+    {
+      "id": "ee2d8e09-a3c3-45e6-b796-79b08f7bfdab",
+      "interface_a": "ed0e996e-e1de-41ec-ae8a-f3a87e346dde",
+      "interface_b": "d4bae841-bf5f-438c-bc14-4c6a16aa1136",
+      "label": "OOB-mgmt-lan-GigabitEthernet0/0<->hq-mgmt-br-port",
+      "node_a": "2ceda08f-04dd-4c98-9b88-f074427485ca",
+      "node_b": "f1f67f07-85bd-442b-840d-e5e9f93cd7e4"
+    }
+  ],
+  "nodes": [
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "302b1db3-5e14-45e4-9769-4094d912a6a2",
+      "label": "internet",
+      "node_definition": "csr1000v",
+      "ram": 0,
+      "tags": [],
+      "x": -250,
+      "y": 0,
+      "interfaces": [
+        {
+          "id": "2fd2b74e-2290-4088-818e-aafcc176550b",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "9be9eb3f-c6eb-4106-9948-8328368241f2",
+          "label": "GigabitEthernet1",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "cd12202f-2fca-4363-8ae5-539751094324",
+          "label": "GigabitEthernet2",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "46151d0e-5943-4f0b-8317-eba4730aad13",
+          "label": "GigabitEthernet3",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "37df3621-c790-4771-9b2d-b504e0c85aaa",
+          "label": "GigabitEthernet4",
+          "slot": 3,
+          "type": "physical"
+        },
+        {
+          "id": "5f252735-9118-4f6b-b379-86c3de1b4611",
+          "label": "GigabitEthernet5",
+          "slot": 4,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "dd48e270-c192-4cad-990e-83e3c0fe11d2",
+      "label": "hq-rtr1",
+      "node_definition": "csr1000v",
+      "ram": 0,
+      "tags": [],
+      "x": 0,
+      "y": 0,
+      "interfaces": [
+        {
+          "id": "73b89a49-6b8e-4522-b93e-1bea83bb4360",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "bf9543a4-b223-41a8-b3f9-6ed25af346cd",
+          "label": "GigabitEthernet1",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "9f784ee7-5468-4ccb-bb3e-5b31357b0417",
+          "label": "GigabitEthernet2",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "94d2865b-66f7-4ecd-b786-897f571e7f57",
+          "label": "GigabitEthernet3",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "e1a7a800-05ac-4f89-b6bb-143543b19af7",
+          "label": "GigabitEthernet4",
+          "slot": 3,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "c499b260-1679-4d61-b807-e5db80ffd9d0",
+      "label": "hq-dmz-lan",
+      "node_definition": "unmanaged_switch",
+      "ram": 0,
+      "tags": [],
+      "x": 250,
+      "y": 0,
+      "interfaces": [
+        {
+          "id": "41a63781-7b47-4670-9e51-c940d64f04d2",
+          "label": "port0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "3b35d1cc-863e-4ec7-989e-32ae6a4853d0",
+          "label": "port1",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "24fd017b-4a37-42a0-adeb-1d67c481254f",
+          "label": "port2",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "30892501-56db-42e3-89b6-98ecf0b7e84c",
+          "label": "port3",
+          "slot": 3,
+          "type": "physical"
+        },
+        {
+          "id": "a311633a-6967-47c5-8d89-028a64b808cd",
+          "label": "port4",
+          "slot": 4,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "f1f67f07-85bd-442b-840d-e5e9f93cd7e4",
+      "label": "hq-mgmt-br",
+      "node_definition": "external_connector",
+      "ram": 0,
+      "tags": [],
+      "x": 350,
+      "y": -300,
+      "interfaces": [
+        {
+          "id": "d4bae841-bf5f-438c-bc14-4c6a16aa1136",
+          "label": "port",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "926f0f14-d1df-4320-8b32-5549e4465e4c",
+      "label": "internet-mgmt",
+      "node_definition": "external_connector",
+      "ram": 0,
+      "tags": [],
+      "x": -250,
+      "y": -150,
+      "interfaces": [
+        {
+          "id": "62e25ca9-fab1-4ce8-bc68-0b97df65b6d3",
+          "label": "port",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "2ceda08f-04dd-4c98-9b88-f074427485ca",
+      "image_definition": "iosvl2-2020",
+      "label": "OOB-mgmt-lan",
+      "node_definition": "iosvl2",
+      "ram": 0,
+      "tags": [],
+      "x": 400,
+      "y": -250,
+      "interfaces": [
+        {
+          "id": "353d6de3-ee2a-4b12-ac24-0ede77b5fe88",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "ed0e996e-e1de-41ec-ae8a-f3a87e346dde",
+          "label": "GigabitEthernet0/0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "b3528aa6-298a-4ef6-b1ba-e79ecaff93ea",
+          "label": "GigabitEthernet0/1",
+          "slot": 1,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "99e83795-51d8-418a-97b8-a68f1c471598",
+      "label": "site2-vedge1",
+      "node_definition": "viptela-edge",
+      "ram": 0,
+      "tags": [],
+      "x": -100,
+      "y": 150,
+      "interfaces": [
+        {
+          "id": "60f55ef9-31c2-4804-a05f-105b27337b81",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "302e5e8f-94c9-4a4f-b873-bb1ad6d4d04f",
+          "label": "ge0/0",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "1c5437cf-c692-4fd3-a434-98ac92d3b6fa",
+          "label": "ge0/1",
+          "slot": 2,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "4bfeed2c-a2ca-4b73-a974-974d32de93d8",
+      "label": "vsmart1",
+      "node_definition": "viptela-smart",
+      "ram": 0,
+      "tags": [],
+      "x": 400,
+      "y": 150,
+      "interfaces": [
+        {
+          "id": "5ea82cd9-809f-46de-85ea-a35d7f55176a",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "c635d9b9-42f7-4c10-85dc-b9a6e477862a",
+          "label": "eth1",
+          "slot": 1,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "54975a72-47a7-4052-980f-0af2bb8907cc",
+      "label": "vbond1",
+      "node_definition": "viptela-bond",
+      "ram": 0,
+      "tags": [],
+      "x": 400,
+      "y": 0,
+      "interfaces": [
+        {
+          "id": "0809f09d-3bc9-46c3-a148-dcc7e77af244",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "926f250b-6f98-46bd-a467-11e08dcf85f8",
+          "label": "ge0/0",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "bdd58b39-dd5e-49df-8818-d4f005742b01",
+          "label": "ge0/1",
+          "slot": 2,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "0bd147bb-bfaf-4698-b8a2-978e975f7290",
+      "label": "site2-host1",
+      "node_definition": "alpine",
+      "ram": 0,
+      "tags": [],
+      "x": -250,
+      "y": 400,
+      "interfaces": [
+        {
+          "id": "88dc9a84-d7d6-4635-b336-ea659608c34c",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "3ea48ff8-ca14-498f-92ff-7c848923c1cc",
+      "label": "site1-vedge1",
+      "node_definition": "viptela-edge",
+      "ram": 0,
+      "tags": [],
+      "x": -500,
+      "y": 200,
+      "interfaces": [
+        {
+          "id": "96f3937c-1b4c-4070-9d9e-6d048254a890",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "9c6fd8d7-e89d-4f23-80ee-947567f75470",
+          "label": "ge0/0",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "899787a1-70a3-4e90-a36e-0e0df82e2721",
+          "label": "ge0/1",
+          "slot": 2,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "c513c0e9-3ea2-4541-a7af-7020fb847b52",
+      "label": "site1-host1",
+      "node_definition": "alpine",
+      "ram": 0,
+      "tags": [],
+      "x": -500,
+      "y": 400,
+      "interfaces": [
+        {
+          "id": "9b7e51ae-bf14-44ce-91a9-3d95c8b0f939",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "61ef5f7b-b6bb-49f4-bd34-957e090d0556",
+      "label": "vmanage1",
+      "node_definition": "viptela-manage",
+      "ram": 0,
+      "tags": [],
+      "x": 400,
+      "y": -100,
+      "interfaces": [
+        {
+          "id": "90c84457-d55e-4387-b660-5a3d1cc0ed85",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "4cc2fb90-b7db-43ca-b528-6229a3864a46",
+          "label": "eth1",
+          "slot": 1,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "1028ee8e-36ed-405e-b8b7-d3ecd5b96c69",
+      "label": "site3-vedge1",
+      "node_definition": "viptela-edge",
+      "ram": 0,
+      "tags": [],
+      "x": -50,
+      "y": 200,
+      "interfaces": [
+        {
+          "id": "50e23465-07f2-451b-a164-a84f1c2ce936",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "e961040f-f403-4af6-a15a-19c8c5ff082d",
+          "label": "ge0/0",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "49d9ed96-309f-4d38-b131-634edfe8d5b8",
+          "label": "ge0/1",
+          "slot": 2,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "f2ee4b01-13e0-4fc8-af86-f65ae1bef9e7",
+      "label": "site3-host1",
+      "node_definition": "alpine",
+      "ram": 0,
+      "tags": [],
+      "x": -50,
+      "y": 400,
+      "interfaces": [
+        {
+          "id": "570bdabe-c99e-4423-b94a-95420727f652",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/test_data/topology-444a78d1-575c-4746-8469-696e580f17b6.json b/tests/test_data/topology-444a78d1-575c-4746-8469-696e580f17b6.json
new file mode 100644
index 0000000..285ebf1
--- /dev/null
+++ b/tests/test_data/topology-444a78d1-575c-4746-8469-696e580f17b6.json
@@ -0,0 +1,342 @@
+{
+  "lab": {
+    "description": "A sample lab for testing IOSv features",
+    "notes": "# IOSv Feature Testing\n\n## Features\n\n- CDP\n- ping\n- OSPF single-area\n- NAT - static\n- HSRP\n- DHCP\n- Routed subinterface\n\n## Test Details\n\nFor more details on test procedure, see the [IOSv](https://developer.cisco.com/docs/modeling-labs/#!iosv) page in the [CML documentation](https://developer.cisco.com/docs/modeling-labs/).",
+    "owner": "640a1c81-f1e7-4159-8fa2-ad35d9a68586",
+    "title": "IOSv Feature Tests",
+    "version": "0.1.0"
+  },
+  "links": [
+    {
+      "id": "8f8980be-a12c-4d42-a319-08169a0cbef7",
+      "interface_a": "29d32387-ddc6-4a1f-9166-a0a4837f1518",
+      "interface_b": "0ddd4fc2-371b-4535-a6f3-bae00382a615",
+      "label": "iosv-0-GigabitEthernet0/1<->csr1000v-0-GigabitEthernet1",
+      "node_a": "99cda47a-ecb2-4d31-86c4-74e7a8201958",
+      "node_b": "0f9565f7-4fa3-4312-8dda-1db183a55950"
+    },
+    {
+      "id": "4c029bda-8f54-42d5-908e-1638b736f957",
+      "interface_a": "f679b1bd-45d5-42d5-aafb-830640ce06b1",
+      "interface_b": "7500827f-8195-4c40-af52-828a3d997ba0",
+      "label": "iosv-1-GigabitEthernet0/2<->iosv-0-GigabitEthernet0/2",
+      "node_a": "56c875d9-4f2a-4688-9fba-660716cff4cb",
+      "node_b": "99cda47a-ecb2-4d31-86c4-74e7a8201958"
+    },
+    {
+      "id": "74df9890-4df6-4e61-8aa7-9459d6381a85",
+      "interface_a": "f458ee87-e94b-4bf7-98c7-68b6db29d287",
+      "interface_b": "de2034d7-0952-4eea-9209-a20f49b56439",
+      "label": "iosv-0-GigabitEthernet0/3<->iosvl2-0-GigabitEthernet0/1",
+      "node_a": "99cda47a-ecb2-4d31-86c4-74e7a8201958",
+      "node_b": "913e62a7-e096-4ed9-bb9f-03ae13106fc5"
+    },
+    {
+      "id": "fbfb78fa-d62d-448f-a5e9-1dacaa1504ad",
+      "interface_a": "22b679b9-6e3b-4163-8a30-608d749489da",
+      "interface_b": "afbea143-f46e-4b7f-a268-66e69f09c372",
+      "label": "iosv-1-GigabitEthernet0/3<->iosvl2-0-GigabitEthernet0/2",
+      "node_a": "56c875d9-4f2a-4688-9fba-660716cff4cb",
+      "node_b": "913e62a7-e096-4ed9-bb9f-03ae13106fc5"
+    },
+    {
+      "id": "0321b86a-50e6-4c6a-8151-60acfdd07775",
+      "interface_a": "ee6b4482-2a38-4222-87bf-18bbfcb14661",
+      "interface_b": "a2b3ad88-41cb-433d-b3e7-74a4a935c7de",
+      "label": "iosv-1-GigabitEthernet0/1<->csr1000v-0-GigabitEthernet2",
+      "node_a": "56c875d9-4f2a-4688-9fba-660716cff4cb",
+      "node_b": "0f9565f7-4fa3-4312-8dda-1db183a55950"
+    },
+    {
+      "id": "4244119a-d0d0-486a-aec7-48b587e0b7dd",
+      "interface_a": "0ba7c156-a84e-465f-921b-ddde786cf55b",
+      "interface_b": "2d0aab8f-a240-4d59-a7f9-d83605dd8187",
+      "label": "iosv-0-GigabitEthernet0/0<->alpine-0-eth0",
+      "node_a": "99cda47a-ecb2-4d31-86c4-74e7a8201958",
+      "node_b": "aa51eca6-ae81-40fc-a713-e1a168280d21"
+    },
+    {
+      "id": "6b92a334-4838-4f1c-b3f3-45c4baf00268",
+      "interface_a": "179cc26c-4f54-40ee-89b0-529fd87535b7",
+      "interface_b": "9fc01a60-8dfd-48fd-b791-32bf4868d1e9",
+      "label": "alpine-1-eth0<->iosvl2-0-GigabitEthernet0/0",
+      "node_a": "e5222bd8-52ff-4e1d-b6c9-89241132fb13",
+      "node_b": "913e62a7-e096-4ed9-bb9f-03ae13106fc5"
+    },
+    {
+      "id": "d344bab0-c445-49d7-a1ab-a9132a4bcf4d",
+      "interface_a": "ff9b0126-44ae-4c74-b161-4adff1690b10",
+      "interface_b": "a1877e07-4d6c-480a-9386-f09f1087e700",
+      "label": "iosvl2-0-GigabitEthernet0/3<->ext-conn-0-port",
+      "node_a": "913e62a7-e096-4ed9-bb9f-03ae13106fc5",
+      "node_b": "004c00c9-2606-485c-8ff9-d698e430fa6a"
+    }
+  ],
+  "nodes": [
+    {
+      "boot_disk_size": 0,
+      "compute_id": "12f1f885-05ab-4820-ae45-6b85da3fcfee",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "99cda47a-ecb2-4d31-86c4-74e7a8201958",
+      "image_definition": "",
+      "label": "iosv-0",
+      "node_definition": "iosv",
+      "ram": 512,
+      "tags": [],
+      "x": -400,
+      "y": 0,
+      "interfaces": [
+        {
+          "id": "6abb45c3-6ed4-4ca1-9271-da7cb34abdf1",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "0ba7c156-a84e-465f-921b-ddde786cf55b",
+          "label": "GigabitEthernet0/0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "29d32387-ddc6-4a1f-9166-a0a4837f1518",
+          "label": "GigabitEthernet0/1",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "7500827f-8195-4c40-af52-828a3d997ba0",
+          "label": "GigabitEthernet0/2",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "f458ee87-e94b-4bf7-98c7-68b6db29d287",
+          "label": "GigabitEthernet0/3",
+          "slot": 3,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": "4d9f3b26-9bc9-440e-bb6d-ea1d20c513de",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "913e62a7-e096-4ed9-bb9f-03ae13106fc5",
+      "image_definition": "",
+      "label": "iosvl2-0",
+      "node_definition": "iosvl2",
+      "ram": 768,
+      "tags": [],
+      "x": -250,
+      "y": 0,
+      "interfaces": [
+        {
+          "id": "8cb32401-2de8-4962-9592-65adb7ca0e10",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "9fc01a60-8dfd-48fd-b791-32bf4868d1e9",
+          "label": "GigabitEthernet0/0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "de2034d7-0952-4eea-9209-a20f49b56439",
+          "label": "GigabitEthernet0/1",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "afbea143-f46e-4b7f-a268-66e69f09c372",
+          "label": "GigabitEthernet0/2",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "ff9b0126-44ae-4c74-b161-4adff1690b10",
+          "label": "GigabitEthernet0/3",
+          "slot": 3,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": "99c887f5-052e-4864-a583-49fa7c4b68a9",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "0f9565f7-4fa3-4312-8dda-1db183a55950",
+      "image_definition": "",
+      "label": "csr1000v-0",
+      "node_definition": "csr1000v",
+      "ram": 3072,
+      "tags": [],
+      "x": -550,
+      "y": 0,
+      "interfaces": [
+        {
+          "id": "cd083a93-4369-4e62-9f3a-bba756e89541",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "0ddd4fc2-371b-4535-a6f3-bae00382a615",
+          "label": "GigabitEthernet1",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "a2b3ad88-41cb-433d-b3e7-74a4a935c7de",
+          "label": "GigabitEthernet2",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "a42d389c-b2ac-45b4-a199-7d69b696594f",
+          "label": "GigabitEthernet3",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "b492e0f8-c6a0-450e-8db2-a6f2b46cb664",
+          "label": "GigabitEthernet4",
+          "slot": 3,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": "e51bc75b-ce49-40d9-845f-7282af60a552",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "56c875d9-4f2a-4688-9fba-660716cff4cb",
+      "image_definition": "",
+      "label": "iosv-1",
+      "node_definition": "iosv",
+      "ram": 512,
+      "tags": [],
+      "x": -400,
+      "y": 150,
+      "interfaces": [
+        {
+          "id": "13a0bc33-55cd-4c0f-8362-0f5916e82106",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "2ad37a84-c53f-4d80-a5c5-2aa69151ee9b",
+          "label": "GigabitEthernet0/0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "ee6b4482-2a38-4222-87bf-18bbfcb14661",
+          "label": "GigabitEthernet0/1",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "f679b1bd-45d5-42d5-aafb-830640ce06b1",
+          "label": "GigabitEthernet0/2",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "22b679b9-6e3b-4163-8a30-608d749489da",
+          "label": "GigabitEthernet0/3",
+          "slot": 3,
+          "type": "physical"
+        },
+        {
+          "id": "e5211fb5-0737-4ee0-9dd2-ee73d8e88e5f",
+          "label": "GigabitEthernet0/4",
+          "slot": 4,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 16,
+      "compute_id": "7ccddd94-8ed3-46cf-99f3-3cbcc4ad625f",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "aa51eca6-ae81-40fc-a713-e1a168280d21",
+      "image_definition": "",
+      "label": "alpine-0",
+      "node_definition": "alpine",
+      "ram": 512,
+      "tags": [],
+      "x": -396,
+      "y": -164,
+      "interfaces": [
+        {
+          "id": "2d0aab8f-a240-4d59-a7f9-d83605dd8187",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 16,
+      "compute_id": "62289009-e296-4607-b24d-6eae7bfbac73",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "e5222bd8-52ff-4e1d-b6c9-89241132fb13",
+      "image_definition": "",
+      "label": "alpine-1",
+      "node_definition": "alpine",
+      "ram": 512,
+      "tags": [],
+      "x": -100,
+      "y": 0,
+      "interfaces": [
+        {
+          "id": "179cc26c-4f54-40ee-89b0-529fd87535b7",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "004c00c9-2606-485c-8ff9-d698e430fa6a",
+      "label": "ext-conn-0",
+      "node_definition": "external_connector",
+      "ram": 0,
+      "tags": [],
+      "x": -248,
+      "y": -166,
+      "interfaces": [
+        {
+          "id": "a1877e07-4d6c-480a-9386-f09f1087e700",
+          "label": "port",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/test_data/topology-863799a0-3d09-4af4-be26-cad997b6ab27.json b/tests/test_data/topology-863799a0-3d09-4af4-be26-cad997b6ab27.json
new file mode 100644
index 0000000..a254fef
--- /dev/null
+++ b/tests/test_data/topology-863799a0-3d09-4af4-be26-cad997b6ab27.json
@@ -0,0 +1,305 @@
+{
+  "lab": {
+    "description": "A sample lab for testing CSR 1000v features",
+    "notes": "# CSR 1000v Feature Testing\n\n## Features\n\n- CDP\n- ping\n- OSPF single-area\n- NAT - static\n- HSRP\n- DHCP\n- Routed subinterface\n- Restconf\n- Netconf\n\n## Test Details\n\nFor more details on test procedure, see the [CSR 1000v](https://developer.cisco.com/docs/modeling-labs/#!csr-1000v) page in the [CML documentation](https://developer.cisco.com/docs/modeling-labs/).",
+    "owner": "640a1c81-f1e7-4159-8fa2-ad35d9a68586",
+    "title": "CSR 1000v Feature Tests",
+    "version": "0.1.0"
+  },
+  "links": [
+    {
+      "id": "e7869898-9a4a-40af-813b-f4fbe10f6546",
+      "interface_a": "fc2796d5-abd5-41e9-abe9-c471c45f3734",
+      "interface_b": "ffb9634f-3889-466c-9d77-cc60c0dfde0f",
+      "label": "iosv-0-GigabitEthernet0/2<->csr1000v-2-GigabitEthernet1",
+      "node_a": "6d583887-a109-4df2-b7fe-45c27379a49d",
+      "node_b": "e79a0eb7-5c10-4538-8776-b832df78ba8b"
+    },
+    {
+      "id": "01fff06c-62de-4421-b335-499316576075",
+      "interface_a": "10dfbc02-838b-4cde-9af3-5dfed2b3e369",
+      "interface_b": "3b5c912c-208e-4a2f-92f1-5f4ae524c49d",
+      "label": "csr1000v-2-GigabitEthernet3<->iosvl2-0-GigabitEthernet0/2",
+      "node_a": "e79a0eb7-5c10-4538-8776-b832df78ba8b",
+      "node_b": "d9a0bb6a-dbdf-45e5-9222-18dc5fb4adf0"
+    },
+    {
+      "id": "793acf56-3c20-43bb-a295-e1372491c722",
+      "interface_a": "8f2aab3a-77e2-447f-9b63-31a991cc1b85",
+      "interface_b": "bdac665f-4c26-47dd-8ffb-6ba24553eba7",
+      "label": "csr1000v-0-GigabitEthernet3<->iosvl2-0-GigabitEthernet0/1",
+      "node_a": "90e9329f-7d20-4eb2-b396-06d0aa497ce9",
+      "node_b": "d9a0bb6a-dbdf-45e5-9222-18dc5fb4adf0"
+    },
+    {
+      "id": "ffdc378e-3f24-425f-8435-9cfa30b5ccd5",
+      "interface_a": "55e76e62-b5af-4875-a028-48a956257e72",
+      "interface_b": "e598f094-2cba-47aa-b704-2d3b1c562323",
+      "label": "csr1000v-2-GigabitEthernet2<->csr1000v-0-GigabitEthernet2",
+      "node_a": "e79a0eb7-5c10-4538-8776-b832df78ba8b",
+      "node_b": "90e9329f-7d20-4eb2-b396-06d0aa497ce9"
+    },
+    {
+      "id": "02ab4e89-051c-4af3-98b1-e26f708b54ea",
+      "interface_a": "27975a0f-a63c-46c9-9987-6cfc4cafa2c6",
+      "interface_b": "bd8ba744-c484-42c0-b096-93546787c681",
+      "label": "iosv-0-GigabitEthernet0/1<->csr1000v-0-GigabitEthernet1",
+      "node_a": "6d583887-a109-4df2-b7fe-45c27379a49d",
+      "node_b": "90e9329f-7d20-4eb2-b396-06d0aa497ce9"
+    },
+    {
+      "id": "bbfac097-5b23-461b-9f7d-77c7738b8e72",
+      "interface_a": "52bcdd81-9055-496f-823d-163937e39de2",
+      "interface_b": "e1622f5f-151a-4ee9-b090-7c87c775cae5",
+      "label": "ubuntu-0-ens2<->iosvl2-0-GigabitEthernet0/0",
+      "node_a": "9c840733-ac3f-4195-8839-b2c0379bc719",
+      "node_b": "d9a0bb6a-dbdf-45e5-9222-18dc5fb4adf0"
+    },
+    {
+      "id": "ce004107-dcc6-484e-8afc-4df7d6c28651",
+      "interface_a": "9ca58f2d-c96d-48bb-abde-0eb333d13a96",
+      "interface_b": "35cf19ca-ca56-45a7-8295-efb0416af1ce",
+      "label": "alpine-0-eth0<->csr1000v-0-GigabitEthernet4",
+      "node_a": "f4220194-d8b4-42ed-9967-226e1ed92ca0",
+      "node_b": "90e9329f-7d20-4eb2-b396-06d0aa497ce9"
+    }
+  ],
+  "nodes": [
+    {
+      "boot_disk_size": 0,
+      "compute_id": "4d9f3b26-9bc9-440e-bb6d-ea1d20c513de",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "e79a0eb7-5c10-4538-8776-b832df78ba8b",
+      "image_definition": "",
+      "label": "csr1000v-2",
+      "node_definition": "csr1000v",
+      "ram": 3072,
+      "tags": [],
+      "x": -600,
+      "y": -50,
+      "interfaces": [
+        {
+          "id": "438e9533-6390-46b0-9e03-841e57543da7",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "ffb9634f-3889-466c-9d77-cc60c0dfde0f",
+          "label": "GigabitEthernet1",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "55e76e62-b5af-4875-a028-48a956257e72",
+          "label": "GigabitEthernet2",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "10dfbc02-838b-4cde-9af3-5dfed2b3e369",
+          "label": "GigabitEthernet3",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "dcaa0e1c-6245-4320-8b21-32a57af15929",
+          "label": "GigabitEthernet4",
+          "slot": 3,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 16,
+      "compute_id": "e51bc75b-ce49-40d9-845f-7282af60a552",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "f4220194-d8b4-42ed-9967-226e1ed92ca0",
+      "image_definition": "",
+      "label": "alpine-0",
+      "node_definition": "alpine",
+      "ram": 512,
+      "tags": [],
+      "x": -600,
+      "y": -350,
+      "interfaces": [
+        {
+          "id": "9ca58f2d-c96d-48bb-abde-0eb333d13a96",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": "95006138-2ba9-47f0-8296-79b8ae4524c0",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "6d583887-a109-4df2-b7fe-45c27379a49d",
+      "image_definition": "",
+      "label": "iosv-0",
+      "node_definition": "iosv",
+      "ram": 512,
+      "tags": [],
+      "x": -800,
+      "y": -250,
+      "interfaces": [
+        {
+          "id": "9b571174-a38e-4d30-8aa6-bbd958e00845",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "7bd4984c-29df-4a42-b221-996dfdfc60bd",
+          "label": "GigabitEthernet0/0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "27975a0f-a63c-46c9-9987-6cfc4cafa2c6",
+          "label": "GigabitEthernet0/1",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "fc2796d5-abd5-41e9-abe9-c471c45f3734",
+          "label": "GigabitEthernet0/2",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "12a5df49-ad47-4bba-8969-34d32db7bea3",
+          "label": "GigabitEthernet0/3",
+          "slot": 3,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": "4d9f3b26-9bc9-440e-bb6d-ea1d20c513de",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "90e9329f-7d20-4eb2-b396-06d0aa497ce9",
+      "image_definition": "",
+      "label": "csr1000v-0",
+      "node_definition": "csr1000v",
+      "ram": 3072,
+      "tags": [],
+      "x": -600,
+      "y": -250,
+      "interfaces": [
+        {
+          "id": "a3f7c4ff-585d-495b-a60a-e66f087e6901",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "bd8ba744-c484-42c0-b096-93546787c681",
+          "label": "GigabitEthernet1",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "e598f094-2cba-47aa-b704-2d3b1c562323",
+          "label": "GigabitEthernet2",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "8f2aab3a-77e2-447f-9b63-31a991cc1b85",
+          "label": "GigabitEthernet3",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "35cf19ca-ca56-45a7-8295-efb0416af1ce",
+          "label": "GigabitEthernet4",
+          "slot": 3,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": "95006138-2ba9-47f0-8296-79b8ae4524c0",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "d9a0bb6a-dbdf-45e5-9222-18dc5fb4adf0",
+      "image_definition": "",
+      "label": "iosvl2-0",
+      "node_definition": "iosvl2",
+      "ram": 768,
+      "tags": [],
+      "x": -500,
+      "y": -150,
+      "interfaces": [
+        {
+          "id": "5d984624-a520-4537-ac78-51effc6734be",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "e1622f5f-151a-4ee9-b090-7c87c775cae5",
+          "label": "GigabitEthernet0/0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "bdac665f-4c26-47dd-8ffb-6ba24553eba7",
+          "label": "GigabitEthernet0/1",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "3b5c912c-208e-4a2f-92f1-5f4ae524c49d",
+          "label": "GigabitEthernet0/2",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "49542943-033c-4bd4-b36d-6a0a85a091e6",
+          "label": "GigabitEthernet0/3",
+          "slot": 3,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 64,
+      "compute_id": "fb362ccf-096e-4d80-a987-4a03a016fe15",
+      "cpu_limit": 100,
+      "cpus": 1,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "9c840733-ac3f-4195-8839-b2c0379bc719",
+      "image_definition": "",
+      "label": "ubuntu-0",
+      "node_definition": "ubuntu",
+      "ram": 2048,
+      "tags": [],
+      "x": -350,
+      "y": -150,
+      "interfaces": [
+        {
+          "id": "52bcdd81-9055-496f-823d-163937e39de2",
+          "label": "ens2",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/test_data/topology-df76a038-076f-4744-85c0-b2e1daf1bc06.json b/tests/test_data/topology-df76a038-076f-4744-85c0-b2e1daf1bc06.json
new file mode 100644
index 0000000..061901e
--- /dev/null
+++ b/tests/test_data/topology-df76a038-076f-4744-85c0-b2e1daf1bc06.json
@@ -0,0 +1,1062 @@
+{
+  "lab": {
+    "description": "",
+    "notes": "",
+    "owner": "640a1c81-f1e7-4159-8fa2-ad35d9a68586",
+    "title": "DevNet Sandbox SD-WAN",
+    "version": "0.1.0"
+  },
+  "links": [
+    {
+      "id": "a14cda09-f07c-4f45-9339-56429b4101ed",
+      "interface_a": "9a1e8389-357a-4bad-bfd4-39e6c1107618",
+      "interface_b": "16572d46-d943-4248-afea-8214d23ebc5d",
+      "label": "hq-GigabitEthernet2<->internet-GigabitEthernet2",
+      "node_a": "3d92ed02-98e0-41d8-ab9c-63dfbd063a48",
+      "node_b": "5132cd86-c1bd-4989-a218-f1b420f0340c"
+    },
+    {
+      "id": "15b9c0d0-529f-4e5c-b936-419d635ef33e",
+      "interface_a": "2cabc191-d225-4129-bf7c-68cecd50a30f",
+      "interface_b": "9dec200f-0757-4d74-a3ef-32b3ccff0758",
+      "label": "hq-GigabitEthernet3<->DMZ-port1",
+      "node_a": "3d92ed02-98e0-41d8-ab9c-63dfbd063a48",
+      "node_b": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34"
+    },
+    {
+      "id": "550e598d-ac81-46f6-82eb-70845cddb6c4",
+      "interface_a": "338958d0-8b2f-475d-97bf-f6fc89d1ad82",
+      "interface_b": "baa1d561-a0af-4937-91f2-82480c4bb6ea",
+      "label": "hq-GigabitEthernet4<->core-GigabitEthernet2",
+      "node_a": "3d92ed02-98e0-41d8-ab9c-63dfbd063a48",
+      "node_b": "3d3af2eb-9535-4dd4-9adc-cc7d0ae38245"
+    },
+    {
+      "id": "eccb95d0-4805-4cbb-869d-c6f0cc3585e4",
+      "interface_a": "900fc59e-bbdd-4b1a-a3c9-f01b61aa2e8c",
+      "interface_b": "caae58eb-5198-4974-acb8-1488335cbae1",
+      "label": "hqdc-port1<->core-GigabitEthernet3",
+      "node_a": "1dda713d-b300-4e6e-90e4-abc41d532e87",
+      "node_b": "3d3af2eb-9535-4dd4-9adc-cc7d0ae38245"
+    },
+    {
+      "id": "baa26cf8-6605-40cd-8cf1-26fe1dddcc4b",
+      "interface_a": "6fe3d067-4731-41fc-b022-3232f0763971",
+      "interface_b": "36a21b52-b080-4ed9-b823-839da1489408",
+      "label": "server1-ens4<->hqdc-port2",
+      "node_a": "6098d180-e37f-4277-a0f8-65fab6d99546",
+      "node_b": "1dda713d-b300-4e6e-90e4-abc41d532e87"
+    },
+    {
+      "id": "c19c11c1-6b73-4bb6-99aa-7b528d2ea542",
+      "interface_a": "bdb70af6-a87b-4443-a8f8-f0d4512c2980",
+      "interface_b": "34437710-a8a8-48b8-aede-dd13b5234fc0",
+      "label": "internet-GigabitEthernet5<->sp1-GigabitEthernet2",
+      "node_a": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+      "node_b": "3d5ccdd5-0837-4f16-bfc4-9ffd13806ac8"
+    },
+    {
+      "id": "e0ffe483-3643-4e45-bdd2-636693c50d10",
+      "interface_a": "ac515b7f-f5d2-4776-9a04-368ef88434b5",
+      "interface_b": "042085e6-5eff-4d79-8cb4-b9133cb07bec",
+      "label": "sp1-GigabitEthernet3<->spdc-port1",
+      "node_a": "3d5ccdd5-0837-4f16-bfc4-9ffd13806ac8",
+      "node_b": "dc7c06c1-1c91-4b43-b467-64bc11f83223"
+    },
+    {
+      "id": "9dde85ed-2652-4aba-a993-7dce622e6f6b",
+      "interface_a": "05e979ef-4ecf-4f09-8afd-cce22810585b",
+      "interface_b": "5d241c59-c211-423b-b60a-473d72bbfc2b",
+      "label": "vsmart1-ge0/0<->DMZ-port4",
+      "node_a": "a651ed0b-fbe5-47ad-8c77-47f66764c6eb",
+      "node_b": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34"
+    },
+    {
+      "id": "5ac4717e-6c90-4d5d-a7f2-400df0cc4a10",
+      "interface_a": "3ec51ff3-c949-49bf-953c-2bf4fa1b21a1",
+      "interface_b": "f21ef32e-f6c1-440f-b8e2-9cebd56d616a",
+      "label": "vmanage1-ge0/0<->DMZ-port3",
+      "node_a": "9ea578b6-1f9d-47dd-8db8-5d13118cda7d",
+      "node_b": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34"
+    },
+    {
+      "id": "81c82498-19d0-4f9d-adb2-a9e60804f126",
+      "interface_a": "13bb3872-4212-4c70-8421-18dc5a66dae2",
+      "interface_b": "67453763-94e9-41be-9bac-a6ba2146a636",
+      "label": "vbond1-ge0/0<->DMZ-port2",
+      "node_a": "db15e434-e9be-4c42-b998-1b08548daa5b",
+      "node_b": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34"
+    },
+    {
+      "id": "40b3e18b-2aa8-4df1-8da0-8ab812959201",
+      "interface_a": "6fb483d7-ccc5-4c73-b11b-b3da61061978",
+      "interface_b": "93cbd70e-111f-4b7c-bf6d-19629b88f34c",
+      "label": "vedge-hq-ge0/0<->DMZ-port5",
+      "node_a": "4fd590a3-a359-41ec-91a5-ac2711138b7c",
+      "node_b": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34"
+    },
+    {
+      "id": "2cde1162-3ad7-4e18-b11d-0d59e86bff25",
+      "interface_a": "ef4ef551-86b0-4f9e-a8ac-3814ba296ba1",
+      "interface_b": "5883e5fb-a214-4c3f-897f-58758247c121",
+      "label": "core-GigabitEthernet4<->vedge-hq-ge0/1",
+      "node_a": "3d3af2eb-9535-4dd4-9adc-cc7d0ae38245",
+      "node_b": "4fd590a3-a359-41ec-91a5-ac2711138b7c"
+    },
+    {
+      "id": "eece855f-ef7c-416f-b663-55853fb3ecde",
+      "interface_a": "f5039ace-856a-4509-ac33-e1d8376db689",
+      "interface_b": "33ac79ad-2831-49ee-8178-991fd42a6138",
+      "label": "spdc-port2<->service1-ens4",
+      "node_a": "dc7c06c1-1c91-4b43-b467-64bc11f83223",
+      "node_b": "e4b81866-1132-43b0-bfe0-b2a3f85c16ad"
+    },
+    {
+      "id": "57fd4851-ea86-4184-9f89-8673cf9a3a11",
+      "interface_a": "f9e0f235-d639-4f33-ba68-44a9406bd585",
+      "interface_b": "3801b38a-3d77-483c-9b74-c46d9205a397",
+      "label": "internet-GigabitEthernet3<->vedge1-ge0/0",
+      "node_a": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+      "node_b": "073f07e9-fbde-4e0a-9f6a-ad39ac5e78b0"
+    },
+    {
+      "id": "b98f9abd-b103-463a-8b59-af7593b4c552",
+      "interface_a": "ac92e084-e5ca-43ca-b392-7d4ea2f7d8c1",
+      "interface_b": "db251011-18b0-47ad-90bd-ae52278df382",
+      "label": "vedge1-ge0/1<->host1-ens4",
+      "node_a": "073f07e9-fbde-4e0a-9f6a-ad39ac5e78b0",
+      "node_b": "929917c0-ce1a-4c08-a8b9-d260192918f1"
+    },
+    {
+      "id": "aadc84d2-8391-432e-a8c8-bdec73620c3b",
+      "interface_a": "e5d0910b-1c53-4716-9ce8-e1043749d552",
+      "interface_b": "27c40f85-ca97-4c72-9268-88b60bd88dc6",
+      "label": "internet-GigabitEthernet4<->vedge2-ge0/0",
+      "node_a": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+      "node_b": "b26badb4-0c3a-4069-aeba-7a9c5e56f300"
+    },
+    {
+      "id": "9015e932-11da-49eb-8cad-0199b1ec8705",
+      "interface_a": "2061df34-3854-4cee-b5f8-4f65531713b2",
+      "interface_b": "87eb6a51-e427-4fb1-9694-17a443a252d0",
+      "label": "vedge2-ge0/1<->host2-ens4",
+      "node_a": "b26badb4-0c3a-4069-aeba-7a9c5e56f300",
+      "node_b": "ddff687e-fe77-44f7-93ec-11a2493931a3"
+    },
+    {
+      "id": "e82ae6d2-2931-4dda-b33d-6172a4fa3e98",
+      "interface_a": "7f82517a-6d9a-40b4-a2ea-7dc21dd541ca",
+      "interface_b": "77632842-2461-447f-90a3-12998f402836",
+      "label": "bridge-to-sandbox-port<->sandbox-backend-port0",
+      "node_a": "a8e87a75-d502-4b34-bd53-7a707ffa2452",
+      "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3"
+    },
+    {
+      "id": "3fb2273c-825c-482e-90a7-66f838e8989f",
+      "interface_a": "2c205c36-2e97-4b51-b6e6-8809745cf045",
+      "interface_b": "645a9e6d-5f3e-4f4d-b105-6c96c13ae27c",
+      "label": "vbond1-eth0<->sandbox-backend-port1",
+      "node_a": "db15e434-e9be-4c42-b998-1b08548daa5b",
+      "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3"
+    },
+    {
+      "id": "688fe683-6bd5-421a-879c-1dc0f960160e",
+      "interface_a": "514f434e-c598-449f-ac50-85ea48f11b35",
+      "interface_b": "24001347-bf82-44e8-b86d-1f9b09d8cf13",
+      "label": "vmanage1-eth0<->sandbox-backend-port2",
+      "node_a": "9ea578b6-1f9d-47dd-8db8-5d13118cda7d",
+      "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3"
+    },
+    {
+      "id": "cc9e0ca0-129d-4c40-86b5-22d8748a2bae",
+      "interface_a": "62e32275-9685-4da8-968f-b75b7a8949e8",
+      "interface_b": "a20f8c27-7d64-43e6-a61d-aa81a6441128",
+      "label": "vsmart1-eth0<->sandbox-backend-port3",
+      "node_a": "a651ed0b-fbe5-47ad-8c77-47f66764c6eb",
+      "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3"
+    },
+    {
+      "id": "ae178622-d545-4a09-9010-e0bcf5feff45",
+      "interface_a": "90233900-9352-42b1-819e-f05322d0f464",
+      "interface_b": "4f21b16a-1fb0-4d87-b4b2-ba8f3646d70f",
+      "label": "server1-ens3<->sandbox-backend-port4",
+      "node_a": "6098d180-e37f-4277-a0f8-65fab6d99546",
+      "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3"
+    },
+    {
+      "id": "517f6c20-1b79-49ca-aed3-a800c856a937",
+      "interface_a": "bd8f4be2-eb3e-4c2c-b3d0-aa22b674d0a8",
+      "interface_b": "f43bdd76-1279-4caa-bb2c-98090579cf19",
+      "label": "vedge-hq-eth0<->sandbox-backend-port5",
+      "node_a": "4fd590a3-a359-41ec-91a5-ac2711138b7c",
+      "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3"
+    },
+    {
+      "id": "2b1c93fc-1062-4c5a-8e44-55d6d9f177d0",
+      "interface_a": "4168b051-5a1e-46c3-a309-b4f7f7500f0e",
+      "interface_b": "175c1b5d-dfba-4b7b-9d74-c048e44e32bb",
+      "label": "core-GigabitEthernet1<->sandbox-backend-port6",
+      "node_a": "3d3af2eb-9535-4dd4-9adc-cc7d0ae38245",
+      "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3"
+    },
+    {
+      "id": "5e1e0747-3dc1-4c3a-b76c-33be9539c0c7",
+      "interface_a": "9d542987-5ece-480d-bdba-9dddd591cad0",
+      "interface_b": "d1838f35-0652-4f19-891c-84e42d895f2c",
+      "label": "hq-GigabitEthernet1<->sandbox-backend-port7",
+      "node_a": "3d92ed02-98e0-41d8-ab9c-63dfbd063a48",
+      "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3"
+    },
+    {
+      "id": "8d55a4c5-0e55-4392-b7e1-ae4201d93797",
+      "interface_a": "5516d4ed-7cf7-4fe0-90b6-9fdff4c203d3",
+      "interface_b": "7fa1c8e6-d42f-4831-9fc2-95acc414280d",
+      "label": "internet-GigabitEthernet1<->sandbox-backend-port8",
+      "node_a": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+      "node_b": "c78b15e4-ed33-4c90-88ff-241e014193a3"
+    },
+    {
+      "id": "1d5e0635-e938-4b86-aade-bcee9ca73775",
+      "interface_a": "ea4d6195-55f0-4432-aaa5-0a36bb9e692e",
+      "interface_b": "3a4a5d10-ec2f-41aa-84cb-4dafb8944e9a",
+      "label": "sandbox-backend-port9<->sp1-GigabitEthernet9",
+      "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+      "node_b": "3d5ccdd5-0837-4f16-bfc4-9ffd13806ac8"
+    },
+    {
+      "id": "54c3d1de-7d59-47e4-9c1b-3be6ad8b7f5f",
+      "interface_a": "e86ac6d4-8c6e-4176-a445-3314467c6a09",
+      "interface_b": "cf58cf0a-3806-4967-993b-2da6399d3766",
+      "label": "sandbox-backend-port10<->service1-ens3",
+      "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+      "node_b": "e4b81866-1132-43b0-bfe0-b2a3f85c16ad"
+    },
+    {
+      "id": "b037c4b5-32d8-4fd6-9cd7-b822bd7bfe15",
+      "interface_a": "87fd44a0-b725-48d7-bb9f-d17fff927555",
+      "interface_b": "c79fd894-722b-46ad-8367-6ffe4100746d",
+      "label": "sandbox-backend-port11<->vedge1-eth0",
+      "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+      "node_b": "073f07e9-fbde-4e0a-9f6a-ad39ac5e78b0"
+    },
+    {
+      "id": "b44393b8-a725-4717-a473-5a0ec52e0eca",
+      "interface_a": "2607737c-cbb9-43bc-a35e-1cc468b07cd6",
+      "interface_b": "d5ccac14-1d98-48bd-b731-11f5e56e79db",
+      "label": "sandbox-backend-port12<->host1-ens3",
+      "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+      "node_b": "929917c0-ce1a-4c08-a8b9-d260192918f1"
+    },
+    {
+      "id": "eb6e7349-10e0-4694-952e-369f6648c659",
+      "interface_a": "f7dba06c-d8bb-4485-9ba6-95fe16159073",
+      "interface_b": "40e06471-8e11-46c4-a6a0-5348b4d8b598",
+      "label": "sandbox-backend-port13<->vedge2-eth0",
+      "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+      "node_b": "b26badb4-0c3a-4069-aeba-7a9c5e56f300"
+    },
+    {
+      "id": "9c864f7d-5881-462b-8d0c-1280f08ccda2",
+      "interface_a": "aa8f89d3-2c03-40a1-87a0-6e1f29c4ac6b",
+      "interface_b": "a009fcea-03a0-4f02-b333-0d3e0e5d4528",
+      "label": "sandbox-backend-port14<->host2-ens3",
+      "node_a": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+      "node_b": "ddff687e-fe77-44f7-93ec-11a2493931a3"
+    }
+  ],
+  "nodes": [
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "5132cd86-c1bd-4989-a218-f1b420f0340c",
+      "label": "internet",
+      "node_definition": "csr1000v",
+      "ram": 0,
+      "tags": [],
+      "x": 257,
+      "y": 339,
+      "interfaces": [
+        {
+          "id": "a970031c-4b9a-4b98-9373-7c03bf8e55b6",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "5516d4ed-7cf7-4fe0-90b6-9fdff4c203d3",
+          "label": "GigabitEthernet1",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "16572d46-d943-4248-afea-8214d23ebc5d",
+          "label": "GigabitEthernet2",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "f9e0f235-d639-4f33-ba68-44a9406bd585",
+          "label": "GigabitEthernet3",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "e5d0910b-1c53-4716-9ce8-e1043749d552",
+          "label": "GigabitEthernet4",
+          "slot": 3,
+          "type": "physical"
+        },
+        {
+          "id": "bdb70af6-a87b-4443-a8f8-f0d4512c2980",
+          "label": "GigabitEthernet5",
+          "slot": 4,
+          "type": "physical"
+        },
+        {
+          "id": "325010fd-514b-47b6-be12-87f44ee5de56",
+          "label": "GigabitEthernet6",
+          "slot": 5,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "929917c0-ce1a-4c08-a8b9-d260192918f1",
+      "label": "host1",
+      "node_definition": "ubuntu",
+      "ram": 0,
+      "tags": [],
+      "x": 141,
+      "y": 570,
+      "interfaces": [
+        {
+          "id": "d5ccac14-1d98-48bd-b731-11f5e56e79db",
+          "label": "ens3",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "db251011-18b0-47ad-90bd-ae52278df382",
+          "label": "ens4",
+          "slot": 1,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "ddff687e-fe77-44f7-93ec-11a2493931a3",
+      "label": "host2",
+      "node_definition": "ubuntu",
+      "ram": 0,
+      "tags": [],
+      "x": 337,
+      "y": 564,
+      "interfaces": [
+        {
+          "id": "a009fcea-03a0-4f02-b333-0d3e0e5d4528",
+          "label": "ens3",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "87eb6a51-e427-4fb1-9694-17a443a252d0",
+          "label": "ens4",
+          "slot": 1,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "db15e434-e9be-4c42-b998-1b08548daa5b",
+      "label": "vbond1",
+      "node_definition": "viptela-bond",
+      "ram": 0,
+      "tags": [],
+      "x": 190,
+      "y": 50,
+      "interfaces": [
+        {
+          "id": "2c205c36-2e97-4b51-b6e6-8809745cf045",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "13bb3872-4212-4c70-8421-18dc5a66dae2",
+          "label": "ge0/0",
+          "slot": 1,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "9ea578b6-1f9d-47dd-8db8-5d13118cda7d",
+      "label": "vmanage1",
+      "node_definition": "viptela-manage",
+      "ram": 0,
+      "tags": [],
+      "x": 250,
+      "y": 0,
+      "interfaces": [
+        {
+          "id": "514f434e-c598-449f-ac50-85ea48f11b35",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "3ec51ff3-c949-49bf-953c-2bf4fa1b21a1",
+          "label": "ge0/0",
+          "slot": 1,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "a651ed0b-fbe5-47ad-8c77-47f66764c6eb",
+      "label": "vsmart1",
+      "node_definition": "viptela-smart",
+      "ram": 0,
+      "tags": [],
+      "x": 327,
+      "y": 50,
+      "interfaces": [
+        {
+          "id": "62e32275-9685-4da8-968f-b75b7a8949e8",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "05e979ef-4ecf-4f09-8afd-cce22810585b",
+          "label": "ge0/0",
+          "slot": 1,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "4fd590a3-a359-41ec-91a5-ac2711138b7c",
+      "label": "vedge-hq",
+      "node_definition": "viptela-edge",
+      "ram": 0,
+      "tags": [],
+      "x": 148,
+      "y": 133,
+      "interfaces": [
+        {
+          "id": "bd8f4be2-eb3e-4c2c-b3d0-aa22b674d0a8",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "6fb483d7-ccc5-4c73-b11b-b3da61061978",
+          "label": "ge0/0",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "5883e5fb-a214-4c3f-897f-58758247c121",
+          "label": "ge0/1",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "4294be68-79a0-445c-81ea-536df2ad236f",
+          "label": "ge0/2",
+          "slot": 3,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "073f07e9-fbde-4e0a-9f6a-ad39ac5e78b0",
+      "label": "vedge1",
+      "node_definition": "viptela-edge",
+      "ram": 0,
+      "tags": [],
+      "x": 143,
+      "y": 461,
+      "interfaces": [
+        {
+          "id": "c79fd894-722b-46ad-8367-6ffe4100746d",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "3801b38a-3d77-483c-9b74-c46d9205a397",
+          "label": "ge0/0",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "ac92e084-e5ca-43ca-b392-7d4ea2f7d8c1",
+          "label": "ge0/1",
+          "slot": 2,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "b26badb4-0c3a-4069-aeba-7a9c5e56f300",
+      "label": "vedge2",
+      "node_definition": "viptela-edge",
+      "ram": 0,
+      "tags": [],
+      "x": 337,
+      "y": 458,
+      "interfaces": [
+        {
+          "id": "40e06471-8e11-46c4-a6a0-5348b4d8b598",
+          "label": "eth0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "27c40f85-ca97-4c72-9268-88b60bd88dc6",
+          "label": "ge0/0",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "2061df34-3854-4cee-b5f8-4f65531713b2",
+          "label": "ge0/1",
+          "slot": 2,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "1fe9ee6d-9a6b-439d-be64-6a5722ddfd34",
+      "label": "DMZ",
+      "node_definition": "unmanaged_switch",
+      "ram": 0,
+      "tags": [],
+      "x": 260,
+      "y": 132,
+      "interfaces": [
+        {
+          "id": "9dec200f-0757-4d74-a3ef-32b3ccff0758",
+          "label": "port1",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "67453763-94e9-41be-9bac-a6ba2146a636",
+          "label": "port2",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "f21ef32e-f6c1-440f-b8e2-9cebd56d616a",
+          "label": "port3",
+          "slot": 3,
+          "type": "physical"
+        },
+        {
+          "id": "5d241c59-c211-423b-b60a-473d72bbfc2b",
+          "label": "port4",
+          "slot": 4,
+          "type": "physical"
+        },
+        {
+          "id": "93cbd70e-111f-4b7c-bf6d-19629b88f34c",
+          "label": "port5",
+          "slot": 5,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "3d92ed02-98e0-41d8-ab9c-63dfbd063a48",
+      "label": "hq",
+      "node_definition": "csr1000v",
+      "ram": 0,
+      "tags": [],
+      "x": 258,
+      "y": 249,
+      "interfaces": [
+        {
+          "id": "d75dc363-4676-4d98-abe5-8abda72991fc",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "9d542987-5ece-480d-bdba-9dddd591cad0",
+          "label": "GigabitEthernet1",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "9a1e8389-357a-4bad-bfd4-39e6c1107618",
+          "label": "GigabitEthernet2",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "2cabc191-d225-4129-bf7c-68cecd50a30f",
+          "label": "GigabitEthernet3",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "338958d0-8b2f-475d-97bf-f6fc89d1ad82",
+          "label": "GigabitEthernet4",
+          "slot": 3,
+          "type": "physical"
+        },
+        {
+          "id": "a6463eca-49af-44da-8932-8f927c62068f",
+          "label": "GigabitEthernet5",
+          "slot": 4,
+          "type": "physical"
+        },
+        {
+          "id": "c3f2311d-277d-4977-9b64-8e53020c0fc3",
+          "label": "GigabitEthernet6",
+          "slot": 5,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "3d3af2eb-9535-4dd4-9adc-cc7d0ae38245",
+      "label": "core",
+      "node_definition": "csr1000v",
+      "ram": 0,
+      "tags": [],
+      "x": 150,
+      "y": 250,
+      "interfaces": [
+        {
+          "id": "4412b944-2954-45fe-9e7e-e61a15dd2c8e",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "4168b051-5a1e-46c3-a309-b4f7f7500f0e",
+          "label": "GigabitEthernet1",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "baa1d561-a0af-4937-91f2-82480c4bb6ea",
+          "label": "GigabitEthernet2",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "caae58eb-5198-4974-acb8-1488335cbae1",
+          "label": "GigabitEthernet3",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "ef4ef551-86b0-4f9e-a8ac-3814ba296ba1",
+          "label": "GigabitEthernet4",
+          "slot": 3,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "1dda713d-b300-4e6e-90e4-abc41d532e87",
+      "label": "hqdc",
+      "node_definition": "unmanaged_switch",
+      "ram": 0,
+      "tags": [],
+      "x": 50,
+      "y": 250,
+      "interfaces": [
+        {
+          "id": "900fc59e-bbdd-4b1a-a3c9-f01b61aa2e8c",
+          "label": "port1",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "36a21b52-b080-4ed9-b823-839da1489408",
+          "label": "port2",
+          "slot": 2,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "6098d180-e37f-4277-a0f8-65fab6d99546",
+      "label": "server1",
+      "node_definition": "ubuntu",
+      "ram": 0,
+      "tags": [],
+      "x": 50,
+      "y": 131,
+      "interfaces": [
+        {
+          "id": "90233900-9352-42b1-819e-f05322d0f464",
+          "label": "ens3",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "6fe3d067-4731-41fc-b022-3232f0763971",
+          "label": "ens4",
+          "slot": 1,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "3d5ccdd5-0837-4f16-bfc4-9ffd13806ac8",
+      "label": "sp1",
+      "node_definition": "csr1000v",
+      "ram": 0,
+      "tags": [],
+      "x": 552,
+      "y": 340,
+      "interfaces": [
+        {
+          "id": "026802d2-e97b-4940-8be9-8ca9af35c5a8",
+          "label": "Loopback0",
+          "type": "loopback"
+        },
+        {
+          "id": "4aafbda0-4a63-460c-a642-400596697a4f",
+          "label": "GigabitEthernet1",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "34437710-a8a8-48b8-aede-dd13b5234fc0",
+          "label": "GigabitEthernet2",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "ac515b7f-f5d2-4776-9a04-368ef88434b5",
+          "label": "GigabitEthernet3",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "49ee7b45-ab79-4b26-b91e-a4cbbcea99fc",
+          "label": "GigabitEthernet4",
+          "slot": 3,
+          "type": "physical"
+        },
+        {
+          "id": "05d96912-7429-4d9b-9dde-9c6d4a8204cd",
+          "label": "GigabitEthernet5",
+          "slot": 4,
+          "type": "physical"
+        },
+        {
+          "id": "c673973f-7987-4b6d-b351-82b683cac264",
+          "label": "GigabitEthernet6",
+          "slot": 5,
+          "type": "physical"
+        },
+        {
+          "id": "a8c078a1-037d-47db-a72e-7e0012f60ee9",
+          "label": "GigabitEthernet7",
+          "slot": 6,
+          "type": "physical"
+        },
+        {
+          "id": "53c4cc2f-aaa5-4b82-b306-6581a1d68d67",
+          "label": "GigabitEthernet8",
+          "slot": 7,
+          "type": "physical"
+        },
+        {
+          "id": "3a4a5d10-ec2f-41aa-84cb-4dafb8944e9a",
+          "label": "GigabitEthernet9",
+          "slot": 8,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "e4b81866-1132-43b0-bfe0-b2a3f85c16ad",
+      "label": "service1",
+      "node_definition": "ubuntu",
+      "ram": 0,
+      "tags": [],
+      "x": 805,
+      "y": 338,
+      "interfaces": [
+        {
+          "id": "cf58cf0a-3806-4967-993b-2da6399d3766",
+          "label": "ens3",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "33ac79ad-2831-49ee-8178-991fd42a6138",
+          "label": "ens4",
+          "slot": 1,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "dc7c06c1-1c91-4b43-b467-64bc11f83223",
+      "label": "spdc",
+      "node_definition": "unmanaged_switch",
+      "ram": 0,
+      "tags": [],
+      "x": 665,
+      "y": 339,
+      "interfaces": [
+        {
+          "id": "042085e6-5eff-4d79-8cb4-b9133cb07bec",
+          "label": "port1",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "f5039ace-856a-4509-ac33-e1d8376db689",
+          "label": "port2",
+          "slot": 2,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": true,
+      "id": "c78b15e4-ed33-4c90-88ff-241e014193a3",
+      "label": "sandbox-backend",
+      "node_definition": "unmanaged_switch",
+      "ram": 0,
+      "tags": [],
+      "x": 800,
+      "y": 200,
+      "interfaces": [
+        {
+          "id": "77632842-2461-447f-90a3-12998f402836",
+          "label": "port0",
+          "slot": 0,
+          "type": "physical"
+        },
+        {
+          "id": "645a9e6d-5f3e-4f4d-b105-6c96c13ae27c",
+          "label": "port1",
+          "slot": 1,
+          "type": "physical"
+        },
+        {
+          "id": "24001347-bf82-44e8-b86d-1f9b09d8cf13",
+          "label": "port2",
+          "slot": 2,
+          "type": "physical"
+        },
+        {
+          "id": "a20f8c27-7d64-43e6-a61d-aa81a6441128",
+          "label": "port3",
+          "slot": 3,
+          "type": "physical"
+        },
+        {
+          "id": "4f21b16a-1fb0-4d87-b4b2-ba8f3646d70f",
+          "label": "port4",
+          "slot": 4,
+          "type": "physical"
+        },
+        {
+          "id": "f43bdd76-1279-4caa-bb2c-98090579cf19",
+          "label": "port5",
+          "slot": 5,
+          "type": "physical"
+        },
+        {
+          "id": "175c1b5d-dfba-4b7b-9d74-c048e44e32bb",
+          "label": "port6",
+          "slot": 6,
+          "type": "physical"
+        },
+        {
+          "id": "d1838f35-0652-4f19-891c-84e42d895f2c",
+          "label": "port7",
+          "slot": 7,
+          "type": "physical"
+        },
+        {
+          "id": "7fa1c8e6-d42f-4831-9fc2-95acc414280d",
+          "label": "port8",
+          "slot": 8,
+          "type": "physical"
+        },
+        {
+          "id": "ea4d6195-55f0-4432-aaa5-0a36bb9e692e",
+          "label": "port9",
+          "slot": 9,
+          "type": "physical"
+        },
+        {
+          "id": "e86ac6d4-8c6e-4176-a445-3314467c6a09",
+          "label": "port10",
+          "slot": 10,
+          "type": "physical"
+        },
+        {
+          "id": "87fd44a0-b725-48d7-bb9f-d17fff927555",
+          "label": "port11",
+          "slot": 11,
+          "type": "physical"
+        },
+        {
+          "id": "2607737c-cbb9-43bc-a35e-1cc468b07cd6",
+          "label": "port12",
+          "slot": 12,
+          "type": "physical"
+        },
+        {
+          "id": "f7dba06c-d8bb-4485-9ba6-95fe16159073",
+          "label": "port13",
+          "slot": 13,
+          "type": "physical"
+        },
+        {
+          "id": "aa8f89d3-2c03-40a1-87a0-6e1f29c4ac6b",
+          "label": "port14",
+          "slot": 14,
+          "type": "physical"
+        },
+        {
+          "id": "6fdc439c-17f9-4042-998e-b080fbde8223",
+          "label": "port15",
+          "slot": 15,
+          "type": "physical"
+        }
+      ]
+    },
+    {
+      "boot_disk_size": 0,
+      "compute_id": null,
+      "cpu_limit": 100,
+      "cpus": 0,
+      "data_volume": 0,
+      "hide_links": false,
+      "id": "a8e87a75-d502-4b34-bd53-7a707ffa2452",
+      "label": "bridge-to-sandbox",
+      "node_definition": "external_connector",
+      "ram": 0,
+      "tags": [],
+      "x": 800,
+      "y": 50,
+      "interfaces": [
+        {
+          "id": "7f82517a-6d9a-40b4-a2ea-7dc21dd541ca",
+          "label": "port",
+          "slot": 0,
+          "type": "physical"
+        }
+      ]
+    }
+  ]
+}