Repository: aristanetworks/cvprac (language: python)

Issue:
test: Adding test for device_decommissioning
Added test for decommissioning devices post CVP 2021.3.0 to the existing `test_api_inventory` function (note that it takes around 2-3 minutes for decomissioning to be successfuly)

when you run the test with `make systest` you should see: `Verify add_device_to_inventory and delete_device(s) ... ok`




Discussion / hints:

Fix patch:
diff --git a/Makefile b/Makefile
index 49d01c3..2c437d8 100644
--- a/Makefile
+++ b/Makefile
@@ -48,8 +48,8 @@ coverage_report:
 	$(COVERAGE) report -m
 
 pep8:
-	-pep8 -r --ignore=$(PEP8_IGNORE) cvprac/
-	-pep8 -r --ignore=$(PEP8_IGNORE),E402 test/
+	-pep8 -r --max-line-length=120 --ignore=$(PEP8_IGNORE) cvprac/
+	-pep8 -r --max-line-length=120 --ignore=$(PEP8_IGNORE),E402 test/
 
 pyflakes:
 	pyflakes cvprac/ test/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..67ad0f5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,416 @@
+# Arista Cloudvision® Portal RESTful API Client
+
+[![pypi](https://img.shields.io/pypi/v/cvprac.svg)](https://pypi.python.org/pypi/cvprac)
+
+## Table of Contents
+
+1. [Overview](#overview)
+    - [Requirements](#requirements)
+1. [Installation](#installation)
+    - [Development: Run from Source](#development-run-from-source)
+1. [Getting Started](#getting-started)
+    - [Connecting](#connecting)
+    - [CVP On Premises](#cvp-on-premises)
+    - [CVaaS](#cvaas)
+    - [CVP Version Handling](#cvp-version-handling)
+    - [Examples](#examples)
+1. [Notes For API Class Usage](#notes-for-api-class-usage)
+    - [Containers](#containers)
+1. [Testing](#testing)
+1. [Contact or Questions](#contact-or-questions)
+1. [Contributing](#contributing)
+    - [Working With Git](#working-with-git)
+    - [Submitting Pull Requests](#submitting-pull-requests)
+    - [Pull Request Semantics](#pull-request-semantics)
+1. [License](#license)
+
+## Overview
+
+This module provides a RESTful API client for Cloudvision® Portal (CVP)
+which can be used for building applications that work with Arista CVP.
+
+When the class is instantiated the logging is configured. Either syslog,
+file logging, both, or none can be enabled. If neither syslog nor
+filename is specified then no logging will be performed.
+
+This class supports creating a connection to a CVP node and then issuing
+subsequent GET and POST requests to CVP. A GET or POST request will be
+automatically retried on the same node if the request receives a
+requests.exceptions.Timeout or ReadTimeout error. A GET or POST request
+will be automatically retried on the same node if the request receives a
+CvpSessionLogOutError. For this case a login will be performed before
+the request is retried. For either case, the maximum number of times a
+request will be retried on the same node is specified by the class
+attribute NUM\_RETRY\_REQUESTS.
+
+If more than one CVP node is specified when creating a connection, and a
+GET or POST request that receives a requests.exceptions.ConnectionError,
+requests.exceptions.HTTPError, or a requests.exceptions.TooManyRedirects
+will be retried on the next CVP node in the list. If a GET or POST
+request that receives a requests.exceptions.Timeout or
+CvpSessionLogOutError and the retries on the same node exceed
+NUM\_RETRY\_REQUESTS, then the request will be retried on the next node
+on the list.
+
+If any of the errors persists across all nodes then the GET or POST
+request will fail and the last error that occurred will be raised.
+
+The class provides connect, get, and post methods that allow the user to
+make RESTful API calls to CVP. See the example below using the get
+method.
+
+The class provides a wrapper function around the CVP RESTful API
+operations. Each API method takes the RESTful API parameters as method
+parameters to the operation method. The API class was added to the
+client class because the API functions are required when using the CVP
+RESTful API and placing them in this library avoids duplicating the
+calls in every application that uses this class. See the examples below
+using the API methods.
+
+### Requirements
+
+- Python 2.7 or later
+- Python logging module
+- Python requests module version 1.0.0 or later
+
+## Installation
+
+The source code for cvprac is provided on Github at
+<https://github.com/aristanetworks/cvprac>. All current development is
+done in the develop branch. Stable released versions are tagged in the
+master branch and uploaded to <https://pypi.python.org>.
+
+If your platform has internet access you can use the Python Package
+manager to install cvprac.
+
+    admin:~ admin$ sudo pip install cvprac
+
+You can upgrade cvprac
+
+    admin:~ admin$ sudo pip install --upgrade cvprac
+
+### Development: Run from Source
+
+We recommend running cvprac in a virtual environment. For more
+information, read this:
+<http://docs.python-guide.org/en/latest/dev/virtualenvs/>
+
+These instructions will help you install and run cvprac from source.
+This is useful if you plan on contributing or if you would always like
+to see the latest code in the develop branch. Note that these steps
+require the pip and git commands.
+
+#### Step 1: Clone the cvprac Github repo
+
+    # Go to a directory where you'd like to keep the source
+    admin:~ admin$ cd ~/projects
+    admin:~ admin$ git clone https://github.com/aristanetworks/cvprac
+    admin:~ admin$ cd cvprac
+
+#### Step 2: Check out the desired version or branch
+
+    # Go to a directory where you'd like to keep the source
+    admin:~ admin$ cd ~/projects/cvprac
+
+    # To see a list of available versions or branches
+    admin:~ admin$ git tag
+    admin:~ admin$ git branch
+
+    # Checkout the desired version of code
+    admin:~ admin$ git checkout v1.0.3
+
+#### Step 3: Install cvprac using Pip with -e switch
+
+    # Go to a directory where you'd like to keep the source
+    admin:~ admin$ cd ~/projects/cvprac
+
+    # Install
+    admin:~ admin$ sudo pip install -e ~/projects/cvprac
+
+#### Step 4: Install cvprac development requirements
+
+    # Go to a directory where you'd like to keep the source
+    admin:~ admin$ pip install -r dev-requirements.txt
+
+## Getting Started
+
+Once the package has been installed you can run the following example to
+verify that everything has been installed properly.
+
+### Connecting
+
+Connecting to CVP will depend on your CVP setup. Several options are
+outlined below.
+
+### CVP On Premises
+
+CVP On Premises is for users with CVP running on a local server or
+cluster of servers. This is the standard form of connection. Multiple
+examples below demonstrate connecting to CVP On Premises setups.
+
+### CVaaS
+
+CVaaS is CloudVision as a Service. Users with CVaaS must use a REST API
+token for accessing CVP with REST APIs.
+
+- In the case where users authenticate with CVP (CVaaS) using Oauth a
+- REST API token is required to be generated and used for running REST
+- APIs. In this case no username/password login is necessary, but the
+- API token (via api\_token parameter) must be provided to cvprac client
+- with the is\_cvaas parameter. In the case that the api\_token is used
+- for REST APIs the username and password will be ignored and the tenant
+- parameter is not needed.
+
+An example of a CVaaS connection is shown below.
+
+Note that the token parameter was previously cvaas\_token but this has
+been converted to api\_token because tokens are also available for usage
+with On Prem CVP deployments. The api\_token parameter name is more
+generic in this sense. If you are using the cvaas\_token parameter
+please convert to api\_token because the cvaas\_token parameter will be
+deprecated in the future.
+
+### CVP Version Handling
+
+The CVP RESTful APIs often change between releases of CVP. Cvprac
+attempts to mask these API changes from the user via making appropriate
+API calls based on the CVP version while attempting to maintain return
+data and not changing function names when possible. This helps maintain
+backward compatibility for users when they upgrade CVP so that any
+custom automation/scripts will continue to work. In some cases
+maintaining return data requires additional API calls so there are cases
+where this comes with the cost of a slight performance hit. Users are
+free to access the clients get(), post() and delete() methods and make
+API calls directly if they want to avoid the potential time delay of
+some API functions. The current API version information handled by
+cvprac is shown below.
+
+- Current latest API version is 4.0 API version is set to latest
+- available version for CVaaS API version is set to 4.0 for 2020.1.1 and
+- beyond. API version is set to 3.0 for 2019.0.0 through 2020.1.0 API
+- version is set to 2.0 for 2018.2.X API version is set to 1.0 for
+- 2018.1.X and prior
+
+### Examples
+
+Example using CVP On Prem client get method directly:
+
+    >>> from cvprac.cvp_client import CvpClient
+    >>> clnt = CvpClient()
+    >>> clnt.connect(['cvp1', 'cvp2', 'cvp3'], 'cvp_user', 'cvp_word')
+    >>> result = clnt.get('/cvpInfo/getCvpInfo.do')
+    >>> print result
+    {u'version': u'2016.1.0'}
+    >>>
+
+Same example as above using the API method:
+
+    >>> from cvprac.cvp_client import CvpClient
+    >>> clnt = CvpClient()
+    >>> clnt.connect(['cvp1', 'cvp2', 'cvp3'], 'cvp_user', 'cvp_word')
+    >>> result = clnt.api.get_cvp_info()
+    >>> print result
+    {u'version': u'2016.1.0'}
+    >>>
+
+Same example as above but connecting to CVaaS with a token: Note that
+the username and password parameters are required by the connect
+function but will be ignored when using api\_token:
+
+    >>> from cvprac.cvp_client import CvpClient
+    >>> clnt = CvpClient()
+    >>> clnt.connect(nodes=['cvaas'], username='', password='', is_cvaas=True, api_token='user token')
+    >>> result = clnt.api.get_cvp_info()
+    >>> print result
+    {u'version': u'cvaas'}
+    >>>
+
+Example using the API method to create a container, wait 5 seconds, then
+delete the container. Before running this example manually create a
+container named DC-1 on your CVP node.
+
+    >>> import time
+    >>> from cvprac.cvp_client import CvpClient
+    >>> clnt = CvpClient()
+    >>> clnt.connect(['cvp1'], 'cvp_user', 'cvp_word')
+    >>> parent = clnt.api.search_topology('DC-1')
+    >>> clnt.api.add_container('TORs', 'DC-1', parent['containerList'][0]['key'])
+    >>> child = clnt.api.search_topology('TORs')
+    >>> time.sleep(5)
+    >>> result = clnt.api.delete_container('TORs', child['containerList'][0]['key'], 'DC-1', parent['containerList'][0]['key'])
+    >>>
+
+## Notes for API Class Usage
+
+### Containers
+
+With the API the containers are added for all cases. If you add the
+container to the original root container 'Tenant' then you have to do a
+refresh from the GUI to see the container after it is added or deleted.
+If the root container has been renamed or the parent container is not
+the root container then an add or delete will update the GUI without
+requiring a manual refresh.
+
+## Testing
+
+The cvprac module provides system tests. To run the system tests, you
+will need to update the `cvp_nodes.yaml` file found in test/fixtures.
+
+Requirements for running the system tests:
+
+- Need one CVP node for test with a test user account. Create the same
+  account on the switch used for testing. The user account information
+  follows:
+
+        username: CvpRacTest
+        password: AristaInnovates
+
+        If switch does not have correct username and/or password then the tests that
+        execute tasks will fail with the following error:
+
+        AssertionError: Execution for task id 220 failed and in the test log is the error:
+
+        Failure response received from the netElement : ' Unauthorized User '
+
+- Test has dedicated access to the CVP node.
+- CVP node contains at least one device in a container.
+- Container or device has at least one configlet applied.
+
+To run the system tests:
+
+- run `make tests` from the root of the cvprac source folder.
+
+## Contact or Questions
+
+Cvprac is developed by Arista EOS+ CS and supported by the Arista EOS+
+community. Support for the code is provided on a best effort basis by
+the Arista EOS+ CS team and the community. You can contact the team that
+develops these modules by sending an email to <eosplus-dev@arista.com>.
+
+For customers that are looking for a premium level of support, please
+contact your local account team or email <eosplus@arista.com> for help.
+
+## Contributing
+
+Contributing pull requests are gladly welcomed for this repository. Not only contributing to the code but also we encourage the users to contribute in the form of examples, docs, tutorials, and user guides.
+
+Please note that all contributions that modify the library behavior
+require corresponding test cases otherwise the pull request will be
+rejected.
+
+### Working With Git
+
+It is recommended to fork the project and then start development on the forked repository's **develop** branch. This can achieved with the below steps:
+
+- [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the repo then clone your fork,
+   and configure the remotes:
+
+        # Clone your fork of the repo into the current directory
+        git clone https://github.com/<your-username>/cvprac
+
+        # Navigate to the newly cloned directory
+        cd cvprac
+
+        # Assign the original repo to a remote called "upstream"
+        git remote add upstream https://github.com/aristanetworks/cvprac.git
+
+- If you cloned a while ago, get the latest changes from upstream:
+
+        # Work on the develop branch of the forked repo
+        git checkout develop
+
+        # Pull the latest changes from the develop branch of aristanetworks cvprac
+        git pull upstream develop
+
+- Create a new topic branch (off the main project development branch) to
+   contain your feature, change, or fix:
+
+       git checkout -b <topic-branch-name>
+
+- Commit your changes in logical chunks. Please adhere to these [git commit
+   message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
+   or your code is unlikely to be merged into the main project. Use Git's
+   [git rebase](https://docs.github.com/en/get-started/using-git/about-git-rebase)
+   feature to tidy up your commits before making them public.
+
+- Locally merge (or rebase) the upstream development branch into your topic branch every time before pushing it to your fork:
+
+        # Here the <dev-branch> is develop
+        git pull [--rebase] upstream <dev-branch>
+
+- Push your topic branch up to your fork:
+
+        git push origin <topic-branch-name>
+
+- [Open a Pull Request](https://github.com/aristanetworks/cvprac/pulls)
+    with a clear title, description and explain how to test the feature.
+
+### Submitting Pull Requests
+
+- It is recommended to open an issue before starting work on a pull request to make sure if the same issue is not reported previously and someone is already working on that. When suggesting a new feature, also make sure it won't conflict with any work that's already in progress.
+
+- Once the issue is opened either self-assign the issue or ask the maintainer to assign it for you. This will make sure no others are working on the same issue.
+
+- All new functionality must include relevant tests where applicable.
+
+- When submitting a pull request, please be sure to work off of the **develop** branch and not from other branches. The **develop** branch is used for ongoing development, while the **master** will hold the last stable version.
+
+- To automate release-notes creation and make filtering process easier, it is strongly recommended to use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) syntax at least for Pull Request (PR) title.
+
+- All code submissions must follow the below criteria:
+
+  - The issue/PR title should follow the semantic as described [here](#pull-request-semantics)
+  - All the tests are updated and are passed successfully
+  - Python syntax is valid
+
+### Pull Request Semantics
+
+The Pull Request title should start with one of the below to easily segregate if its a feature add or a bug or something related documentation etc.
+
+It is strongly recommended to use one from the below:
+
+- ```Feat```: Create a capability e.g. feature, test, dependency
+- ```Fix```: Fix an issue e.g. bug, typo, accident, misstatement
+- ```Doc```: Refactor of documentation, e.g. help files
+- ```Test```: Add or refactor anything regarding test, e.g add a new testCases or missing testCases
+- ```Refactor```: A code change that MUST be just a refactoring
+- ```Bump```: Increase the version of something e.g. dependency
+- ```Revert```: Change back to the previous commit
+- ```Optimize```: Refactor of performance, e.g. speed up code
+- ```CI```: Update CI components, e.g. molecule files or Github Actions
+- ```Cut```: Remove a capability e.g. feature, test, dependency
+
+For example:
+
+- Feat: Add support for decommissioning APIs
+- Test: Add missing test cases for change control
+- Doc: Document new examples for change control
+
+## License
+
+Copyright© 2020, Arista Networks, Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+- Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+- Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+- Neither the name of Arista Networks nor the names of its
+    contributors may be used to endorse or promote products derived from
+    this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS
+IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 7ffedea..0000000
--- a/README.rst
+++ /dev/null
@@ -1,368 +0,0 @@
-Arista Cloudvision\ |reg| Portal RESTful API Client
-===================================================
-|pypi_version_badge|
-|System_test_Status|
-
-Table of Contents
-=================
-#. `Overview`_
-
-   -  `Requirements`_
-
-#. `Installation`_
-
-   -  `Development: Run from Source`_
-
-#. `Getting Started`_
-
-   -  `Connecting`_
-   -  `CVP On Premises`_
-   -  `CVaaS`_
-   -  `CVP Version Handling`_
-   -  `Examples`_
-
-#. `Testing`_
-#. `License`_
-
-Overview
-========
-
-This module provides a RESTful API client for Cloudvision\ |reg| Portal (CVP)
-which can be used for building applications that work with Arista CVP.
-
-When the class is instantiated the logging is configured. Either syslog,
-file logging, both, or none can be enabled. If neither syslog nor
-filename is specified then no logging will be performed.
-
-This class supports creating a connection to a CVP node and then issuing
-subsequent GET and POST requests to CVP. A GET or POST request will be
-automatically retried on the same node if the request receives a
-requests.exceptions.Timeout or ReadTimeout error. A GET or POST request
-will be automatically retried on the same node if the request receives a
-CvpSessionLogOutError. For this case a login will be performed before
-the request is retried. For either case, the maximum number of times a
-request will be retried on the same node is specified by the class
-attribute NUM\_RETRY\_REQUESTS.
-
-If more than one CVP node is specified when creating a connection, and a
-GET or POST request that receives a requests.exceptions.ConnectionError,
-requests.exceptions.HTTPError, or a requests.exceptions.TooManyRedirects
-will be retried on the next CVP node in the list. If a GET or POST
-request that receives a requests.exceptions.Timeout or
-CvpSessionLogOutError and the retries on the same node exceed
-NUM\_RETRY\_REQUESTS, then the request will be retried on the next node
-on the list.
-
-If any of the errors persists across all nodes then the GET or POST
-request will fail and the last error that occurred will be raised.
-
-The class provides connect, get, and post methods that allow the user to
-make RESTful API calls to CVP. See the example below using the get
-method.
-
-The class provides a wrapper function around the CVP RESTful API
-operations. Each API method takes the RESTful API parameters as method
-parameters to the operation method. The API class was added to the
-client class because the API functions are required when using the CVP
-RESTful API and placing them in this library avoids duplicating the
-calls in every application that uses this class. See the examples below
-using the API methods.
-
-Requirements
-------------
-
--  Python 2.7 or later
--  Python logging module
--  Python requests module version 1.0.0 or later
-
-Installation
-============
-
-The source code for cvprac is provided on Github at
-https://github.com/aristanetworks/cvprac. All current development is
-done in the develop branch. Stable released versions are tagged in the
-master branch and uploaded to https://pypi.python.org.
-
-If your platform has internet access you can use the Python Package
-manager to install cvprac.
-
-::
-
-    admin:~ admin$ sudo pip install cvprac
-
-You can upgrade cvprac
-
-::
-
-    admin:~ admin$ sudo pip install --upgrade cvprac
-
-Development: Run from Source
-----------------------------
-
-We recommend running cvprac in a virtual environment. For more
-information, read this:
-http://docs.python-guide.org/en/latest/dev/virtualenvs/
-
-These instructions will help you install and run cvprac from source.
-This is useful if you plan on contributing or if you would always like
-to see the latest code in the develop branch. Note that these steps
-require the pip and git commands.
-
-Step 1: Clone the cvprac Github repo
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-::
-
-    # Go to a directory where you'd like to keep the source
-    admin:~ admin$ cd ~/projects
-    admin:~ admin$ git clone https://github.com/aristanetworks/cvprac
-    admin:~ admin$ cd cvprac
-
-Step 2: Check out the desired version or branch
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-::
-
-    # Go to a directory where you'd like to keep the source
-    admin:~ admin$ cd ~/projects/cvprac
-
-    # To see a list of available versions or branches
-    admin:~ admin$ git tag
-    admin:~ admin$ git branch
-
-    # Checkout the desired version of code
-    admin:~ admin$ git checkout v1.0.3
-
-Step 3: Install cvprac using Pip with -e switch
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-::
-
-    # Go to a directory where you'd like to keep the source
-    admin:~ admin$ cd ~/projects/cvprac
-
-    # Install
-    admin:~ admin$ sudo pip install -e ~/projects/cvprac
-
-Step 4: Install cvprac development requirements
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-::
-
-    # Go to a directory where you'd like to keep the source
-    admin:~ admin$ pip install -r dev-requirements.txt
-
-Getting Started
-===============
-
-Once the package has been installed you can run the following example to
-verify that everything has been installed properly.
-
-Connecting
-----------
-
-Connecting to CVP will depend on your CVP setup. Several options are outlined below.
-
-CVP On Premises
----------------
-
-CVP On Premises is for users with CVP running on a local server or cluster of servers. This is the
-standard form of connection. Multiple examples below demonstrate connecting to CVP On Premises setups.
-
-CVaaS
------
-
-CVaaS is CloudVision as a Service. Users with CVaaS must use a REST API token for accessing CVP with REST APIs.
-
-   In the case where users authenticate with CVP (CVaaS) using Oauth a REST API token is required to be generated
-   and used for running REST APIs. In this case no username/password login is necessary, but the API token
-   (via api_token parameter) must be provided to cvprac client with the is_cvaas parameter.
-   In the case that the api_token is used for REST APIs the username and password will be ignored and
-   the tenant parameter is not needed.
-
-An example of a CVaaS connection is shown below.
-
-Note that the token parameter was previously cvaas_token but this has been converted to api_token because
-tokens are also available for usage with On Prem CVP deployments. The api_token parameter name is more
-generic in this sense. If you are using the cvaas_token parameter please convert to api_token because the
-cvaas_token parameter will be deprecated in the future.
-
-
-CVP Version Handling
---------------------
-
-The CVP RESTful APIs often change between releases of CVP. Cvprac attempts to mask these API changes from
-the user via making appropriate API calls based on the CVP version while attempting to maintain return data and
-not changing function names when possible. This helps maintain backward compatibility for users when they upgrade CVP
-so that any custom automation/scripts will continue to work. In some cases maintaining return data requires
-additional API calls so there are cases where this comes with the cost of a slight performance hit. Users are free
-to access the clients get(), post() and delete() methods and make API calls directly if they want to avoid the
-potential time delay of some API functions. The current API version information handled by cvprac is shown below.
-
-  Current latest API version is 4.0
-  API version is set to latest available version for CVaaS
-  API version is set to 4.0 for 2020.1.1 and beyond.
-  API version is set to 3.0 for 2019.0.0 through 2020.1.0
-  API version is set to 2.0 for 2018.2.X
-  API version is set to 1.0 for 2018.1.X and prior
-
-Examples
---------
-
-Example using CVP On Prem client get method directly:
-
-::
-
-    >>> from cvprac.cvp_client import CvpClient
-    >>> clnt = CvpClient()
-    >>> clnt.connect(['cvp1', 'cvp2', 'cvp3'], 'cvp_user', 'cvp_word')
-    >>> result = clnt.get('/cvpInfo/getCvpInfo.do')
-    >>> print result
-    {u'version': u'2016.1.0'}
-    >>>
-
-Same example as above using the API method:
-
-::
-
-    >>> from cvprac.cvp_client import CvpClient
-    >>> clnt = CvpClient()
-    >>> clnt.connect(['cvp1', 'cvp2', 'cvp3'], 'cvp_user', 'cvp_word')
-    >>> result = clnt.api.get_cvp_info()
-    >>> print result
-    {u'version': u'2016.1.0'}
-    >>>
-
-Same example as above but connecting to CVaaS with a token:
-Note that the username and password parameters are required by the connect function but will be ignored when using api_token:
-
-::
-
-    >>> from cvprac.cvp_client import CvpClient
-    >>> clnt = CvpClient()
-    >>> clnt.connect(nodes=['cvaas'], username='', password='', is_cvaas=True, api_token='user token')
-    >>> result = clnt.api.get_cvp_info()
-    >>> print result
-    {u'version': u'cvaas'}
-    >>>
-
-Example using the API method to create a container, wait 5 seconds, then
-delete the container. Before running this example manually create a
-container named DC-1 on your CVP node.
-
-::
-
-    >>> import time
-    >>> from cvprac.cvp_client import CvpClient
-    >>> clnt = CvpClient()
-    >>> clnt.connect(['cvp1'], 'cvp_user', 'cvp_word')
-    >>> parent = clnt.api.search_topology('DC-1')
-    >>> clnt.api.add_container('TORs', 'DC-1', parent['containerList'][0]['key'])
-    >>> child = clnt.api.search_topology('TORs')
-    >>> time.sleep(5)
-    >>> result = clnt.api.delete_container('TORs', child['containerList'][0]['key'], 'DC-1', parent['containerList'][0]['key'])
-    >>>
-
-Notes for API Class Usage
-=========================
-
-Containers
-----------
-
-With the API the containers are added for all cases. If you add the
-container to the original root container ‘Tenant’ then you have to do a
-refresh from the GUI to see the container after it is added or deleted.
-If the root container has been renamed or the parent container is not
-the root container then an add or delete will update the GUI without
-requiring a manual refresh.
-
-Testing
-=======
-
-The cvprac module provides system tests. To run the system tests, you
-will need to update the ``cvp_nodes.yaml`` file found in test/fixtures.
-
-Requirements for running the system tests:
-
--  Need one CVP node for test with a test user account. Create the same
-   account on the switch used for testing. The user account information
-   follows:
-
-::
-
-    username: CvpRacTest
-    password: AristaInnovates
-
-    If switch does not have correct username and/or password then the tests that
-    execute tasks will fail with the following error:
-
-    AssertionError: Execution for task id 220 failed
-
-    and in the test log is the error:
-
-    Failure response received from the netElement : ' Unauthorized User '
-
--  Test has dedicated access to the CVP node.
-
--  CVP node contains at least one device in a container.
-
--  Container or device has at least one configlet applied.
-
-To run the system tests:
-
--  run ``make tests`` from the root of the cvprac source folder.
-
-Contributing
-============
-
-Contributing pull requests are gladly welcomed for this repository.
-Please note that all contributions that modify the library behavior
-require corresponding test cases otherwise the pull request will be
-rejected.
-
-Contact/Questions
-=================
-
-Cvprac is developed by Arista EOS+ CS and supported by the Arista EOS+ community. Support for the code is provided on a best effort basis by the Arista EOS+ CS team and the community. You can contact the team that develops these modules by sending an email to eosplus-dev@arista.com.
-
-For customers that are looking for a premium level of support, please contact your local account team or email eosplus@arista.com for help.
-
-License
-=======
-
-Copyright\ |copy| 2020, Arista Networks, Inc. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-* Redistributions of source code must retain the above copyright notice,
-  this list of conditions and the following disclaimer.
-
-- Redistributions in binary form must reproduce the above copyright
-  notice, this list of conditions and the following disclaimer in the
-  documentation and/or other materials provided with the distribution.
-
-* Neither the name of Arista Networks nor the names of its contributors
-  may be used to endorse or promote products derived from this software
-  without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
-IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
-TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
-PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS BE
-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
-THE POSSIBILITY OF SUCH DAMAGE.
-
-.. |copy|   unicode:: U+000A9 .. COPYRIGHT SIGN
-.. |trademark| unicode:: U+2122 .. TRADEMARK SIGN
-.. |reg| unicode:: U+000AE .. REGISTERED SIGN
-.. |pypi_version_badge| image:: https://img.shields.io/pypi/v/cvprac.svg
-    :target: https://pypi.python.org/pypi/cvprac
-.. |System_test_Status| image:: https://revproxy.arista.com/eosplus/ci/buildStatus/icon?job=Pipeline_jerearista_test/cvprac-rb/develop&style=plastic
-   :target: https://revproxy.arista.com/eosplus/ci/job/Pipeline_jerearista_test/cvprac-rb/develop
diff --git a/VERSION b/VERSION
index 238d6e8..9084fa2 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.0.7
+1.1.0
diff --git a/cvprac/__init__.py b/cvprac/__init__.py
index 4828605..655b918 100644
--- a/cvprac/__init__.py
+++ b/cvprac/__init__.py
@@ -32,5 +32,5 @@
 ''' RESTful API Client class for Cloudvision(R) Portal
 '''
 
-__version__ = '1.0.7'
+__version__ = '1.1.0'
 __author__ = 'Arista Networks, Inc.'
diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py
index df1e097..858032b 100644
--- a/cvprac/cvp_api.py
+++ b/cvprac/cvp_api.py
@@ -367,8 +367,8 @@ def get_configlets(self, start=0, end=0):
         configlets = self.clnt.get('/configlet/getConfiglets.do?'
                                    'startIndex=%d&endIndex=%d' % (start, end),
                                    timeout=self.request_timeout)
-        if self.clnt.apiversion == 1.0:
-            self.log.debug('v1 Inventory API Call')
+        if self.clnt.apiversion == 1.0 or self.clnt.apiversion >= 4.0:
+            self.log.debug('v1/v4+ Inventory API Call')
             return configlets
         else:
             self.log.debug('v2 Inventory API Call')
@@ -1231,7 +1231,7 @@ def add_note_to_configlet(self, key, note):
         return self.clnt.post('/configlet/addNoteToConfiglet.do',
                               data=data, timeout=self.request_timeout)
 
-    def validate_config(self, device_mac, config):
+    def validate_config_for_device(self, device_mac, config):
         ''' Validate a config against a device
 
             Args:
@@ -1242,12 +1242,27 @@ def validate_config(self, device_mac, config):
                 response (dict): A dict that contains the result of the
                     validation operation
         '''
-        self.log.debug('validate_config: name: %s config: %s'
+        self.log.debug('validate_config_for_device: device_mac: %s config: %s'
                        % (device_mac, config))
         body = {'netElementId': device_mac, 'config': config}
-        # Invoke the validate API call
-        result = self.clnt.post('/configlet/validateConfig.do', data=body,
-                                timeout=self.request_timeout)
+        return self.clnt.post('/configlet/validateConfig.do', data=body,
+                              timeout=self.request_timeout)
+
+    def validate_config(self, device_mac, config):
+        ''' Validate a config against a device and parse response to
+            produce log messages are return a flag for the config validity.
+
+            Args:
+                device_mac (str): Device MAC address
+                config (str): Switch config statements
+
+            Returns:
+                response (boolean): A flag signifying if the config is valid or
+                    not.
+        '''
+        self.log.debug('validate_config: device_mac: %s config: %s'
+                       % (device_mac, config))
+        result = self.validate_config_for_device(device_mac, config)
         validated = True
         if 'warningCount' in result and result['warnings']:
             for warning in result['warnings']:
@@ -2446,7 +2461,7 @@ def create_change_control(self, name, change_control_tasks, timezone,
         return self.clnt.post('/changeControl/addOrUpdateChangeControl.do',
                               data=data, timeout=self.request_timeout)
 
-    def create_change_control_v3(self, cc_id, name, tasks):
+    def create_change_control_v3(self, cc_id, name, tasks, sequential=True):
         ''' Create change control with provided information and return
             change control ID.
 
@@ -2455,6 +2470,8 @@ def create_change_control_v3(self, cc_id, name, tasks):
                 name (string): The name for the new change control.
                 tasks (list): A list of Task IDs as strings
                     Ex: ['10', '11', '12']
+                sequential (bool): A flag for running tasks sequentially or
+                    in parallel. Defaults to True for running sequentially.
 
             Returns:
                 response (dict): A dict that contains...
@@ -2475,18 +2492,34 @@ def create_change_control_v3(self, cc_id, name, tasks):
 
         self.log.debug('v3 Update change control API Call')
         stages = []
-        for index, task in enumerate(tasks):
-            stage_id = 'stage%d' % index
-            stage = {'stage': [{
-                'id': stage_id,
-                'action': {
-                    'name': 'task',
-                    'args': {
-                        'TaskID': task,
+        if sequential:
+            for index, task in enumerate(tasks):
+                stage_id = 'stage%d' % index
+                stage = {'stage': [{
+                    'id': stage_id,
+                    'action': {
+                        'name': 'task',
+                        'args': {
+                            'TaskID': task,
+                        }
+                    }
+                }]}
+                stages.append(stage)
+        else:
+            stage_rows = []
+            for index, task in enumerate(tasks):
+                stage_id = 'stage%d' % index
+                stage_row = {
+                    'id': stage_id,
+                    'action': {
+                        'name': 'task',
+                        'args': {
+                            'TaskID': task,
+                        }
                     }
                 }
-            }]}
-            stages.append(stage)
+                stage_rows.append(stage_row)
+            stages.append({'stage': stage_rows})
         data = {'config': {
             'id': cc_id,
             'name': name,
@@ -2553,14 +2586,15 @@ def execute_change_controls(self, cc_ids):
         return self.clnt.post('/changeControl/executeCC.do', data=data,
                               timeout=self.request_timeout)
 
-    def approve_change_control(self, cc_id,
-                               timestamp=datetime.utcnow().isoformat() + 'Z'):
+    def approve_change_control(self, cc_id, timestamp=None):
         ''' Cancel the provided change controls.
 
             Args:
                 cc_id (string): The change control IDs to be approved.
                 timestamp(string): The change controls timestamp.
         '''
+        if not timestamp:
+            timestamp = datetime.utcnow().isoformat() + 'Z'
         self.log.debug('approve_change_control')
         if self.clnt.apiversion is None:
             self.get_cvp_info()
@@ -2843,3 +2877,923 @@ def deploy_device(self, device, container, configlets=None,
         if create_task:
             return self._save_topology_v2([])
         return None
+
+    def create_enroll_token(self, duration, devices=None):
+        ''' Create TerminAttr enrollment token for device authentication
+            via certificates.
+
+            Args:
+               devices (list): list of device Serial Numbers for which the
+                   token should be generated. The default is all devices.
+               duration (string): the token's validity time (max 1 month),
+                  accepted formats are: "24h", "86400s", "60m"
+            Returns:
+                response (list) on CVaaS: A list that contains the generated
+                    enrollment token.
+
+                    Ex: [{'enrollmentToken':{'token': <token>, 'groups': [],
+                    'reenrollDevices': <devices list>,
+                    'validFor': <duration e.g 24h>, 'field_mask': None}}]
+                response (dict) on CV on-prem: A dictionary that contains the
+                    generated enrollment token.
+
+                    Ex: {'data': <token>}
+        '''
+        if not devices:
+            devices = ["*"]
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion >= 6.0:
+                self.log.debug('v6 /cvpservice/enroll/createToken')
+                data = {"reenrollDevices": devices, "duration": duration}
+                return self.clnt.post('/cvpservice/enroll/createToken',
+                                      data=data, timeout=self.request_timeout)
+            self.log.warning(
+                'Enrollment Tokens only supported on CVP 2021.2.0+')
+            return None
+        data = {
+            "enrollmentToken": {"reenrollDevices": devices,
+                                "validFor": duration}
+        }
+        return self.clnt.post(
+            '/api/v3/services/admin.Enrollment/AddEnrollmentToken',
+            data=data, timeout=self.request_timeout)
+
+    def get_all_tags(self, element_type='ELEMENT_TYPE_UNSPECIFIED', workspace_id=''):
+        ''' Get all device and/or interface tags from the mainline workspace or all other workspaces
+            Args:
+               element_type (str): Can be ELEMENT_TYPE_DEVICE, ELEMENT_TYPE_INTERFACE and
+                  ELEMENT_TYPE_UNSPECIFIED
+                  set to ELEMENT_TYPE_UNSPECIFIED by default which fetches all tags
+               workspace_id (str): The ID of the workspace, by default it is set to an empty string
+                  which will use the mainline workspace
+            Returns:
+               response (dict): A dict that contains a list of key-value tags
+        '''
+        tag_url = '/api/resources/tag/v2/Tag/all'
+        payload = {
+            "partialEqFilter": [
+                {"key": {"elementType": element_type, "workspaceId": workspace_id}}
+            ]
+        }
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning('Tag.V2 Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 {}'.format(tag_url))
+        return self.clnt.post(tag_url, data=payload)
+
+    def get_tag_edits(self, workspace_id):
+        ''' Show all tags edits in a workspace
+
+            Args:
+                workspace_id: The ID of the workspace
+
+            Returns:
+               response (dict): A dict that contains...
+                    Ex.: {'data': [{'result': {'value': {'key': {'workspaceId': 'testget',
+                          'elementType': 'string', 'label': 'string', 'value': 'string'},
+                          'remove': False}, 'time': 'rfc3339 time', 'type': 'INITIAL'}}]}
+        '''
+        tag_url = '/api/resources/tag/v2/TagConfig/all'
+        payload = {
+            "partialEqFilter": [
+                {
+                    "key": {
+                        "workspace_id": workspace_id
+                    }
+                }
+            ]
+        }
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning('Tag.V2 Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 ' + tag_url + ' ' + str(payload))
+        return self.clnt.post(tag_url, data=payload)
+
+    def get_tag_assignment_edits(self, workspace_id):
+        ''' Show all tags assignment edits in a workspace
+
+            Args:
+                workspace_id: The ID of the workspace
+
+            Returns:
+               response (dict): A dict that contains...
+                Ex: {'result': {'value': {'key': {'workspaceId': 'string', 'elementType': 'string',
+                'label': 'string', 'value': 'string', 'deviceId': 'string'},
+                'remove': False}, 'time': 'rfc3339', 'type': 'INITIAL'}}
+        '''
+        tag_url = '/api/resources/tag/v2/TagAssignmentConfig/all'
+        payload = {
+            "partialEqFilter": [
+                {
+                    "key": {
+                        "workspace_id": workspace_id
+                    }
+                }
+            ]
+        }
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning('Tag.V2 Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 ' + tag_url + ' ' + str(payload))
+        return self.clnt.post(tag_url, data=payload)
+
+    def tag_config(self, element_type, workspace_id, tag_label, tag_value, remove=False):
+        ''' Create/Delete device or interface tags.
+            Tag creation with the tag.v2 resource API has to be done within a workspace.
+
+            Args:
+               element_type (str): Can be ELEMENT_TYPE_DEVICE or ELEMENT_TYPE_INTERFACE to
+                    create device and interface tag respectively.
+               workspace_id(str): The ID of the workspace.
+                    This should be generated by the create_workspace() API call.
+               tag_label(str): the label of the desired tag
+               tag_value(str): the value of the desired tag
+               remove (Boolean): When set to True it will remove the device/interface tag.
+                    When set to False (default) it will create the device/interface tag.
+
+            Returns:
+                response (dict): A dict that contains...
+                    Ex: {'value': {'key': {'workspaceId': 'string', 'elementType': 'string',
+                                        'label': 'string', 'value': 'string'}},
+                         'time': 'rfc3339 time'}
+        '''
+        tag_url = '/api/resources/tag/v2/TagConfig'
+        payload = {
+            "key": {
+                "elementType": element_type,
+                "workspaceId": workspace_id,
+                "label": tag_label,
+                "value": tag_value
+            },
+            "remove": remove
+        }
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning('Tag.V2 Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 {} '.format(tag_url) + str(payload))
+        return self.clnt.post(tag_url, data=payload)
+
+    def tag_assignment_config(self, element_type, workspace_id, tag_label,
+                              tag_value, device_id, interface_id, remove=False):
+        ''' Assign/Unassign device or interface tags.
+            Tag assignment with the tag.v2 resource API has to be done within a workspace.
+
+            Args:
+               element_type (str): can be ELEMENT_TYPE_DEVICE or ELEMENT_TYPE_INTERFACE to
+                    create device and interface tag respectively
+               workspace_id(str): the ID of the workspace. This should be generated by
+                    the create_workspace() API call.
+               tag_label(str): the label of the desired tag
+               tag_value(str): the value of the desired tag
+               device_id (str): the Serial Number of the device
+               interface_id (str): the interface name of the device, e.g.: Ethernet1
+               remove (Boolean): When set to True it will remove the device/interface
+                    tag assignment.
+                    When set to False (default) it will create the device/interface tag assignment.
+            Returns:
+               response (dict): A dict that contains...
+
+                    Ex: {'value': {'key': {'workspaceId': 'string', 'elementType': 'string',
+                                           'label': 'string', 'value': 'string',
+                                           'deviceId': 'string', 'interfaceId': 'string'},
+                                   'remove': Boolean},'time': 'rfc3339 time'}
+
+        '''
+        tag_url = '/api/resources/tag/v2/TagAssignmentConfig'
+        payload = {
+            "key": {
+                "elementType": element_type,
+                "workspaceId": workspace_id,
+                "label": tag_label,
+                "value": tag_value,
+                "deviceId": device_id,
+                "interfaceId": interface_id
+            },
+            "remove": remove
+        }
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning('Tag.V2 Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 {} '.format(tag_url) + str(payload))
+        return self.clnt.post(tag_url, data=payload)
+
+    def get_all_workspaces(self):
+        ''' Get state information for all workspaces
+
+            Returns:
+               response (dict): A dict that contains a list of key-values for workspaces
+        '''
+        workspace_url = '/api/resources/workspace/v1/Workspace/all'
+        payload = {}
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning('Workspace Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 {}'.format(workspace_url))
+        return self.clnt.post(workspace_url, data=payload)
+
+    def get_workspace(self, workspace_id):
+        ''' Get state information for all workspaces
+
+            Returns:
+               response (dict): A dict that contains a list of key-values for workspaces
+        '''
+        workspace_url = '/api/resources/workspace/v1/Workspace?key.workspaceId={}'.format(
+            workspace_id)
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning('Workspace Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 {}'.format(workspace_url))
+        return self.clnt.get(workspace_url)
+
+    def workspace_config(self, workspace_id, display_name,
+                         description='', request='REQUEST_UNSPECIFIED',
+                         request_id=''):
+        ''' Create, Build and Submit workspaces.
+
+            Args:
+                workspace_id (str): The (unique) name of the workspace.
+                    Previously used names cannot be used if the workspace was closed or abandoned.
+                display_name (str): The display name of the workspace.
+                description (str): The description of the workspace.
+                request (string): Can have the following values:
+                    - REQUEST_UNSPECIFIED
+                    - REQUEST_START_BUILD
+                    - REQUEST_CANCEL_BUILD
+                    - REQUEST_SUBMIT
+                    - REQUEST_ABANDON
+                    - REQUEST_ROLLBACK
+                request_id (str): An arbitrary requestId that is required for the
+                    build and submit process.
+
+            Returns:
+               response (dict): A dict that contains...
+                    Ex: {'value': {'key': {'workspaceId': 'string'},
+                                'displayName': 'string','description': 'string',
+                                'requestParams': {'requestId': 'string'}
+                                },
+                         'time': 'rfc3339 time'}
+        '''
+        workspace_url = '/api/resources/workspace/v1/WorkspaceConfig'
+        payload = {
+            "key": {
+                "workspaceId": workspace_id
+            },
+            "displayName": display_name,
+            "description": description,
+            "request": request,
+            "requestParams": {
+                "requestId": request_id
+            }
+        }
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning('Workspace Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 ' + str(workspace_url) + ' ' + str(payload))
+        return self.clnt.post(workspace_url, data=payload)
+
+    def workspace_build_status(self, workspace_id, build_id):
+        ''' Verify the state of the workspace build process.
+
+            Args:
+                workspace_id (str): The (unique) name of the workspace.
+                build_id (str): The buildId of the workspace for which the
+                    build process was requested.
+            Returns:
+                response (dict): A dict that contains...
+                    Ex: {'value': {'key': {'workspaceId': 'string', 'buildId': 'string'},
+                         'state': 'BUILD_STATE_SUCCESS', 'buildResults': {'values': ...
+        '''
+        params = 'key.workspaceId={}&key.buildId={}'.format(workspace_id, build_id)
+        workspace_url = '/api/resources/workspace/v1/WorkspaceBuild?' + params
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning('Workspace Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 {}'.format(workspace_url + params))
+        return self.clnt.get(workspace_url, timeout=self.request_timeout)
+
+    def change_control_get_one(self, cc_id, cc_time=None):
+        ''' Get the configuration and status of a change control using Resource APIs.
+            Supported versions: CVP 2021.2.0 or newer and CVaaS.
+
+            Args:
+               cc_id (str): The ID of the change control.
+               cc_time (str): Time indicates the time for which you are interested in the data.
+                    If no time is given, the server will use the time at which it makes the request.
+                    The time format is RFC 3339, e.g.: 2021-12-24T11:30:00.00Z.
+            Returns:
+               response (dict): A dict that contains...
+                  Ex: {"value":{"key":{"id":"rL6Tog6UU"}, "change":{"name":"Change 20211213_210554",
+                       "rootStageId":"kZUWqyIArD",
+                       "stages":{"values":{"kZUWqyIArD":{"name":"Change 20211213_210554 Root",
+                       "rows":{"values":[{"values":["vazWhKyVRR"]}]}},
+                       "vazWhKyVRR":{"name":"Update Config",
+                       "action":{"name":"task", "timeout":3000,
+                       "args":{"values":{"TaskID":"538"}}}}}},
+                       "notes":"", "time":"2021-12-13T21:05:58.813750128Z", "user":"cvpadmin"},
+                       "approve":{"value":true, "time":"2021-12-13T21:11:26.788753264Z",
+                       "user":"cvpadmin"}}, "time":"2021-12-13T21:11:26.788753264Z"}%
+        '''
+        if cc_time is None:
+            params = 'key.id={}'.format(cc_id)
+        else:
+            params = 'key.id={}&time={}'.format(cc_id, cc_time)
+        cc_url = '/api/resources/changecontrol/v1/ChangeControl?' + params
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning(
+                    'Change Control Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 {}'.format(cc_url))
+        try:
+            response = self.clnt.get(cc_url, timeout=self.request_timeout)
+        except Exception as error:
+            if 'resource not found' in str(error):
+                return None
+            raise error
+        return response
+
+    def change_control_get_all(self):
+        ''' Get the configuration and status of all Change Controls using Resource APIs.
+            Supported versions: CVP 2021.2.0 or newer and CVaaS.
+
+            Returns:
+               response (dict): A dict that contains a list of all Change Controls.
+        '''
+        cc_url = '/api/resources/changecontrol/v1/ChangeControl/all'
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning(
+                    'Change Control Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 {}'.format(cc_url))
+        return self.clnt.get(cc_url, timeout=self.request_timeout)
+
+    def change_control_approval_get_one(self, cc_id, cc_time=None):
+        ''' Get the state of a specific Change Control's approve config using Resource APIs.
+            Supported versions: CVP 2021.2.0 or newer and CVaaS.
+
+            Args:
+               cc_id (str): The ID of the change control.
+               cc_time (str): Time indicates the time for which you are interested in the data.
+                    If no time is given, the server will use the time at which it makes the request.
+                    The time format is RFC 3339, e.g.: 2021-12-24T11:30:00.00Z.
+            Returns:
+               response (dict): A dict that contains...
+                    Ex: {'value': {'key': {'id': '<CC ID>'}, 'approve': {'value': True},
+                         'version': '2021-12-13T21:05:58.813750128Z'},
+                         'time': '2021-12-13T21:11:26.788753264Z'}
+        '''
+        if cc_time is None:
+            params = 'key.id={}'.format(cc_id)
+        else:
+            params = 'key.id={}&time={}'.format(cc_id, cc_time)
+        cc_url = '/api/resources/changecontrol/v1/ApproveConfig?' + params
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning(
+                    'Change Control Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        cc_status = self.change_control_get_one(cc_id)
+        if cc_status is None:
+            return None
+        if 'value' in cc_status and 'approve' not in cc_status['value']:
+            self.log.warning("The change has not been approved yet."
+                             " A change has to be approved at least once for the 'approve'"
+                             " state to be populated.")
+            return None
+        return self.clnt.get(cc_url, timeout=self.request_timeout)
+
+    def change_control_approval_get_all(self):
+        ''' Get state information for all Change Control Approvals using Resource APIs.
+            Supported versions: CVP 2021.2.0 or newer and CVaaS.
+
+            Returns:
+               response (dict): A dict that contains a list of all Change Control Approval Configs.
+        '''
+        cc_url = '/api/resources/changecontrol/v1/ApproveConfig/all'
+        # For on-prem check the version as it is only supported from 2021.2.0+
+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning(
+                    'Change Control Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 {}'.format(cc_url))
+        return self.clnt.get(cc_url, timeout=self.request_timeout)
+
+    def change_control_approve(self, cc_id, notes="", approve=True):
+        ''' Approve/Unapprove a change control using Resource APIs.
+            Supported versions: CVP 2021.2.0 or newer and CVaaS.
+
+            Args:
+              cc_id (str): The ID of the change control.
+              notes (str): An optional approval note.
+              approve (bool): Set to True to approve a change and to False to unapprove a change.
+              The default is True.
+        '''
+        cc_url = '/api/resources/changecontrol/v1/ApproveConfig'
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        # Since the get_change_control already checks this, no need to check it again
+        cc_status = self.change_control_get_one(cc_id)
+        if cc_status is None:
+            return None
+        if ('value' in cc_status and 'change' in cc_status['value'] and
+                'time' in cc_status['value']['change']):
+            version = cc_status['value']['change']['time']
+        else:
+            self.log.error('The version timestamp was not found in the CC status.')
+            return None
+        payload = {
+            "key": {
+                "id": cc_id
+            },
+            "approve": {
+                "value": approve,
+                "notes": notes
+            },
+            "version": version
+        }
+        return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout)
+
+    def change_control_delete(self, cc_id):
+        ''' Delete a pending Change Control using Resource APIs.
+            Supported versions: CVP 2021.2.0 or newer and CVaaS.
+
+            Args:
+              cc_id (str): The ID of the change control.
+        '''
+        params = 'key.id={}'.format(cc_id)
+        cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig?' + params
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning(
+                    'Change Control Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 {}'.format(cc_url))
+        return self.clnt.delete(cc_url, timeout=self.request_timeout)
+
+    def change_control_create_with_custom_stages(self, custom_cc=None):
+        ''' Create a Change Control with custom stage hierarchy using Resource APIs.
+            Supported versions: CVP 2021.2.0 or newer and CVaaS.
+
+            Args:
+                custom_cc (dict): A dictionary with the entire stage hierarchy.
+                Ex1: {'key': {'id': '409b94d1-c0cb-4d74-8f88-89f66f13f109'},
+                     'change': {'name': 'Change_20211217_034338',
+                     'notes': 'cvprac CC',
+                     'rootStageId': 'root',
+                     'stages': {'values': {'root': {'name': 'root',
+                                                'rows': {'values': [{'values': ['1-2']},
+                                                                    {'values': ['3']}]}},
+                                        '1-2': {'name': 'stages 1-2',
+                                                'rows': {'values': [{'values': ['1ab']},
+                                                                    {'values': ['2']}]}},
+                                        '1a': {'action': {'args': {'values': {'TaskID': '1242'}},
+                                                            'name': 'task',
+                                                            'timeout': 3000},
+                                                'name': 'stage 1a'},
+                                        '1ab': {'name': 'stage 1ab',
+                                                'rows': {'values': [{'values': ['1a',
+                                                                                '1b']}]}},
+                                        '1b': {'action': {'args': {'values': {'TaskID': '1243'}},
+                                                            'timeout': 3000},
+                                                'name': 'stage 1b'},
+                                        '2': {'action': {'args': {'values': {'TaskID': '1240'}},
+                                                        'name': 'task',
+                                                        'timeout': 3000},
+                                                'name': 'stage 2'},
+                                        '3': {'action': {'args': {'values': {'TaskID': '1241'}},
+                                                        'name': 'task',
+                                                        'timeout': 3000},
+                                                'name': 'stage 3'},
+                                        }}}}
+                The above would result in the following hierarchy:
+                    root (series)
+                    |- stages 1-2 (series)
+                    |  |- stage 1ab (parallel)
+                    |  |    |- stage 1a
+                    |  |    |- stage 1b
+                    |  |- stage 2
+                    |- stage 3
+
+                Ex2 (MLAG ISSU):
+                    {'key': {'id': 'PXs9cKimC'},
+                     'change': {'name': 'Change 20211217_040530',
+                     'notes': '',
+                     'rootStageId': 'root',
+                     'stages': {'values': { 'root': {'name': 'Change 20211217_040530 Root',
+                                                     'rows': {
+                                                         'values': [{'values': ['left-leafs']}],
+                                                     }},
+                                            'upgrade1': {'action': {
+                                                             'args': {'values': {'TaskID': '1242'}},
+                                                             'name': 'task',
+                                                             'timeout': 3000},
+                                                         'name': 'Image Upgrade'},
+                                            'pre-mlag-check-l2': {'action': {
+                                                                      'args': {
+                                                                          'values': {
+                                                                              'DeviceID': 'SN2'}},
+                                                                      'name': 'mlaghealthcheck'},
+                                                          'name': 'Check MLAG Health'},
+                                            'left-leafs': {'name': 'left-leafs',
+                                                          'rows': {
+                                                              'values': [{'values': ['leaf1']},
+                                                                         {'values': ['leaf2']}]}},
+                                            'upgrade2': {'action': {'args': {
+                                                                      'values': {'TaskID': '1243'}},
+                                                                      'name': 'task',
+                                                                      'timeout': 3000},
+                                                           'name': 'Image Upgrade'},
+                                            'pre-mlag-check-l1': {'action': {
+                                                                      'args': {
+                                                                          'values': {
+                                                                              'DeviceID': 'SN1'}},
+                                                                      'name': 'mlaghealthcheck'},
+                                                          'name': 'Check MLAG Health'},
+                                            'post-mlag-check-l2': {'action': {
+                                                                       'args': {
+                                                                           'values': {
+                                                                               'DeviceID': 'SN1'}},
+                                                                       'name': 'mlaghealthcheck'},
+                                                           'name': 'Check MLAG Health'},
+                                            'leaf1': {'name': 'leaf1',
+                                                          'rows': {
+                                                              'values': [{'values': [
+                                                                              'pre-mlag-check-l1']},
+                                                                         {'values': [
+                                                                              'upgrade1']},
+                                                                         {'values': [
+                                                                              'post-mlag-check-l1'],
+                                                                         }]}},
+                                            'post-mlag-check-l1': {'action': {
+                                                                       'args': {
+                                                                           'values': {
+                                                                               'DeviceID': 'SN2'}},
+                                                                      'name': 'mlaghealthcheck'},
+                                                           'name': 'Check MLAG Health'},
+                                            'leaf2': {'name': 'leaf2',
+                                                      'rows': {'values': [{'values': [
+                                                                              'pre-mlag-check-l2']},
+                                                                          {'values': [
+                                                                               'upgrade2']},
+                                                                          {'values': [
+                                                                              'post-mlag-check-l2'],
+                                                                          }]}}}}}
+                    }
+                    The above would result in the following hierarchy:
+                    root (series)
+                    |- left-leafs (series)
+                       |- leaf1 (series)
+                       |    |- pre-mlag-check-l1
+                       |    |- upgrade1
+                       |    |- post-mlag-check-l1
+                       |- leaf2 (series)
+                            |- pre-mlag-check-l1
+                            |- upgrade1
+                            |- post-mlag-check-l1
+
+            Returns:
+                response (dict): A dict that contains...
+                Ex: {'value': {'key': {'id':cc_id,
+                      'time': '...'}
+        '''
+        payload = custom_cc
+        cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig'
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning(
+                    'Change Control Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload))
+        return self.clnt.post(cc_url, data=payload)
+
+    def change_control_create_for_tasks(self, cc_id, name, tasks, series=True):
+        ''' Create a simple Change Control for tasks using Resource APIs.
+            Supported versions: CVP 2021.2.0 or newer and CVaaS.
+
+            This function will create a change with either all Task actions in series or parallel.
+            For custom stage hierarchy the change_control_create_with_custom_stages()
+            should be used.
+            Args:
+                cc_id (string): The ID for the new change control.
+                name (string): The name for the new change control.
+                tasks (list): A list of Task IDs as strings
+                    Ex: ['10', '11', '12']
+                series (bool): A flag for running tasks in series or
+                    in parallel. Defaults to True for running in series.
+            Returns:
+                response (dict): A dict that contains...
+                Ex: {'value': {'key': {'id':cc_id,
+                      'time': '...'}
+        '''
+        stages = {'values': {'root': {'name': 'root', 'rows': {'values': []}}}}
+        if series:
+            for index, task in enumerate(tasks):
+                stage_id = 'stage%d' % index
+                stages['values']['root']['rows']['values'].append({'values': [stage_id]})
+                stages['values'][stage_id] = {
+                    'action': {
+                        'args': {
+                            'values': {
+                                'TaskID': task,
+                            },
+                        },
+                        'name': 'task',
+                        'timeout': 3000,
+                    },
+                    'name': stage_id,
+                }
+        else:
+            stages['values']['root']['rows']['values'].append({'values': []})
+            for index, task in enumerate(tasks):
+                stage_id = 'stage%d' % index
+                stages['values']['root']['rows']['values'][0]['values'].append(stage_id)
+                stages['values'][stage_id] = {
+                    'action': {
+                        'args': {
+                            'values': {
+                                'TaskID': task,
+                            },
+                        },
+                        'name': 'task',
+                        'timeout': 3000,
+                    },
+                    'name': stage_id,
+                }
+        payload = {
+            'key': {
+                'id': cc_id
+            },
+            'change': {
+                'name': name,
+                'rootStageId': 'root',
+                'notes': 'randomString',
+                'stages': stages
+            }
+        }
+        cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig'
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning(
+                    'Change Control Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload))
+        return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout)
+
+    def change_control_start(self, cc_id, notes=""):
+        ''' Start a Change Control using Resource APIs.
+            Supported versions: CVP 2021.2.0 or newer and CVaaS.
+            Args:
+                cc_id (string): The ID for the new change control.
+                notes (string): An optional note.
+            Returns:
+                response (dict): A dict that contains...
+                Ex: {"value":{"key":{"id":cc_id}, "start":{"value":true, "notes":"note"}},
+                     "time":"2021-12-14T21:02:21.830306071Z"}
+        '''
+        payload = {
+            "key": {
+                "id": cc_id
+            },
+            "start": {
+                "value": True,
+                "notes": notes
+            }
+        }
+        cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig'
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning(
+                    'Change Control Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload))
+        return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout)
+
+    def change_control_stop(self, cc_id, notes=""):
+        ''' Stop a Change Control using Resource APIs.
+            Supported versions: CVP 2021.2.0 or newer and CVaaS.
+
+            Args:
+                cc_id (string): The ID for the new change control.
+                notes (string): An optional note.
+            Returns:
+                response (dict): A dict that contains...
+                Ex: {"value":{"key":{"id":cc_id}, "start":{"value":false, "notes":"note"}},
+                     "time":"2021-12-14T21:02:21.830306071Z"}
+        '''
+        payload = {
+            "key": {
+                "id": cc_id
+            },
+            "start": {
+                "value": False,
+                "notes": notes
+            }
+        }
+        cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig'
+        # For on-prem check the version as it is only supported from 2021.2.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 6.0:
+                self.log.warning(
+                    'Change Control Resource APIs are supported from 2021.2.0 or newer.')
+                return None
+        self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload))
+        return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout)
+
+    def change_control_schedule(self, cc_id, schedule_time, notes=""):
+        ''' Schedule a Change Control using Resource APIs.
+            Supported versions: CVP 2022.1.0 or newer and CVaaS.
+
+            Args:
+                cc_id (string): The ID for the new change control.
+                schedule_time (string): rfc3339 time format, e.g: 2021-12-23T02:07:00.0Z
+                notes (string): An optional note.
+            Returns:
+                response (dict): A dict that contains...
+                Ex: {"value":{"key":{"id":"5821c7c1-e276-4387-b60a"},
+                              "schedule":{"value":"2021-12-23T02:07:00Z",
+                                          "notes":"CC schedule via curl"}},
+                     "time":"2021-12-23T02:06:18.739965204Z"}
+        '''
+        payload = {
+            "key": {
+                "id": cc_id
+            },
+            "schedule": {
+                "value": schedule_time,
+                "notes": notes
+            }
+        }
+        cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig'
+        # For on-prem check the version as it is only supported from 2022.1.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 8.0:
+                self.log.warning(
+                    'Change Control Scheduling via Resource APIs are supported from 2022.1.0 or newer.')
+                return None
+        self.log.debug('v8 ' + str(cc_url) + ' ' + str(payload))
+        return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout)
+
+    def device_decommissioning(self, device_id, request_id):
+        ''' Decommission a device using Resource APIs.
+            Supported versions: CVP 2021.3.0 or newer and CVaaS.
+            Args:
+                device_id (string): Serial Number of the device.
+                request_id (string): Key identifies the request to decommission the device.
+                    Recommended to generate uuid with str(uuid.uuid4()).
+            Returns:
+                response (dict): Returns None if the device is not found else returns A dict that contains...
+                Ex: {'value': {'key': {'requestId': '4a4ba5a2-9886-4cd5-84d6-bdaf85a9f091'},
+                     'deviceId': 'BAD032986065E8DC14CBB6472EC314A6'},
+                     'time': '2022-02-12T02:58:30.765459650Z'}
+        '''
+        device_info = self.get_device_by_serial(device_id)
+        if device_info is not None and 'serialNumber' in device_info:
+            payload = {
+                "key": {
+                    "request_id": request_id
+                },
+                "device_id": device_id
+            }
+            url = '/api/resources/inventory/v1/DeviceDecommissioningConfig'
+            # For on-prem check the version as it is only supported from 2021.3.0+
+            if not self.clnt.is_cvaas:
+                if self.clnt.apiversion is None:
+                    self.get_cvp_info()
+                if self.clnt.apiversion < 7.0:
+                    self.log.warning(
+                        'Decommissioning via Resource APIs are supported from 2021.3.0 or newer.')
+                    return None
+            self.log.debug('v7 ' + str(url) + ' ' + str(payload))
+            return self.clnt.post(url, data=payload, timeout=self.request_timeout)
+        else:
+            self.log.warning('Device with %s serial number does not exist (or is not registered) to decommission' % device_id)
+            return None
+
+    def device_decommissioning_status_get_one(self, request_id):
+        ''' Get the decommission status of a device using Resource APIs.
+            Supported versions: CVP 2021.3.0 or newer and CVaaS.
+            Args:
+                request_id (string): key identifies the request to decommission the device
+            Returns:
+                response (dict): A dict that contains...
+                Ex:{"result":{"value":{"key":{"requestId":"123456789"},
+                  "status":"DECOMMISSIONING_STATUS_IN_PROGRESS",
+                  "statusMessage":"Disabled TerminAttr, waiting for device to be marked inactive"},
+                  "time":"2022-02-04T19:41:46.376310308Z","type":"INITIAL"}}
+        '''
+        params = 'key.requestId={}'.format(request_id)
+        url = '/api/resources/inventory/v1/DeviceDecommissioning?' + params
+        # For on-prem check the version as it is only supported from 2021.3.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 7.0:
+                self.log.warning(
+                    'Decommissioning via Resource APIs are supported from 2021.3.0 or newer.')
+                return None
+        self.log.debug('v7 ' + str(url))
+        return self.clnt.get(url, timeout=self.request_timeout)
+
+    def device_decommissioning_status_get_all(self, status="DECOMMISSIONING_STATUS_UNSPECIFIED"):
+        ''' Get the decommissioning status of all devices using Resource APIs.
+            Supported versions: CVP 2021.3.0 or newer and CVaaS.
+            Args:
+                status (enum): By default it will get the decommissioning status for all devices.
+                    Possible values:
+                        "DECOMMISSIONING_STATUS_UNSPECIFIED" or 0,
+                        "DECOMMISSIONING_STATUS_IN_PROGRESS" or 1,
+                        "DECOMMISSIONING_STATUS_FAILURE" or 2,
+                        "DECOMMISSIONING_STATUS_SUCCESS" or 3
+            Returns:
+                response (dict): A dict that contains...
+                Ex: {"result":{"value":{"key":{"requestId":"123456789"},
+                "status":"DECOMMISSIONING_STATUS_IN_PROGRESS",
+                "statusMessage":"Disabled TerminAttr, waiting for device to be marked inactive"},
+                "time":"2022-02-04T19:41:46.376310308Z","type":"INITIAL"}}
+        '''
+        payload = {
+            "partialEqFilter": [
+                {
+                    "status": status,
+                }
+            ]
+        }
+        url = '/api/resources/inventory/v1/DeviceDecommissioning/all'
+        # For on-prem check the version as it is only supported from 2021.3.0+
+        if not self.clnt.is_cvaas:
+            if self.clnt.apiversion is None:
+                self.get_cvp_info()
+            if self.clnt.apiversion < 7.0:
+                self.log.warning(
+                    'Decommissioning via Resource APIs are supported from 2021.3.0 or newer.')
+                return None
+        self.log.debug('v7 ' + str(url))
+        return self.clnt.post(url, data=payload, timeout=self.request_timeout)
diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py
index f759ed4..c48262f 100644
--- a/cvprac/cvp_client.py
+++ b/cvprac/cvp_client.py
@@ -114,7 +114,7 @@ class CvpClient(object):
     # Maximum number of times to retry a get or post to the same
     # CVP node.
     NUM_RETRY_REQUESTS = 3
-    LATEST_API_VERSION = 5.0
+    LATEST_API_VERSION = 8.0
 
     def __init__(self, logger='cvprac', syslog=False, filename=None,
                  log_level='INFO'):
@@ -199,8 +199,11 @@ def set_version(self, version):
             For CVP versions 2018.1.X and prior, use api version 1.0
             For CVP versions 2018.2.X, use api version 2.0
             For CVP versions 2019.0.0 through 2020.1.0, use api version 3.0
-            For CVP versions 2020.1.1 through 2020.2.X, use api version 4.0
-            For CVP versions 2020.3.0 and beyond, use api version 5.0
+            For CVP versions 2020.1.1 through 2020.2.3, use api version 4.0
+            For CVP versions 2020.2.4 through 2021.1.x, use api version 5.0
+            For CVP versions 2021.2.x, use api version 6.0
+            For CVP versions 2021.3.x, use api version 7.0
+            For CVP versions 2022.1.0 and beyond, use api version 8.0
 
             Args:
                 version (str): The CVP version in use.
@@ -208,8 +211,11 @@ def set_version(self, version):
         self.version = version
         self.log.info('Version %s', version)
         # Set apiversion to latest available API version for CVaaS
-        # Set apiversion to 5.0 for 2020.2.4 and beyond
-        # Set apiversion to 4.0 for 2020.1.1 through 2020.2.X
+        # Set apiversion to 8.0 for 2022.1.x
+        # Set apiversion to 7.0 for 2021.3.x
+        # Set apiversion to 6.0 for 2021.2.x
+        # Set apiversion to 5.0 for 2020.2.4 through 2021.1.x
+        # Set apiversion to 4.0 for 2020.1.1 through 2020.2.3
         # Set apiversion to 3.0 for 2019.0.0 through 2020.1.0
         # Set apiversion to 2.0 for 2018.2.X
         # Set apiversion to 1.0 for 2018.1.X and prior
@@ -225,7 +231,16 @@ def set_version(self, version):
                               ' Appending 0. Updated Version String - %s',
                               ".".join(version_components))
             full_version = ".".join(version_components)
-            if parse_version(full_version) >= parse_version('2020.2.4'):
+            if parse_version(full_version) >= parse_version('2022.1.0'):
+                self.log.info('Setting API version to v8')
+                self.apiversion = 8.0
+            elif parse_version(full_version) >= parse_version('2021.3.0'):
+                self.log.info('Setting API version to v7')
+                self.apiversion = 7.0
+            elif parse_version(full_version) >= parse_version('2021.2.0'):
+                self.log.info('Setting API version to v6')
+                self.apiversion = 6.0
+            elif parse_version(full_version) >= parse_version('2020.2.4'):
                 self.log.info('Setting API version to v5')
                 self.apiversion = 5.0
             elif parse_version(full_version) >= parse_version('2020.1.1'):
@@ -728,7 +743,9 @@ def _send_request(self, req_type, full_url, timeout, data=None,
                     else:
                         fhs = dict()
                         fhs['Accept'] = self.headers['Accept']
-                        fhs['APP_SESSION_ID'] = self.headers['APP_SESSION_ID']
+                        if 'APP_SESSION_ID' in self.headers:
+                            fhs['APP_SESSION_ID'] = self.headers[
+                                'APP_SESSION_ID']
                         if 'Authorization' in self.headers:
                             fhs['Authorization'] = self.headers[
                                 'Authorization']
diff --git a/docs/labs/README.md b/docs/labs/README.md
index 43652fb..132ee64 100644
--- a/docs/labs/README.md
+++ b/docs/labs/README.md
@@ -3,13 +3,21 @@
 The following lab examples will walk through the most commonly used REST API calls using cvprac
 to help users interact with Arista CloudVision easily and automate the provisioning of network devices.
 
+## Table of Contents
+
+1. [Authentication](#authentication)
+    - [Password Authentication](#password-authentication)
+    - [Service Account Token Authentication](#service-account-token-authentication)
+1. [Known Limitations](#known-limitations)
+
 ## Authentication
 
 There are two ways to authenticate using the REST APIs:
+
 - user/password (on-prem only)
 - service account token (available on CVP 2020.3.0+ and CVaaS)
 
-### User/password authentication
+### Password Authentication
 
 ```python
 from cvprac.cvp_client import CvpClient
@@ -17,9 +25,9 @@ clnt = CvpClient()
 clnt.connect(['10.83.13.33'],'cvpadmin', 'arastra')
 ```
 
-### Service account token authentication
+### Service Account Token Authentication
 
-To access the CloudVision as-a-Service and send API requests, “Service Account Token” is needed.
+To access the CloudVision as-a-Service and send API requests, "Service Account Token" is needed.
 After obtaining the service account token, it can be used for authentication when sending API requests.
 
 Service accounts can be created from the Settings page where a service token can be generated as seen below:
@@ -50,4 +58,10 @@ with open("token.tok") as f:
 
 clnt = CvpClient()
 clnt.connect(nodes=['10.83.13.33'], username='',password='',api_token=token)
-```
\ No newline at end of file
+```
+
+## Known Limitations
+
+- for any APIs that interact with EOS devices, the service account name must match the name of the username
+  configured on EOS and CVP
+- Support for REST API bindings for the Resource APIs (Lab 8) was added in CVP 2021.1.0
diff --git a/docs/labs/lab02-inventory-operations/get_running_configs.py b/docs/labs/lab02-inventory-operations/get_running_configs.py
new file mode 100644
index 0000000..5478f2e
--- /dev/null
+++ b/docs/labs/lab02-inventory-operations/get_running_configs.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2021 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the COPYING file.
+
+from cvprac.cvp_client import CvpClient
+import ssl
+ssl._create_default_https_context = ssl._create_unverified_context
+import requests.packages.urllib3
+requests.packages.urllib3.disable_warnings()
+
+# Create connection to CloudVision
+clnt = CvpClient()
+clnt.connect(nodes=['cvp1'], username="username",password="password")
+
+# Get the full inventory
+inventory = clnt.api.get_inventory()
+
+# Create a list of MAC addresses
+device_macs = []
+for i in inventory:
+    device_macs.append(i['systemMacAddress'])
+
+# Create a dictionary with MAC to running-config mapping
+running_configs = {}
+for i in device_macs:
+    running_configs[i] = clnt.api.get_device_configuration(i)
+
+# Write the running-configs of each device using the hostname as the filename
+for i in inventory:
+    with open(i['fqdn']+'.cfg', 'w') as f:
+        f.write(running_configs[i['systemMacAddress']])
\ No newline at end of file
diff --git a/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py b/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py
new file mode 100644
index 0000000..3b90d70
--- /dev/null
+++ b/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py
@@ -0,0 +1,30 @@
+from cvprac.cvp_client import CvpClient
+import ssl
+ssl._create_default_https_context = ssl._create_unverified_context
+import requests.packages.urllib3
+requests.packages.urllib3.disable_warnings()
+
+clnt = CvpClient()
+clnt.connect(nodes=['cvp1'], username="username",password="password")
+
+ts = "2021-11-19T15:04:05.0Z" # rfc3339 time
+uri = "/api/v3/services/compliancecheck.Compliance/GetConfig"
+
+# Fetch the inventory
+inventory = clnt.api.get_inventory()
+
+# Iterate through all devices and get the running-config at the specified time for each device
+for device in inventory:
+    sn = device['serialNumber']
+    data = {"request":{
+        "device_id": sn,
+        "timestamp": ts,
+        "type":"RUNNING_CONFIG"
+        }
+    }
+    try:
+        resultRunningConfig = clnt.post(uri, data=data)[0]['config']
+        with open(device['hostname']+'.cfg','w') as f:
+            f.write(resultRunningConfig)
+    except Exception as e:
+        print("Not able to get configuration for device {} - exception {}".format(device['fqdn'], e))
diff --git a/docs/labs/lab03-configlet-management/backup_configletsV2.py b/docs/labs/lab03-configlet-management/backup_configletsV2.py
index 8dff0b2..0247f19 100644
--- a/docs/labs/lab03-configlet-management/backup_configletsV2.py
+++ b/docs/labs/lab03-configlet-management/backup_configletsV2.py
@@ -42,6 +42,7 @@ def get_list_of_configlets():
 if __name__ == "__main__":
 
     results = get_list_of_configlets()
-    for configlet in results[0]['data']:
-        with open(configlet['name'],'w') as f:
-            f.write(configlet['config'])
+    for future in results:
+        for configlet in future['data']:
+            with open(configlet['name'],'w') as f:
+                f.write(configlet['config'])
diff --git a/docs/labs/lab06-provisioning/change_control_custom_rapi.py b/docs/labs/lab06-provisioning/change_control_custom_rapi.py
new file mode 100644
index 0000000..0290af7
--- /dev/null
+++ b/docs/labs/lab06-provisioning/change_control_custom_rapi.py
@@ -0,0 +1,81 @@
+# Copyright (c) 2021 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the COPYING file.
+#
+# NOTE: The following example is using the new Change Control Resource APIs supported in 2021.2.0 or newer and in CVaaS.
+# For CVaaS service-account token based auth has to be used.
+
+from cvprac.cvp_client import CvpClient
+import ssl
+import uuid
+from datetime import datetime
+ssl._create_default_https_context = ssl._create_unverified_context
+import requests.packages.urllib3
+requests.packages.urllib3.disable_warnings()
+
+# Create connection to CloudVision
+clnt = CvpClient()
+clnt.connect(['cvp1'],'username', 'password')
+
+
+cc_id = str(uuid.uuid4())
+name = f"Change_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+
+# Create custom stage hierarchy
+# The below example would result in the following hierarchy:
+# root (series)
+# |- stages 1-2 (series)
+# |  |- stage 1ab (parallel)
+# |  |    |- stage 1a
+# |  |    |- stage 1b
+# |  |- stage 2
+# |- stage 3
+data = {'key': {
+            'id': cc_id
+            },
+        'change': {
+            'name': cc_id,
+            'notes': 'cvprac CC',
+            'rootStageId': 'root',
+            'stages': {'values': {'root': {'name': 'root',
+                                           'rows': {'values': [{'values': ['1-2']},
+                                                               {'values': ['3']}]
+                                            }
+                                        },
+                                  '1-2': {'name': 'stages 1-2',
+                                          'rows': {'values': [{'values': ['1ab']},
+                                                              {'values': ['2']}]}},
+                                  '1ab': {'name': 'stage 1ab',
+                                          'rows': {'values': [{'values': ['1a','1b']}]
+                                            }
+                                        },
+                                  '1a': {'action': {'args': {'values': {'TaskID': '1242'}},
+                                                    'name': 'task',
+                                                    'timeout': 3000},
+                                         'name': 'stage 1a'},
+                                  '1b': {'action': {'args': {'values': {'TaskID': '1243'}},
+                                                    'name': 'task',
+                                                    'timeout': 3000},
+                                         'name': 'stage 1b'},
+                                  '2': {'action': {'args': {'values': {'TaskID': '1240'}},
+                                                   'name': 'task',
+                                                   'timeout': 3000},
+                                        'name': 'stage 2'},
+                                  '3': {'action': {'args': {'values': {'TaskID': '1241'}},
+                                                   'name': 'task',
+                                                   'timeout': 3000},
+                                        'name': 'stage 3'},
+                }
+            }
+        }
+ }
+# Create change control from custom stage hierarchy data
+clnt.api.change_control_create_with_custom_stages(data)
+
+# Approve the change control
+approval_note = "Approve CC via cvprac" # notes are optional
+clnt.api.change_control_approve(cc_id, notes=approval_note)
+
+# Start the change control
+start_note = "Starting CC via cvprac" # notes are optional
+clnt.api.change_control_start(cc_id, notes=start_note)
diff --git a/docs/labs/lab06-provisioning/change_control_workflow.py b/docs/labs/lab06-provisioning/change_control_workflow.py
index af1ee4b..c374edf 100644
--- a/docs/labs/lab06-provisioning/change_control_workflow.py
+++ b/docs/labs/lab06-provisioning/change_control_workflow.py
@@ -9,6 +9,7 @@
 requests.packages.urllib3.disable_warnings()
 from datetime import datetime
 
+# Note API token auth method is not yet supported with Change Controls
 clnt = CvpClient()
 clnt.connect(['cvp1'],'username', 'password')
 
diff --git a/docs/labs/lab06-provisioning/change_control_workflow_rapi.py b/docs/labs/lab06-provisioning/change_control_workflow_rapi.py
new file mode 100644
index 0000000..299d16a
--- /dev/null
+++ b/docs/labs/lab06-provisioning/change_control_workflow_rapi.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2021 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the COPYING file.
+#
+# NOTE: The following example is using the new Change Control Resource APIs supported in 2021.2.0 or newer and in CVaaS.
+# For CVaaS service-account token based auth has to be used.
+
+from cvprac.cvp_client import CvpClient
+import ssl
+import uuid
+from datetime import datetime
+ssl._create_default_https_context = ssl._create_unverified_context
+import requests.packages.urllib3
+requests.packages.urllib3.disable_warnings()
+
+# Create connection to CloudVision
+clnt = CvpClient()
+clnt.connect(['cvp1'],'username', 'password')
+
+# Generate change control id and change control name
+cc_id = str(uuid.uuid4())
+name = f"Change_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+
+# Select the tasks and create a CC where all tasks will be run in parallel
+tasks = ["1249","1250","1251","1252"]
+clnt.api.change_control_create_for_tasks(cc_id, name, tasks, series=False)
+
+# Approve the change control
+approve_note = "Approving CC via cvprac"
+clnt.api.change_control_approve(cc_id, notes=approve_note)
+
+# # Schedule the change control
+# # Executing scheduled CCs might only work post 2021.3.0+
+# schedule_note = "Scheduling CC via cvprac"
+# schedule_time = "2021-12-23T03:17:00Z"
+# clnt.api.change_control_schedule(cc_id,schedule_time,notes=schedule_note)
+
+# Start the change control
+start_note = "Start the CC via cvprac"
+clnt.api.change_control_start(cc_id, notes=start_note)
\ No newline at end of file
diff --git a/docs/labs/lab06-provisioning/mlag_issu.py b/docs/labs/lab06-provisioning/mlag_issu.py
new file mode 100644
index 0000000..307d418
--- /dev/null
+++ b/docs/labs/lab06-provisioning/mlag_issu.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python3
+#
+# python3 mlag_issu <upgrade inventory file> <MLAG peer to upgrade: 'peer1' or 'peer2'>"
+#
+# # Example of upgrade inventory file (YAML)
+# cvp_hosts:
+#     - 192.168.0.191
+#     - 192.168.0.192
+#     - 192.168.0.193
+# cvp_username: cvpadmin
+# target_eos_version: 4.25.4M
+# target_terminattr_version: 1.13.6
+# mlag_couples:
+#     - peer1: leaf101-1
+#       peer2: leaf101-2
+#     - peer1: leaf102-1
+#       peer2: leaf102-2
+#
+# Note: upgrades are performed in parallel
+
+import sys
+import time
+import string
+import random
+from getpass import getpass
+import requests
+from requests.packages.urllib3.exceptions import InsecureRequestWarning
+from datetime import datetime
+from cvprac.cvp_client import CvpClient
+from cvprac.cvp_client_errors import CvpLoginError, CvpApiError
+from pprint import pprint
+from operator import itemgetter
+import yaml
+
+class CvpDeviceUpgrader(object):
+    def __init__(self, hosts, username, password):
+        requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
+        self.cvp_hosts = hosts
+        self.cvp_user = username
+        self.cvp_password = password
+        self.session = self._open_cvp_session()
+
+    def _open_cvp_session(self):
+        try:
+            client = CvpClient()
+            client.connect(
+                nodes=self.cvp_hosts,
+                username=self.cvp_user,
+                password=self.cvp_password,
+                request_timeout=300,
+                connect_timeout=30
+            )
+            return(client)
+        except CvpLoginError as e:
+            print(f"Cannot connect to CVP API: {e}")
+            exit()
+
+    def create_mlag_issu_change_control(self, taskIDs, deviceIDs):
+        cc_id = f"CC_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+        pre_upgrade_stage = {'stage': [{
+            'id': f"preU_{cc_id}",
+            'name': 'pre_upgrade',
+            'stage_row':[{'stage': [{
+                'id': ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(9)),
+                'action': {
+                    'name': 'mlaghealthcheck',
+                    'timeout': 0,
+                    'args': {
+                        'DeviceID': device_id
+                    }
+                }
+            } for device_id in deviceIDs]}]
+        }]}
+        upgrade_stage = {'stage': [{
+            'id': f"U_{cc_id}",
+            'name': 'upgrade',
+            'stage_row': [{'stage': [{
+                'id': task_id,
+                'action': {
+                    'name': 'task',
+                    'args': {
+                        'TaskID': task_id
+                    }
+                }
+            } for task_id in taskIDs]}]
+        }]}
+        post_upgrade_stage = {'stage': [{
+            'id': f"postU_{cc_id}",
+            'name': 'post_upgrade',
+            'stage_row': [{'stage': [{
+                'id': ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(9)),
+                'action': {
+                    'name': 'mlaghealthcheck',
+                    'timeout': 0,
+                    'args': {
+                        'DeviceID': device_id
+                    }
+                }
+            } for device_id in deviceIDs]}]
+        }]}
+        cc_data = {'config': {
+            'id': cc_id,
+            'name': f"Change Control {cc_id}",
+            'root_stage': {
+                'id': 'root',
+                'name': f"Change Control {cc_id} root",
+                'stage_row': [pre_upgrade_stage, upgrade_stage, post_upgrade_stage],
+            }
+        }}
+        try:
+            res = self.session.post('/api/v3/services/ccapi.ChangeControl/Update',
+                data=cc_data,
+                timeout=self.session.api.request_timeout
+            )
+        except Exception as e:
+            print(str(e))
+            return(None)
+        print(f"Change control {res[0]['id']} created at {res[0]['update_timestamp']}")
+        return(res[0]['id'])
+
+    def get_mlag_issu_change_control_logs(self, ccID, startTime):
+        end_time = int(time.time() * 1000)
+        cc_logs_data = {'category': 'ChangeControl',
+            'objectKey': ccID,
+            'dataSize': 15000,
+            'startTime': startTime,
+            'endTime': end_time
+        }
+        logs = self.session.post('/cvpservice/audit/getLogs.do',
+            data=cc_logs_data,
+            timeout=self.session.api.request_timeout
+        )
+        for log in sorted(logs['data'], key=itemgetter('dateTimeInLongFormat')):
+            if log['subObjectName'] and 'Command(s)' not in log['activity']:
+                log_date = datetime.fromtimestamp(log['dateTimeInLongFormat']/1000)
+                print(f"{log_date} {log['subObjectName']}: {log['activity']}")
+        return(end_time + 1)
+
+    def run_mlag_issu_change_control(self, ccID):
+        print(f"Automatic approval of change control {ccID}")
+        self.session.api.approve_change_control(ccID, datetime.utcnow().isoformat() + 'Z')
+        time.sleep(2)
+        print(f"Starting the execution of change control {ccID}")
+        start_time = int(time.time() * 1000)
+        self.session.api.execute_change_controls([ccID])
+        time.sleep(2)
+        cc_status = self.session.api.get_change_control_status(ccID)[0]['status']
+        start_time = self.get_mlag_issu_change_control_logs(ccID, start_time)
+        while cc_status['state'] == 'Running':
+            time.sleep(30)
+            cc_status = self.session.api.get_change_control_status(ccID)[0]['status']
+            start_time = self.get_mlag_issu_change_control_logs(ccID, start_time)
+        print(f"Change control {ccID} final status: {cc_status['state']}")
+        if cc_status['error']:
+            print(f"Change control {ccID} had the following errors: {cc_status['error']}")
+        else:
+            print(f"Change control {ccID} completed without errors")
+
+def main():
+    if len(sys.argv) != 3:
+        print(f"Usage: python3 {sys.argv[0]} <input file path> <MLAG peer to upgrade: peer1/peer2>")
+        exit()
+    try:
+        with open(sys.argv[1], 'r') as yf:
+            params = yaml.safe_load(yf)
+    except Exception as e:
+        print(e)
+        exit()
+    cvp_password = getpass(prompt=f"CVP password for user {params['cvp_username']}: ")
+    cvpdu = CvpDeviceUpgrader(
+        hosts=params['cvp_hosts'],
+        username=params['cvp_username'],
+        password=cvp_password
+    )
+    image_bundle = None
+    for bundle in cvpdu.session.api.get_image_bundles()['data']:
+        eos_match = False
+        terminattr_match = False
+        for img in bundle['imageIds']:
+            if params['target_eos_version'] in img:
+                eos_match = True
+            elif params['target_terminattr_version'] in img:
+                terminattr_match = True
+        if eos_match and terminattr_match:
+            image_bundle = bundle
+            break
+    if image_bundle is None:
+        print(f"Cannot find an image bundle with EOS {params['target_eos_version']} and TerminAttr {params['target_terminattr_version']}")
+        exit()
+    hostnames = [couple[sys.argv[2]] for couple in params['mlag_couples']]
+    devices_to_upgrade = list()
+    inventory = cvpdu.session.api.get_inventory()
+    for hostname in hostnames:
+        provisioned = False
+        for dev in inventory:
+            if dev['hostname'] == hostname:
+                provisioned = True
+                devices_to_upgrade.append(dev)
+                break
+        if not provisioned:
+            print(f"Device with hostname {hostname} is not provisioned in CVP")
+    if not devices_to_upgrade:
+        print('none of the mentioned devices is provisioned in CVP')
+        exit()
+    print(f"Devices to upgrade: {', '.join([dev['hostname'] for dev in devices_to_upgrade])}")
+    task_ids = list()
+    for device in devices_to_upgrade:
+        response = cvpdu.session.api.apply_image_to_device(image_bundle, device)['data']
+        if response['status'] == 'success':
+            task_ids.extend(response['taskIds'])
+    device_ids = [device['serialNumber'] for device in devices_to_upgrade]
+    cc_id = cvpdu.create_mlag_issu_change_control(task_ids, device_ids)
+    if cc_id is None:
+        print('Failed to create the MLAG ISSU change control')
+        exit()
+    time.sleep(2)
+    cvpdu.run_mlag_issu_change_control(cc_id)
+
+if __name__ == '__main__':
+    main()
diff --git a/docs/labs/lab06-provisioning/vc_task_retrigger.py b/docs/labs/lab06-provisioning/vc_task_retrigger.py
index 512adaa..b5586ee 100644
--- a/docs/labs/lab06-provisioning/vc_task_retrigger.py
+++ b/docs/labs/lab06-provisioning/vc_task_retrigger.py
@@ -4,39 +4,112 @@
 
 # Example on how to re-trigger task creation if a config push task was previously
 # cancelled and the device is still config out of sync
-
-from cvprac.cvp_client import CvpClient
+import argparse
 import ssl
-ssl._create_default_https_context = ssl._create_unverified_context
+import sys
+from pkg_resources import parse_version
+from getpass import getpass
+from cvprac.cvp_client import CvpClient
 import requests.packages.urllib3
 requests.packages.urllib3.disable_warnings()
 
-# Create connection to CloudVision
-clnt = CvpClient()
-
-clnt.connect(nodes=['cvp1'], username="username",password="password")
-
-# Trigger tasks after they were cancelled
-
-devices = ["tp-avd-leaf1", "tp-avd-leaf2", "tp-avd-leaf3", "tp-avd-leaf4", "tp-avd-spine1", "tp-avd-spine2"]
-
-compliance = {"0001": "Config is out of sync",
-              "0003": "Config & image out of sync",
-              "0004": "Config, Image and Device time are in sync",
-              "0005": "Device is not reachable",
-              "0008": "Config, Image and Extensions are out of sync",
-              "0009": "Config and Extensions are out of sync",
-              "0012": "Config, Image, Extension and Device time are out of sync",
-              "0013": "Config, Image and Device time are out of sync",
-              "0014": "Config, Extensions and Device time are out of sync",
-              "0016": "Config and Device time are out of sync",
-}
-
-for dev in devices:
-   # generate device object
-   device = clnt.api.get_device_by_name(dev)
-   # generate configlet list
-   cl = clnt.api.get_configlets_by_device_id(device['systemMacAddress'])
-   # generate a task if config is out of sync
-   if device['complianceCode'] in compliance.keys():
-      print(clnt.api.apply_configlets_to_device("", device, cl))
+
+if ((sys.version_info.major == 3) or
+        (sys.version_info.major == 2 and sys.version_info.minor == 7 and
+         sys.version_info.micro >= 5)):
+    ssl._create_default_https_context = ssl._create_unverified_context
+
+
+def main():
+        
+    compliance = {"0001": "Config is out of sync",
+        "0003": "Config & image out of sync",
+        "0004": "Config, Image and Device time are in sync",
+        "0005": "Device is not reachable",
+        "0008": "Config, Image and Extensions are out of sync",
+        "0009": "Config and Extensions are out of sync",
+        "0012": "Config, Image, Extension and Device time are out of sync",
+        "0013": "Config, Image and Device time are out of sync",
+        "0014": "Config, Extensions and Device time are out of sync",
+        "0016": "Config and Device time are out of sync"
+    }
+    # Create connection to CloudVision
+    clnt = CvpClient()
+
+    parser = argparse.ArgumentParser(
+        description='Script to recreate a task, if a previous config push was cancelled')
+    parser.add_argument('-u', '--username', default='username')
+    parser.add_argument('-p', '--password', default=None)
+    parser.add_argument('-c', '--cvpserver', action='append')
+    parser.add_argument('-f', '--filter', action='append', default=None)
+    args = parser.parse_args()
+
+    if args.password is None:
+        args.password = getpass()
+
+    for cvpserver in args.cvpserver:
+        print("Connecting to %s" % cvpserver)
+        try:
+            clnt.connect(nodes=[cvpserver], username=args.username, password=args.password)
+        except Exception as e:
+            print("Unable to connect to CVP: %s" % str(e))
+
+        # Get the current CVP version
+        cvp_release = clnt.api.get_cvp_info()['version']
+        if parse_version(cvp_release) < parse_version('2020.3.0'):
+            # For older CVP, we manually trigger a compliance check
+            try:
+                clnt.api.check_compliance('root', 'container')
+            except:
+                # Bad practice, but the check compliance applied to a container can't actually work
+                # since the complianceIndication  key doesn't exist on the container level
+                pass
+        else:
+            # with continuous compliance checks, triggering the check is no longer required
+            pass
+
+        device_filters = []
+        if args.filter is not None:
+            for entry in args.filter:
+                device_filters.extend(entry.split(','))
+
+        # Get inventory
+        print("Collecting inventory...")
+        devices = clnt.api.get_inventory()
+        print("%d devices in inventory" % len(devices) )
+
+        for switch in devices:
+            if (switch['status'] == 'Registered' and
+                    switch['parentContainerId'] != 'undefined_container'):
+
+                if len(device_filters) > 0:
+                    # iterate over device filters, and update task for
+                    # any devices not in compliance
+                    
+                    for filter_term in device_filters:
+                        print("Checking device: %s" % switch['hostname'])
+                        if filter_term in switch['hostname']:
+                            # generate configlet list
+                            cl = clnt.api.get_configlets_by_device_id(switch['systemMacAddress'])
+                            # generate a task if config is out of sync
+                            if switch['complianceCode'] in compliance.keys():
+                                print(clnt.api.apply_configlets_to_device("", switch, cl))
+                            else:
+                                print("%s is compliant, nothing to do" % switch['hostname'])
+                        else:
+                            print("Skipping %s due to filter" % switch['hostname'])
+                else:
+                    print("Checking device: %s" % switch['hostname'])
+                    cl = clnt.api.get_configlets_by_device_id(switch['systemMacAddress'])
+                    # generate a task if config is out of sync
+                    if switch['complianceCode'] in compliance.keys():
+                        print(clnt.api.apply_configlets_to_device("", switch, cl))
+
+            else:
+                print("Skipping %s, device is unregistered for provisioning" % switch['hostname'])
+
+    return 0
+
+
+if __name__ == "__main__":
+    main()
diff --git a/docs/labs/lab07-aaa/create_terminattr_tokens.py b/docs/labs/lab07-aaa/create_terminattr_tokens.py
new file mode 100644
index 0000000..d874913
--- /dev/null
+++ b/docs/labs/lab07-aaa/create_terminattr_tokens.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2021 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the COPYING file.
+#
+# Example script to generate the TerminAttr token via REST API from CVaaS and CV on-prem
+# and save them to a file
+
+from cvprac.cvp_client import CvpClient
+from pprint import pprint as pp
+import ssl
+ssl._create_default_https_context = ssl._create_unverified_context
+import requests.packages.urllib3
+requests.packages.urllib3.disable_warnings()
+
+# Reading the service account token from a file
+with open("cvaas.tok") as f:
+    token = f.read().strip('\n')
+
+clnt = CvpClient()
+clnt.connect(nodes=['www.arista.io'], username='',password='',is_cvaas=True, api_token=token)
+
+terminattr_token = clnt.api.create_enroll_token('720h')
+with open('cv-onboarding-token', 'w') as f:
+    f.write(terminattr_token[0]['enrollmentToken']['token'])
+
+primary = CvpClient()
+primary.connect(nodes=['cvp1'], username='username',password='password')
+
+terminattr_token = primary.api.create_enroll_token('720h')
+
+with open('token', 'w') as f:
+    f.write(terminattr_token['data'])
diff --git a/docs/labs/lab08-resource-apis/topology_tag_assignment.py b/docs/labs/lab08-resource-apis/topology_tag_assignment.py
new file mode 100644
index 0000000..45493eb
--- /dev/null
+++ b/docs/labs/lab08-resource-apis/topology_tag_assignment.py
@@ -0,0 +1,80 @@
+# Copyright (c) 2021 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the COPYING file.
+
+from cvprac.cvp_client import CvpClient
+
+# Reading the service account token from a file
+with open("token.tok") as f:
+    token = f.read().strip('\n')
+
+# Create connection to CloudVision
+clnt = CvpClient()
+clnt.connect(nodes=['cvp1'], username='',password='',api_token=token)
+
+tags_common = [{"key": "topology_hint_pod", "value": "tp-avd-pod1"},
+               {"key": "topology_hint_datacenter", "value": "tp-avd-dc1"}]
+tags_leaf1 = [{"key": "topology_hint_rack", "value": "tp-avd-leafs1"}]
+tags_leaf2 = [{"key": "topology_hint_rack", "value": "tp-avd-leafs2"}]
+tags_spines = [{"key": "topology_hint_rack", "value": "tp-avd-spines"}]
+
+# Create workspace
+display_name = 'TestTagCVPRAC'
+workspace_id = "TestTagCVPRAC"
+clnt.api.workspace_config(workspace_id,display_name)
+
+### Create tags
+element_type = "ELEMENT_TYPE_DEVICE"
+
+for tag in tags_common+tags_leaf1+tags_leaf2+tags_spines:
+    tag_label = tag['key']
+    tag_value = tag['value']
+    clnt.api.tag_config(element_type, workspace_id, tag_label, tag_value)
+
+### Assign tags
+devices = {"leafs1":["BAD032986065E8DC14CBB6472EC314A6","0123F2E4462997EB155B7C50EC148767"],
+		   "leafs2":["8520AF39790A4EC959550166DC5DEADE", "6323DA7D2B542B5D09630F87351BEA41"],
+		   "spines":["CD0EADBEEA126915EA78E0FB4DC776CA", "2568DB4A33177968A78C4FD5A8232159"]}
+
+for tag in tags_common+tags_leaf1:
+    tag_label = tag['key']
+    tag_value = tag['value']
+    interface_id = ''
+    for leaf in devices['leafs1']:
+        device_id = leaf
+        clnt.api.tag_assignment_config(element_type, workspace_id, tag_label, tag_value, device_id, interface_id)
+for tag in tags_common+tags_leaf2:
+    tag_label = tag['key']
+    tag_value = tag['value']
+    interface_id = ''
+    for leaf in devices['leafs2']:
+        device_id = leaf
+        clnt.api.tag_assignment_config(element_type, workspace_id, tag_label, tag_value, device_id, interface_id)
+for tag in tags_common+tags_spines:
+    tag_label = tag['key']
+    tag_value = tag['value']
+    interface_id = ''
+    for spine in devices['spines']:
+        device_id = spine
+        clnt.api.tag_assignment_config(element_type, workspace_id, tag_label, tag_value, device_id, interface_id)
+
+### Start build
+request = 'REQUEST_START_BUILD'
+request_id = 'b1'
+description='testing cvprac build'
+clnt.api.workspace_config(workspace_id=workspace_id,display_name=display_name,description=description,request=request,request_id=request_id)
+
+### Check workspace build status and proceed only after it finishes building
+b = 0
+while b == 0:
+    build_id = request_id
+    request = clnt.api.workspace_build_status(workspace_id,build_id)
+    if request['value']['state'] == 'BUILD_STATE_SUCCESS':
+        b = b+1
+    else:
+        continue
+
+### Submit workspace
+request = 'REQUEST_SUBMIT'
+request_id = 's1'
+clnt.api.workspace_config(workspace_id=workspace_id,display_name=display_name,description=description,request=request,request_id=request_id)
diff --git a/docs/release-notes-1.1.0.rst b/docs/release-notes-1.1.0.rst
new file mode 100644
index 0000000..a0974dd
--- /dev/null
+++ b/docs/release-notes-1.1.0.rst
@@ -0,0 +1,33 @@
+######
+v1.1.0
+######
+
+2022-5-20
+
+New Modules
+^^^^^^^^^^^
+
+* Added new method for creating TerminAttr enrollment token. (`9bf409b <https://github.com/aristanetworks/cvprac/commit/9bf409b864774490dabac6fdcef24dc3735ad240>`_) [`noredistribution <https://github.com/noredistribution>`_]
+* Added new methods for managing tags and workspaces. (`e0b8185 <https://github.com/aristanetworks/cvprac/commit/e0b818597b78345759be20b3319c3d574e56f732>`_) [`noredistribution <https://github.com/noredistribution>`_]
+* Additional methods for managing workspaces. (`71eea87 <https://github.com/aristanetworks/cvprac/commit/71eea87a012950165f7e87b2bb8b83556da5b4bf>`_) [`mharista <https://github.com/mharista>`_]
+* Added new methods for change control resource APIs. (`d0b1916 <https://github.com/aristanetworks/cvprac/commit/d0b19164012fc6358fb71bdba21ebd44ec126ca2>`_) [`noredistribution <https://github.com/noredistribution>`_]
+* Added new methods for change control scheduling and device decommissioning. (`370d02f <https://github.com/aristanetworks/cvprac/commit/370d02fe8a33436337d0866d54a0201f76a5c44b>`_) [`noredistribution <https://github.com/noredistribution>`_]
+
+Enhancements
+^^^^^^^^^^^^
+
+* Added ability to run change control tasks sequentially or in parallel. (`5fd48d5e <https://github.com/aristanetworks/cvprac/commit/5fd48d5e33b6f657650b6bde949e202ba644776c>`_) [`mharista <https://github.com/mharista>`_]
+* Improved system test setup and base classes. (`36029b5 <https://github.com/aristanetworks/cvprac/commit/36029b5c0f5d47b6cddeca89b33380afb69f2ec2>`_) [`KonikaChaurasiya-GSLab <https://github.com/KonikaChaurasiya-GSLab>`_]
+* Do not run getConfigletByName for every configlet to make get_configlets more efficient. (`3c1a3eb <https://github.com/aristanetworks/cvprac/commit/3c1a3eb2ae9ebe2ae3f07835ac86cdbd33d34baa>`_) [`noredistribution <https://github.com/noredistribution>`_]
+* Added system tests for new change control resource APIs and more system test infrastructure enhancements. (`4564ef3 <https://github.com/aristanetworks/cvprac/commit/4564ef327f1b8abd743adb791ed742d586fc5587>`_) [`KonikaChaurasiya-GSLab <https://github.com/KonikaChaurasiya-GSLab>`_]
+* Added system test for new decommission device APIs. (`4fd4b7f <https://github.com/aristanetworks/cvprac/commit/4fd4b7f476ca08323fdf4c96df41665e7f78ec96>`_) [`noredistribution <https://github.com/noredistribution>`_]
+* Documentation format updates. (`ac4f2ff <https://github.com/aristanetworks/cvprac/commit/ac4f2ff8f45fd68b42a5ce8b68e0a565e9dbe8a8>`_) [`tgodaA <https://github.com/tgodaA>`_]
+* Assorted system test format updates for various CVP versions support.
+* Added support for CVP versions up to CVP 2022.1.0.
+
+Fixed
+^^^^^
+
+* Removed timestamp variable declaration from approve change control function. (`2b1e6ac <https://github.com/aristanetworks/cvprac/commit/2b1e6ac44fbc912ea60841eb10ab2d19b1f59c65>`_) [`mharista <https://github.com/mharista>`_]
+* Add wrapper function for validate_config that will return the full response data structure. (`0540c7a <https://github.com/aristanetworks/cvprac/commit/0540c7ac64cba0b6a8ad4de00f828519d1d5f557>`_) [`mharista <https://github.com/mharista>`_]
+* Fix request headers for CVaaS. (`f57ea4e <https://github.com/aristanetworks/cvprac/commit/f57ea4e6089130eccbc91f6831fbb3d2054020af>`_) [`mharista <https://github.com/mharista>`_]


Test patch:
diff --git a/test/fixtures/cvp_nodes.yaml b/test/fixtures/cvp_nodes.yaml
index 42c764d..f9e09db 100644
--- a/test/fixtures/cvp_nodes.yaml
+++ b/test/fixtures/cvp_nodes.yaml
@@ -1,9 +1,12 @@
 - node: cvp
   username: CvpRacTest
   password: AristaInnovates
+  device: python-test-2
 - node: cvp2
   username: CvpRacTest
   password: AristaInnovates
+  device: python-test-2
 - node: cvp3
   username: CvpRacTest
   password: AristaInnovates
+  device: python-test-2
diff --git a/test/lib/systestlib.py b/test/lib/systestlib.py
index 0890502..c08c95e 100644
--- a/test/lib/systestlib.py
+++ b/test/lib/systestlib.py
@@ -54,15 +54,16 @@ class DutSystemTest(unittest.TestCase):
     def __init__(self, *args, **kwargs):
         super(DutSystemTest, self).__init__(*args, **kwargs)
 
-    def setUp(self):
+    @classmethod
+    def setUpClass(cls):
         ''' Read in the list of CVP node names or IP addresses to use
             for testing.
 
             Format for duts list of dicts:
             { node: nodename, user: username, password: password }
         '''
-        self.duts = {}
+        cls.duts = {}
         filename = get_fixture('cvp_nodes.yaml')
         with open(filename, 'r') as stream:
-            self.duts = yaml.load(stream)
+            cls.duts = yaml.safe_load(stream)
             stream.close()
diff --git a/test/system/test_cvp_base.py b/test/system/test_cvp_base.py
new file mode 100644
index 0000000..de147f5
--- /dev/null
+++ b/test/system/test_cvp_base.py
@@ -0,0 +1,184 @@
+''' Base class for TestCvpClient and TestCvpClientCC class.
+'''
+from pprint import pprint
+import os
+import sys
+import re
+import time
+import uuid
+import urllib3
+from cvprac.cvp_client import CvpClient
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '../lib'))
+from systestlib import DutSystemTest
+
+
+
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+#
+# sys.path.append(os.path.join(os.path.dirname(__file__), '../lib'))
+# from systestlib import DutSystemTest
+
+
+class TestCvpClientBase(DutSystemTest):
+    ''' Base class for TestCvpClient and TestCvpClientCC class.
+    '''
+    # pylint: disable=too-many-public-methods
+    # pylint: disable=invalid-name
+    @classmethod
+    def setUpClass(cls):
+        ''' Instantiate the CvpClient class and connect to the CVP node.
+            Log messages to the /tmp/TestCvpClient.log
+        '''
+        super(TestCvpClientBase, cls).setUpClass()
+        cls.clnt = CvpClient(filename='/tmp/TestCvpClient.log')
+        assert cls.clnt is not None
+        assert cls.clnt.last_used_node is None
+        dut = cls.duts[0]
+        cert = dut.get("cert", False)
+        username = dut.get("username", "")
+        password = dut.get("password", "")
+        api_token = dut.get("api_token", None)
+        is_cvaas = dut.get("is_cvaas", False)
+
+        cls.clnt.connect([dut['node']], username, password, 10,
+                         cert=cert, is_cvaas=is_cvaas, api_token=api_token)
+
+        cls.api = cls.clnt.api
+        assert cls.api is not None
+
+        # Verify that there is at least one device in the inventory
+        err_msg = 'CVP node must contain at least one device'
+        result = cls.api.get_inventory()
+        assert result is not None
+        if len(result) < 1:
+            raise AssertionError(err_msg)
+        assert len(result) >= 1
+        device = [res for res in result if res['hostname']
+                  == dut.get("device", "")]
+        cls.device = device[0]
+        # Get the container for the device on the list and
+        # use that container as the parent container.
+        result = cls.api.search_topology(cls.device['fqdn'])
+        assert result is not None
+        dev_container = result['netElementContainerList']
+        assert len(dev_container) >= 1
+        info = dev_container[0]
+        result = cls.api.search_topology(info['containerName'])
+        assert result is not None
+        cls.container = result['containerList'][0]
+
+        # Get the configlets assigned to the device.  There must be at least 1.
+        err_msg = 'CVP node device must have at least one configlet assigned'
+        key = info['netElementKey']
+        result = cls.api.get_configlets_by_device_id(key)
+        if len(result) < 1:
+            raise AssertionError(err_msg)
+        cls.dev_configlets = result
+
+    @classmethod
+    def tearDownClass(cls):
+        ''' Destroy the CvpClient class.
+        '''
+        super(TestCvpClientBase, cls).tearDownClass()
+        cls.api = None
+        cls.clnt = None
+
+    def cancel_task(self, task_id):
+        """ Cancel task
+        """
+        pprint('CANCELING TASK...')
+        data = {'data': [task_id]}
+        self.clnt.post('/task/cancelTask.do', data=data)
+
+    def setUp(self):
+        '''
+            Set the task_id, cc_id and cc_name. It executes before each test case
+        '''
+        self.task_id = None
+        self.cc_id = str(uuid.uuid4())
+        # self.cc_name = 'test_api_%d %s' % time.time()
+        self.cc_name = f'test_api_{time.time()}'
+
+    def tearDown(self):
+        '''
+            Delete the change control if its present and cancel the task
+            if it exists
+        '''
+        chg_ctrl_get_one = self.api.change_control_get_one(self.cc_id)
+        if chg_ctrl_get_one:
+            # Delete CC
+            self.delete_change_control(self.cc_id)
+            # Cancel task
+            if self.task_id:
+                task = self.api.get_task_by_id(self.task_id)
+                if task and task['currentTaskName'] != "Cancelled":
+                    self.cancel_task(self.task_id)
+
+    def _get_next_task_id(self):
+        ''' Return the next task id.
+
+            Returns:
+                task_id (str): Task ID
+        '''
+        # Get all the tasks and the task id of the next task created is
+        # the length + 1.
+        results = self.api.get_tasks()
+        self.assertIsNotNone(results)
+        latest_task = str(int(results['data'][0]['workOrderId']) + 1)
+        return latest_task
+
+    def _create_task(self):
+        ''' Create a task by making a simple change to a configlet assigned
+            to the device.
+
+            Returns:
+                (task_id, config)
+                task_id (str): Task ID
+                config (str): Previous configlets contents
+        '''
+        task_id = self._get_next_task_id()
+        # Update the lldp time in the first configlet in the list.
+        configlet = None
+        for conf in self.dev_configlets:
+            if conf['netElementCount'] == 1:
+                configlet = conf
+                break
+        if configlet is None:
+            configlet = self.dev_configlets[0]
+
+        config = configlet['config']
+        org_config = config
+
+        match = re.match(r'lldp timer (\d+)', config)
+        if match is not None:
+            value = int(match.group(1)) + 1
+            repl = f'lldp timer {value}'
+            config = re.sub(match.group(0), repl, config)
+        else:
+            value = 13
+            config = f'lldp timer {value}\n' + config
+        configlet['config'] = config
+
+        # Updating the configlet will cause a task to be created to apply
+        # the change to the device.
+        self.api.update_configlet(config, configlet['key'], configlet['name'])
+
+        # Wait 30 seconds for task to get created
+        cnt = 30
+        if self.clnt.apiversion is None:
+            self.api.get_cvp_info()
+        if self.clnt.apiversion >= 2.0:
+            # Increase timeout by 30 sec for CVP 2018.2 and beyond
+            cnt += 30
+        while cnt > 0:
+            time.sleep(1)
+            result = self.api.get_task_by_id(task_id)
+            if result is not None:
+                break
+            cnt -= 1
+        err_msg = f'Timeout waiting for task id {task_id} to be created'
+        self.assertGreater(cnt, 0, msg=err_msg)
+        self.task_id = task_id
+        return task_id, org_config
diff --git a/test/system/test_cvp_change_control_api.py b/test/system/test_cvp_change_control_api.py
new file mode 100644
index 0000000..1a9af6d
--- /dev/null
+++ b/test/system/test_cvp_change_control_api.py
@@ -0,0 +1,787 @@
+'''
+    Tests for change control apis
+'''
+from pprint import pprint
+import time
+import unittest
+from test_cvp_base import TestCvpClientBase
+import urllib3
+from cvprac.cvp_client_errors import CvpRequestError
+urllib3.disable_warnings(
+    urllib3.exceptions.InsecureRequestWarning)
+
+
+CHANGE_CONTROL_ID_INVALID = 'InvalidCVPRACSystestCCID'
+INVALID_TASK_ID = 'InvalidCVPRACSystestTASKID'
+INVALID_CCID = 'InvalidCVPRACSystestCCID'
+APPROVE_NOTE = "Approving CC via cvprac cc system tests"
+START_NOTE = "Start the CC via cvprac cc system tests"
+STOP_NOTE = "Stop the CC via cvprac cc system tests"
+TASK_ID = None
+
+
+class TestCvpClientCC(TestCvpClientBase):
+    """ Test cases for the CvpClientCC class.
+    """
+    @classmethod
+    def setUpClass(cls):
+        """ Initialize variables
+        """
+        super(TestCvpClientCC, cls).setUpClass()
+
+    @classmethod
+    def tearDownClass(cls):
+        """ Reset variables value to None
+        """
+        super(TestCvpClientCC,
+              cls).tearDownClass()
+
+    def get_version(self):
+        """ Get api version
+        """
+        if self.clnt.apiversion is None:
+            self.api.get_cvp_info()
+        if self.clnt.apiversion >= 6.0:
+            pprint('RUN TEST FOR V6 (2021.2.0+) CHANGE CONTROL RESOURCE APIs')
+            return True
+        pprint(f'SKIPPING TEST FOR API - {self.clnt.apiversion}')
+        return False
+
+    def get_cc_status(self, cc_id):
+        """ Get change control status
+        """
+        params = f'key.id={cc_id}'
+        cc_url = f'/api/resources/changecontrol/v1/ChangeControl?{params}'
+        response = self.clnt.get(
+            cc_url, timeout=30)
+        return response
+
+    def get_device_list(self):
+        """ Get device list from inventory
+        """
+        inventory = self.api.get_inventory()
+        device_list = []
+        if len(inventory) >= 1:
+            # for i in range(0, len(inventory)):
+            #     device_list.append(
+            #         inventory[i]['serialNumber'])
+            device_list = [inventory[i]['serialNumber'] for i in range(len(inventory))]
+        else:
+            raise Exception("No device found")
+        return device_list
+
+    def create_snapshot(self):
+        """ Create snapshot for change control with custom stages
+        """
+        pprint('CREATING SNAPSHOT...')
+        device_list = self.get_device_list()
+        template_details = {
+            "commands": [
+                "show version"
+            ],
+            "deviceList": [
+                device_list[0]
+            ],
+            "frequency": "350",
+            "name": "show version"
+        }
+        response = self.clnt.post(
+            '/snapshot/templates/schedule?', data=template_details)
+        if response['status'] == "success":
+            template_id = response['templateKey']
+        else:
+            raise Exception(
+                "Snapshot not created")
+        return template_id
+
+    def delete_snapshot(self, snaps):
+        """ Delete snapshot
+        """
+        pprint('DELETING SNAPSHOT...')
+        response = self.clnt.delete(
+            '/snapshot/templates?', data=snaps)
+        return response
+
+    def create_task(self):
+        """ Create new task
+        """
+        pprint('CREATING TASKS...')
+        # global task_id
+        (task_id, _) = self._create_task()
+        self.task_id = task_id
+        return task_id
+
+    def create_change_control_for_task(self, task_id):
+        """ Create change control for tasks
+        """
+        pprint('CREATING CHANGE CONTROL...')
+        chg_ctrl = self.api.change_control_create_for_tasks(
+            self.cc_id, self.cc_name, [task_id])
+        assert chg_ctrl is not None
+        assert chg_ctrl['value']['key']['id'] == self.cc_id
+        assert chg_ctrl['value']['change']['name'] == self.cc_name
+        return chg_ctrl
+
+    def approve_change_control(self):
+        """ Approve change control
+        """
+        pprint('APPROVING CHANGE CONTROL...')
+        # Approve the change control
+        approve_chg_ctrl = self.api.change_control_approve(
+            self.cc_id, notes=APPROVE_NOTE)
+        assert approve_chg_ctrl is not None
+        assert approve_chg_ctrl['value']['approve']['value'] is True
+        assert approve_chg_ctrl['value']['approve']['notes'] == APPROVE_NOTE
+        assert approve_chg_ctrl['value']['key']['id'] == self.cc_id
+        return approve_chg_ctrl
+
+    def start_change_control(self, cc_id):
+        """ Start change control
+        """
+        pprint('STARTING CHANGE CONTROL...')
+        # Start the change control
+        start_chg_ctrl = self.api.change_control_start(
+            cc_id, notes=START_NOTE)
+        assert start_chg_ctrl is not None
+        assert start_chg_ctrl['value']['start']['value'] is True
+        assert start_chg_ctrl['value']['start']['notes'] == START_NOTE
+        assert start_chg_ctrl['value']['key']['id'] == cc_id
+        return start_chg_ctrl
+
+    def stop_change_control(self):
+        """ Stop change control
+        """
+        pprint('STOPPING CHANGE CONTROL...')
+        # Stop the change control
+        stop_chg_ctrl = self.api.change_control_stop(
+            self.cc_id, notes=STOP_NOTE)
+        assert stop_chg_ctrl is not None
+        assert stop_chg_ctrl['value']['start']['value'] is False
+        assert stop_chg_ctrl['value']['start']['notes'] == STOP_NOTE
+        assert stop_chg_ctrl['value']['key']['id'] == self.cc_id
+        return stop_chg_ctrl
+
+    def delete_change_control(self, cc_id):
+        """ Delete change control
+        """
+        pprint('DELETING CHANGE CONTROL...')
+        delete_chg_ctrl = self.api.change_control_delete(
+            cc_id)
+        assert delete_chg_ctrl is not None
+        assert delete_chg_ctrl['key']['id'] == self.cc_id
+        return delete_chg_ctrl
+
+    def change_control_get_one(self, cc_id):
+        """ Change control get one
+        """
+        chg_ctrl_get_one = self.api.change_control_get_one(
+            cc_id)
+        return chg_ctrl_get_one
+
+    def cancel_task(self, task_id):
+        """ Cancel task
+        """
+        pprint('CANCELING TASK...')
+        data = {'data': [task_id]}
+        self.clnt.post(
+            '/task/cancelTask.do', data=data)
+
+    def test_api_change_control_create_for_task(self):
+        """ Verify change_control_create_for_tasks
+        """
+        pprint(
+            "test_api_change_control_create_for_tasks")
+        if self.get_version():
+            # Create Task
+            task_id = self.create_task()
+
+            # Create change control;
+            chg_ctrl = self.create_change_control_for_task(
+                task_id)
+            time.sleep(1)
+
+            # Verify CC
+            response = self.get_cc_status(
+                self.cc_id)
+
+            assert response is not None
+            assert chg_ctrl['value']['key']['id'] == response['value']['key']['id']
+            assert chg_ctrl['value']['change']['name'] == response['value']['change']['name']
+            time.sleep(2)
+
+            approve_chg_ctrl = self.approve_change_control()
+
+            # Start change control
+            start_chg_ctrl = self.start_change_control(
+                self.cc_id)
+
+            # Verify start CC
+            response = self.get_cc_status(
+                self.cc_id)
+
+            # Verify create CC
+            assert response is not None
+            assert chg_ctrl['value']['key']['id'] == response['value']['key']['id']
+            assert chg_ctrl['value']['change']['name'] == response['value']['change']['name']
+            time.sleep(2)
+
+            # Verify approve CC
+            assert approve_chg_ctrl['value']['key']['id'] == response['value']['key']['id']
+            assert approve_chg_ctrl['value']['approve'][
+                'notes'] == response['value']['approve']['notes']
+            assert approve_chg_ctrl['value']['approve'][
+                'value'] == response['value']['approve']['value']
+
+            # Verify Start CC
+            assert start_chg_ctrl['value']['key']['id'] == response['value']['key']['id']
+            assert start_chg_ctrl['value']['start']['value'] == response['value']['start']['value']
+            assert start_chg_ctrl['value']['start']['notes'] == response['value']['start']['notes']
+
+            # Stop change control
+            stop_chg_ctrl = self.stop_change_control()
+
+            # Verify stop CC
+            response = self.get_cc_status(
+                self.cc_id)
+
+            assert response is not None
+            assert stop_chg_ctrl['value']['key']['id'] == response['value']['key']['id']
+            assert stop_chg_ctrl['value']['start']['value'] == response['value']['start']['value']
+            assert stop_chg_ctrl['value']['start']['notes'] == response['value']['start']['notes']
+
+            # Delete change control
+            pprint('DELETING CHANGE CONTROL...')
+            self.delete_change_control(self.cc_id)
+
+            # Verify Delete CC
+            with self.assertRaises(CvpRequestError):
+                self.get_cc_status(self.cc_id)
+
+    def test_api_change_control_approval_get_one(self):
+        """ Verify change_control_approval_get_one
+         """
+        pprint(
+            "test_api_change_control_approval_get_one")
+        if self.get_version():
+            # Create task
+            task_id = self.create_task()
+
+            # Create change control
+            self.create_change_control_for_task(
+                task_id)
+
+            # Approve change control
+            self.approve_change_control()
+
+            pprint('APPROVAL GET ONE...')
+            approval_get_one = self.api.change_control_approval_get_one(
+                self.cc_id)
+            assert approval_get_one is not None
+            assert approval_get_one['value']['approve']['value'] is True
+            assert approval_get_one['value']['approve']['notes'] == APPROVE_NOTE
+            assert approval_get_one['value']['key']['id'] == self.cc_id
+            time.sleep(1)
+
+            # verify with actual api output
+
+            response = self.get_cc_status(
+                self.cc_id)
+
+            assert response is not None
+            assert approval_get_one['value']['key']['id'] == response['value']['key']['id']
+            assert approval_get_one['value']['approve'][
+                'notes'] == response['value']['approve']['notes']
+            assert approval_get_one['value']['approve'][
+                'value'] == response['value']['approve']['value']
+
+            # Delete change control
+            self.delete_change_control(self.cc_id)
+
+            # Cancel Task
+            self.cancel_task(task_id)
+
+    def test_api_change_control_approval_get_one_without_approve(self):
+        """ Verify change_control_approval_get_one_without_approve
+         """
+        pprint(
+            "test_api_change_control_approval_get_one_without_approve")
+        if self.get_version():
+            # Create task
+            task_id = self.create_task()
+
+            # Create CC
+            self.create_change_control_for_task(
+                task_id)
+
+            pprint(
+                'APPROVAL GET ONE WITHOUT APPROVE...')
+            approval_get_one = self.api.change_control_approval_get_one(
+                self.cc_id)
+            assert approval_get_one is None
+
+            # Delete CC
+            self.delete_change_control(self.cc_id)
+
+            # Cancel Task
+            self.cancel_task(task_id)
+
+    def test_api_change_control_create_for_empty_tasks_list(self):
+        """ Verify change_control_create_for_tasks for empty task list
+        """
+        pprint(
+            "test_api_change_control_create_for_empty_tasks_list")
+        if self.get_version():
+            # Set client apiversion if it is not already set
+            pprint(
+                'RUN TEST FOR V3 CHANGE CONTROL APIs')
+            with self.assertRaises(CvpRequestError):
+                self.create_change_control_for_task(
+                    [])
+
+    def test_api_change_control_create_for_none_task_id_in_list(self):
+        """ Verify change_control_create_for_tasks for none task id in list
+        """
+        # Set client apiversion if it is not already set
+        pprint(
+            "test_api_change_control_create_for_none_task_id_in_list")
+        if self.get_version():
+            pprint(
+                'CREATE CHANGE CONTROL FOR LIST OF NONE TASK IDs...')
+            with self.assertRaises(CvpRequestError):
+                self.create_change_control_for_task([
+                                                     None])
+
+    def test_api_change_control_create_for_none_task_ids_not_list(self):
+        """ Verify change_control_create_for_tasks for none task ids list
+        """
+        # Set client apiversion if it is not already set
+        pprint(
+            "test_api_change_control_create_for_none_task_ids_not_list")
+        if self.get_version():
+            pprint(
+                'CREATE CHANGE CONTROL FOR NONE TASK IDs...')
+            with self.assertRaises(CvpRequestError):
+                self.create_change_control_for_task(
+                    None)
+
+    def test_api_change_control_create_for_invalid_task_id(self):
+        """ Verify change_control_create_for_tasks for invalid task id
+        """
+        pprint(
+            "test_api_change_control_create_for_invalid_task_id")
+        # Set client apiversion if it is not already set
+        if self.get_version():
+            pprint(
+                'CREATING CHANGE CONTROL FOR INVALID TASK IDs...')
+
+            # Create change control for random task
+            chg_ctrl = self.create_change_control_for_task(
+                INVALID_TASK_ID)
+
+            # Approve the change control
+            self.approve_change_control()
+
+            # Verify CC
+            response = self.get_cc_status(
+                self.cc_id)
+
+            assert response is not None
+            assert chg_ctrl['value']['key']['id'] == response['value']['key']['id']
+            assert chg_ctrl['value']['change']['name'] == response['value']['change']['name']
+
+            # Delete CC
+            self.delete_change_control(self.cc_id)
+
+    def test_api_change_control_approve_invalid_tasks(self):
+        """ Verify test_api_change_control_approve_invalid_tasks
+                """
+        pprint(
+            "test_api_change_control_approve_invalid_tasks")
+        if self.get_version():
+            pprint(
+                'APPROVING CHANGE CONTROL FOR INVALID TASKS...')
+            # Approve the change control
+            approve_chg_ctrl = self.api.change_control_approve(
+                CHANGE_CONTROL_ID_INVALID, notes=APPROVE_NOTE)
+            assert approve_chg_ctrl is None
+
+    def test_api_change_control_start_invalid_tasks(self):
+        """ Verify test_api_change_control_start_invalid_tasks
+        """
+        pprint("test_api_change_control_start_invalid_tasks")
+        if self.get_version():
+            pprint('STARTING CHANGE CONTROL FOR INVALID TASKS...')
+            # Start the change control
+            dut = self.duts[0]
+            node = dut['node'] + ":443"
+            # CVP 2022.1.0 format The forward slashes in the error string are likely a bug
+            pprint('SETTING DEFAULT ERROR MESSAGE FORMAT FOR CVP 2022.1.0')
+            err_msg = 'POST: https://' + node + '/api/resources/changecontrol/v1/' \
+                                                'ChangeControlConfig : Request Error:' \
+                                                ' Not Found - {"code":5, "message":"change' \
+                                                ' control with ID' \
+                                                ' \\\\"InvalidCVPRACSystestCCID\\\\"' \
+                                                ' does not exist"}'
+            if self.clnt.apiversion < 8.0:
+                # CVP 2021.X.X format
+                pprint('USING ERROR MESSAGE FORMAT FOR CVP 2021.X.X')
+                err_msg = "POST: https://" + node + "/api/resources/changecontrol/v1/" \
+                                                    "ChangeControlConfig : Request Error: " \
+                                                    "Bad Request -" \
+                                                    " {\"code\":9,[ ]?\"message\":\"not approved\"}"
+            with self.assertRaisesRegex(CvpRequestError, err_msg):
+                self.start_change_control(CHANGE_CONTROL_ID_INVALID)
+
+    def test_api_change_control_delete_invalid_cc(self):
+        """ Verify test_api_change_control_delete_invalid_cc
+                 """
+        pprint(
+            "test_api_change_control_delete_invalid_cc")
+        if self.get_version():
+            pprint('DELETING CHANGE CONTROL...')
+            with self.assertRaises(CvpRequestError):
+                self.delete_change_control(
+                    CHANGE_CONTROL_ID_INVALID)
+
+    def test_api_change_control_get_one(self):
+        """ Verify change_control_get_one
+         """
+        pprint("test_api_change_control_get_one")
+        if self.get_version():
+            # Create task
+            task_id = self.create_task()
+
+            # Create CC
+            self.create_change_control_for_task(
+                task_id)
+
+            pprint("CHANGE CONTROL GET ONE...")
+            # chg_ctrl_get_one = self.api.change_control_get_one(self.cc_id)
+            chg_ctrl_get_one = self.change_control_get_one(
+                self.cc_id)
+            assert chg_ctrl_get_one is not None
+            assert chg_ctrl_get_one['value']['key']['id'] == self.cc_id
+            assert chg_ctrl_get_one['value']['change']['name'] == self.cc_name
+            assert chg_ctrl_get_one['value']['change']['stages']['values']['stage0'] \
+                ['action']['args']['values']['TaskID'] == task_id
+            # verify with actual api output
+            response = self.get_cc_status(
+                self.cc_id)
+            assert chg_ctrl_get_one['value']['key']['id'] == response['value']['key']['id']
+            assert chg_ctrl_get_one['value']['change']['name'] == response['value']['change'][
+                'name']
+            assert chg_ctrl_get_one['value']['change']['stages']['values']['stage0']['action'] \
+                ['args']['values']['TaskID'] == response['value']['change']['stages']['values'] \
+                ['stage0']['action']['args']['values']['TaskID']
+
+            # Delete CC
+            self.delete_change_control(self.cc_id)
+
+            # Cancel Task
+            self.cancel_task(task_id)
+
+    def test_api_change_control_get_one_without_ccid(self):
+        """ Verify change_control_get_one_without_ccid
+         """
+        pprint(
+            "test_api_change_control_get_one_without_ccid")
+        if self.get_version():
+            pprint(
+                "CHANGE CONTROL GET ONE WITHOUT CC_ID...")
+            err_msg = "change_control_get_one() missing 1 required positional argument: 'cc_id'"
+            with self.assertRaises(TypeError) as ex:
+                self.change_control_get_one()
+                self.assertEqual(
+                    err_msg, ex.exception)
+
+    def test_api_change_control_get_one_with_none_ccid(self):
+        """ Verify change_control_get_one_with_none_ccid
+         """
+        pprint(
+            "test_api_change_control_get_one_with_none_ccid")
+        if self.get_version():
+            pprint(
+                "CHANGE CONTROL GET WITH NONE CC_ID...")
+            chg_ctrl_get_one = self.change_control_get_one(
+                None)
+            assert chg_ctrl_get_one is None
+
+    def test_api_change_control_get_one_with_invalid_ccid(self):
+        """ Verify change_control_get_one_with_invalid_ccid
+         """
+        pprint(
+            "test_api_change_control_get_one_with_invalid_ccid")
+        if self.get_version():
+            pprint(
+                "CHANGE CONTROL GET WITH INVALID CC_ID...")
+            chg_ctrl_get_one = self.change_control_get_one(
+                INVALID_CCID)
+            assert chg_ctrl_get_one is None
+
+    def test_api_change_control_get_all(self):
+        """ Verify change_control_get_all
+         """
+        pprint("test_api_change_control_get_all")
+        ids = []
+        if self.get_version():
+            pprint("CHANGE CONTROL GET ALL...")
+            # Create task
+            task_id = self.create_task()
+
+            # Create CC
+            self.create_change_control_for_task(
+                task_id)
+
+            chg_ctrl_get_all = self.api.change_control_get_all()
+            for i in range(len(chg_ctrl_get_all['data'])):
+                ids.append(chg_ctrl_get_all['data'][i]
+                           ['result']['value']['key']['id'])
+            assert self.cc_id in ids
+
+            # Delete CC
+            self.delete_change_control(self.cc_id)
+
+            # Cancel Task
+            self.cancel_task(task_id)
+
+    def test_api_change_control_get_all_without_create_chg_ctrl(self):
+        """ Verify change_control_get_all_without_create_chg_ctrl
+         """
+        pprint("test_api_change_control_get_all_without_create_chg_ctrl")
+        ids = []
+        if self.get_version():
+            pprint("CHANGE CONTROL GET ALL WITHOUT CHANGE CONTROL CREATION...")
+            resp = self.api.change_control_get_all()
+            if 'data' in resp:
+                for i in range(len(resp['data'])):
+                    ids.append(resp['data'][i]['result']['value']['key']['id'])
+            assert self.cc_id not in ids
+
+    def test_api_change_control_approval_get_all(self):
+        """ Verify change_control_approval_get_all
+         """
+        pprint("test_api_change_control_approval_get_all")
+        ids = []
+        if self.get_version():
+            # Create task
+            task_id = self.create_task()
+
+            # Create CC
+            self.create_change_control_for_task(
+                task_id)
+
+            # Approve CC
+            self.approve_change_control()
+
+            pprint(
+                "CHANGE CONTROL APPROVAL GET ALL...")
+            chg_ctrl_approval_get_all = self.api.change_control_approval_get_all()
+            for i in range(len(chg_ctrl_approval_get_all['data'])):
+                ids.append(
+                    chg_ctrl_approval_get_all['data'][i]['result']['value']['key']['id'])
+                if chg_ctrl_approval_get_all['data'][i]['result']['value']['key']['id'] \
+                        == self.cc_id:
+                    check_index = i
+            assert self.cc_id in ids
+            assert chg_ctrl_approval_get_all['data'][check_index][
+                'result']['value']['approve']['value'] is True
+
+            # Delete CC
+            self.delete_change_control(self.cc_id)
+
+            # Cancel Task
+            self.cancel_task(task_id)
+
+    def test_api_change_control_approval_get_all_without_approve(self):
+        """ Verify change_control_approval_get_all_without_approve
+         """
+        pprint("test_api_change_control_approval_get_all_without_approve")
+        if self.get_version():
+            # Create task
+            task_id = self.create_task()
+
+            # Create CC
+            self.create_change_control_for_task(
+                task_id)
+
+            ids = []
+            pprint("CHANGE CONTROL APPROVAL GET ALL WITHOUT APPROVE...")
+            resp = self.api.change_control_approval_get_all()
+            if 'data' in resp:
+                for i in range(len(resp['data'])):
+                    ids.append(resp['data'][i]['result']['value']['key']['id'])
+            assert self.cc_id not in ids
+
+            # Delete CC
+            self.delete_change_control(self.cc_id)
+            # Cancel Task
+            self.cancel_task(task_id)
+
+    def test_api_change_control_create_with_custom_stages(self):
+        """ Verify test_api_change_control_create_with_custom_stages
+        """
+        pprint(
+            "test_api_change_control_create_with_custom_stages")
+        devices = self.get_device_list()
+        if len(devices) > 1:
+            device_id_1 = devices[0]
+            device_id_2 = devices[1]
+        elif len(devices) == 1:
+            device_id_1 = devices[0]
+            device_id_2 = devices[0]
+
+        template_id = [
+            self.create_snapshot(), self.create_snapshot()]
+        time.sleep(1)
+        custom_cc = {'key': {
+            'id': self.cc_id
+        },
+            'change': {
+            'name': self.cc_name,
+            'notes': 'cvprac CC',
+            'rootStageId': 'root',
+            'stages': {'values': {'root': {'name': 'root',
+                                           'rows': {'values': [{'values': ['1-2']},
+                                                               {'values': ['3']}]
+                                                    }
+                                           },
+                                  '1-2': {'name': 'stages 1-2',
+                                          'rows': {'values': [{'values': ['1ab']},
+                                                              {'values': ['2']}]}},
+                                  '1ab': {'name': 'stage 1ab',
+                                          'rows': {'values': [{'values': ['1a', '1b']}]
+                                                   }
+                                          },
+                                  '2': {'name': 'stage 2ab',
+                                        'rows': {'values': [{'values': ['2a', '2b']}]
+                                                 }
+                                        },
+                                  '1a': {'action': {'args': {'values': {'DeviceID': device_id_1,
+                                                                    'TemplateID': template_id[0]}},
+                                                    'name': 'snapshot',
+                                                    'timeout': 3000},
+                                         'name': 'stage 1a'},
+                                  '1b': {'action': {'args': {'values': {'DeviceID': device_id_1,
+                                                                    'TemplateID': template_id[0]}},
+                                                    'name': 'snapshot',
+                                                    'timeout': 3000},
+                                         'name': 'stage 1b'},
+                                  '2a': {'action': {'args': {'values': {'DeviceID': device_id_1,
+                                                                    'TemplateID': template_id[0]}},
+                                                    'name': 'snapshot',
+                                                    'timeout': 3000},
+                                         'name': 'stage 2a'},
+                                  '2b': {'action': {'args': {'values': {'DeviceID': device_id_2,
+                                                                    'TemplateID': template_id[1]}},
+                                                    'name': 'snapshot',
+                                                    'timeout': 3000},
+                                         'name': 'stage 2a'},
+                                  '3': {'action': {'args': {'values': {'DeviceID': device_id_2,
+                                                                    'TemplateID': template_id[1]}},
+                                                   'name': 'snapshot',
+                                                   'timeout': 3000},
+                                        'name': 'stage 3'},
+                                  }}}}
+
+        if self.get_version():
+            pprint(
+                "CHANGE CONTROL CREATE WITH CUSTOM STAGES...")
+            chg_ctrl_create_with_custom_stages = self.api.change_control_create_with_custom_stages(
+                custom_cc)
+            assert chg_ctrl_create_with_custom_stages[
+                'value']['key']['id'] == self.cc_id
+            assert chg_ctrl_create_with_custom_stages[
+                'value']['change']['name'] == self.cc_name
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages'][
+                'values']['1a']['action']['args']['values']['DeviceID'] == device_id_1
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages'][
+                'values']['1a']['action']['args']['values']['TemplateID'] == template_id[0]
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages'][
+                'values']['1b']['action']['args']['values']['DeviceID'] == device_id_1
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages'][
+                'values']['1b']['action']['args']['values']['TemplateID'] == template_id[0]
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages'][
+                'values']['2a']['action']['args']['values']['DeviceID'] == device_id_1
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages'][
+                'values']['2a']['action']['args']['values']['TemplateID'] == template_id[0]
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages'][
+                'values']['2b']['action']['args']['values']['DeviceID'] == device_id_2
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages'][
+                'values']['2b']['action']['args']['values']['TemplateID'] == template_id[1]
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages'][
+                'values']['3']['action']['args']['values']['DeviceID'] == device_id_2
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages'][
+                'values']['3']['action']['args']['values']['TemplateID'] == template_id[1]
+
+            response = self.get_cc_status(
+                self.cc_id)
+
+            assert chg_ctrl_create_with_custom_stages['value'][
+                'key']['id'] == response['value']['key']['id']
+            assert chg_ctrl_create_with_custom_stages['value'][
+                'change']['name'] == response['value']['change']['name']
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages']['values'][
+                '1a']['action']['args']['values']['DeviceID'] == response['value']['change'][
+                'stages']['values']['1a']['action']['args']['values']['DeviceID']
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages']['values'][
+                '1a']['action']['args']['values']['TemplateID'] == response['value']['change'][
+                'stages']['values']['1a']['action']['args']['values']['TemplateID']
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages']['values'][
+                '1b']['action']['args']['values']['DeviceID'] == response['value']['change'][
+                'stages']['values']['1b']['action']['args']['values']['DeviceID']
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages']['values'][
+                '1b']['action']['args']['values']['TemplateID'] == response['value']['change'][
+                'stages']['values']['1b']['action']['args']['values']['TemplateID']
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages']['values'][
+                '2a']['action']['args']['values']['DeviceID'] == response['value']['change'][
+                'stages']['values']['2a']['action']['args']['values']['DeviceID']
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages']['values'][
+                '2a']['action']['args']['values']['TemplateID'] == response['value']['change'][
+                'stages']['values']['2a']['action']['args']['values']['TemplateID']
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages']['values'][
+                '2b']['action']['args']['values']['DeviceID'] == response['value']['change'][
+                'stages']['values']['2b']['action']['args']['values']['DeviceID']
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages']['values'][
+                '2b']['action']['args']['values']['TemplateID'] == response['value']['change'][
+                'stages']['values']['2b']['action']['args']['values']['TemplateID']
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages']['values'][
+                '3']['action']['args']['values']['DeviceID'] == response['value']['change'][
+                'stages']['values']['3']['action']['args']['values']['DeviceID']
+            assert chg_ctrl_create_with_custom_stages['value']['change']['stages']['values'][
+                '3']['action']['args']['values']['TemplateID'] == response['value']['change'][
+                'stages']['values']['3']['action']['args']['values']['TemplateID']
+            self.approve_change_control()
+            self.start_change_control(self.cc_id)
+            time.sleep(1)
+            self.stop_change_control()
+            self.delete_change_control(self.cc_id)
+            delete_snap = self.delete_snapshot(
+                template_id)
+            assert delete_snap['result'] == 'Success'
+
+    def test_api_change_control_create_with_custom_stages_without_custom_cc(self):
+        """ Verify test_api_change_control_create_with_custom_stages
+        """
+        pprint(
+            "test_api_change_control_create_with_custom_stages_without_custom_cc")
+        if self.get_version():
+            pprint(
+                "CHANGE CONTROL CREATE WITH CUSTOM STAGES WITHOUT CUSTOM CC...")
+            with self.assertRaises(CvpRequestError):
+                self.api.change_control_create_with_custom_stages()
+
+    def test_api_change_control_create_with_custom_stages_with_none_custom_cc(self):
+        """ Verify test_api_change_control_create_with_custom_stages
+        """
+        pprint(
+            "test_api_change_control_create_with_custom_stages_with_none_custom_cc")
+        if self.get_version():
+            pprint(
+                "CHANGE CONTROL CREATE WITH CUSTOM STAGES WITH NONE CUSTOM CC...")
+            with self.assertRaises(CvpRequestError):
+                self.api.change_control_create_with_custom_stages(
+                    None)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/test/system/test_cvp_client.py b/test/system/test_cvp_client.py
index d92ffa9..621feae 100644
--- a/test/system/test_cvp_client.py
+++ b/test/system/test_cvp_client.py
@@ -44,15 +44,14 @@
 import os
 import sys
 import unittest
+from pprint import pprint
 from requests.exceptions import Timeout
-
+sys.path.append(os.path.join(os.path.dirname(__file__), '../lib'))
+from systestlib import DutSystemTest
 from cvprac.cvp_client import CvpClient
 from cvprac.cvp_client_errors import CvpApiError, CvpLoginError, \
     CvpRequestError, CvpSessionLogOutError
 
-sys.path.append(os.path.join(os.path.dirname(__file__), '../lib'))
-from systestlib import DutSystemTest
-
 
 class TestCvpClient(DutSystemTest):
     ''' Test cases for the CvpClient class.
@@ -92,7 +91,7 @@ def _change_passwd(self, nodes, username, old_password, new_password):
                 'network-admin'
             ]
         }
-        result = clnt.post("/user/updateUser.do?userId=%s" % username, data)
+        result = clnt.post(f"/user/updateUser.do?userId={username}", data)
         self.assertEqual('success', result['data'])
 
     def test_clnt_init(self):
@@ -281,8 +280,15 @@ def test_get_cvp_url_bad(self):
         '''
         dut = self.duts[0]
         self.clnt.connect([dut['node']], dut['username'], dut['password'])
-        with self.assertRaises(CvpApiError):
-            self.clnt.get('/aaa/bogus.do')
+        # Error raised changes in CVP 2021.2.0
+        if not self.clnt.apiversion:
+            self.clnt.api.get_cvp_info()
+        if self.clnt.apiversion < 6.0:
+            with self.assertRaises(CvpApiError):
+                self.clnt.get('/aaa/bogus.do')
+        else:
+            with self.assertRaises(CvpRequestError):
+                self.clnt.get('/aaa/bogus.do')
 
     def test_get_handle_timeout(self):
         ''' Verify get with bad URL returns an error
@@ -299,6 +305,13 @@ def test_get_except_fail_reconnect(self):
         dut = self.duts[0]
         nodes = ['bogus', dut['node']]
         self.clnt.connect(nodes, dut['username'], dut['password'])
+
+        if self.clnt.apiversion is None:
+            self.clnt.api.get_cvp_info()
+        if self.clnt.apiversion == 7.0:
+            pprint("Skip test case for issue in CVP 2021.3.1")
+            self.skipTest("Skip test case for issue in CVP 2021.3.1")
+
         # Change the password for the CVP user so that a session reconnect
         # to any node will fail
         self._change_passwd(nodes, dut['username'], dut['password'],
@@ -320,7 +333,7 @@ def test_get_except_fail_reconnect(self):
         try:
             # Try a get request and expect a CvpSessionLogOutError
             result = self.clnt.get('/cvpInfo/getCvpInfo.do')
-        except (CvpSessionLogOutError, CvpApiError) as error:
+        except (CvpSessionLogOutError, CvpApiError):
             pass
         except Exception as error:
             # Unexpected error, restore password and re-raise the error.
@@ -388,8 +401,15 @@ def test_post_cvp_url_bad(self):
         '''
         dut = self.duts[0]
         self.clnt.connect([dut['node']], dut['username'], dut['password'])
-        with self.assertRaises(CvpApiError):
-            self.clnt.post('/aaa/bogus.do', None)
+        # Error raised changes in CVP 2021.2.0
+        if not self.clnt.apiversion:
+            self.clnt.api.get_cvp_info()
+        if self.clnt.apiversion < 6.0:
+            with self.assertRaises(CvpApiError):
+                self.clnt.post('/aaa/bogus.do', None)
+        else:
+            with self.assertRaises(CvpRequestError):
+                self.clnt.post('/aaa/bogus.do', None)
 
     def test_post_except_fail_reconn(self):
         ''' Verify exception raised if session fails and cannot be
@@ -398,6 +418,13 @@ def test_post_except_fail_reconn(self):
         dut = self.duts[0]
         nodes = ['bogus', dut['node']]
         self.clnt.connect(nodes, dut['username'], dut['password'])
+
+        if self.clnt.apiversion is None:
+            self.clnt.api.get_cvp_info()
+        if self.clnt.apiversion == 7.0:
+            pprint("Skip test case for issue in CVP 2021.3.1")
+            self.skipTest("Skip test case for issue in CVP 2021.3.1")
+
         # Change the password for the CVP user so that a session reconnect
         # to any node will fail
         self._change_passwd(nodes, dut['username'], dut['password'],
@@ -419,7 +446,7 @@ def test_post_except_fail_reconn(self):
         try:
             # Try a post request and expect a CvpSessionLogOutError
             result = self.clnt.post('/login/logout.do', None)
-        except (CvpSessionLogOutError, CvpApiError) as error:
+        except (CvpSessionLogOutError, CvpApiError):
             pass
         except Exception as error:
             # Unexpected error, restore password and re-raise the error.
diff --git a/test/system/test_cvp_api.py b/test/system/test_cvp_client_api.py
similarity index 77%
rename from test/system/test_cvp_api.py
rename to test/system/test_cvp_client_api.py
index 0f57b34..6c8dc4b 100644
--- a/test/system/test_cvp_api.py
+++ b/test/system/test_cvp_client_api.py
@@ -49,144 +49,25 @@
          Failure response received from the netElement : ' Unauthorized User '
 '''
 import os
-import re
 import shutil
 import sys
 import time
 import unittest
+import uuid
 from pprint import pprint
-from requests.exceptions import Timeout
-
 import urllib3
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-from cvprac.cvp_client import CvpClient
+from test_cvp_base import TestCvpClientBase
+from requests.exceptions import Timeout
 from cvprac.cvp_client_errors import CvpApiError
 
-sys.path.append(os.path.join(os.path.dirname(__file__), '../lib'))
-from systestlib import DutSystemTest
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
 
 
-class TestCvpClient(DutSystemTest):
+class TestCvpClient(TestCvpClientBase):
     ''' Test cases for the CvpClient class.
     '''
     # pylint: disable=too-many-public-methods
     # pylint: disable=invalid-name
-    def setUp(self):
-        ''' Instantiate the CvpClient class and connect to the CVP node.
-            Log messages to the /tmp/TestCvpClient.log
-        '''
-        super(TestCvpClient, self).setUp()
-        self.clnt = CvpClient(filename='/tmp/TestCvpClient.log')
-        self.assertIsNotNone(self.clnt)
-        self.assertIsNone(self.clnt.last_used_node)
-        dut = self.duts[0]
-        cert = False
-        if 'cert' in dut:
-            cert = dut['cert']
-        self.clnt.connect([dut['node']], dut['username'], dut['password'], 10,
-                          cert=cert)
-        self.api = self.clnt.api
-        self.assertIsNotNone(self.api)
-
-        # Verify that there is at least one device in the inventory
-        err_msg = 'CVP node must contain at least one device'
-        result = self.api.get_inventory()
-        self.assertIsNotNone(result)
-        self.assertGreaterEqual(len(result), 1, msg=err_msg)
-        self.device = result[0]
-
-        # Get the container for the device on the list and
-        # use that container as the parent container.
-        result = self.api.search_topology(self.device['fqdn'])
-        self.assertIsNotNone(result)
-        dev_container = result['netElementContainerList']
-        self.assertGreaterEqual(len(dev_container), 1)
-        info = dev_container[0]
-        result = self.api.search_topology(info['containerName'])
-        self.assertIsNotNone(result)
-        self.container = result['containerList'][0]
-
-        # Get the configlets assigned to the device.  There must be at least 1.
-        err_msg = 'CVP node device must have at least one configlet assigned'
-        key = info['netElementKey']
-        result = self.api.get_configlets_by_device_id(key)
-        self.assertGreaterEqual(len(result), 1, msg=err_msg)
-        self.dev_configlets = result
-
-    def tearDown(self):
-        ''' Destroy the CvpClient class.
-        '''
-        super(TestCvpClient, self).tearDown()
-        self.api = None
-        self.clnt = None
-
-    def _get_next_task_id(self):
-        ''' Return the next task id.
-
-            Returns:
-                task_id (str): Task ID
-        '''
-        # Get all the tasks and the task id of the next task created is
-        # the length + 1.
-        results = self.api.get_tasks()
-        self.assertIsNotNone(results)
-        return str(int(results['total']) + 1)
-
-    def _create_task(self):
-        ''' Create a task by making a simple change to a configlet assigned
-            to the device.
-
-            Returns:
-                (task_id, config)
-                task_id (str): Task ID
-                config (str): Previous configlets contents
-        '''
-        task_id = self._get_next_task_id()
-
-        # Update the lldp time in the first configlet in the list.
-        configlet = None
-        for conf in self.dev_configlets:
-            if conf['netElementCount'] == 1:
-                configlet = conf
-                break
-        if configlet is None:
-            configlet = self.dev_configlets[0]
-
-        config = configlet['config']
-        org_config = config
-
-        match = re.match(r'lldp timer (\d+)', config)
-        if match is not None:
-            value = int(match.group(1)) + 1
-            repl = 'lldp timer %d' % value
-            config = re.sub(match.group(0), repl, config)
-        else:
-            value = 13
-            config = ('lldp timer %d\n' % value) + config
-        configlet['config'] = config
-
-        # Updating the configlet will cause a task to be created to apply
-        # the change to the device.
-        self.api.update_configlet(config, configlet['key'], configlet['name'])
-
-        # Wait 30 seconds for task to get created
-        cnt = 30
-        if self.clnt.apiversion is None:
-            self.api.get_cvp_info()
-        if self.clnt.apiversion >= 2.0:
-            # Increase timeout by 30 sec for CVP 2018.2 and beyond
-            cnt += 30
-        while cnt > 0:
-            time.sleep(1)
-            result = self.api.get_task_by_id(task_id)
-            if result is not None:
-                break
-            cnt -= 1
-        err_msg = 'Timeout waiting for task id %s to be created' % task_id
-        self.assertGreater(cnt, 0, msg=err_msg)
-
-        return task_id, org_config
 
     def test_api_get_cvp_info(self):
         ''' Verify get_cvp_info and verify setting of client last_used_node
@@ -206,13 +87,14 @@ def test_api_user_operations(self):
         # pylint: disable=too-many-branches
         dut = self.duts[0]
         # Test Get User
-        result = self.api.get_user(dut['username'])
-        self.assertIsNotNone(result)
-        self.assertIn('user', result)
-        self.assertIn('userId', result['user'])
-        self.assertEqual(result['user']['userId'], 'cvpadmin')
-        self.assertIn('userStatus', result['user'])
-        self.assertIsNotNone(result['roles'])
+        if 'username' in dut:
+            result = self.api.get_user(dut['username'])
+            self.assertIsNotNone(result)
+            self.assertIn('user', result)
+            self.assertIn('userId', result['user'])
+            self.assertEqual(result['user']['userId'], 'cvpadmin')
+            self.assertIn('userStatus', result['user'])
+            self.assertIsNotNone(result['roles'])
 
         # Check if test user exists
         try:
@@ -326,9 +208,26 @@ def test_api_check_compliance(self):
         '''
         key = self.device['key']
         ntype = self.device['type']
-        result = self.api.check_compliance(key, ntype)
-        self.assertEqual(result['complianceCode'], '0000')
-        self.assertEqual(result['complianceIndication'], 'NONE')
+        # Test compliance started timing out often around CVP2020
+        orig_timeout = self.api.request_timeout
+        if not self.clnt.apiversion:
+            self.api.get_cvp_info()
+        if self.clnt.apiversion >= 4.0:
+            self.api.request_timeout = orig_timeout * 3
+        for _ in range(0, 2):
+            result = self.api.check_compliance(key, ntype)
+            self.assertIsNotNone(result)
+            self.assertIn('complianceCode', result)
+            if result['complianceCode'] == '0000':
+                self.assertIn('complianceIndication', result)
+                self.assertEqual(result['complianceIndication'], 'NONE')
+                break
+            # Wait for CVP to get back into compliance.
+            # Starting around CVP 2020 it takes a bit for CVP to get
+            # back into compliance after previous tests.
+            time.sleep(10)
+        if self.clnt.apiversion >= 4.0:
+            self.api.request_timeout = orig_timeout
 
     def test_api_task_operations(self):
         ''' Verify get_task_by_id, get_task_by_status, add_note_to_task,
@@ -350,7 +249,7 @@ def test_api_task_operations(self):
             if actual_task_id == task_id:
                 found = True
                 break
-        err_msg = 'Task id: %s not in list of PENDING tasks' % task_id
+        err_msg = f'Task id: {task_id} not in list of PENDING tasks'
         self.assertTrue(found, msg=err_msg)
 
         # Test add_note_to_task
@@ -365,6 +264,7 @@ def test_api_task_operations(self):
 
         result = self.api.check_compliance(self.device['key'],
                                            self.device['type'])
+
         self.assertEqual(result['complianceCode'], '0001')
 
         # Test cancel_task
@@ -376,6 +276,7 @@ def test_api_task_operations(self):
 
         result = self.api.check_compliance(self.device['key'],
                                            self.device['type'])
+
         self.assertEqual(result['complianceCode'], '0001')
 
         # Get the task logs
@@ -384,6 +285,7 @@ def test_api_task_operations(self):
 
         result = self.api.check_compliance(self.device['key'],
                                            self.device['type'])
+
         self.assertEqual(result['complianceCode'], '0001')
 
         # Restore the configlet to what it was before the task was created.
@@ -506,7 +408,10 @@ def test_api_get_configlets(self):
 
         # Make sure the device configlets are all returned by the
         # get_configlets call
-        for cfglt_name in dev_cfglts:
+
+        for cfglt_name in list(dev_cfglts.keys()):
+            del dev_cfglts[cfglt_name]['dateTimeInLongFormat']
+            del rslt_cfglts[cfglt_name]['dateTimeInLongFormat']
             self.assertIn(cfglt_name, rslt_cfglts)
             self.assertDictEqual(dev_cfglts[cfglt_name],
                                  rslt_cfglts[cfglt_name])
@@ -555,16 +460,16 @@ def test_api_get_configlet_builder(self):
         # Verify the following keys and types are
         # returned by the request
         exp_data = {
-            u'isAssigned': bool,
-            u'formList': list,
-            u'main_script': dict,
+            'isAssigned': bool,
+            'formList': list,
+            'main_script': dict,
         }
         # Handle unicode type for Python 2 vs Python 3
         if sys.version_info.major < 3:
             exp_data[u'name'] = (unicode, str)
         else:
             exp_data[u'name'] = str
-        for key in exp_data:
+        for key in list(exp_data.keys()):
             self.assertIn(key, result['data'])
             self.assertIsInstance(result['data'][key], exp_data[key])
 
@@ -594,7 +499,7 @@ def test_api_get_configlets_by_container_id(self):
             'total': int,
             'configletMapper': dict,
         }
-        for key in exp_data:
+        for key in list(exp_data.keys()):
             self.assertIn(key, result)
             self.assertIsInstance(result[key], exp_data[key])
 
@@ -609,7 +514,7 @@ def test_api_get_configlets_by_netelement_id(self):
             'total': int,
             'configletMapper': dict,
         }
-        for key in exp_data:
+        for key in list(exp_data.keys()):
             self.assertIn(key, result)
             self.assertIsInstance(result[key], exp_data[key])
 
@@ -625,7 +530,7 @@ def test_api_get_applied_devices(self):
                 'data': list,
                 'total': int,
             }
-            for key in exp_data:
+            for key in list(exp_data.keys()):
                 self.assertIn(key, result)
                 self.assertIsInstance(result[key], exp_data[key])
 
@@ -641,7 +546,7 @@ def test_api_get_applied_containers(self):
                 'data': list,
                 'total': int,
             }
-            for key in exp_data:
+            for key in list(exp_data.keys()):
                 self.assertIn(key, result)
                 self.assertIsInstance(result[key], exp_data[key])
 
@@ -678,7 +583,7 @@ def test_api_get_device_configuration(self):
         config_lines = result.splitlines()
         for line in config_lines:
             if 'hostname' in line:
-                self.assertEqual(line, 'hostname %s' % self.device['fqdn'])
+                self.assertEqual(line, f"hostname {self.device['fqdn']}")
 
     def test_api_get_device_by_name_bad(self):
         ''' Verify get_device_by_name with bad fqdn
@@ -823,7 +728,7 @@ def test_api_add_update_delete_configlet_builder(self):
     def test_api_update_reconcile_configlet(self):
         ''' Verify update_reconcile_configlet
         '''
-        rec_configlet_name = 'RECONCILE_{}'.format(self.device['ipAddress'])
+        rec_configlet_name = f"RECONCILE_{self.device['ipAddress']}"
         # Verify this reconcile configlet doesn't already exist
         with self.assertRaises(CvpApiError):
             self.api.get_configlet_by_name(rec_configlet_name)
@@ -896,7 +801,7 @@ def test_api_add_delete_configlet(self):
     def test_api_add_note_to_configlet(self):
         ''' Verify add_note_to_configlet
         '''
-        name = 'test_configlet_with_note_%d' % time.time()
+        name = f'test_configlet_with_note_{time.time()}'
         config = 'lldp timer 9'
 
         # Add the configlet
@@ -930,12 +835,13 @@ def _execute_task(self, task_id):
             time.sleep(1)
             result = self.api.get_task_by_id(task_id)
             status = result['workOrderUserDefinedStatus']
-            if status == 'Completed' or status == 'Failed':
+            if status in ['Completed', 'Failed']:
+                # if status == 'Completed' or status == 'Failed':
                 break
             cnt -= 1
-        err_msg = 'Execution for task id %s failed' % task_id
+        err_msg = f'Execution for task id {task_id} failed'
         self.assertNotEqual(status, 'Failed', msg=err_msg)
-        err_msg = 'Timeout waiting for task id %s to execute' % task_id
+        err_msg = f'Timeout waiting for task id {task_id} to execute'
         self.assertGreater(cnt, 0, msg=err_msg)
 
     def _execute_long_running_task(self, task_id):
@@ -952,13 +858,13 @@ def _execute_long_running_task(self, task_id):
             time.sleep(10)
             result = self.api.get_task_by_id(task_id)
             status = result['workOrderUserDefinedStatus']
-            if status == 'Completed' or status == 'Failed':
+            if status in ['Completed', 'Failed']:
+                # if status == 'Completed' or status == 'Failed':
                 break
             cnt -= 1
-        err_msg = 'Execution for task id %s failed' % task_id
+        err_msg = f'Execution for task id {task_id} failed'
         self.assertNotEqual(status, 'Failed', msg=err_msg)
-        err_msg = ('Timeout waiting for long running task id %s to execute'
-                   % task_id)
+        err_msg = f'Timeout waiting for long running task id {task_id} to execute'
         self.assertGreater(cnt, 0, msg=err_msg)
 
     def test_api_execute_task(self):
@@ -994,8 +900,12 @@ def test_api_get_devices_in_container(self):
         all_devices = self.api.get_inventory()
 
         # Grab key of container to test from first device in inventory
-        device = all_devices[0]
-        parent_cont = device['parentContainerId']
+        test_dev = all_devices[0]
+        for dev in all_devices:
+            # If device provided in cvp_nodes.yaml is in inventory use it for the test
+            if dev['fqdn'] == self.device['fqdn']:
+                test_dev = dev
+        parent_cont = test_dev['parentContainerId']
 
         # Make list of all devices from full inventory that are in the
         # same container as the first device
@@ -1114,13 +1024,11 @@ def test_api_delete_container_with_children(self):
                                       parent['name'], parent['key'])
         except CvpApiError as error:
             if 'Only empty container can be deleted' in error.msg:
-                pprint('CVP Version {} raises error when attempting to'
-                       ' delete container with'
-                       ' children'.format(self.clnt.apiversion))
+                pprint(f'CVP Version {self.clnt.apiversion} raises error when attempting to'
+                       ' delete container with children')
             elif 'Container was not deleted. Check for children' in error.msg:
-                pprint('CVP Version {} does not raise error when attempting to'
-                       ' delete container with'
-                       ' children'.format(self.clnt.apiversion))
+                pprint(f'CVP Version {self.clnt.apiversion} does not raise error when attempting to'
+                       ' delete container with children')
 
         # Delete child container first
         resp = self.api.delete_container(new_child_container['name'],
@@ -1453,9 +1361,9 @@ def test_api_get_all_temp_actions(self):
                 'configletNamesList': [test_configlet_name],
                 'ignoreConfigletList': [],
                 'ignoreConfigletNamesList': [],
-                'configletBuilderList' : [],
-                'configletBuilderNamesList' : [],
-                'ignoreConfigletBuilderList' : [],
+                'configletBuilderList': [],
+                'configletBuilderNamesList': [],
+                'ignoreConfigletBuilderList': [],
                 'ignoreConfigletBuilderNamesList': [],
             }]
         }
@@ -1501,17 +1409,17 @@ def test_api_get_default_snapshot_template(self):
         result = self.api.get_default_snapshot_template()
         if result is not None:
             expected = {
-                u'ccTasksTagged': 0,
-                u'classId': 63,
-                u'commandCount': 1,
-                u'createdBy': u'System',
-                u'default': True,
-                u'factoryId': 1,
-                u'id': 63,
-                u'isDefault': True,
-                u'key': u'Initial_Template',
-                u'name': u'Show_Inventory',
-                u'note': u'',
+                'ccTasksTagged': 0,
+                'classId': 63,
+                'commandCount': 1,
+                'createdBy': 'System',
+                'default': True,
+                'factoryId': 1,
+                'id': 63,
+                'isDefault': True,
+                'key': 'Initial_Template',
+                'name': 'Show_Inventory',
+                'note': '',
             }
 
             # Remove the snapshotCount, totalSnapshotCount and
@@ -1544,8 +1452,8 @@ def test_api_add_image_cancel_image(self):
         orig_images = self.api.get_images()
 
         # Copy the test image file with a timestamp appended
-        image_file_name = 'image-file-%s.swix' % time.time()
-        image_file = 'test/fixtures/%s' % image_file_name
+        image_file_name = f'image-file-{time.time()}.swix'
+        image_file = f'test/fixtures/{image_file_name}'
         shutil.copyfile('test/fixtures/image-file.swix', image_file)
 
         # Upload the image to the cluster
@@ -1613,7 +1521,7 @@ def test_api_save_update_delete_image_bundle(self):
                 image.pop(key, None)
 
         # Create a new bundle with the same images
-        original_name = 'test_image_bundle_%d' % time.time()
+        original_name = f'test_image_bundle_{time.time()}'
         result = self.api.save_image_bundle(original_name, images)
         expected = r'Bundle\s*:\s+%s successfully created' % original_name
         self.assertRegexpMatches(result['data'], expected)
@@ -1679,11 +1587,16 @@ def test_api_apply_remove_image_device(self):
         '''
         bundles = self.api.get_image_bundles()
         devices = self.api.get_inventory()
+        test_dev = devices[0]
+        for dev in devices:
+            # If device provided in cvp_nodes.yaml is in inventory use it for the test
+            if dev['fqdn'] == self.device['fqdn']:
+                test_dev = dev
         # Verify at least one image bundle and device exist
         if bundles['total'] > 0 and devices:
             # Get device and image bundle
             b = self.api.get_image_bundle_by_name(bundles['data'][0]['name'])
-            d = self.api.get_device_by_name(devices[0]['fqdn'])
+            d = self.api.get_device_by_name(test_dev['fqdn'])
             applied_devices_count = b['appliedDevicesCount']
 
             # Apply image and verify at least one task id was created
@@ -1757,24 +1670,46 @@ def test_api_apply_remove_image_container(self):
     def test_api_inventory(self):
         ''' Verify add_device_to_inventory and delete_device(s)
         '''
-        # Get a device
+        # pylint: disable=too-many-locals
+        # Get a test device
         full_inv = self.api.get_inventory()
-        device = full_inv[0]
+        test_dev = full_inv[0]
+        for dev in full_inv:
+            # If device provided in cvp_nodes.yaml is in inventory use it for the test
+            if dev['fqdn'] == self.device['fqdn']:
+                test_dev = dev
         # Record number of current/original non connected devices
         orig_non_connect_count = self.api.get_non_connected_device_count()
-        # Get devices current container assigned
-        orig_cont = self.api.get_parent_container_for_device(device['key'])
-        # Get devices current configlets
-        orig_configlets = self.api.get_configlets_by_device_id(device['key'])
+        # Get test devices current container assigned
+        orig_cont = self.api.get_parent_container_for_device(test_dev['key'])
+        # Get test devices current configlets
+        orig_configlets = self.api.get_configlets_by_device_id(test_dev['key'])
         # delete from inventory
-        self.api.delete_device(device['systemMacAddress'])
-        # sleep to allow delete to complete
-        time.sleep(1)
+        if self.clnt.apiversion <= 7.0:
+            self.api.delete_device(test_dev['systemMacAddress'])
+            # sleep to allow delete to complete
+            time.sleep(1)
+        else:
+            req_id = str(uuid.uuid4())
+            self.api.device_decommissioning(test_dev['serialNumber'], req_id)
+            decomm_status = "DECOMMISSIONING_STATUS_SUCCESS"
+            decomm = ""
+            decomm_timer = 0
+            while decomm != decomm_status or decomm_timer < 600:
+                decomm = self.api.device_decommissioning_status_get_one(req_id)['value']['status']
+                time.sleep(10)
+                decomm_timer += 10
         # verify not found in inventory
-        res = self.api.get_device_by_name(device['fqdn'])
+        res = self.api.get_device_by_name(test_dev['fqdn'])
         self.assertEqual(res, {})
         # add back to inventory
-        self.api.add_device_to_inventory(device['ipAddress'],
+        # Adding test device back to inv started timing out in CVP2021.2.0
+        orig_timeout = self.api.request_timeout
+        if not self.clnt.apiversion:
+            self.api.get_cvp_info()
+        if self.clnt.apiversion >= 6.0:
+            self.api.request_timeout = orig_timeout * 3
+        self.api.add_device_to_inventory(test_dev['ipAddress'],
                                          orig_cont['name'],
                                          orig_cont['key'], True)
         # get non connected device count until it is back to equal or less
@@ -1797,12 +1732,12 @@ def test_api_inventory(self):
         post_save_inv = self.api.get_inventory()
         self.assertEqual(len(post_save_inv), len(full_inv))
         # verify device is found in inventory again
-        re_added_dev = self.api.get_device_by_name(device['fqdn'])
+        re_added_dev = self.api.get_device_by_name(test_dev['fqdn'])
         self.assertEqual(re_added_dev['systemMacAddress'],
-                         device['systemMacAddress'])
+                         test_dev['systemMacAddress'])
         # apply original configlets back to device
         results = self.api.apply_configlets_to_device("test_api_inventory",
-                                                      device, orig_configlets,
+                                                      test_dev, orig_configlets,
                                                       create_task=True)
         # execute returned task and wait for it to complete
         task_res = self.api.execute_task(results['data']['taskIds'][0])
@@ -1812,16 +1747,18 @@ def test_api_inventory(self):
             task_status = self.api.get_task_by_id(
                 results['data']['taskIds'][0])
             time.sleep(1)
+        if self.clnt.apiversion >= 6.0:
+            self.api.request_timeout = orig_timeout
 
         # delete from inventory
-        # self.api.delete_device(device['systemMacAddress'])
+        # self.api.delete_device(test_dev['systemMacAddress'])
         # verify not found in inventory
-        # res = self.api.get_device_by_name(device['fqdn'])
+        # res = self.api.get_device_by_name(test_dev['fqdn'])
         # self.assertEqual(res, {})
         # dut = self.duts[0]
-        # self.api.retry_add_to_inventory(device['ipAddress'],
-        #                                device['systemMacAddress'],
-        #                                dut['username'], dut['password'])
+        # self.api.retry_add_to_inventory(test_dev['ipAddress'],
+        #                                 test_dev['systemMacAddress'],
+        #                                 dut['username'], dut['password'])
 
     def test_api_change_control(self):
         ''' Verify get_change_control_info and execute_change_control.
@@ -1830,7 +1767,7 @@ def test_api_change_control(self):
         if self.clnt.apiversion is None:
             self.api.get_cvp_info()
         if self.clnt.apiversion < 3.0:
-            chg_ctrl_name = 'test_api_%d' % time.time()
+            chg_ctrl_name = f'test_api_{time.time()}'
             (task_id, _) = self._create_task()
             chg_ctrl_tasks = [{
                 'taskId': task_id,
@@ -1858,8 +1795,7 @@ def test_api_change_control(self):
                 chg_ctrl_executed = self.api.get_change_control_info(cc_id)
                 if chg_ctrl_executed['status'] == 'Completed':
                     break
-                else:
-                    time.sleep(2)
+                time.sleep(2)
             # For 2018.2 give a few extra seconds for device status to get
             # back in compliance.
             if self.clnt.apiversion >= 2.0:
@@ -1867,10 +1803,44 @@ def test_api_change_control(self):
             else:
                 time.sleep(2)
         else:
-            pprint('SKIPPING TEST FOR API - {0}'.format(
-                self.clnt.apiversion))
+            pprint(f'SKIPPING TEST FOR API - {self.clnt.apiversion}')
             time.sleep(1)
 
+    # def test_api_change_control_v3(self):
+    #     ''' Verify create_change_control_v3
+    #     '''
+    #     # Set client apiversion if it is not already set
+    #     if self.clnt.apiversion is None:
+    #         self.api.get_cvp_info()
+    #     if self.clnt.apiversion > 3.0:
+    #         pprint('RUN TEST FOR V3 CHANGE CONTROL APIs')
+    #         chg_ctrl_name = 'test_api_%d' % time.time()
+    #         (task_id, _) = self._create_task()
+    #         chg_ctrl = self.api.create_change_control_v3(
+    #             chg_ctrl_name, chg_ctrl_name, [task_id], True)
+    #         cc_id = ''
+    #         if len(chg_ctrl) > 0:
+    #             if 'id' in chg_ctrl[0]:
+    #                 cc_id = chg_ctrl[0]['id']
+    #         print('')
+    #         print(chg_ctrl)
+    #         print(cc_id)
+    #         print('')
+    #
+    #         if cc_id != '':
+    #             # Verify the pending change control information
+    #             status_url = '/cvpservice/changeControl/' \
+    #                          'getChangeControlInformation.do?' \
+    #                          'startIndex=0&endIndex=0&ccId={}'.format(cc_id)
+    #             chg_ctrl_pending = self.clnt.get(status_url)
+    #             print('')
+    #             print(chg_ctrl_pending)
+    #             print('')
+    #     else:
+    #         pprint('SKIPPING TEST FOR API - {0}'.format(
+    #             self.clnt.apiversion))
+    #         time.sleep(1)
+
     def test_api_cancel_change_control(self):
         ''' Verify cancel_change_control.
         '''
@@ -1878,7 +1848,7 @@ def test_api_cancel_change_control(self):
         if self.clnt.apiversion is None:
             self.api.get_cvp_info()
         if self.clnt.apiversion < 3.0:
-            chg_ctrl_name = 'test_api_%d' % time.time()
+            chg_ctrl_name = f'test_api_{time.time()}'
             (task_id, _) = self._create_task()
             chg_ctrl_tasks = [{
                 'taskId': task_id,
@@ -1901,8 +1871,7 @@ def test_api_cancel_change_control(self):
             chg_ctrl_cancelled = self.api.get_change_control_info(cc_id)
             self.assertEqual(chg_ctrl_cancelled['status'], 'Cancelled')
         else:
-            pprint('SKIPPING TEST FOR API - {0}'.format(
-                self.clnt.apiversion))
+            pprint(f'SKIPPING TEST FOR API - {self.clnt.apiversion}')
             time.sleep(1)
 
     def test_api_delete_change_control(self):
@@ -1912,7 +1881,7 @@ def test_api_delete_change_control(self):
         if self.clnt.apiversion is None:
             self.api.get_cvp_info()
         if self.clnt.apiversion < 3.0:
-            chg_ctrl_name = 'test_api_%d' % time.time()
+            chg_ctrl_name = f'test_api_{time.time()}'
             (task_id, _) = self._create_task()
             chg_ctrl_tasks = [{
                 'taskId': task_id,
@@ -1940,8 +1909,7 @@ def test_api_delete_change_control(self):
             time.sleep(1)
             self.assertIsNotNone(cancel_task_resp)
         else:
-            pprint('SKIPPING TEST FOR API - {0}'.format(
-                self.clnt.apiversion))
+            pprint(f'SKIPPING TEST FOR API - {self.clnt.apiversion}')
             time.sleep(1)
 
     def test_api_filter_topology(self):
@@ -1972,6 +1940,19 @@ def test_api_filter_topology(self):
         # Skip this in comparison
         topo_dev_data.pop('containerName', None)
         known_dev_data.pop('containerName', None)
+        # As of CVP 2021.2.0 we are seeing the complianceCode and
+        # complianceIndication not matching the results of checkCompliance.
+        # Remove fields.
+        if self.clnt.apiversion >= 6.0:
+            if (topo_dev_data['complianceCode'] !=
+                    known_dev_data['complianceCode']):
+                pprint(f"\ncomplianceCode in filter data hasn't settled yet."
+                       f" {topo_dev_data['complianceCode']}"
+                       f" != {known_dev_data['complianceCode']}\n")
+                topo_dev_data.pop('complianceCode')
+                topo_dev_data.pop('complianceIndication')
+                known_dev_data.pop('complianceCode')
+                known_dev_data.pop('complianceIndication')
 
         # Test expected parameter keys are in return data.
         # Test values for parameters with consistent return values
@@ -1984,6 +1965,260 @@ def test_api_filter_topology(self):
             if self.clnt.apiversion == 1.0 or key not in diff_val_form_keys:
                 self.assertEqual(topo_dev_data[key], known_dev_data[key])
 
+    def test_api_create_enroll_token(self):
+        ''' Verify enrollment token creation on on-prem and CVaaS.
+        '''
+        # Set client apiversion if it is not already set
+        if self.clnt.apiversion is None:
+            self.api.get_cvp_info()
+        if self.clnt.apiversion >= 6.0:
+            # Test if the returned value has a "data" key for on-prem
+            # Format of enroll token returned by on-prem should be:
+            # {'data': <token>}
+            if not self.clnt.is_cvaas:
+                gen_token = self.api.create_enroll_token("24h")
+                self.assertEqual(list(gen_token.keys())[0], "data")
+            # Else if CVaaS is used check if the returned list has an
+            # "enrollmentToken" key
+            # The format of enroll token returned by CVaaS should be:
+            # [{'enrollmentToken':{'token': <token>, 'groups': [],
+            #   'reenrollDevices': <devices list>,
+            #   'validFor': <duration e.g 24h>,
+            #   'field_mask': None}}]
+            else:
+                gen_token = self.api.create_enroll_token("24h")
+                self.assertEqual(list(gen_token[0].keys()), "enrollmentToken")
+        else:
+            pprint(f'SKIPPING TEST (test_api_create_enroll_token) FOR API - {self.clnt.apiversion}')
+            time.sleep(1)
+
+    def test_api_tags(self):
+        ''' Test Tags Resource API Endpoints
+        '''
+        # pylint: disable=too-many-statements
+        # Set client apiversion if it is not already set
+        if self.clnt.apiversion is None:
+            self.api.get_cvp_info()
+        if self.clnt.apiversion >= 6.0:
+            system_tags = self.api.get_all_tags()
+            if 'data' in system_tags:
+                self.assertNotEqual(len(system_tags['data']), 0,
+                                    "Expected system tags to not be zero")
+
+            existing_workspaces = self.api.get_all_workspaces()
+            self.assertIn('data', existing_workspaces)
+            self.assertNotEqual(len(existing_workspaces['data']), 0,
+                                "Expected existing workspaces to not be zero")
+            # Test Create New Workspace
+            new_uuid = uuid.uuid1()
+            new_workspace_id = 'CVPRAC_TEST_' + str(new_uuid)
+            response = self.api.workspace_config(new_workspace_id,
+                                                 "CVPRAC_TEST_NAME",
+                                                 "CVPRAC TEST",
+                                                 "REQUEST_UNSPECIFIED")
+            self.assertIn('value', response)
+            self.assertIn('key', response['value'])
+            self.assertEqual(new_workspace_id,
+                             response['value']['key']['workspaceId'])
+            self.assertEqual(response['value']
+                             ['displayName'], "CVPRAC_TEST_NAME")
+
+            # Test getting all tags for new workspace. Should be None.
+            new_workspace_tags = self.api.get_all_tags(element_type='ELEMENT_TYPE_UNSPECIFIED',
+                                                       workspace_id=new_workspace_id)
+            self.assertIn('data', new_workspace_tags)
+            self.assertEqual(len(new_workspace_tags['data']), 0,
+                             "Expected new workspace to have no tags yet")
+
+            # Test getting new workspace
+            result = self.api.get_workspace(new_workspace_id)
+            self.assertIn('value', result)
+            self.assertIn('key', result['value'])
+            self.assertEqual(new_workspace_id,
+                             result['value']['key']['workspaceId'])
+
+            # Pre tag creation and edit Get tag assignment edits
+            response = self.api.get_tag_assignment_edits(new_workspace_id)
+            self.assertIn('data', response)
+            self.assertEqual(len(response['data']), 0)
+
+            # Pre tag creation and edit Get tag edits
+            response = self.api.get_tag_edits(new_workspace_id)
+            self.assertIn('data', response)
+            self.assertEqual(len(response['data']), 0)
+
+            # Test config of new tag for Interface
+            response = self.api.tag_config("ELEMENT_TYPE_INTERFACE", new_workspace_id,
+                                           "cvpractestint", "TAGTESTINT", remove=False)
+            self.assertIn('value', response)
+            self.assertIn('key', response['value'])
+            self.assertEqual(new_workspace_id,
+                             response['value']['key']['workspaceId'])
+            self.assertEqual(
+                "cvpractestint", response['value']['key']['label'])
+            self.assertEqual("TAGTESTINT", response['value']['key']['value'])
+            self.assertEqual("ELEMENT_TYPE_INTERFACE",
+                             response['value']['key']['elementType'])
+
+            # Test config of new tag for Device
+            response = self.api.tag_config("ELEMENT_TYPE_DEVICE", new_workspace_id,
+                                           "cvpractestdev", "TAGTESTDEV", remove=False)
+            self.assertIn('value', response)
+            self.assertIn('key', response['value'])
+            self.assertEqual(new_workspace_id,
+                             response['value']['key']['workspaceId'])
+            self.assertEqual(
+                "cvpractestdev", response['value']['key']['label'])
+            self.assertEqual("TAGTESTDEV", response['value']['key']['value'])
+            self.assertEqual("ELEMENT_TYPE_DEVICE",
+                             response['value']['key']['elementType'])
+
+            # Test getting all tags for workspace again
+            result = self.api.get_all_tags(element_type="ELEMENT_TYPE_UNSPECIFIED",
+                                           workspace_id=new_workspace_id)
+            self.assertIn('data', result)
+            self.assertEqual(len(result['data']), 2)
+            for tag_data in result['data']:
+                self.assertEqual(tag_data['result']['value']['key']['workspaceId'],
+                                 new_workspace_id)
+
+            # Test getting device tags for workspace
+            result = self.api.get_all_tags(element_type="ELEMENT_TYPE_DEVICE",
+                                           workspace_id=new_workspace_id)
+            self.assertNotIn('data', result)
+
+            # Test assign tag to device
+            response = self.api.tag_assignment_config("ELEMENT_TYPE_DEVICE", new_workspace_id,
+                                                      "cvpractestdev", "TAGTESTDEV",
+                                                      self.device['serialNumber'], "",
+                                                      remove=False)
+            self.assertEqual(response['value']['key']
+                             ['deviceId'], self.device['serialNumber'])
+            self.assertEqual(response['value']['key']['interfaceId'], "")
+
+            # Test assign tag to interface
+            response = self.api.tag_assignment_config("ELEMENT_TYPE_INTERFACE", new_workspace_id,
+                                                      "cvpractestint", "TAGTESTINT",
+                                                      self.device['serialNumber'], "Ethernet1",
+                                                      remove=False)
+            self.assertEqual(response['value']['key']
+                             ['deviceId'], self.device['serialNumber'])
+            self.assertEqual(response['value']['key']
+                             ['interfaceId'], "Ethernet1")
+
+            # Post tag creation and edit Get tag assignment edits
+            response = self.api.get_tag_assignment_edits(new_workspace_id)
+            self.assertIn('data', response)
+            self.assertEqual(len(response['data']), 2)
+
+            # Post tag creation and edit Get tag edits
+            response = self.api.get_tag_edits(new_workspace_id)
+            self.assertIn('data', response)
+            self.assertEqual(len(response['data']), 2)
+
+            # Test Workspace Build
+            new_build_id = new_workspace_id + '_BUILD_' + str(new_uuid)
+            response = self.api.workspace_config(new_workspace_id,
+                                                 "CVPRAC_TEST_NAME",
+                                                 'CVPRAC TEST',
+                                                 'REQUEST_START_BUILD',
+                                                 new_build_id)
+            self.assertIn('value', response)
+            self.assertEqual(response['value']['key']
+                             ['workspaceId'], new_workspace_id)
+            self.assertEqual(response['value']
+                             ['request'], 'REQUEST_START_BUILD')
+            self.assertEqual(
+                response['value']['requestParams']['requestId'], new_build_id)
+
+            # Test getting new workspace post build
+            result = self.api.get_workspace(new_workspace_id)
+            self.assertIn('value', result)
+            self.assertIn('key', result['value'])
+            self.assertEqual(new_workspace_id,
+                             result['value']['key']['workspaceId'])
+            self.assertEqual('WORKSPACE_STATE_PENDING',
+                             result['value']['state'])
+            last_build_id = result['value']['lastBuildId']
+
+            # Test checking build status
+            response = self.api.workspace_build_status(
+                new_workspace_id, last_build_id)
+            self.assertIn('value', response)
+            build_state_options = {
+                'BUILD_STATE_SUCCESS': 'BUILD_STATE_SUCCESS',
+                'BUILD_STATE_IN_PROGRESS': 'BUILD_STATE_IN_PROGRESS',
+            }
+            self.assertIn(response['value']['state'], build_state_options)
+
+            # Test Remove Device Tag
+            response = self.api.tag_config("ELEMENT_TYPE_DEVICE", new_workspace_id,
+                                           "cvpractestdev", "TAGTESTDEV", remove=True)
+            self.assertIn('value', response)
+            self.assertEqual(response['value']['remove'], True)
+
+            response = self.api.tag_config("ELEMENT_TYPE_INTERFACE", new_workspace_id,
+                                           "cvpractestint", "TAGTESTINT", remove=True)
+            self.assertIn('value', response)
+            self.assertEqual(response['value']['remove'], True)
+
+            # Test read workspace tags post removal
+            result = self.api.get_all_tags(element_type="ELEMENT_TYPE_UNSPECIFIED",
+                                           workspace_id=new_workspace_id)
+            self.assertIn('data', result)
+            self.assertEqual(len(result['data']), 0)
+
+            # Rebuild workspace after changes. As of CVP 2021.3.1 the submit
+            # of the workspace will fail it if hasn't been built.
+            if self.clnt.apiversion >= 7.0:
+                # Test Second Workspace Build
+                second_build_id = new_workspace_id + '_BUILD2_' + str(new_uuid)
+                response = self.api.workspace_config(new_workspace_id,
+                                                     "CVPRAC_TEST_NAME",
+                                                     'CVPRAC TEST',
+                                                     'REQUEST_START_BUILD',
+                                                     second_build_id)
+                self.assertIn('value', response)
+                self.assertEqual(response['value']['key']
+                                 ['workspaceId'], new_workspace_id)
+                self.assertEqual(response['value']
+                                 ['request'], 'REQUEST_START_BUILD')
+                self.assertEqual(
+                    response['value']['requestParams']['requestId'], second_build_id)
+
+                # Test getting new workspace post second build
+                result = self.api.get_workspace(new_workspace_id)
+                self.assertIn('value', result)
+                # check needsBuild parameter added in CVP 2022.1.0
+                if self.clnt.apiversion > 7.0:
+                    self.assertIn('needsBuild', result['value'])
+                    self.assertEqual(result['value']['needsBuild'], False)
+
+            # Test Submit Workspace
+            new_submit_id = new_workspace_id + '_SUBMIT_' + str(new_uuid)
+            response = self.api.workspace_config(new_workspace_id,
+                                                 "CVPRAC_TEST_NAME",
+                                                 'CVPRAC TEST',
+                                                 'REQUEST_SUBMIT',
+                                                 new_submit_id)
+            self.assertEqual(response['value']['key']
+                             ['workspaceId'], new_workspace_id)
+            self.assertEqual(response['value']['request'], 'REQUEST_SUBMIT')
+            self.assertEqual(
+                response['value']['requestParams']['requestId'], new_submit_id)
+
+            # Test getting new workspace post submit
+            result = self.api.get_workspace(new_workspace_id)
+            self.assertIn('value', result)
+            self.assertIn('key', result['value'])
+            self.assertEqual(new_workspace_id,
+                             result['value']['key']['workspaceId'])
+            self.assertEqual('WORKSPACE_STATE_SUBMITTED',
+                             result['value']['state'])
+        else:
+            pprint(f'SKIPPING TEST (test_api_tags) FOR API - {self.clnt.apiversion}')
+            time.sleep(1)
+
     # def test_api_deploy_device(self):
     #     ''' Verify deploy_device
     #     '''