Merge "Use base.delete_server in test_servers.py"
diff --git a/.zuul.yaml b/.zuul.yaml
index 48d14ff..642af23 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,7 +1,6 @@
 - job:
     name: devstack-tempest
     parent: devstack
-    nodeset: openstack-single-node
     description: |
       Base Tempest job.
 
@@ -51,6 +50,40 @@
         ENABLE_FILE_INJECTION: true
 
 - job:
+    name: devstack-tempest-ipv6
+    parent: devstack-ipv6
+    description: |
+      Base Tempest IPv6 job.
+    required-projects:
+      - git.openstack.org/openstack/tempest
+    timeout: 7200
+    roles:
+      - zuul: git.openstack.org/openstack-dev/devstack
+    vars:
+      devstack_services:
+        tempest: true
+      devstack_local_conf:
+        test-config:
+          $TEMPEST_CONFIG:
+            compute:
+              min_compute_nodes: "{{ groups['compute'] | default(['controller']) | length }}"
+      test_results_stage_name: test_results
+      zuul_copy_output:
+        '{{ devstack_base_dir }}/tempest/etc/tempest.conf': logs
+        '{{ devstack_base_dir }}/tempest/etc/accounts.yaml': logs
+        '{{ devstack_base_dir }}/tempest/tempest.log': logs
+        '{{ stage_dir }}/{{ test_results_stage_name }}.subunit': logs
+        '{{ stage_dir }}/{{ test_results_stage_name }}.html': logs
+        '{{ stage_dir }}/stackviz': logs
+      extensions_to_txt:
+        conf: true
+        log: true
+        yaml: true
+        yml: true
+    run: playbooks/devstack-tempest.yaml
+    post-run: playbooks/post-tempest.yaml
+
+- job:
     name: tempest-full
     parent: devstack-tempest
     # This currently works from stable/pike on.
@@ -66,6 +99,7 @@
       tox_envlist: full
       devstack_localrc:
         ENABLE_FILE_INJECTION: true
+        ENABLE_VOLUME_MULTIATTACH: true
 
 - job:
     name: tempest-full-oslo-master
@@ -128,6 +162,7 @@
       devstack_localrc:
         USE_PYTHON3: true
         FORCE_CONFIG_DRIVE: true
+        ENABLE_VOLUME_MULTIATTACH: true
       devstack_services:
         s-account: false
         s-container: false
@@ -137,12 +172,30 @@
         c-bak: false
 
 - job:
-    name: tempest-multinode-full
+    name: tempest-full-py3-ipv6
+    parent: devstack-tempest-ipv6
+    # This currently works from stable/pike on.
+    # Before stable/pike, legacy version of tempest-full
+    # 'legacy-tempest-dsvm-neutron-full' run.
+    branches: ^(?!stable/ocata).*$
+    description: |
+      Base integration test with Neutron networking, IPv6 and py3.
+    vars:
+      tox_envlist: full
+      devstack_localrc:
+        USE_PYTHON3: true
+        FORCE_CONFIG_DRIVE: true
+      devstack_services:
+        s-account: false
+        s-container: false
+        s-object: false
+        s-proxy: false
+        # without Swift, c-bak cannot run (in the Gate at least)
+        c-bak: false
+
+- job:
+    name: tempest-multinode-full-base
     parent: devstack-tempest
-    nodeset: openstack-two-node
-    # Until the devstack changes are backported, only run this on master
-    branches:
-      - master
     description: |
       Base multinode integration test with Neutron networking and py27.
       Former names for this job were:
@@ -167,40 +220,37 @@
           LIVE_MIGRATION_AVAILABLE: true
           USE_BLOCK_MIGRATION_FOR_LIVE_MIGRATION: true
 
-- nodeset:
-    name: openstack-bionic-node
-    nodes:
-      - name: controller
-        label: ubuntu-bionic
-    groups:
-      - name: tempest
-        nodes:
-          - controller
+- job:
+    name: tempest-multinode-full
+    parent: tempest-multinode-full-base
+    nodeset: openstack-two-node-bionic
+    # This job runs on Bionic from stable/stein on.
+    branches: ^(?!stable/(ocata|pike|queens|rocky)).*$
 
 - job:
-    name: tempest-full-py36
-    parent: tempest-full-py3
-    nodeset: openstack-bionic-node
+    name: tempest-multinode-full
+    parent: tempest-multinode-full-base
+    nodeset: openstack-two-node-xenial
+    # This job runs on Xenial and this is for stable/pike, stable/queens
+    # and stable/rocky. This job is prepared to make sure all stable branches
+    # before stable/stein will keep running on xenial. This job can be
+    # removed once stable/rocky is EOL.
     branches:
-      - master
-    description: |
-      Base integration test with Neutron networking and py36.
-    voting: false
+      - stable/pike
+      - stable/queens
+      - stable/rocky
 
-- nodeset:
-    name: openstack-opensuse150-node
-    nodes:
-      - name: controller
-        label: opensuse-150
-    groups:
-      - name: tempest
-        nodes:
-          - controller
+- job:
+    name: tempest-multinode-full-py3
+    parent: tempest-multinode-full
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: true
 
 - job:
     name: tempest-full-py3-opensuse150
     parent: tempest-full-py3
-    nodeset: openstack-opensuse150-node
+    nodeset: devstack-single-node-opensuse-150
     description: |
       Base integration test with Neutron networking and py36 running
       on openSUSE Leap 15.0
@@ -209,8 +259,6 @@
 - job:
     name: tempest-slow
     parent: tempest-multinode-full
-    branches:
-      - master
     description: |
       This multinode integration job will run all the tests tagged as slow.
       It enables the lvm multibackend setup to cover few scenario tests.
@@ -224,31 +272,58 @@
       tox_envlist: slow-serial
       devstack_localrc:
         CINDER_ENABLED_BACKENDS: lvm:lvmdriver-1,lvm:lvmdriver-2
+        ENABLE_VOLUME_MULTIATTACH: true
       tempest_concurrency: 2
+    group-vars:
+      # NOTE(mriedem): The ENABLE_VOLUME_MULTIATTACH variable is used on both
+      # the controller and subnode prior to Rocky so we have to make sure the
+      # variable is set in both locations.
+      subnode:
+        devstack_localrc:
+          ENABLE_VOLUME_MULTIATTACH: true
+
+- job:
+    name: tempest-slow-py3
+    parent: tempest-slow
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: true
+      devstack_services:
+        s-account: false
+        s-container: false
+        s-object: false
+        s-proxy: false
+        # without Swift, c-bak cannot run (in the Gate at least)
+        c-bak: false
 
 - job:
     name: tempest-full-rocky
     parent: tempest-full
+    nodeset: openstack-single-node-xenial
     override-checkout: stable/rocky
 
 - job:
     name: tempest-full-rocky-py3
     parent: tempest-full-py3
+    nodeset: openstack-single-node-xenial
     override-checkout: stable/rocky
 
 - job:
     name: tempest-full-queens
     parent: tempest-full
+    nodeset: openstack-single-node-xenial
     override-checkout: stable/queens
 
 - job:
     name: tempest-full-queens-py3
     parent: tempest-full-py3
+    nodeset: openstack-single-node-xenial
     override-checkout: stable/queens
 
 - job:
     name: tempest-full-pike
     parent: tempest-full
+    nodeset: openstack-single-node-xenial
     override-checkout: stable/pike
 
 - job:
@@ -256,7 +331,7 @@
     parent: tox
     description: |
       Run tempest plugin sanity check script using tox.
-    nodeset: ubuntu-xenial
+    nodeset: ubuntu-bionic
     vars:
       tox_envlist: plugin-sanity-check
     voting: false
@@ -269,22 +344,26 @@
       - ^tempest/hacking/.*$
       - ^tempest/tests/.*$
     required-projects:
+      - git.openstack.org/openstack/airship-tempest-plugin
       - git.openstack.org/openstack/almanach
       - git.openstack.org/openstack/aodh
       - git.openstack.org/openstack/barbican-tempest-plugin
+      - git.openstack.org/openstack/blazar-tempest-plugin
       - git.openstack.org/openstack/ceilometer
-      - git.openstack.org/openstack/cinder
-      - git.openstack.org/openstack/congress
+      - git.openstack.org/openstack/cinder-tempest-plugin
+      - git.openstack.org/openstack/cloudkitty-tempest-plugin
+      - git.openstack.org/openstack/congress-tempest-plugin
       - git.openstack.org/openstack/designate-tempest-plugin
-      - git.openstack.org/openstack/ec2-api
+      - git.openstack.org/openstack/ec2api-tempest-plugin
       - git.openstack.org/openstack/freezer
       - git.openstack.org/openstack/freezer-api
       - git.openstack.org/openstack/freezer-tempest-plugin
+      - git.openstack.org/openstack/gabbi-tempest
       - git.openstack.org/openstack/gce-api
       - git.openstack.org/openstack/glare
-      - git.openstack.org/openstack/heat
+      - git.openstack.org/openstack/heat-tempest-plugin
       - git.openstack.org/openstack/intel-nfv-ci-tests
-      - git.openstack.org/openstack/ironic
+      - git.openstack.org/openstack/ironic-tempest-plugin
       - git.openstack.org/openstack/ironic-inspector
       - git.openstack.org/openstack/keystone-tempest-plugin
       - git.openstack.org/openstack/kingbird
@@ -293,19 +372,21 @@
       - git.openstack.org/openstack/magnum-tempest-plugin
       - git.openstack.org/openstack/manila
       - git.openstack.org/openstack/manila-tempest-plugin
-      - git.openstack.org/openstack/mistral
+      - git.openstack.org/openstack/mistral-tempest-plugin
       - git.openstack.org/openstack/mogan
       - git.openstack.org/openstack/monasca-api
       - git.openstack.org/openstack/monasca-log-api
-      - git.openstack.org/openstack/murano
+      - git.openstack.org/openstack/monasca-tempest-plugin
+      - git.openstack.org/openstack/murano-tempest-plugin
+      - git.openstack.org/openstack/networking-ansible
       - git.openstack.org/openstack/networking-bgpvpn
       - git.openstack.org/openstack/networking-cisco
       - git.openstack.org/openstack/networking-fortinet
       - git.openstack.org/openstack/networking-generic-switch
-      - git.openstack.org/openstack/networking-l2gw
+      - git.openstack.org/openstack/networking-l2gw-tempest-plugin
       - git.openstack.org/openstack/networking-midonet
-      - git.openstack.org/openstack/networking-plumgrid
       - git.openstack.org/openstack/networking-sfc
+      - git.openstack.org/openstack/networking-spp
       - git.openstack.org/openstack/neutron
       - git.openstack.org/openstack/neutron-dynamic-routing
       - git.openstack.org/openstack/neutron-fwaas
@@ -314,21 +395,27 @@
       - git.openstack.org/openstack/neutron-vpnaas
       - git.openstack.org/openstack/nova-lxd
       - git.openstack.org/openstack/novajoin-tempest-plugin
+      - git.openstack.org/openstack/octavia
       - git.openstack.org/openstack/octavia-tempest-plugin
       - git.openstack.org/openstack/oswin-tempest-plugin
       - git.openstack.org/openstack/panko
       - git.openstack.org/openstack/patrole
+      - git.openstack.org/openstack/python-watcherclient
       - git.openstack.org/openstack/qinling
       - git.openstack.org/openstack/requirements
       - git.openstack.org/openstack/sahara-tests
       - git.openstack.org/openstack/senlin
       - git.openstack.org/openstack/senlin-tempest-plugin
+      - git.openstack.org/openstack/solum-tempest-plugin
       - git.openstack.org/openstack/tap-as-a-service
+      - git.openstack.org/openstack/telemetry-tempest-plugin
       - git.openstack.org/openstack/tempest-horizon
+      - git.openstack.org/openstack/tobiko
       - git.openstack.org/openstack/trio2o
-      - git.openstack.org/openstack/trove
+      - git.openstack.org/openstack/tripleo-common-tempest-plugin
+      - git.openstack.org/openstack/trove-tempest-plugin
       - git.openstack.org/openstack/valet
-      - git.openstack.org/openstack/vitrage
+      - git.openstack.org/openstack/vitrage-tempest-plugin
       - git.openstack.org/openstack/vmware-nsx-tempest-plugin
       - git.openstack.org/openstack/watcher-tempest-plugin
       - git.openstack.org/openstack/zaqar-tempest-plugin
@@ -389,11 +476,12 @@
     templates:
       - check-requirements
       - integrated-gate
-      - integrated-gate-py35
+      - integrated-gate-py3
       - openstack-cover-jobs
       - openstack-python-jobs
       - openstack-python35-jobs
       - openstack-python36-jobs
+      - openstack-python37-jobs
       - publish-openstack-docs-pti
       - release-notes-jobs-python3
     check:
@@ -403,7 +491,13 @@
               - ^playbooks/
               - ^roles/
               - ^.zuul.yaml$
-        - nova-multiattach:
+        - devstack-tempest-ipv6:
+            voting: false
+            files:
+              - ^playbooks/
+              - ^roles/
+              - ^.zuul.yaml$
+        - tempest-full-parallel:
             # Define list of irrelevant files to use everywhere else
             irrelevant-files: &tempest-irrelevant-files
               - ^(test-|)requirements.txt$
@@ -414,11 +508,11 @@
               - ^setup.cfg$
               - ^tempest/hacking/.*$
               - ^tempest/tests/.*$
-        - tempest-full-parallel:
-            irrelevant-files: *tempest-irrelevant-files
+              - ^tools/.*$
         - tempest-full-py3:
             irrelevant-files: *tempest-irrelevant-files
-        - tempest-full-py36:
+        - tempest-full-py3-ipv6:
+            voting: false
             irrelevant-files: *tempest-irrelevant-files
         - tempest-full-rocky:
             irrelevant-files: *tempest-irrelevant-files
@@ -432,10 +526,22 @@
             irrelevant-files: *tempest-irrelevant-files
         - tempest-multinode-full:
             irrelevant-files: *tempest-irrelevant-files
-        - tempest-tox-plugin-sanity-check
+        - tempest-multinode-full-py3:
+            irrelevant-files: *tempest-irrelevant-files
+        - tempest-tox-plugin-sanity-check:
+            irrelevant-files:
+              - ^(test-|)requirements.txt$
+              - ^.*\.rst$
+              - ^doc/.*$
+              - ^etc/.*$
+              - ^releasenotes/.*$
+              - ^setup.cfg$
+              - ^tempest/hacking/.*$
+              - ^tempest/tests/.*$
+              # tools/ is not here since this relies on a script in tools/.
         - tempest-slow:
             irrelevant-files: *tempest-irrelevant-files
-        - nova-cells-v1:
+        - tempest-slow-py3:
             irrelevant-files: *tempest-irrelevant-files
         - nova-live-migration:
             voting: false
@@ -444,6 +550,8 @@
             irrelevant-files: *tempest-irrelevant-files
         - neutron-grenade:
             irrelevant-files: *tempest-irrelevant-files
+        - grenade-py3:
+            irrelevant-files: *tempest-irrelevant-files
         - devstack-plugin-ceph-tempest:
             voting: false
             irrelevant-files: *tempest-irrelevant-files
@@ -466,7 +574,6 @@
         - tempest-full:
             irrelevant-files: *tempest-irrelevant-files
         - interop-tempest-consistency:
-            voting: false
             irrelevant-files: *tempest-irrelevant-files
         - tempest-full-test-account-py3:
             voting: false
@@ -476,9 +583,7 @@
             irrelevant-files: *tempest-irrelevant-files
     gate:
       jobs:
-        - nova-multiattach:
-            irrelevant-files: *tempest-irrelevant-files
-        - tempest-slow:
+        - tempest-slow-py3:
             irrelevant-files: *tempest-irrelevant-files
         - neutron-grenade-multinode:
             irrelevant-files: *tempest-irrelevant-files
@@ -486,6 +591,8 @@
             irrelevant-files: *tempest-irrelevant-files
         - neutron-grenade:
             irrelevant-files: *tempest-irrelevant-files
+        - grenade-py3:
+            irrelevant-files: *tempest-irrelevant-files
     experimental:
       jobs:
         - tempest-cinder-v2-api:
@@ -496,7 +603,9 @@
             irrelevant-files: *tempest-irrelevant-files
         - neutron-tempest-dvr-ha-multinode-full:
             irrelevant-files: *tempest-irrelevant-files
-        - legacy-tempest-dsvm-nova-v20-api:
+        - nova-cells-v1:
+            irrelevant-files: *tempest-irrelevant-files
+        - nova-tempest-v2-api:
             irrelevant-files: *tempest-irrelevant-files
         - legacy-tempest-dsvm-lvm-multibackend:
             irrelevant-files: *tempest-irrelevant-files
diff --git a/HACKING.rst b/HACKING.rst
index e767b25..eb6551a 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -6,7 +6,7 @@
 - Step 2: Read on
 
 Tempest Specific Commandments
-------------------------------
+-----------------------------
 
 - [T102] Cannot import OpenStack python clients in tempest/api &
   tempest/scenario tests
@@ -35,6 +35,30 @@
 - Clean up test data at the completion of each test
 - Use configuration files for values that will vary by environment
 
+Supported OpenStack Components
+------------------------------
+
+Tempest's :ref:`library` and :ref:`plugin interface <tempest_plugin>` can be
+leveraged to support integration testing for virtually any OpenStack component.
+
+However, Tempest only offers **in-tree** integration testing coverage for the
+following components:
+
+* Cinder
+* Glance
+* Keystone
+* Neutron
+* Nova
+* Swift
+
+Historically, Tempest offered in-tree testing for other components as well, but
+since the introduction of the `External Plugin Interface`_, Tempest's in-tree
+testing scope has been limited to the projects above. Integration tests for
+projects not included above should go into one of the
+`relevant plugin projects`_.
+
+.. _External Plugin Interface: https://specs.openstack.org/openstack/qa-specs/specs/tempest/implemented/tempest-external-plugin-interface.html
+.. _relevant plugin projects: https://docs.openstack.org/tempest/latest/plugin-registry.html#detected-plugins
 
 Exception Handling
 ------------------
@@ -431,7 +455,7 @@
 by modifying Tempest's `lib installation script`_ for previous branches
 (because DevStack is branched).
 
-.. _lib installation script: http://git.openstack.org/cgit/openstack-dev/devstack/tree/lib/tempest
+.. _lib installation script: https://git.openstack.org/cgit/openstack-dev/devstack/tree/lib/tempest
 
 2. Bug fix on core project needing Tempest changes
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/README.rst b/README.rst
index 307ceb3..73930f1 100644
--- a/README.rst
+++ b/README.rst
@@ -165,7 +165,7 @@
 interface and when Z is incremented it's a bug fix release for the library.
 Also note that both Y and Z are reset to 0 at each increment of X.
 
-.. _semver: http://semver.org/
+.. _semver: https://semver.org/
 
 Configuration
 -------------
@@ -218,7 +218,7 @@
 argument is no longer required, however it may perform faster if included.
 
 For more information on these options and details about stestr, please see the
-`stestr documentation <http://stestr.readthedocs.io/en/latest/MANUAL.html>`_.
+`stestr documentation <https://stestr.readthedocs.io/en/latest/MANUAL.html>`_.
 
 Python 3.x
 ----------
diff --git a/REVIEWING.rst b/REVIEWING.rst
index bf63ed2..31fedce 100644
--- a/REVIEWING.rst
+++ b/REVIEWING.rst
@@ -159,8 +159,32 @@
 
 When to approve
 ---------------
-* Every patch needs two +2s before being approved.
-* It's ok to hold off on an approval until a subject matter expert reviews it
-* If a patch has already been approved but requires a trivial rebase to merge,
-  you do not have to wait for a second +2, since the patch has already had
-  two +2s.
+* It's OK to hold off on an approval until a subject matter expert reviews it.
+* Every patch needs two +2's before being approved.
+* However, a single Tempest core reviewer can approve patches without waiting
+  for another +2 in the following cases:
+
+  * If a patch has already been approved but requires a trivial rebase to
+    merge, then there is no need to wait for a second +2, since the patch has
+    already had two +2's.
+  * If any trivial patch set fixes one of the items below:
+
+    * Documentation or code comment typo
+    * Documentation ref link
+    * Example: `example`_
+
+    .. note::
+
+      Any other small documentation, CI job, or code change does not fall under
+      this category.
+
+  * If the patch **unblocks** a failing project gate, provided that:
+
+    * the project's PTL +1's the change
+    * the patch does not affect any other project's testing gates
+    * the patch does not cause any negative side effects
+
+  Note that such a policy should be used judiciously, as we should strive to
+  have two +2's on each patch set, prior to approval.
+
+.. _example: https://review.openstack.org/#/c/611032/
diff --git a/doc/source/data/tempest-plugins-registry.header b/doc/source/data/tempest-plugins-registry.header
index 0de12b7..831d8a6 100644
--- a/doc/source/data/tempest-plugins-registry.header
+++ b/doc/source/data/tempest-plugins-registry.header
@@ -3,9 +3,9 @@
   job.  You should edit the files data/tempest-plugins-registry.footer
   and data/tempest-plugins-registry.header instead of this one.
 
-==========================
- Tempest Plugin Registry
-==========================
+=======================
+Tempest Plugin Registry
+=======================
 
 Since we've created the external plugin mechanism, it's gotten used by
 a lot of projects. The following is a list of plugins that currently
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 983fa24..4b1c145 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -338,8 +338,8 @@
 
   .. _2.26: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id23
 
-* `2.28`_

-

+  * `2.28`_
+
   .. _2.28: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id25
 
   * `2.32`_
diff --git a/doc/source/plugin.rst b/doc/source/plugin.rst
index 9958792..dc0e94c 100644
--- a/doc/source/plugin.rst
+++ b/doc/source/plugin.rst
@@ -96,7 +96,7 @@
 that users don't have to worry about inadvertently installing a Tempest plugin
 when they install another package.
 
-.. _Branchless Tempest Spec: http://specs.openstack.org/openstack/qa-specs/specs/tempest/implemented/branchless-tempest.html
+.. _Branchless Tempest Spec: https://specs.openstack.org/openstack/qa-specs/specs/tempest/implemented/branchless-tempest.html
 
 The sole advantage to integrating a plugin into an existing python project is
 that it enables you to land code changes at the same time you land test changes
diff --git a/doc/source/test_removal.rst b/doc/source/test_removal.rst
index e249bdd..ff4fa09 100644
--- a/doc/source/test_removal.rst
+++ b/doc/source/test_removal.rst
@@ -128,8 +128,9 @@
 people to respond to removal proposals please add things to the agenda by the
 Monday before the meeting.
 
-The other option is to raise the removal on the openstack-dev mailing list.
-(for example see: http://lists.openstack.org/pipermail/openstack-dev/2016-February/086218.html )
+The other option is to raise the removal on the openstack-discuss mailing list.
+(for example see: http://lists.openstack.org/pipermail/openstack-dev/2016-February/086218.html
+or http://lists.openstack.org/pipermail/openstack-discuss/2019-March/003574.html )
 This will raise the issue to the wider community and attract at least the same
 (most likely more) attention than discussing it during the irc meeting. The
 only downside is that it might take more time to get a response, given the
diff --git a/playbooks/devstack-tempest.yaml b/playbooks/devstack-tempest.yaml
index 01155a8..5f87abd 100644
--- a/playbooks/devstack-tempest.yaml
+++ b/playbooks/devstack-tempest.yaml
@@ -7,6 +7,11 @@
 
 # We run tests only on one node, regardless how many nodes are in the system
 - hosts: tempest
+  environment:
+    # This enviroment variable is used by the optional tempest-gabbi
+    # job provided by the gabbi-tempest plugin. It can be safely ignored
+    # if that plugin is not being used.
+    GABBI_TEMPEST_PATH: "{{ gabbi_tempest_path | default('') }}"
   roles:
     - setup-tempest-run-dir
     - setup-tempest-data-dir
diff --git a/releasenotes/notes/Placement-client-for-placement-based-minimum-bw-allocation-27ed0938118752b6.yaml b/releasenotes/notes/Placement-client-for-placement-based-minimum-bw-allocation-27ed0938118752b6.yaml
new file mode 100644
index 0000000..21b74a6
--- /dev/null
+++ b/releasenotes/notes/Placement-client-for-placement-based-minimum-bw-allocation-27ed0938118752b6.yaml
@@ -0,0 +1,17 @@
+---
+features:
+  - |
+    Add basic read-only Placement client to Tempest to make possible the
+    testing of the placement based bandwidth allocation feature.
+    The following API calls are available for tempest from now:
+
+    * GET /allocation_candidates
+    * GET /allocations/{consumer_uuid}
+
+    Add new config group ``placement``, with the config options:
+
+    * ``endpoint_type`` to use for communication with placement service.
+    * ``catalog_type`` of the placement service.
+    * ``region`` as the placement region name to use.
+    * ``min_microversion`` and ``max_microversion`` as the range between
+      placement API requests are sent.
diff --git a/releasenotes/notes/add-immutable-user-source-support-dd17772a997075e0.yaml b/releasenotes/notes/add-immutable-user-source-support-dd17772a997075e0.yaml
new file mode 100644
index 0000000..931d689
--- /dev/null
+++ b/releasenotes/notes/add-immutable-user-source-support-dd17772a997075e0.yaml
@@ -0,0 +1,11 @@
+---
+features:
+  - |
+    Add a new config setting ``immutable_user_source`` in the
+    ``[identity-feature-enabled]`` group that defaults to false.
+    This setting, combined with the usage of the ``@testtools.skipIf()``
+    decorator, will allow tests that require user creation, deletion,
+    or modification to skip instead of failing in environments that
+    are LDAP-backed. In such environments, the user source is read-only,
+    so this feature flag is needed to allow such tests to gracefully skip
+    without having to blacklist them.
diff --git a/releasenotes/notes/add-profiler-config-options-db7c4ae6d338ee5c.yaml b/releasenotes/notes/add-profiler-config-options-db7c4ae6d338ee5c.yaml
new file mode 100644
index 0000000..2245044
--- /dev/null
+++ b/releasenotes/notes/add-profiler-config-options-db7c4ae6d338ee5c.yaml
@@ -0,0 +1,10 @@
+---
+features:
+  - |
+    Add support of `OSProfiler library`_ for profiling and distributed
+    tracing of OpenStack. A new config option ``key`` in section ``profiler``
+    is added, the option sets the secret key used to enable profiling in
+    OpenStack services. The value needs to correspond to the one specified
+    in [profiler]/hmac_keys option of OpenStack services.
+
+    .. _OSProfiler library: https://docs.openstack.org/osprofiler/
diff --git a/releasenotes/notes/bug-1799883-6ea95fc64f70c9ef.yaml b/releasenotes/notes/bug-1799883-6ea95fc64f70c9ef.yaml
new file mode 100644
index 0000000..630908f
--- /dev/null
+++ b/releasenotes/notes/bug-1799883-6ea95fc64f70c9ef.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+  - |
+    Fixed bug #1799883. ``tempest workspace register`` and ``tempest workspace move`` CLI
+    will now validate the value of the ``--path`` CLI argument and raise an exception if
+    it is None or empty string. Earlier both CLI actions were accepting None or empty path
+    which was confusing.
diff --git a/releasenotes/notes/bug-1808473-54ada26ab78e7b02.yaml b/releasenotes/notes/bug-1808473-54ada26ab78e7b02.yaml
new file mode 100644
index 0000000..c280198
--- /dev/null
+++ b/releasenotes/notes/bug-1808473-54ada26ab78e7b02.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+  - |
+    Fixed bug #1808473. ``tempest run`` CLI will error if a non-exist config file is
+    input to parameter --config-file. Earlier non-exist config value was silently
+    getting ignored and the default config file was used instead which used to give
+    false behavior to the user on using the passed config file.
diff --git a/releasenotes/notes/conditional-attr-a8564ec5a70ec840.yaml b/releasenotes/notes/conditional-attr-a8564ec5a70ec840.yaml
new file mode 100644
index 0000000..c707f14
--- /dev/null
+++ b/releasenotes/notes/conditional-attr-a8564ec5a70ec840.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    The ``tempest.lib.decorators.attr`` decorator now supports a ``condition``
+    kwarg which can be used to conditionally apply the attr to the test
+    function if the condition evaluates to True.
diff --git a/releasenotes/notes/correct-port-profile-config-option-d67f5cb31f1bc34c.yaml b/releasenotes/notes/correct-port-profile-config-option-d67f5cb31f1bc34c.yaml
new file mode 100644
index 0000000..7510d47
--- /dev/null
+++ b/releasenotes/notes/correct-port-profile-config-option-d67f5cb31f1bc34c.yaml
@@ -0,0 +1,17 @@
+---
+fixes:
+  - |
+    Patch https://review.openstack.org/#/c/499575/ introduced
+    support creating Neutron port with certain capabilities.
+    Currently capabilities list interpreted as string this change
+    fix it.
+
+    tempest.conf
+    [network]
+    port_profile = capabilities:[switchdev]
+
+    result:
+    {'capabilities':'[switchdev]'}
+
+    expected:
+    {'capabilities': ['switchdev']}
diff --git a/releasenotes/notes/deprecate-scheduler-available-filters-cbca2017ba3cf2aa.yaml b/releasenotes/notes/deprecate-scheduler-available-filters-cbca2017ba3cf2aa.yaml
new file mode 100644
index 0000000..d0c3a7d
--- /dev/null
+++ b/releasenotes/notes/deprecate-scheduler-available-filters-cbca2017ba3cf2aa.yaml
@@ -0,0 +1,13 @@
+---
+deprecations:
+  - |
+    The ``scheduler_available_filters`` option is being deprecated in favor of
+    ``scheduler_enabled_filters``. The new name is more indicative of what the
+    option means. ``scheduler_enabled_filters``'s default value is set to the
+    default value of Nova's ``enabled_filters``.
+    ``scheduler_available_filters``'s default was `all`. There was confusion
+    around this value. Sometimes it was understood to mean the default Nova
+    filters are enabled, other times it was understood to mean all filters are
+    enabled. While `all` is still allowed for ``scheduler_enabled_filters`` for
+    backwards compatibility, it is strongly recommended to provide an explicit
+    list of filters that matches what's configured in nova.conf.
diff --git a/releasenotes/notes/enable-volume-multiattach-fd5e9bf0e96b56ce.yaml b/releasenotes/notes/enable-volume-multiattach-fd5e9bf0e96b56ce.yaml
new file mode 100644
index 0000000..0959b22
--- /dev/null
+++ b/releasenotes/notes/enable-volume-multiattach-fd5e9bf0e96b56ce.yaml
@@ -0,0 +1,10 @@
+---
+upgrade:
+  - |
+    The ``tempest-full``, ``tempest-full-py3`` and ``tempest-slow`` zuul v3
+    job configurations now set ``ENABLE_VOLUME_MULTIATTACH: true`` in the
+    ``devstack_localrc`` variables section. If you have a plugin job
+    configuration that inherits from one of these jobs and the backend cinder
+    volume driver or nova compute driver do not support volume multiattach then
+    you should override and set this variable to
+    ``ENABLE_VOLUME_MULTIATTACH: false`` in your job configuration.
diff --git a/releasenotes/notes/tempest-stein-release-18bad34136a2e6ef.yaml b/releasenotes/notes/tempest-stein-release-18bad34136a2e6ef.yaml
new file mode 100644
index 0000000..212cf7d
--- /dev/null
+++ b/releasenotes/notes/tempest-stein-release-18bad34136a2e6ef.yaml
@@ -0,0 +1,18 @@
+---
+prelude: >
+    This release is to tag the Tempest for OpenStack Stein release.
+    This release marks the start of Stein release support in Tempest and
+    the end of support for Ocata in Tempest.
+    After this release, Tempest will support below OpenStack Releases:
+
+      * Stein
+      * Rocky
+      * Queens
+      * Pike
+
+    Current development of Tempest is for OpenStack Train development
+    cycle. Every Tempest commit is also tested against master during
+    the Train cycle. However, this does not necessarily mean that using
+    Tempest as of this tag will work against a Train (or future release)
+    cloud.
+    To be on safe side, use this tag to test the OpenStack Stein release.
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index 3be014f..e5d5bfe 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -6,6 +6,7 @@
    :maxdepth: 1
 
    unreleased
+   v20.0.0
    v19.0.0
    v18.0.0
    v17.2.0
diff --git a/releasenotes/source/v20.0.0.rst b/releasenotes/source/v20.0.0.rst
new file mode 100644
index 0000000..28c5431
--- /dev/null
+++ b/releasenotes/source/v20.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v20.0.0 Release Notes
+=====================
+
+.. release-notes:: 20.0.0 Release Notes
+   :version: 20.0.0
diff --git a/roles/run-tempest/README.rst b/roles/run-tempest/README.rst
index 71b8e4f..e1787b6 100644
--- a/roles/run-tempest/README.rst
+++ b/roles/run-tempest/README.rst
@@ -56,3 +56,14 @@
                (?x)    # Ignore comments and whitespaces
                # Line with only a comment.
                (tempest.api.identity).*$
+
+.. zuul:rolevar:: tox_extra_args
+   :default: ''
+
+   String of extra command line options to pass to tox.
+
+   Here is an example of running tox with --sitepackages option:
+
+       ::
+           vars:
+             tox_extra_args: --sitepackages
diff --git a/roles/run-tempest/defaults/main.yaml b/roles/run-tempest/defaults/main.yaml
index c89eb93..06918b5 100644
--- a/roles/run-tempest/defaults/main.yaml
+++ b/roles/run-tempest/defaults/main.yaml
@@ -2,3 +2,4 @@
 tempest_test_regex: ''
 tox_envlist: smoke
 tempest_black_regex: ''
+tox_extra_args: ''
diff --git a/roles/run-tempest/tasks/main.yaml b/roles/run-tempest/tasks/main.yaml
index 54ddc71..16086aa 100644
--- a/roles/run-tempest/tasks/main.yaml
+++ b/roles/run-tempest/tasks/main.yaml
@@ -35,7 +35,7 @@
       when: blacklist_stat.stat.exists
 
 - name: Run Tempest
-  command: tox -e {{tox_envlist}} -- {{tempest_test_regex|quote}} {{blacklist_option|default('')}} \
+  command: tox -e {{tox_envlist}} {{tox_extra_args}} -- {{tempest_test_regex|quote}} {{blacklist_option|default('')}} \
             --concurrency={{tempest_concurrency|default(default_concurrency)}} \
             --black-regex={{tempest_black_regex|quote}}
   args:
diff --git a/setup.cfg b/setup.cfg
index 96ee7ea..fcbe956 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,7 +4,7 @@
 description-file =
     README.rst
 author = OpenStack
-author-email = openstack-dev@lists.openstack.org
+author-email = openstack-discuss@lists.openstack.org
 home-page = https://docs.openstack.org/tempest/latest/
 classifier =
     Intended Audience :: Information Technology
diff --git a/tempest/README.rst b/tempest/README.rst
index a5f4a92..b345032 100644
--- a/tempest/README.rst
+++ b/tempest/README.rst
@@ -9,7 +9,7 @@
 OpenStack clouds.
 
 As such Tempest tests come in many flavors, each with their own rules
-and guidelines. Below is the overview of the Tempest respository structure
+and guidelines. Below is the overview of the Tempest repository structure
 to make this clear.
 
 .. code-block:: console
diff --git a/tempest/api/compute/admin/test_keypairs_v210.py b/tempest/api/compute/admin/test_keypairs_v210.py
index 24ea8a1..40ed532 100644
--- a/tempest/api/compute/admin/test_keypairs_v210.py
+++ b/tempest/api/compute/admin/test_keypairs_v210.py
@@ -56,7 +56,7 @@
         self.assertEqual(first_keyname, keypair_detail['name'])
         self.assertEqual(user_id, keypair_detail['user_id'],
                          "The fetched keypair is not for requested user!")
-        # Create a admin keypair
+        # Create an admin keypair
         admin_keypair = self.create_keypair(keypair_type='ssh',
                                             client=self.client)
         admin_keypair.pop('private_key', None)
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 5a60dc6..b1a7c52 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -50,8 +50,6 @@
         # a subnet so the instance being migrated has a single port, but
         # we need that to make sure we are properly updating the port
         # host bindings during the live migration.
-        # TODO(mriedem): SSH validation before and after the instance is
-        # live migrated would be a nice test wrinkle addition.
         cls.set_network_resources(network=True, subnet=True)
         super(LiveMigrationTestBase, cls).setup_credentials()
 
diff --git a/tempest/api/compute/admin/test_servers_on_multinodes.py b/tempest/api/compute/admin/test_servers_on_multinodes.py
index 5cd98f4..bebc8c5 100644
--- a/tempest/api/compute/admin/test_servers_on_multinodes.py
+++ b/tempest/api/compute/admin/test_servers_on_multinodes.py
@@ -105,7 +105,7 @@
         asserts the servers are in the group and on different hosts.
         """
         hosts = self._create_servers_with_group('anti-affinity')
-        hostnames = hosts.values()
+        hostnames = list(hosts.values())
         self.assertNotEqual(hostnames[0], hostnames[1],
                             'Servers are on the same host: %s' % hosts)
 
@@ -120,6 +120,6 @@
         asserts the servers are in the group and on same host.
         """
         hosts = self._create_servers_with_group('affinity')
-        hostnames = hosts.values()
+        hostnames = list(hosts.values())
         self.assertEqual(hostnames[0], hostnames[1],
                          'Servers are on the different hosts: %s' % hosts)
diff --git a/tempest/api/compute/admin/test_volume_swap.py b/tempest/api/compute/admin/test_volume_swap.py
index a853182..cc83c04 100644
--- a/tempest/api/compute/admin/test_volume_swap.py
+++ b/tempest/api/compute/admin/test_volume_swap.py
@@ -70,6 +70,7 @@
     """The test suite for swapping of volume with admin user.
 
     The following is the scenario outline:
+
     1. Create a volume "volume1" with non-admin.
     2. Create a volume "volume2" with non-admin.
     3. Boot an instance "instance1" with non-admin.
@@ -147,6 +148,13 @@
     # so it's marked as such.
     @decorators.attr(type='slow')
     @decorators.idempotent_id('e8f8f9d1-d7b7-4cd2-8213-ab85ef697b6e')
+    # For some reason this test intermittently fails on teardown when there are
+    # multiple compute nodes and the servers are split across the computes.
+    # For now, just skip this test if there are multiple computes.
+    # Alternatively we could put the servers in an affinity group if there are
+    # multiple computes but that would just side-step the underlying bug.
+    @decorators.skip_because(bug='1807723',
+                             condition=CONF.compute.min_compute_nodes > 1)
     @utils.services('volume')
     def test_volume_swap_with_multiattach(self):
         # Create two volumes.
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 4e2b59b..624a99e 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -457,7 +457,7 @@
             else:
                 msg = ('When validation.connect_method equals floating, '
                        'validation_resources cannot be None')
-                raise exceptions.InvalidParam(invalid_param=msg)
+                raise lib_exc.InvalidParam(invalid_param=msg)
         elif CONF.validation.connect_method == 'fixed':
             addresses = server['addresses'][CONF.validation.network_for_ssh]
             for address in addresses:
@@ -477,7 +477,7 @@
         """Create a volume and wait for it to become 'available'.
 
         :param image_ref: Specify an image id to create a bootable volume.
-        :**kwargs: other parameters to create volume.
+        :param kwargs: other parameters to create volume.
         :returns: The available volume.
         """
         if 'size' not in kwargs:
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index 2a5d607..eeb58d6 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -24,6 +24,7 @@
 from tempest.common import waiters
 from tempest import config
 from tempest.lib.common.utils.linux import remote_client
+from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 
@@ -168,7 +169,9 @@
         iface = self.interfaces_client.create_interface(
             server['id'], net_id=network_id,
             fixed_ips=fixed_ips)['interfaceAttachment']
-        self.addCleanup(self.ports_client.delete_port, iface['port_id'])
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.ports_client.delete_port,
+                        iface['port_id'])
         self._check_interface(iface, server_id=server['id'],
                               fixed_ip=ip_list[0])
         return iface
@@ -253,23 +256,13 @@
         if not (CONF.auth.use_dynamic_credentials and
                 CONF.auth.create_isolated_networks and
                 not CONF.network.shared_physical_network):
-                raise self.skipException("Only owner network supports "
-                                         "creating interface by fixed ip.")
+            raise self.skipException("Only owner network supports "
+                                     "creating interface by fixed ip.")
 
         server, ifs = self._create_server_get_interfaces()
         interface_count = len(ifs)
         self.assertGreater(interface_count, 0)
 
-        try:
-            iface = self._test_create_interface(server)
-        except lib_exc.BadRequest as e:
-            msg = ('Multiple possible networks found, use a Network ID to be '
-                   'more specific.')
-            if not CONF.compute.fixed_network_name and six.text_type(e) == msg:
-                raise
-        else:
-            ifs.append(iface)
-
         iface = self._test_create_interface_by_fixed_ips(server, ifs)
         ifs.append(iface)
 
@@ -338,22 +331,73 @@
     @decorators.idempotent_id('c7e0e60b-ee45-43d0-abeb-8596fd42a2f9')
     @utils.services('network')
     def test_add_remove_fixed_ip(self):
+        # NOTE(zhufl) By default only project that is admin or network owner
+        # or project with role advsvc is authorised to add interfaces with
+        # fixed-ip, so if we don't create network for each project, do not
+        # test
+        if not (CONF.auth.use_dynamic_credentials and
+                CONF.auth.create_isolated_networks and
+                not CONF.network.shared_physical_network):
+            raise self.skipException("Only owner network supports "
+                                     "creating interface by fixed ip.")
+
         # Add and Remove the fixed IP to server.
         server, ifs = self._create_server_get_interfaces()
-        interface_count = len(ifs)
-        self.assertGreater(interface_count, 0)
+        original_interface_count = len(ifs)  # This is the number of ports.
+        self.assertGreater(original_interface_count, 0)
+        # Get the starting list of IPs on the server.
+        addresses = self.os_primary.servers_client.list_addresses(
+            server['id'])['addresses']
+        # There should be one entry for the single network mapped to a list of
+        # addresses, which at this point should have at least one entry.
+        # Note that we could start with two addresses depending on how tempest
+        # is configured for using floating IPs.
+        self.assertEqual(1, len(addresses), addresses)  # number of networks
+        # Keep track of the original addresses so we can know which IP is new.
+        original_ips = [addr['addr'] for addr in list(addresses.values())[0]]
+        original_ip_count = len(original_ips)
+        self.assertGreater(original_ip_count, 0, addresses)  # at least 1
         network_id = ifs[0]['net_id']
+        # Add another fixed IP to the server. This should result in another
+        # fixed IP on the same network (and same port since we only have one
+        # port).
         self.servers_client.add_fixed_ip(server['id'], networkId=network_id)
-        # Remove the fixed IP from server.
+        # Wait for the ips count to increase by one.
+
+        def _wait_for_ip_increase():
+            _addresses = self.os_primary.servers_client.list_addresses(
+                server['id'])['addresses']
+            return len(list(_addresses.values())[0]) == original_ip_count + 1
+
+        if not test_utils.call_until_true(
+                _wait_for_ip_increase, CONF.compute.build_timeout,
+                CONF.compute.build_interval):
+            raise lib_exc.TimeoutException(
+                'Timed out while waiting for IP count to increase.')
+
+        # Remove the fixed IP that we just added.
         server_detail = self.os_primary.servers_client.show_server(
             server['id'])['server']
         # Get the Fixed IP from server.
         fixed_ip = None
         for ip_set in server_detail['addresses']:
             for ip in server_detail['addresses'][ip_set]:
-                if ip['OS-EXT-IPS:type'] == 'fixed':
+                if (ip['OS-EXT-IPS:type'] == 'fixed' and
+                        ip['addr'] not in original_ips):
                     fixed_ip = ip['addr']
                     break
             if fixed_ip is not None:
                 break
         self.servers_client.remove_fixed_ip(server['id'], address=fixed_ip)
+        # Wait for the interface count to decrease by one.
+
+        def _wait_for_ip_decrease():
+            _addresses = self.os_primary.servers_client.list_addresses(
+                server['id'])['addresses']
+            return len(list(_addresses.values())[0]) == original_ip_count
+
+        if not test_utils.call_until_true(
+                _wait_for_ip_decrease, CONF.compute.build_timeout,
+                CONF.compute.build_interval):
+            raise lib_exc.TimeoutException(
+                'Timed out while waiting for IP count to decrease.')
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index d40f937..e817587 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -12,9 +12,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import json
-
 from oslo_log import log as logging
+from oslo_serialization import jsonutils as json
 
 from tempest.api.compute import base
 from tempest.common import utils
diff --git a/tempest/api/compute/servers/test_list_servers_negative.py b/tempest/api/compute/servers/test_list_servers_negative.py
index 18a78f0..f0915de 100644
--- a/tempest/api/compute/servers/test_list_servers_negative.py
+++ b/tempest/api/compute/servers/test_list_servers_negative.py
@@ -80,7 +80,7 @@
     @decorators.idempotent_id('fcdf192d-0f74-4d89-911f-1ec002b822c4')
     def test_list_servers_status_non_existing(self):
         # When invalid status is specified, up to microversion 2.37,
-        # an empty list is returnd, and starting from microversion 2.38,
+        # an empty list is returned, and starting from microversion 2.38,
         # a 400 error is returned in that case.
         if self.is_requested_microversion_compatible('2.37'):
             body = self.client.list_servers(status='non_existing_status')
diff --git a/tempest/api/compute/servers/test_novnc.py b/tempest/api/compute/servers/test_novnc.py
index 5801db1..daf6a06 100644
--- a/tempest/api/compute/servers/test_novnc.py
+++ b/tempest/api/compute/servers/test_novnc.py
@@ -143,7 +143,7 @@
         data_length = len(data) if data is not None else 0
         self.assertFalse(data_length <= 24 or
                          data_length != (struct.unpack(">L",
-                                         data[20:24])[0] + 24),
+                                                       data[20:24])[0] + 24),
                          'Server initialization was not the right format.')
         # Since the rest of the data on the screen is arbitrary, we will
         # close the socket and end our validation of the data at this point
@@ -151,7 +151,7 @@
         # initialization was the right format
         self.assertFalse(data_length <= 24 or
                          data_length != (struct.unpack(">L",
-                                         data[20:24])[0] + 24))
+                                                       data[20:24])[0] + 24))
 
     def _validate_websocket_upgrade(self):
         self.assertTrue(
diff --git a/tempest/api/compute/servers/test_server_rescue.py b/tempest/api/compute/servers/test_server_rescue.py
index b0ef3bc..6629794 100644
--- a/tempest/api/compute/servers/test_server_rescue.py
+++ b/tempest/api/compute/servers/test_server_rescue.py
@@ -24,11 +24,11 @@
 CONF = config.CONF
 
 
-class ServerRescueTestJSON(base.BaseV2ComputeTest):
+class ServerRescueTestBase(base.BaseV2ComputeTest):
 
     @classmethod
     def skip_checks(cls):
-        super(ServerRescueTestJSON, cls).skip_checks()
+        super(ServerRescueTestBase, cls).skip_checks()
         if not CONF.compute_feature_enabled.rescue:
             msg = "Server rescue not available."
             raise cls.skipException(msg)
@@ -36,11 +36,11 @@
     @classmethod
     def setup_credentials(cls):
         cls.set_network_resources(network=True, subnet=True, router=True)
-        super(ServerRescueTestJSON, cls).setup_credentials()
+        super(ServerRescueTestBase, cls).setup_credentials()
 
     @classmethod
     def resource_setup(cls):
-        super(ServerRescueTestJSON, cls).resource_setup()
+        super(ServerRescueTestBase, cls).resource_setup()
 
         password = data_utils.rand_password()
         server = cls.create_test_server(adminPass=password,
@@ -50,6 +50,9 @@
                                        'RESCUE')
         cls.rescued_server_id = server['id']
 
+
+class ServerRescueTestJSON(ServerRescueTestBase):
+
     @decorators.idempotent_id('fd032140-714c-42e4-a8fd-adcd8df06be6')
     def test_rescue_unrescue_instance(self):
         password = data_utils.rand_password()
@@ -62,6 +65,15 @@
         waiters.wait_for_server_status(self.servers_client, server['id'],
                                        'ACTIVE')
 
+
+class ServerRescueTestJSONUnderV235(ServerRescueTestBase):
+
+    max_microversion = '2.35'
+
+    # TODO(zhufl): After 2.35 we should switch to neutron client to create
+    # floating ip, but that will need admin credential, so the testcases will
+    # have to be added in somewhere in admin directory.
+
     @decorators.idempotent_id('4842e0cf-e87d-4d9d-b61f-f4791da3cacc')
     @testtools.skipUnless(CONF.network.public_network_id,
                           'The public_network_id option must be specified.')
diff --git a/tempest/api/compute/volumes/test_attach_volume.py b/tempest/api/compute/volumes/test_attach_volume.py
index f7b5b4b..8bb4eaa 100644
--- a/tempest/api/compute/volumes/test_attach_volume.py
+++ b/tempest/api/compute/volumes/test_attach_volume.py
@@ -65,6 +65,8 @@
 class AttachVolumeTestJSON(BaseAttachVolumeTest):
 
     @decorators.idempotent_id('52e9045a-e90d-4c0d-9087-79d657faffff')
+    # This test is conditionally marked slow if SSH validation is enabled.
+    @decorators.attr(type='slow', condition=CONF.validation.run_validation)
     def test_attach_detach_volume(self):
         # Stop and Start a server with an attached volume, ensuring that
         # the volume remains attached.
diff --git a/tempest/api/identity/admin/v2/test_services.py b/tempest/api/identity/admin/v2/test_services.py
index e2ed5ef..03543ac 100644
--- a/tempest/api/identity/admin/v2/test_services.py
+++ b/tempest/api/identity/admin/v2/test_services.py
@@ -89,14 +89,10 @@
             service = self.services_client.create_service(
                 name=name, type=s_type,
                 description=description)['OS-KSADM:service']
+            self.addCleanup(self.services_client.delete_service, service['id'])
             services.append(service)
         service_ids = [svc['id'] for svc in services]
 
-        def delete_services():
-            for service_id in service_ids:
-                self.services_client.delete_service(service_id)
-
-        self.addCleanup(delete_services)
         # List and Verify Services
         body = self.services_client.list_services()['OS-KSADM:services']
         found = [serv for serv in body if serv['id'] in service_ids]
diff --git a/tempest/api/identity/admin/v2/test_tenants.py b/tempest/api/identity/admin/v2/test_tenants.py
index cda721c..f68754e 100644
--- a/tempest/api/identity/admin/v2/test_tenants.py
+++ b/tempest/api/identity/admin/v2/test_tenants.py
@@ -50,7 +50,7 @@
                          'been sent in response for create')
         body = self.tenants_client.show_tenant(tenant_id)['tenant']
         desc2 = body['description']
-        self.assertEqual(desc2, tenant_desc, 'Description does not appear'
+        self.assertEqual(desc2, tenant_desc, 'Description does not appear '
                          'to be set')
         self.tenants_client.delete_tenant(tenant_id)
 
diff --git a/tempest/api/identity/admin/v3/test_default_project_id.py b/tempest/api/identity/admin/v3/test_default_project_id.py
index 302a0e5..73fddb7 100644
--- a/tempest/api/identity/admin/v3/test_default_project_id.py
+++ b/tempest/api/identity/admin/v3/test_default_project_id.py
@@ -9,6 +9,8 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
+import testtools
+
 from tempest.api.identity import base
 from tempest import clients
 from tempest import config
@@ -19,7 +21,7 @@
 CONF = config.CONF
 
 
-class TestDefaultProjectId (base.BaseIdentityV3AdminTest):
+class TestDefaultProjectId(base.BaseIdentityV3AdminTest):
 
     @classmethod
     def setup_credentials(cls):
@@ -32,6 +34,10 @@
         self.domains_client.update_domain(domain_id, enabled=False)
         self.domains_client.delete_domain(domain_id)
 
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an '
+                      'immutable user source and solely '
+                      'provides read-only access to users.')
     @decorators.idempotent_id('d6110661-6a71-49a7-a453-b5e26640ff6d')
     def test_default_project_id(self):
         # create a domain
@@ -51,9 +57,10 @@
         # create a user in the domain, with the previous project as his
         # default project
         user_name = data_utils.rand_name('user')
+        user_pass = data_utils.rand_password()
         user_body = self.users_client.create_user(
             name=user_name,
-            password=user_name,
+            password=user_pass,
             domain_id=dom_id,
             default_project_id=proj_id)['user']
         user_id = user_body['id']
@@ -72,7 +79,7 @@
 
         # create a new client with user's credentials (NOTE: unscoped token!)
         creds = auth.KeystoneV3Credentials(username=user_name,
-                                           password=user_name,
+                                           password=user_pass,
                                            user_domain_name=dom_name)
         auth_provider = clients.get_auth_provider(creds)
         creds = auth_provider.fill_credentials()
diff --git a/tempest/api/identity/admin/v3/test_domain_configuration.py b/tempest/api/identity/admin/v3/test_domain_configuration.py
index c4e0622..c0b18ca 100644
--- a/tempest/api/identity/admin/v3/test_domain_configuration.py
+++ b/tempest/api/identity/admin/v3/test_domain_configuration.py
@@ -21,6 +21,10 @@
 
 
 class DomainConfigurationTestJSON(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     custom_config = {
         "identity": {
diff --git a/tempest/api/identity/admin/v3/test_domains.py b/tempest/api/identity/admin/v3/test_domains.py
index 72b6be4..07175f4 100644
--- a/tempest/api/identity/admin/v3/test_domains.py
+++ b/tempest/api/identity/admin/v3/test_domains.py
@@ -153,18 +153,3 @@
         expected_data = {'name': d_name, 'enabled': True}
         self.assertEqual('', domain['description'])
         self.assertDictContainsSubset(expected_data, domain)
-
-
-class DefaultDomainTestJSON(base.BaseIdentityV3AdminTest):
-
-    @classmethod
-    def resource_setup(cls):
-        cls.domain_id = CONF.identity.default_domain_id
-        super(DefaultDomainTestJSON, cls).resource_setup()
-
-    @decorators.attr(type='smoke')
-    @decorators.idempotent_id('17a5de24-e6a0-4e4a-a9ee-d85b6e5612b5')
-    def test_default_domain_exists(self):
-        domain = self.domains_client.show_domain(self.domain_id)['domain']
-
-        self.assertTrue(domain['enabled'])
diff --git a/tempest/api/identity/admin/v3/test_domains_negative.py b/tempest/api/identity/admin/v3/test_domains_negative.py
index 56f7d32..b3c68fb 100644
--- a/tempest/api/identity/admin/v3/test_domains_negative.py
+++ b/tempest/api/identity/admin/v3/test_domains_negative.py
@@ -20,6 +20,10 @@
 
 
 class DomainsNegativeTestJSON(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     @decorators.attr(type=['negative', 'gate'])
     @decorators.idempotent_id('1f3fbff5-4e44-400d-9ca1-d953f05f609b')
diff --git a/tempest/api/identity/admin/v3/test_endpoint_groups.py b/tempest/api/identity/admin/v3/test_endpoint_groups.py
index eef93c2..625568d 100644
--- a/tempest/api/identity/admin/v3/test_endpoint_groups.py
+++ b/tempest/api/identity/admin/v3/test_endpoint_groups.py
@@ -20,6 +20,10 @@
 
 
 class EndPointGroupsTest(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     @classmethod
     def setup_clients(cls):
diff --git a/tempest/api/identity/admin/v3/test_groups.py b/tempest/api/identity/admin/v3/test_groups.py
index 37ce266..df0d79d 100644
--- a/tempest/api/identity/admin/v3/test_groups.py
+++ b/tempest/api/identity/admin/v3/test_groups.py
@@ -12,6 +12,7 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
+import testtools
 
 from tempest.api.identity import base
 from tempest import config
@@ -22,6 +23,10 @@
 
 
 class GroupsV3TestJSON(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     @classmethod
     def resource_setup(cls):
@@ -68,6 +73,10 @@
 
     @decorators.attr(type='smoke')
     @decorators.idempotent_id('1598521a-2f36-4606-8df9-30772bd51339')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an '
+                      'immutable user source and solely '
+                      'provides read-only access to users.')
     def test_group_users_add_list_delete(self):
         group = self.setup_test_group(domain_id=self.domain['id'])
         # add user into group
@@ -90,6 +99,10 @@
         self.assertEqual(len(group_users), 0)
 
     @decorators.idempotent_id('64573281-d26a-4a52-b899-503cb0f4e4ec')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an '
+                      'immutable user source and solely '
+                      'provides read-only access to users.')
     def test_list_user_groups(self):
         # create a user
         user = self.create_test_user()
diff --git a/tempest/api/identity/admin/v3/test_inherits.py b/tempest/api/identity/admin/v3/test_inherits.py
index 68c0225..2672f71 100644
--- a/tempest/api/identity/admin/v3/test_inherits.py
+++ b/tempest/api/identity/admin/v3/test_inherits.py
@@ -9,14 +9,22 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
+import testtools
 
 from tempest.api.identity import base
 from tempest.common import utils
+from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 
+CONF = config.CONF
+
 
 class InheritsV3TestJSON(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     @classmethod
     def skip_checks(cls):
@@ -30,7 +38,7 @@
         u_name = data_utils.rand_name('user-')
         u_desc = '%s description' % u_name
         u_email = '%s@testmail.tm' % u_name
-        u_password = data_utils.rand_name('pass-')
+        u_password = data_utils.rand_password()
         cls.domain = cls.create_domain()
         cls.project = cls.projects_client.create_project(
             data_utils.rand_name('project-'),
@@ -43,18 +51,26 @@
             domain_id=cls.domain['id'])['group']
         cls.addClassResourceCleanup(cls.groups_client.delete_group,
                                     cls.group['id'])
-        cls.user = cls.users_client.create_user(
-            name=u_name, description=u_desc, password=u_password,
-            email=u_email, project_id=cls.project['id'],
-            domain_id=cls.domain['id'])['user']
-        cls.addClassResourceCleanup(cls.users_client.delete_user,
-                                    cls.user['id'])
+        if not CONF.identity_feature_enabled.immutable_user_source:
+            cls.user = cls.users_client.create_user(
+                name=u_name,
+                description=u_desc,
+                password=u_password,
+                email=u_email,
+                project_id=cls.project['id'],
+                domain_id=cls.domain['id']
+            )['user']
+            cls.addClassResourceCleanup(cls.users_client.delete_user,
+                                        cls.user['id'])
 
     def _list_assertions(self, body, fetched_role_ids, role_id):
         self.assertEqual(len(body), 1)
         self.assertIn(role_id, fetched_role_ids)
 
     @decorators.idempotent_id('4e6f0366-97c8-423c-b2be-41eae6ac91c8')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an immutable user '
+                      'source and solely provides read-only access to users.')
     def test_inherit_assign_list_check_revoke_roles_on_domains_user(self):
         # Create role
         src_role = self.setup_test_role()
@@ -103,6 +119,9 @@
             self.domain['id'], self.group['id'], src_role['id'])
 
     @decorators.idempotent_id('18b70e45-7687-4b72-8277-b8f1a47d7591')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an immutable user '
+                      'source and solely provides read-only access to users.')
     def test_inherit_assign_check_revoke_roles_on_projects_user(self):
         # Create role
         src_role = self.setup_test_role()
@@ -134,6 +153,9 @@
              self.project['id'], self.group['id'], src_role['id']))
 
     @decorators.idempotent_id('3acf666e-5354-42ac-8e17-8b68893bcd36')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an immutable user '
+                      'source and solely provides read-only access to users.')
     def test_inherit_assign_list_revoke_user_roles_on_domain(self):
         # Create role
         src_role = self.setup_test_role()
@@ -178,6 +200,9 @@
         self.assertEmpty(assignments)
 
     @decorators.idempotent_id('9f02ccd9-9b57-46b4-8f77-dd5a736f3a06')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an immutable user '
+                      'source and solely provides read-only access to users.')
     def test_inherit_assign_list_revoke_user_roles_on_project_tree(self):
         # Create role
         src_role = self.setup_test_role()
diff --git a/tempest/api/identity/admin/v3/test_list_projects.py b/tempest/api/identity/admin/v3/test_list_projects.py
index 50f3186..299a618 100644
--- a/tempest/api/identity/admin/v3/test_list_projects.py
+++ b/tempest/api/identity/admin/v3/test_list_projects.py
@@ -13,11 +13,14 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from oslo_log import log as logging
+
 from tempest.api.identity import base
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 
+LOG = logging.getLogger(__name__)
 CONF = config.CONF
 
 
@@ -26,6 +29,9 @@
     def _list_projects_with_params(self, included, excluded, params, key):
         # Validate that projects in ``included`` belongs to the projects
         # returned that match ``params`` but not projects in ``excluded``
+        all_projects = self.projects_client.list_projects()['projects']
+        LOG.debug("Complete list of projects available in keystone: %s",
+                  all_projects)
         body = self.projects_client.list_projects(params)['projects']
         for p in included:
             self.assertIn(p[key], map(lambda x: x[key], body))
@@ -39,13 +45,12 @@
     def resource_setup(cls):
         super(ListProjectsTestJSON, cls).resource_setup()
         cls.project_ids = list()
-        # Create a domain
-        cls.domain = cls.create_domain()
+        cls.domain_id = cls.os_admin.credentials.domain_id
         # Create project with domain
         cls.p1_name = data_utils.rand_name('project')
         cls.p1 = cls.projects_client.create_project(
             cls.p1_name, enabled=False,
-            domain_id=cls.domain['id'])['project']
+            domain_id=cls.domain_id)['project']
         cls.addClassResourceCleanup(cls.projects_client.delete_project,
                                     cls.p1['id'])
         cls.project_ids.append(cls.p1['id'])
diff --git a/tempest/api/identity/admin/v3/test_oauth_consumers.py b/tempest/api/identity/admin/v3/test_oauth_consumers.py
index 062cce5..7a85f84 100644
--- a/tempest/api/identity/admin/v3/test_oauth_consumers.py
+++ b/tempest/api/identity/admin/v3/test_oauth_consumers.py
@@ -21,6 +21,10 @@
 
 
 class OAUTHConsumersV3Test(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     def _create_consumer(self):
         """Creates a consumer with a random description."""
diff --git a/tempest/api/identity/admin/v3/test_project_tags.py b/tempest/api/identity/admin/v3/test_project_tags.py
index d05173b..b7878a8 100644
--- a/tempest/api/identity/admin/v3/test_project_tags.py
+++ b/tempest/api/identity/admin/v3/test_project_tags.py
@@ -25,6 +25,10 @@
 
 
 class IdentityV3ProjectTagsTest(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     @decorators.idempotent_id('7c123aac-999d-416a-a0fb-84b915ab10de')
     @testtools.skipUnless(CONF.identity_feature_enabled.project_tags,
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index 6ddf42e..0b85b19 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -12,13 +12,21 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
+import testtools
 
 from tempest.api.identity import base
+from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 
+CONF = config.CONF
+
 
 class ProjectsTestJSON(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     @decorators.idempotent_id('0ecf465c-0dc4-4532-ab53-91ffeb74d12d')
     def test_project_create_with_description(self):
@@ -31,7 +39,7 @@
                          'been sent in response for create')
         body = self.projects_client.show_project(project_id)['project']
         desc2 = body['description']
-        self.assertEqual(desc2, project_desc, 'Description does not appear'
+        self.assertEqual(desc2, project_desc, 'Description does not appear '
                          'to be set')
 
     @decorators.idempotent_id('5f50fe07-8166-430b-a882-3b2ee0abe26f')
@@ -176,6 +184,10 @@
         self.assertEqual(resp2_en, resp3_en)
 
     @decorators.idempotent_id('59398d4a-5dc5-4f86-9a4c-c26cc804d6c6')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an '
+                      'immutable user source and solely '
+                      'provides read-only access to users.')
     def test_associate_user_to_project(self):
         # Associate a user to a project
         # Create a Project
diff --git a/tempest/api/identity/admin/v3/test_projects_negative.py b/tempest/api/identity/admin/v3/test_projects_negative.py
index 33a9c8c..12f1d4a 100644
--- a/tempest/api/identity/admin/v3/test_projects_negative.py
+++ b/tempest/api/identity/admin/v3/test_projects_negative.py
@@ -22,6 +22,22 @@
 class ProjectsNegativeTestJSON(base.BaseIdentityV3AdminTest):
 
     @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('8d68c012-89e0-4394-8d6b-ccd7196def97')
+    def test_project_delete_by_unauthorized_user(self):
+        # Non-admin user should not be able to delete a project
+        project = self.setup_test_project()
+        self.assertRaises(
+            lib_exc.Forbidden, self.non_admin_projects_client.delete_project,
+            project['id'])
+
+
+class ProjectsNegativeStaticTestJSON(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
+
+    @decorators.attr(type=['negative'])
     @decorators.idempotent_id('24c49279-45dd-4155-887a-cb738c2385aa')
     def test_list_projects_by_unauthorized_user(self):
         # Non-admin user should not be able to list projects
@@ -63,15 +79,6 @@
                           self.projects_client.create_project, project_name)
 
     @decorators.attr(type=['negative'])
-    @decorators.idempotent_id('8d68c012-89e0-4394-8d6b-ccd7196def97')
-    def test_project_delete_by_unauthorized_user(self):
-        # Non-admin user should not be able to delete a project
-        project = self.setup_test_project()
-        self.assertRaises(
-            lib_exc.Forbidden, self.non_admin_projects_client.delete_project,
-            project['id'])
-
-    @decorators.attr(type=['negative'])
     @decorators.idempotent_id('7965b581-60c1-43b7-8169-95d4ab7fc6fb')
     def test_delete_non_existent_project(self):
         # Attempt to delete a non existent project should fail
diff --git a/tempest/api/identity/admin/v3/test_regions.py b/tempest/api/identity/admin/v3/test_regions.py
index f22a528..c8c0151 100644
--- a/tempest/api/identity/admin/v3/test_regions.py
+++ b/tempest/api/identity/admin/v3/test_regions.py
@@ -20,6 +20,10 @@
 
 
 class RegionsTestJSON(base.BaseIdentityV3AdminTest):
+    # NOTE: force_tenant_isolation is true in the base class by default but
+    # overridden to false here to allow test execution for clouds using the
+    # pre-provisioned credentials provider.
+    force_tenant_isolation = False
 
     @classmethod
     def setup_clients(cls):
diff --git a/tempest/api/identity/admin/v3/test_roles.py b/tempest/api/identity/admin/v3/test_roles.py
index 47f663c..b67de95 100644
--- a/tempest/api/identity/admin/v3/test_roles.py
+++ b/tempest/api/identity/admin/v3/test_roles.py
@@ -12,13 +12,17 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
+import testtools
 
 from tempest.api.identity import base
+from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 
+CONF = config.CONF
+
 
 class RolesV3TestJSON(base.BaseIdentityV3AdminTest):
 
@@ -48,16 +52,21 @@
             domain_id=cls.domain['id'])['group']
         cls.addClassResourceCleanup(cls.groups_client.delete_group,
                                     cls.group_body['id'])
-        cls.user_body = cls.users_client.create_user(
-            name=u_name, description=u_desc, password=cls.u_password,
-            email=u_email, project_id=cls.project['id'],
-            domain_id=cls.domain['id'])['user']
-        cls.addClassResourceCleanup(cls.users_client.delete_user,
-                                    cls.user_body['id'])
         cls.role = cls.roles_client.create_role(
             name=data_utils.rand_name('Role'))['role']
         cls.addClassResourceCleanup(cls.roles_client.delete_role,
                                     cls.role['id'])
+        if not CONF.identity_feature_enabled.immutable_user_source:
+            cls.user_body = cls.users_client.create_user(
+                name=u_name,
+                description=u_desc,
+                email=u_email,
+                password=cls.u_password,
+                domain_id=cls.domain['id'],
+                project_id=cls.project['id']
+            )['user']
+            cls.addClassResourceCleanup(cls.users_client.delete_user,
+                                        cls.user_body['id'])
 
     @decorators.attr(type='smoke')
     @decorators.idempotent_id('18afc6c0-46cf-4911-824e-9989cc056c3a')
@@ -84,6 +93,9 @@
         self.assertIn(role['id'], [r['id'] for r in roles])
 
     @decorators.idempotent_id('c6b80012-fe4a-498b-9ce8-eb391c05169f')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an immutable user '
+                      'source and solely provides read-only access to users.')
     def test_grant_list_revoke_role_to_user_on_project(self):
         self.roles_client.create_user_role_on_project(self.project['id'],
                                                       self.user_body['id'],
@@ -102,6 +114,9 @@
             self.project['id'], self.user_body['id'], self.role['id'])
 
     @decorators.idempotent_id('6c9a2940-3625-43a3-ac02-5dcec62ef3bd')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an immutable user '
+                      'source and solely provides read-only access to users.')
     def test_grant_list_revoke_role_to_user_on_domain(self):
         self.roles_client.create_user_role_on_domain(
             self.domain['id'], self.user_body['id'], self.role['id'])
@@ -119,6 +134,9 @@
             self.domain['id'], self.user_body['id'], self.role['id'])
 
     @decorators.idempotent_id('cbf11737-1904-4690-9613-97bcbb3df1c4')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an immutable user '
+                      'source and solely provides read-only access to users.')
     def test_grant_list_revoke_role_to_group_on_project(self):
         # Grant role to group on project
         self.roles_client.create_group_role_on_project(
@@ -254,6 +272,9 @@
         self.assertIn(self.roles[2]['id'], implies_ids)
 
     @decorators.idempotent_id('c8828027-df48-4021-95df-b65b92c7429e')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an immutable user '
+                      'source and solely provides read-only access to users.')
     def test_assignments_for_implied_roles_create_delete(self):
         # Create a grant using "roles[0]"
         self.roles_client.create_user_role_on_project(
@@ -344,6 +365,9 @@
             domain_role1['id'])
 
     @decorators.idempotent_id('3859df7e-5b78-4e4d-b10e-214c8953842a')
+    @testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
+                      'Skipped because environment has an immutable user '
+                      'source and solely provides read-only access to users.')
     def test_assignments_for_domain_roles(self):
         domain_role = self.setup_test_role(domain_id=self.domain['id'])
 
diff --git a/tempest/api/identity/admin/v3/test_tokens.py b/tempest/api/identity/admin/v3/test_tokens.py
index 8ae43d6..5f1b58d 100644
--- a/tempest/api/identity/admin/v3/test_tokens.py
+++ b/tempest/api/identity/admin/v3/test_tokens.py
@@ -42,10 +42,10 @@
         user = self.create_test_user(password=user_password)
 
         # Create a couple projects
-        project1_name = data_utils.rand_name(name='project')
+        project1_name = data_utils.rand_name(name=self.__class__.__name__)
         project1 = self.setup_test_project(name=project1_name)
 
-        project2_name = data_utils.rand_name(name='project')
+        project2_name = data_utils.rand_name(name=self.__class__.__name__)
         project2 = self.setup_test_project(name=project2_name)
         self.addCleanup(self.projects_client.delete_project, project2['id'])
 
diff --git a/tempest/api/identity/admin/v3/test_trusts.py b/tempest/api/identity/admin/v3/test_trusts.py
index 2530072..54a5ab7 100644
--- a/tempest/api/identity/admin/v3/test_trusts.py
+++ b/tempest/api/identity/admin/v3/test_trusts.py
@@ -39,7 +39,6 @@
         # Use alt_username as the trustee
         self.trust_id = None
         self.create_trustor_and_roles()
-        self.addCleanup(self.cleanup_user_and_roles)
 
     def tearDown(self):
         if self.trust_id:
@@ -50,11 +49,13 @@
 
     def create_trustor_and_roles(self):
         # create a project that trusts will be granted on
-        trustor_project_name = data_utils.rand_name(name='project')
+        trustor_project_name = data_utils.rand_name(
+            name=self.__class__.__name__)
         project = self.projects_client.create_project(
             trustor_project_name,
             domain_id=CONF.identity.default_domain_id)['project']
         self.trustor_project_id = project['id']
+        self.addCleanup(self.projects_client.delete_project, project['id'])
         self.assertIsNotNone(self.trustor_project_id)
 
         # Create a trustor User
@@ -69,6 +70,7 @@
             email=u_email,
             project_id=self.trustor_project_id,
             domain_id=CONF.identity.default_domain_id)['user']
+        self.addCleanup(self.users_client.delete_user, user['id'])
         self.trustor_user_id = user['id']
 
         # And two roles, one we'll delegate and one we won't
@@ -76,10 +78,12 @@
         self.not_delegated_role = data_utils.rand_name('NotDelegatedRole')
 
         role = self.roles_client.create_role(name=self.delegated_role)['role']
+        self.addCleanup(self.roles_client.delete_role, role['id'])
         self.delegated_role_id = role['id']
 
         role = self.roles_client.create_role(
             name=self.not_delegated_role)['role']
+        self.addCleanup(self.roles_client.delete_role, role['id'])
         self.not_delegated_role_id = role['id']
 
         # Assign roles to trustor
@@ -109,16 +113,6 @@
         os = clients.Manager(credentials=creds)
         self.trustor_client = os.trusts_client
 
-    def cleanup_user_and_roles(self):
-        if self.trustor_user_id:
-            self.users_client.delete_user(self.trustor_user_id)
-        if self.trustor_project_id:
-            self.projects_client.delete_project(self.trustor_project_id)
-        if self.delegated_role_id:
-            self.roles_client.delete_role(self.delegated_role_id)
-        if self.not_delegated_role_id:
-            self.roles_client.delete_role(self.not_delegated_role_id)
-
     def create_trust(self, impersonate=True, expires=None):
 
         trust_create = self.trustor_client.create_trust(
diff --git a/tempest/api/identity/admin/v3/test_users.py b/tempest/api/identity/admin/v3/test_users.py
index 3813568..8955a93 100644
--- a/tempest/api/identity/admin/v3/test_users.py
+++ b/tempest/api/identity/admin/v3/test_users.py
@@ -28,6 +28,14 @@
 
 class UsersV3TestJSON(base.BaseIdentityV3AdminTest):
 
+    @classmethod
+    def skip_checks(cls):
+        super(UsersV3TestJSON, cls).skip_checks()
+        if CONF.identity_feature_enabled.immutable_user_source:
+            raise cls.skipException('Skipped because environment has an '
+                                    'immutable user source and solely '
+                                    'provides read-only access to users.')
+
     @decorators.idempotent_id('b537d090-afb9-4519-b95d-270b0708e87e')
     def test_user_update(self):
         # Test case to check if updating of user attributes is successful.
diff --git a/tempest/api/identity/v2/test_ec2_credentials.py b/tempest/api/identity/v2/test_ec2_credentials.py
index 237e728..9981ef8 100644
--- a/tempest/api/identity/v2/test_ec2_credentials.py
+++ b/tempest/api/identity/v2/test_ec2_credentials.py
@@ -57,18 +57,19 @@
             self.creds.user_id,
             tenant_id=self.creds.tenant_id)["credential"]
         created_creds.append(creds1['access'])
+        self.addCleanup(
+            self.non_admin_users_client.delete_user_ec2_credential,
+            self.creds.user_id, creds1['access'])
+
         # create second ec2 credentials
         creds2 = self.non_admin_users_client.create_user_ec2_credential(
             self.creds.user_id,
             tenant_id=self.creds.tenant_id)["credential"]
         created_creds.append(creds2['access'])
-        # add credentials to be cleaned up
-        self.addCleanup(
-            self.non_admin_users_client.delete_user_ec2_credential,
-            self.creds.user_id, creds1['access'])
         self.addCleanup(
             self.non_admin_users_client.delete_user_ec2_credential,
             self.creds.user_id, creds2['access'])
+
         # get the list of user ec2 credentials
         resp = self.non_admin_users_client.list_user_ec2_credentials(
             self.creds.user_id)["credentials"]
diff --git a/tempest/api/identity/v3/test_domains.py b/tempest/api/identity/v3/test_domains.py
new file mode 100644
index 0000000..9f132dd
--- /dev/null
+++ b/tempest/api/identity/v3/test_domains.py
@@ -0,0 +1,39 @@
+# Copyright 2013 OpenStack Foundation
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.identity import base
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class DefaultDomainTestJSON(base.BaseIdentityV3Test):
+
+    @classmethod
+    def setup_clients(cls):
+        super(DefaultDomainTestJSON, cls).setup_clients()
+        cls.domains_client = cls.os_primary.domains_client
+
+    @classmethod
+    def resource_setup(cls):
+        super(DefaultDomainTestJSON, cls).resource_setup()
+        cls.domain_id = CONF.identity.default_domain_id
+
+    @decorators.attr(type='smoke')
+    @decorators.idempotent_id('17a5de24-e6a0-4e4a-a9ee-d85b6e5612b5')
+    def test_default_domain_exists(self):
+        domain = self.domains_client.show_domain(self.domain_id)['domain']
+        self.assertTrue(domain['enabled'])
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index aa57daf..c938cee 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -185,7 +185,7 @@
                      for disk_fmt in disk_fmts]
 
         for (container_fmt, disk_fmt) in all_pairs[:6]:
-            LOG.debug("Creating an image"
+            LOG.debug("Creating an image "
                       "(Container format: %s, Disk format: %s).",
                       container_fmt, disk_fmt)
             cls._create_standard_image(container_fmt, disk_fmt)
diff --git a/tempest/api/network/admin/test_agent_management.py b/tempest/api/network/admin/test_agent_management.py
index 30ed176..eaf477c 100644
--- a/tempest/api/network/admin/test_agent_management.py
+++ b/tempest/api/network/admin/test_agent_management.py
@@ -48,11 +48,6 @@
             agent.pop('configurations', None)
         self.assertIn(self.agent, agents)
 
-    @decorators.idempotent_id('e335be47-b9a1-46fd-be30-0874c0b751e6')
-    def test_list_agents_non_admin(self):
-        body = self.agents_client.list_agents()
-        self.assertEmpty(body["agents"])
-
     @decorators.idempotent_id('869bc8e8-0fda-4a30-9b71-f8a7cf58ca9f')
     def test_show_agent(self):
         body = self.admin_agents_client.show_agent(self.agent['id'])
@@ -95,4 +90,4 @@
         non_existent_id = data_utils.rand_uuid()
         self.assertRaises(
             lib_exc.NotFound,
-            self.agents_client.delete_agent, non_existent_id)
+            self.admin_agents_client.delete_agent, non_existent_id)
diff --git a/tempest/api/network/admin/test_l3_agent_scheduler.py b/tempest/api/network/admin/test_l3_agent_scheduler.py
deleted file mode 100644
index 033bf55..0000000
--- a/tempest/api/network/admin/test_l3_agent_scheduler.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Copyright 2013 IBM Corp.
-#
-#    Licensed under the Apache License, Version 2.0 (the "License"); you may
-#    not use this file except in compliance with the License. You may obtain
-#    a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-#    License for the specific language governing permissions and limitations
-#    under the License.
-
-from tempest.api.network import base
-from tempest.common import utils
-from tempest.lib import decorators
-from tempest.lib import exceptions
-
-AGENT_TYPE = 'L3 agent'
-AGENT_MODES = (
-    'legacy',
-    'dvr_snat'
-)
-
-
-class L3AgentSchedulerTestJSON(base.BaseAdminNetworkTest):
-    """Tests the following operations in the Neutron API:
-
-        List routers that the given L3 agent is hosting.
-        List L3 agents hosting the given router.
-        Add and Remove Router to L3 agent
-
-    v2.0 of the Neutron API is assumed.
-
-    The l3_agent_scheduler extension is required for these tests.
-    """
-
-    @classmethod
-    def skip_checks(cls):
-        super(L3AgentSchedulerTestJSON, cls).skip_checks()
-        if not utils.is_extension_enabled('l3_agent_scheduler', 'network'):
-            msg = "L3 Agent Scheduler Extension not enabled."
-            raise cls.skipException(msg)
-
-    @classmethod
-    def resource_setup(cls):
-        super(L3AgentSchedulerTestJSON, cls).resource_setup()
-        agents = cls.admin_agents_client.list_agents(
-            agent_type=AGENT_TYPE)['agents']
-        for agent in agents:
-            if (agent['configurations']['agent_mode'] in AGENT_MODES and
-                agent['alive']):
-                cls.agent = agent
-                break
-        else:
-            msg = "L3 Agent Scheduler enabled in conf, but L3 Agent not found"
-            raise exceptions.InvalidConfiguration(msg)
-        cls.router = cls.create_router()
-
-    @decorators.idempotent_id('b7ce6e89-e837-4ded-9b78-9ed3c9c6a45a')
-    def test_list_routers_on_l3_agent(self):
-        self.admin_agents_client.list_routers_on_l3_agent(self.agent['id'])
-
-    @decorators.idempotent_id('9464e5e7-8625-49c3-8fd1-89c52be59d66')
-    def test_add_list_remove_router_on_l3_agent(self):
-        l3_agent_ids = list()
-        self.admin_agents_client.create_router_on_l3_agent(
-            self.agent['id'],
-            router_id=self.router['id'])
-        body = (
-            self.admin_routers_client.list_l3_agents_hosting_router(
-                self.router['id']))
-        for agent in body['agents']:
-            l3_agent_ids.append(agent['id'])
-            self.assertIn('agent_type', agent)
-            self.assertEqual('L3 agent', agent['agent_type'])
-        self.assertIn(self.agent['id'], l3_agent_ids)
-        body = self.admin_agents_client.delete_router_from_l3_agent(
-            self.agent['id'],
-            self.router['id'])
-        # NOTE(afazekas): The deletion not asserted, because neutron
-        # is not forbidden to reschedule the router to the same agent
diff --git a/tempest/api/network/admin/test_negative_quotas.py b/tempest/api/network/admin/test_negative_quotas.py
index a075b51..9d1e2a7 100644
--- a/tempest/api/network/admin/test_negative_quotas.py
+++ b/tempest/api/network/admin/test_negative_quotas.py
@@ -30,7 +30,7 @@
     It is also assumed that the per-project quota extension API is configured
     in /etc/neutron/neutron.conf as follows:
 
-        quota_driver = neutron.db.quota_db.DbQuotaDriver
+        quota_driver = neutron.db.quota.driver.DbQuotaDriver
     """
 
     @classmethod
diff --git a/tempest/api/network/admin/test_quotas.py b/tempest/api/network/admin/test_quotas.py
index b1e4a58..ef5ebb6 100644
--- a/tempest/api/network/admin/test_quotas.py
+++ b/tempest/api/network/admin/test_quotas.py
@@ -35,7 +35,7 @@
     It is also assumed that the per-project quota extension API is configured
     in /etc/neutron/neutron.conf as follows:
 
-        quota_driver = neutron.db.quota_db.DbQuotaDriver
+        quota_driver = neutron.db.quota.driver.DbQuotaDriver
     """
 
     @classmethod
diff --git a/tempest/api/network/admin/test_routers.py b/tempest/api/network/admin/test_routers.py
index a7355f3..6ce86fb 100644
--- a/tempest/api/network/admin/test_routers.py
+++ b/tempest/api/network/admin/test_routers.py
@@ -111,7 +111,8 @@
     def _verify_gateway_port(self, router_id):
         list_body = self.admin_ports_client.list_ports(
             network_id=CONF.network.public_network_id,
-            device_id=router_id)
+            device_id=router_id,
+            device_owner="network:router_gateway")
         self.assertEqual(len(list_body['ports']), 1)
         gw_port = list_body['ports'][0]
         fixed_ips = gw_port['fixed_ips']
diff --git a/tempest/api/network/test_agent_management_negative.py b/tempest/api/network/test_agent_management_negative.py
new file mode 100644
index 0000000..d1c02ce
--- /dev/null
+++ b/tempest/api/network/test_agent_management_negative.py
@@ -0,0 +1,28 @@
+# Copyright 2018 OpenStack Foundation
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.network import base
+from tempest.lib import decorators
+
+
+class AgentManagementNegativeTest(base.BaseNetworkTest):
+
+    @decorators.idempotent_id('e335be47-b9a1-46fd-be30-0874c0b751e6')
+    @decorators.attr(type=['negative'])
+    def test_list_agents_non_admin(self):
+        """Validate that non-admin user cannot list agents."""
+        # Listing agents requires admin_only permissions.
+        body = self.agents_client.list_agents()
+        self.assertEmpty(body["agents"])
diff --git a/tempest/api/network/test_dhcp_ipv6.py b/tempest/api/network/test_dhcp_ipv6.py
index 0730d58..eb31ed3 100644
--- a/tempest/api/network/test_dhcp_ipv6.py
+++ b/tempest/api/network/test_dhcp_ipv6.py
@@ -135,7 +135,7 @@
             real_ip, eui_ip = self._get_ips_from_subnet(**kwargs)
             self._clean_network()
             self.assertEqual(eui_ip, real_ip,
-                             ('Real port IP %s shall be equal to EUI-64 %s'
+                             ('Real port IP %s shall be equal to EUI-64 %s '
                               'when ipv6_ra_mode=%s,ipv6_address_mode=%s') % (
                                  real_ip, eui_ip,
                                  ra_mode if ra_mode else "Off",
@@ -167,7 +167,7 @@
         self._clean_network()
         self.assertNotEqual(eui_ip, real_ip,
                             ('Real port IP %s equal to EUI-64 %s when '
-                             'ipv6_ra_mode=Off and ipv6_address_mode=Off,'
+                             'ipv6_ra_mode=Off and ipv6_address_mode=Off, '
                              'but shall be taken from fixed IPs') % (
                                 real_ip, eui_ip))
 
@@ -206,7 +206,7 @@
                                  for k in port['fixed_ips']])
                 real_dhcp_ip, real_eui_ip = [real_ips[sub['id']]
                                              for sub in [subnet_dhcp,
-                                             subnet_slaac]]
+                                                         subnet_slaac]]
                 self.ports_client.delete_port(port['id'])
                 self.ports.pop()
                 body = self.ports_client.list_ports()
@@ -257,7 +257,7 @@
                                  for k in port['fixed_ips']])
                 real_dhcp_ip, real_eui_ip = [real_ips[sub['id']]
                                              for sub in [subnet_dhcp,
-                                             subnet_slaac]]
+                                                         subnet_slaac]]
                 self._clean_network()
                 self.assertEqual(real_eui_ip,
                                  eui_ip,
diff --git a/tempest/api/network/test_networks.py b/tempest/api/network/test_networks.py
index 7345fd1..ed8eb52 100644
--- a/tempest/api/network/test_networks.py
+++ b/tempest/api/network/test_networks.py
@@ -317,7 +317,7 @@
 
         subnet = self.create_subnet(
             network, **self.subnet_dict(['gateway', 'host_routes',
-                                        'dns_nameservers',
+                                         'dns_nameservers',
                                          'allocation_pools']))
         subnet_id = subnet['id']
         new_gateway = str(netaddr.IPAddress(
diff --git a/tempest/api/network/test_ports.py b/tempest/api/network/test_ports.py
index 2c9159c..25976ce 100644
--- a/tempest/api/network/test_ports.py
+++ b/tempest/api/network/test_ports.py
@@ -107,7 +107,7 @@
         address = self.cidr
         address.prefixlen = self.mask_bits
         if ((address.version == 4 and address.prefixlen >= 30) or
-           (address.version == 6 and address.prefixlen >= 126)):
+            (address.version == 6 and address.prefixlen >= 126)):
             msg = ("Subnet %s isn't large enough for the test" % address.cidr)
             raise exceptions.InvalidConfiguration(msg)
         allocation_pools = {'allocation_pools': [{'start': str(address[2]),
diff --git a/tempest/api/object_storage/test_container_acl.py b/tempest/api/object_storage/test_container_acl.py
index 765bc6d..e9ca0b1 100644
--- a/tempest/api/object_storage/test_container_acl.py
+++ b/tempest/api/object_storage/test_container_acl.py
@@ -38,9 +38,9 @@
     def test_read_object_with_rights(self):
         # attempt to read object using authorized user
         # update X-Container-Read metadata ACL
-        tenant_name = self.os_roles_operator_alt.credentials.tenant_name
-        username = self.os_roles_operator_alt.credentials.username
-        cont_headers = {'X-Container-Read': tenant_name + ':' + username}
+        tenant_id = self.os_roles_operator_alt.credentials.tenant_id
+        user_id = self.os_roles_operator_alt.credentials.user_id
+        cont_headers = {'X-Container-Read': tenant_id + ':' + user_id}
         container_client = self.os_roles_operator.container_client
         resp_meta, _ = (
             container_client.create_update_or_delete_container_metadata(
@@ -66,9 +66,9 @@
     def test_write_object_with_rights(self):
         # attempt to write object using authorized user
         # update X-Container-Write metadata ACL
-        tenant_name = self.os_roles_operator_alt.credentials.tenant_name
-        username = self.os_roles_operator_alt.credentials.username
-        cont_headers = {'X-Container-Write': tenant_name + ':' + username}
+        tenant_id = self.os_roles_operator_alt.credentials.tenant_id
+        user_id = self.os_roles_operator_alt.credentials.user_id
+        cont_headers = {'X-Container-Write': tenant_id + ':' + user_id}
         container_client = self.os_roles_operator.container_client
         resp_meta, _ = (
             container_client.create_update_or_delete_container_metadata(
diff --git a/tempest/api/object_storage/test_container_quotas.py b/tempest/api/object_storage/test_container_quotas.py
index 982c4a1..fcd9a7c 100644
--- a/tempest/api/object_storage/test_container_quotas.py
+++ b/tempest/api/object_storage/test_container_quotas.py
@@ -31,9 +31,10 @@
 
         Quotas are set by adding meta values to the container,
         and are validated when set:
-          - X-Container-Meta-Quota-Bytes:
+
+        - X-Container-Meta-Quota-Bytes:
                      Maximum size of the container, in bytes.
-          - X-Container-Meta-Quota-Count:
+        - X-Container-Meta-Quota-Count:
                      Maximum object count of the container.
         """
         super(ContainerQuotasTest, self).setUp()
diff --git a/tempest/api/volume/admin/test_volume_quotas_negative.py b/tempest/api/volume/admin/test_volume_quotas_negative.py
index 915aeec..5c7ab15 100644
--- a/tempest/api/volume/admin/test_volume_quotas_negative.py
+++ b/tempest/api/volume/admin/test_volume_quotas_negative.py
@@ -45,13 +45,6 @@
         cls.addClassResourceCleanup(cls.admin_quotas_client.update_quota_set,
                                     cls.demo_tenant_id, **cleanup_quota_set)
 
-        cls.shared_quota_set = {'gigabytes': 2 * CONF.volume.volume_size,
-                                'volumes': 1}
-
-        cls.admin_quotas_client.update_quota_set(
-            cls.demo_tenant_id,
-            **cls.shared_quota_set)
-
         # NOTE(gfidente): no need to delete in tearDown as
         # they are created using utility wrapper methods.
         cls.volume = cls.create_volume()
@@ -59,6 +52,8 @@
     @decorators.attr(type='negative')
     @decorators.idempotent_id('bf544854-d62a-47f2-a681-90f7a47d86b6')
     def test_quota_volumes(self):
+        self.admin_quotas_client.update_quota_set(self.demo_tenant_id,
+                                                  volumes=1, gigabytes=-1)
         self.assertRaises(lib_exc.OverLimit,
                           self.volumes_client.create_volume,
                           size=CONF.volume.volume_size)
@@ -66,17 +61,18 @@
     @decorators.attr(type='negative')
     @decorators.idempotent_id('2dc27eee-8659-4298-b900-169d71a91374')
     def test_quota_volume_gigabytes(self):
-        # NOTE(gfidente): quota set needs to be changed for this test
-        # or we may be limited by the volumes or snaps quota number, not by
-        # actual gigs usage; next line ensures shared set is restored.
-        self.addCleanup(self.admin_quotas_client.update_quota_set,
-                        self.demo_tenant_id,
-                        **self.shared_quota_set)
-        new_quota_set = {'gigabytes': CONF.volume.volume_size,
-                         'volumes': 2, 'snapshots': 1}
         self.admin_quotas_client.update_quota_set(
-            self.demo_tenant_id,
-            **new_quota_set)
+            self.demo_tenant_id, gigabytes=CONF.volume.volume_size, volumes=-1)
         self.assertRaises(lib_exc.OverLimit,
                           self.volumes_client.create_volume,
-                          size=CONF.volume.volume_size)
+                          size=CONF.volume.volume_size * 2)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('d321dc21-d8c6-401f-95fe-49f4845f1a6d')
+    def test_volume_extend_gigabytes_quota_deviation(self):
+        self.admin_quotas_client.update_quota_set(
+            self.demo_tenant_id, gigabytes=CONF.volume.volume_size)
+        self.assertRaises(lib_exc.OverLimit,
+                          self.volumes_client.extend_volume,
+                          self.volume['id'],
+                          new_size=CONF.volume.volume_size * 2)
diff --git a/tempest/clients.py b/tempest/clients.py
index 204ce08..e5d5be1 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -82,10 +82,8 @@
             self.schemas_client = self.image_v2.SchemasClient()
             self.namespace_properties_client = \
                 self.image_v2.NamespacePropertiesClient()
-            self.namespace_tags_client = \
-                self.image_v2.NamespaceTagsClient()
-            self.image_versions_client = \
-                self.image_v2.VersionsClient()
+            self.namespace_tags_client = self.image_v2.NamespaceTagsClient()
+            self.image_versions_client = self.image_v2.VersionsClient()
 
     def _set_compute_clients(self):
         self.agents_client = self.compute.AgentsClient()
@@ -284,8 +282,7 @@
                 self.volume_v3.QuotaClassesClient()
             self.volume_scheduler_stats_v2_client = \
                 self.volume_v3.SchedulerStatsClient()
-            self.volume_transfers_v2_client = \
-                self.volume_v3.TransfersClient()
+            self.volume_transfers_v2_client = self.volume_v3.TransfersClient()
             self.volume_v2_availability_zone_client = \
                 self.volume_v3.AvailabilityZoneClient()
             self.volume_v2_limits_client = self.volume_v3.LimitsClient()
diff --git a/tempest/cmd/account_generator.py b/tempest/cmd/account_generator.py
index 25e91aa..7ea0099 100755
--- a/tempest/cmd/account_generator.py
+++ b/tempest/cmd/account_generator.py
@@ -291,13 +291,16 @@
 
 
 def main(opts=None):
-    setup_logging()
+    log_warning = False
     if not opts:
-        LOG.warning("Use of: 'tempest-account-generator' is deprecated, "
-                    "please use: 'tempest account-generator'")
+        log_warning = True
         opts = get_options()
     if opts.config_file:
         config.CONF.set_config_path(opts.config_file)
+    setup_logging()
+    if log_warning:
+        LOG.warning("Use of: 'tempest-account-generator' is deprecated, "
+                    "please use: 'tempest account-generator'")
     if opts.os_tenant_name:
         LOG.warning("'os-tenant-name' and 'OS_TENANT_NAME' are both "
                     "deprecated, please use 'os-project-name' or "
diff --git a/tempest/cmd/cleanup.py b/tempest/cmd/cleanup.py
index 29abd49..e6db2e9 100644
--- a/tempest/cmd/cleanup.py
+++ b/tempest/cmd/cleanup.py
@@ -197,7 +197,7 @@
             **kwargs))
         kwargs = {'data': project_data,
                   'is_dry_run': is_dry_run,
-                  'saved_state_json': None,
+                  'saved_state_json': self.json_data,
                   'is_preserve': is_preserve,
                   'is_save_state': False,
                   'project_id': project_id}
@@ -279,7 +279,7 @@
                                                        self.admin_id,
                                                        self.admin_role_id)
             except Exception as ex:
-                LOG.exception("Failed removing role from project which still"
+                LOG.exception("Failed removing role from project which still "
                               "exists, exception: %s", ex)
 
     def _project_exists(self, project_id):
@@ -305,13 +305,17 @@
             svc = service(admin_mgr, **kwargs)
             svc.run()
 
-        with open(SAVED_STATE_JSON, 'w+') as f:
-            f.write(json.dumps(data,
-                    sort_keys=True, indent=2, separators=(',', ': ')))
+        for service in self.project_services:
+            svc = service(admin_mgr, **kwargs)
+            svc.run()
 
-    def _load_json(self):
+        with open(SAVED_STATE_JSON, 'w+') as f:
+            f.write(json.dumps(data, sort_keys=True,
+                               indent=2, separators=(',', ': ')))
+
+    def _load_json(self, saved_state_json=SAVED_STATE_JSON):
         try:
-            with open(SAVED_STATE_JSON) as json_file:
+            with open(saved_state_json, 'rb') as json_file:
                 self.json_data = json.load(json_file)
 
         except IOError as ex:
diff --git a/tempest/cmd/cleanup_service.py b/tempest/cmd/cleanup_service.py
index 1a08246..3aed4e8 100644
--- a/tempest/cmd/cleanup_service.py
+++ b/tempest/cmd/cleanup_service.py
@@ -101,7 +101,7 @@
 
         self.tenant_filter = {}
         if hasattr(self, 'tenant_id'):
-            self.tenant_filter['tenant_id'] = self.tenant_id
+            self.tenant_filter['project_id'] = self.tenant_id
 
     def _filter_by_tenant_id(self, item_list):
         if (item_list is None or
@@ -144,6 +144,10 @@
     def list(self):
         client = self.client
         snaps = client.list_snapshots()['snapshots']
+        if not self.is_save_state:
+            # recreate list removing saved snapshots
+            snaps = [snap for snap in snaps if snap['id']
+                     not in self.saved_state_json['snapshots'].keys()]
         LOG.debug("List count, %s Snapshots", len(snaps))
         return snaps
 
@@ -160,6 +164,12 @@
         snaps = self.list()
         self.data['snapshots'] = snaps
 
+    def save_state(self):
+        snaps = self.list()
+        self.data['snapshots'] = {}
+        for snap in snaps:
+            self.data['snapshots'][snap['id']] = snap['name']
+
 
 class ServerService(BaseService):
     def __init__(self, manager, **kwargs):
@@ -171,6 +181,10 @@
         client = self.client
         servers_body = client.list_servers()
         servers = servers_body['servers']
+        if not self.is_save_state:
+            # recreate list removing saved servers
+            servers = [server for server in servers if server['id']
+                       not in self.saved_state_json['servers'].keys()]
         LOG.debug("List count, %s Servers", len(servers))
         return servers
 
@@ -187,17 +201,27 @@
         servers = self.list()
         self.data['servers'] = servers
 
+    def save_state(self):
+        servers = self.list()
+        self.data['servers'] = {}
+        for server in servers:
+            self.data['servers'][server['id']] = server['name']
+
 
 class ServerGroupService(ServerService):
 
     def list(self):
         client = self.server_groups_client
         sgs = client.list_server_groups()['server_groups']
+        if not self.is_save_state:
+            # recreate list removing saved server_groups
+            sgs = [sg for sg in sgs if sg['id']
+                   not in self.saved_state_json['server_groups'].keys()]
         LOG.debug("List count, %s Server Groups", len(sgs))
         return sgs
 
     def delete(self):
-        client = self.client
+        client = self.server_groups_client
         sgs = self.list()
         for sg in sgs:
             try:
@@ -209,6 +233,12 @@
         sgs = self.list()
         self.data['server_groups'] = sgs
 
+    def save_state(self):
+        sgs = self.list()
+        self.data['server_groups'] = {}
+        for sg in sgs:
+            self.data['server_groups'][sg['id']] = sg['name']
+
 
 class KeyPairService(BaseService):
     def __init__(self, manager, **kwargs):
@@ -218,6 +248,11 @@
     def list(self):
         client = self.client
         keypairs = client.list_keypairs()['keypairs']
+        if not self.is_save_state:
+            # recreate list removing saved keypairs
+            keypairs = [keypair for keypair in keypairs
+                        if keypair['keypair']['name']
+                        not in self.saved_state_json['keypairs'].keys()]
         LOG.debug("List count, %s Keypairs", len(keypairs))
         return keypairs
 
@@ -235,56 +270,12 @@
         keypairs = self.list()
         self.data['keypairs'] = keypairs
 
-
-class SecurityGroupService(BaseService):
-    def __init__(self, manager, **kwargs):
-        super(SecurityGroupService, self).__init__(kwargs)
-        self.client = manager.compute_security_groups_client
-
-    def list(self):
-        client = self.client
-        secgrps = client.list_security_groups()['security_groups']
-        secgrp_del = [grp for grp in secgrps if grp['name'] != 'default']
-        LOG.debug("List count, %s Security Groups", len(secgrp_del))
-        return secgrp_del
-
-    def delete(self):
-        client = self.client
-        secgrp_del = self.list()
-        for g in secgrp_del:
-            try:
-                client.delete_security_group(g['id'])
-            except Exception:
-                LOG.exception("Delete Security Groups exception.")
-
-    def dry_run(self):
-        secgrp_del = self.list()
-        self.data['security_groups'] = secgrp_del
-
-
-class FloatingIpService(BaseService):
-    def __init__(self, manager, **kwargs):
-        super(FloatingIpService, self).__init__(kwargs)
-        self.client = manager.compute_floating_ips_client
-
-    def list(self):
-        client = self.client
-        floating_ips = client.list_floating_ips()['floating_ips']
-        LOG.debug("List count, %s Floating IPs", len(floating_ips))
-        return floating_ips
-
-    def delete(self):
-        client = self.client
-        floating_ips = self.list()
-        for f in floating_ips:
-            try:
-                client.delete_floating_ip(f['id'])
-            except Exception:
-                LOG.exception("Delete Floating IPs exception.")
-
-    def dry_run(self):
-        floating_ips = self.list()
-        self.data['floating_ips'] = floating_ips
+    def save_state(self):
+        keypairs = self.list()
+        self.data['keypairs'] = {}
+        for keypair in keypairs:
+            keypair = keypair['keypair']
+            self.data['keypairs'][keypair['name']] = keypair
 
 
 class VolumeService(BaseService):
@@ -295,6 +286,10 @@
     def list(self):
         client = self.client
         vols = client.list_volumes()['volumes']
+        if not self.is_save_state:
+            # recreate list removing saved volumes
+            vols = [vol for vol in vols if vol['id']
+                    not in self.saved_state_json['volumes'].keys()]
         LOG.debug("List count, %s Volumes", len(vols))
         return vols
 
@@ -311,6 +306,12 @@
         vols = self.list()
         self.data['volumes'] = vols
 
+    def save_state(self):
+        vols = self.list()
+        self.data['volumes'] = {}
+        for vol in vols:
+            self.data['volumes'][vol['id']] = vol['name']
+
 
 class VolumeQuotaService(BaseService):
     def __init__(self, manager, **kwargs):
@@ -320,13 +321,13 @@
     def delete(self):
         client = self.client
         try:
-            client.delete_quota_set(self.tenant_id)
+            client.delete_quota_set(self.project_id)
         except Exception:
             LOG.exception("Delete Volume Quotas exception.")
 
     def dry_run(self):
         quotas = self.client.show_quota_set(
-            self.tenant_id, params={'usage': True})['quota_set']
+            self.project_id, params={'usage': True})['quota_set']
         self.data['volume_quotas'] = quotas
 
 
@@ -339,7 +340,7 @@
     def delete(self):
         client = self.client
         try:
-            client.delete_quota_set(self.tenant_id)
+            client.delete_quota_set(self.project_id)
         except Exception:
             LOG.exception("Delete Quotas exception.")
 
@@ -350,9 +351,9 @@
 
 
 # Begin network service classes
-class NetworkService(BaseService):
+class BaseNetworkService(BaseService):
     def __init__(self, manager, **kwargs):
-        super(NetworkService, self).__init__(kwargs)
+        super(BaseNetworkService, self).__init__(kwargs)
         self.networks_client = manager.networks_client
         self.subnets_client = manager.subnets_client
         self.ports_client = manager.ports_client
@@ -361,6 +362,7 @@
         self.metering_label_rules_client = manager.metering_label_rules_client
         self.security_groups_client = manager.security_groups_client
         self.routers_client = manager.routers_client
+        self.subnetpools_client = manager.subnetpools_client
 
     def _filter_by_conf_networks(self, item_list):
         if not item_list or not all(('network_id' in i for i in item_list)):
@@ -369,10 +371,18 @@
         return [item for item in item_list if item['network_id']
                 not in CONF_NETWORKS]
 
+
+class NetworkService(BaseNetworkService):
+
     def list(self):
         client = self.networks_client
         networks = client.list_networks(**self.tenant_filter)
         networks = networks['networks']
+
+        if not self.is_save_state:
+            # recreate list removing saved networks
+            networks = [network for network in networks if network['id']
+                        not in self.saved_state_json['networks'].keys()]
         # filter out networks declared in tempest.conf
         if self.is_preserve:
             networks = [network for network in networks
@@ -393,18 +403,29 @@
         networks = self.list()
         self.data['networks'] = networks
 
+    def save_state(self):
+        networks = self.list()
+        self.data['networks'] = {}
+        for network in networks:
+            self.data['networks'][network['id']] = network
 
-class NetworkFloatingIpService(NetworkService):
+
+class NetworkFloatingIpService(BaseNetworkService):
 
     def list(self):
         client = self.floating_ips_client
         flips = client.list_floatingips(**self.tenant_filter)
         flips = flips['floatingips']
+
+        if not self.is_save_state:
+            # recreate list removing saved flips
+            flips = [flip for flip in flips if flip['id']
+                     not in self.saved_state_json['floatingips'].keys()]
         LOG.debug("List count, %s Network Floating IPs", len(flips))
         return flips
 
     def delete(self):
-        client = self.client
+        client = self.floating_ips_client
         flips = self.list()
         for flip in flips:
             try:
@@ -414,15 +435,26 @@
 
     def dry_run(self):
         flips = self.list()
-        self.data['floating_ips'] = flips
+        self.data['floatingips'] = flips
+
+    def save_state(self):
+        flips = self.list()
+        self.data['floatingips'] = {}
+        for flip in flips:
+            self.data['floatingips'][flip['id']] = flip
 
 
-class NetworkRouterService(NetworkService):
+class NetworkRouterService(BaseNetworkService):
 
     def list(self):
         client = self.routers_client
         routers = client.list_routers(**self.tenant_filter)
         routers = routers['routers']
+
+        if not self.is_save_state:
+            # recreate list removing saved routers
+            routers = [router for router in routers if router['id']
+                       not in self.saved_state_json['routers'].keys()]
         if self.is_preserve:
             routers = [router for router in routers
                        if router['id'] != CONF_PUB_ROUTER]
@@ -450,101 +482,11 @@
         routers = self.list()
         self.data['routers'] = routers
 
-
-class NetworkHealthMonitorService(NetworkService):
-
-    def list(self):
-        client = self.client
-        hms = client.list_health_monitors()
-        hms = hms['health_monitors']
-        hms = self._filter_by_tenant_id(hms)
-        LOG.debug("List count, %s Health Monitors", len(hms))
-        return hms
-
-    def delete(self):
-        client = self.client
-        hms = self.list()
-        for hm in hms:
-            try:
-                client.delete_health_monitor(hm['id'])
-            except Exception:
-                LOG.exception("Delete Health Monitor exception.")
-
-    def dry_run(self):
-        hms = self.list()
-        self.data['health_monitors'] = hms
-
-
-class NetworkMemberService(NetworkService):
-
-    def list(self):
-        client = self.client
-        members = client.list_members()
-        members = members['members']
-        members = self._filter_by_tenant_id(members)
-        LOG.debug("List count, %s Members", len(members))
-        return members
-
-    def delete(self):
-        client = self.client
-        members = self.list()
-        for member in members:
-            try:
-                client.delete_member(member['id'])
-            except Exception:
-                LOG.exception("Delete Member exception.")
-
-    def dry_run(self):
-        members = self.list()
-        self.data['members'] = members
-
-
-class NetworkVipService(NetworkService):
-
-    def list(self):
-        client = self.client
-        vips = client.list_vips()
-        vips = vips['vips']
-        vips = self._filter_by_tenant_id(vips)
-        LOG.debug("List count, %s VIPs", len(vips))
-        return vips
-
-    def delete(self):
-        client = self.client
-        vips = self.list()
-        for vip in vips:
-            try:
-                client.delete_vip(vip['id'])
-            except Exception:
-                LOG.exception("Delete VIP exception.")
-
-    def dry_run(self):
-        vips = self.list()
-        self.data['vips'] = vips
-
-
-class NetworkPoolService(NetworkService):
-
-    def list(self):
-        client = self.client
-        pools = client.list_pools()
-        pools = pools['pools']
-        pools = self._filter_by_tenant_id(pools)
-        LOG.debug("List count, %s Pools", len(pools))
-        return pools
-
-    def delete(self):
-        client = self.client
-        pools = self.list()
-        for pool in pools:
-            try:
-                client.delete_pool(pool['id'])
-            except Exception:
-                LOG.exception("Delete Pool exception.")
-
-    def dry_run(self):
-        pools = self.list()
-        self.data['pools'] = pools
+    def save_state(self):
+        routers = self.list()
+        self.data['routers'] = {}
+        for router in routers:
+            self.data['routers'][router['id']] = router['name']
 
 
 class NetworkMeteringLabelRuleService(NetworkService):
@@ -554,6 +496,11 @@
         rules = client.list_metering_label_rules()
         rules = rules['metering_label_rules']
         rules = self._filter_by_tenant_id(rules)
+
+        if not self.is_save_state:
+            saved_rules = self.saved_state_json['metering_label_rules'].keys()
+            # recreate list removing saved rules
+            rules = [rule for rule in rules if rule['id'] not in saved_rules]
         LOG.debug("List count, %s Metering Label Rules", len(rules))
         return rules
 
@@ -568,16 +515,27 @@
 
     def dry_run(self):
         rules = self.list()
-        self.data['rules'] = rules
+        self.data['metering_label_rules'] = rules
+
+    def save_state(self):
+        rules = self.list()
+        self.data['metering_label_rules'] = {}
+        for rule in rules:
+            self.data['metering_label_rules'][rule['id']] = rule
 
 
-class NetworkMeteringLabelService(NetworkService):
+class NetworkMeteringLabelService(BaseNetworkService):
 
     def list(self):
         client = self.metering_labels_client
         labels = client.list_metering_labels()
         labels = labels['metering_labels']
         labels = self._filter_by_tenant_id(labels)
+
+        if not self.is_save_state:
+            # recreate list removing saved labels
+            labels = [label for label in labels if label['id']
+                      not in self.saved_state_json['metering_labels'].keys()]
         LOG.debug("List count, %s Metering Labels", len(labels))
         return labels
 
@@ -592,10 +550,16 @@
 
     def dry_run(self):
         labels = self.list()
-        self.data['labels'] = labels
+        self.data['metering_labels'] = labels
+
+    def save_state(self):
+        labels = self.list()
+        self.data['metering_labels'] = {}
+        for label in labels:
+            self.data['metering_labels'][label['id']] = label['name']
 
 
-class NetworkPortService(NetworkService):
+class NetworkPortService(BaseNetworkService):
 
     def list(self):
         client = self.ports_client
@@ -604,6 +568,10 @@
                  if port["device_owner"] == "" or
                  port["device_owner"].startswith("compute:")]
 
+        if not self.is_save_state:
+            # recreate list removing saved ports
+            ports = [port for port in ports if port['id']
+                     not in self.saved_state_json['ports'].keys()]
         if self.is_preserve:
             ports = self._filter_by_conf_networks(ports)
 
@@ -623,8 +591,14 @@
         ports = self.list()
         self.data['ports'] = ports
 
+    def save_state(self):
+        ports = self.list()
+        self.data['ports'] = {}
+        for port in ports:
+            self.data['ports'][port['id']] = port['name']
 
-class NetworkSecGroupService(NetworkService):
+
+class NetworkSecGroupService(BaseNetworkService):
     def list(self):
         client = self.security_groups_client
         filter = self.tenant_filter
@@ -633,31 +607,48 @@
                      client.list_security_groups(**filter)['security_groups']
                      if secgroup['name'] != 'default']
 
+        if not self.is_save_state:
+            # recreate list removing saved security_groups
+            secgroups = [secgroup for secgroup in secgroups if secgroup['id']
+                         not in self.saved_state_json['security_groups'].keys()
+                         ]
         if self.is_preserve:
-            secgroups = self._filter_by_conf_networks(secgroups)
+            secgroups = [secgroup for secgroup in secgroups
+                         if secgroup['security_group_rules'][0]['project_id']
+                         not in CONF_PROJECTS]
         LOG.debug("List count, %s security_groups", len(secgroups))
         return secgroups
 
     def delete(self):
-        client = self.client
+        client = self.security_groups_client
         secgroups = self.list()
         for secgroup in secgroups:
             try:
-                client.delete_secgroup(secgroup['id'])
+                client.delete_security_group(secgroup['id'])
             except Exception:
                 LOG.exception("Delete security_group exception.")
 
     def dry_run(self):
         secgroups = self.list()
-        self.data['secgroups'] = secgroups
+        self.data['security_groups'] = secgroups
+
+    def save_state(self):
+        secgroups = self.list()
+        self.data['security_groups'] = {}
+        for secgroup in secgroups:
+            self.data['security_groups'][secgroup['id']] = secgroup['name']
 
 
-class NetworkSubnetService(NetworkService):
+class NetworkSubnetService(BaseNetworkService):
 
     def list(self):
         client = self.subnets_client
         subnets = client.list_subnets(**self.tenant_filter)
         subnets = subnets['subnets']
+        if not self.is_save_state:
+            # recreate list removing saved subnets
+            subnets = [subnet for subnet in subnets if subnet['id']
+                       not in self.saved_state_json['subnets'].keys()]
         if self.is_preserve:
             subnets = self._filter_by_conf_networks(subnets)
         LOG.debug("List count, %s Subnets", len(subnets))
@@ -676,6 +667,47 @@
         subnets = self.list()
         self.data['subnets'] = subnets
 
+    def save_state(self):
+        subnets = self.list()
+        self.data['subnets'] = {}
+        for subnet in subnets:
+            self.data['subnets'][subnet['id']] = subnet['name']
+
+
+class NetworkSubnetPoolsService(BaseNetworkService):
+
+    def list(self):
+        client = self.subnetpools_client
+        pools = client.list_subnetpools(**self.tenant_filter)['subnetpools']
+        if not self.is_save_state:
+            # recreate list removing saved subnet pools
+            pools = [pool for pool in pools if pool['id']
+                     not in self.saved_state_json['subnetpools'].keys()]
+        if self.is_preserve:
+            pools = [pool for pool in pools if pool['project_id']
+                     not in CONF_PROJECTS]
+        LOG.debug("List count, %s Subnet Pools", len(pools))
+        return pools
+
+    def delete(self):
+        client = self.subnetpools_client
+        pools = self.list()
+        for pool in pools:
+            try:
+                client.delete_subnetpool(pool['id'])
+            except Exception:
+                LOG.exception("Delete Subnet Pool exception.")
+
+    def dry_run(self):
+        pools = self.list()
+        self.data['subnetpools'] = pools
+
+    def save_state(self):
+        pools = self.list()
+        self.data['subnetpools'] = {}
+        for pool in pools:
+            self.data['subnetpools'][pool['id']] = pool['name']
+
 
 # begin global services
 class FlavorService(BaseService):
@@ -754,12 +786,6 @@
             self.data['images'][image['id']] = image['name']
 
 
-class IdentityService(BaseService):
-    def __init__(self, manager, **kwargs):
-        super(IdentityService, self).__init__(kwargs)
-        self.client = manager.identity_v3_client
-
-
 class UserService(BaseService):
 
     def __init__(self, manager, **kwargs):
@@ -929,10 +955,7 @@
     if IS_NOVA:
         project_services.append(ServerService)
         project_services.append(KeyPairService)
-        project_services.append(SecurityGroupService)
         project_services.append(ServerGroupService)
-        if not IS_NEUTRON:
-            project_services.append(FloatingIpService)
         project_services.append(NovaQuotaService)
     if IS_NEUTRON:
         project_services.append(NetworkFloatingIpService)
@@ -944,6 +967,7 @@
         project_services.append(NetworkSubnetService)
         project_services.append(NetworkService)
         project_services.append(NetworkSecGroupService)
+        project_services.append(NetworkSubnetPoolsService)
     if IS_CINDER:
         project_services.append(SnapshotService)
         project_services.append(VolumeService)
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
index 84c6d9a..77d4496 100644
--- a/tempest/cmd/run.py
+++ b/tempest/cmd/run.py
@@ -103,6 +103,9 @@
 from tempest.common import credentials_factory as credentials
 from tempest import config
 
+if six.PY2:
+    # Python 2 has not FileNotFoundError exception
+    FileNotFoundError = IOError
 
 CONF = config.CONF
 SAVED_STATE_JSON = "saved_state.json"
@@ -112,7 +115,12 @@
 
     def _set_env(self, config_file=None):
         if config_file:
-            CONF.set_config_path(os.path.abspath(config_file))
+            if os.path.exists(os.path.abspath(config_file)):
+                CONF.set_config_path(os.path.abspath(config_file))
+            else:
+                raise FileNotFoundError(
+                    "Config file: %s doesn't exist" % config_file)
+
         # NOTE(mtreinish): This is needed so that stestr doesn't gobble up any
         # stacktraces on failure.
         if 'TESTR_PDB' in os.environ:
@@ -202,8 +210,8 @@
             svc.run()
 
         with open(SAVED_STATE_JSON, 'w+') as f:
-            f.write(json.dumps(data,
-                    sort_keys=True, indent=2, separators=(',', ': ')))
+            f.write(json.dumps(data, sort_keys=True,
+                               indent=2, separators=(',', ': ')))
 
     def get_parser(self, prog_name):
         parser = super(TempestRun, self).get_parser(prog_name)
@@ -243,8 +251,8 @@
         parser.add_argument('--load-list', '--load_list',
                             help='Path to a non-regex whitelist file, '
                                  'this file contains a separate test '
-                                 'on each newline. This command'
-                                 'supports files created by the tempest'
+                                 'on each newline. This command '
+                                 'supports files created by the tempest '
                                  'run ``--list-tests`` command')
         # list only args
         parser.add_argument('--list-tests', '-l', action='store_true',
@@ -252,6 +260,7 @@
                             default=False)
         # execution args
         parser.add_argument('--concurrency', '-w',
+                            type=int, default=0,
                             help="The number of workers to use, defaults to "
                                  "the number of cpus")
         parallel = parser.add_mutually_exclusive_group()
diff --git a/tempest/cmd/subunit_describe_calls.py b/tempest/cmd/subunit_describe_calls.py
index 8dcf575..081fa7a 100644
--- a/tempest/cmd/subunit_describe_calls.py
+++ b/tempest/cmd/subunit_describe_calls.py
@@ -78,11 +78,11 @@
 import argparse
 import collections
 import io
-import json
 import os
 import re
 import sys
 
+from oslo_serialization import jsonutils as json
 import subunit
 import testtools
 
diff --git a/tempest/cmd/verify_tempest_config.py b/tempest/cmd/verify_tempest_config.py
index 6c2fee8..d25d3ca 100644
--- a/tempest/cmd/verify_tempest_config.py
+++ b/tempest/cmd/verify_tempest_config.py
@@ -365,11 +365,11 @@
         catalog_type = getattr(cfg, 'catalog_type', None)
         if not catalog_type:
             continue
-        else:
-            if cfgname == 'identity':
-                # Keystone is a required service for tempest
-                continue
-            if catalog_type not in services:
+        if cfgname == 'identity':
+            # Keystone is a required service for tempest
+            continue
+        if catalog_type not in services:
+            try:
                 if getattr(CONF.service_available, codename_match[cfgname]):
                     print('Endpoint type %s not found either disable service '
                           '%s or fix the catalog_type in the config file' % (
@@ -377,7 +377,13 @@
                     if update:
                         change_option(codename_match[cfgname],
                                       'service_available', False)
-            else:
+            except KeyError:
+                print('%s is a third party plugin, cannot be verified '
+                      'automatically, but it is suggested that it is set to '
+                      'False because %s service is not available ' % (
+                          cfgname, catalog_type))
+        else:
+            try:
                 if not getattr(CONF.service_available,
                                codename_match[cfgname]):
                     print('Endpoint type %s is available, service %s should be'
@@ -391,6 +397,11 @@
                         avail_services.append(codename_match[cfgname])
                 else:
                     avail_services.append(codename_match[cfgname])
+            except KeyError:
+                print('%s is a third party plugin, cannot be verified '
+                      'automatically, but it is suggested that it is set to '
+                      'True because %s service is available ' % (
+                          cfgname, catalog_type))
     return avail_services
 
 
diff --git a/tempest/cmd/workspace.py b/tempest/cmd/workspace.py
index d276bde..d0c4b28 100644
--- a/tempest/cmd/workspace.py
+++ b/tempest/cmd/workspace.py
@@ -94,7 +94,7 @@
     @lockutils.synchronized('workspaces', external=True)
     def move_workspace(self, name, path):
         self._populate()
-        path = os.path.abspath(os.path.expanduser(path))
+        path = os.path.abspath(os.path.expanduser(path)) if path else path
         self._name_exists(name)
         self._validate_path(path)
         self.workspaces[name] = path
@@ -115,6 +115,7 @@
 
     @lockutils.synchronized('workspaces', external=True)
     def remove_workspace_directory(self, workspace_path):
+        self._validate_path(workspace_path)
         shutil.rmtree(workspace_path)
 
     @lockutils.synchronized('workspaces', external=True)
@@ -136,6 +137,10 @@
             sys.exit(1)
 
     def _validate_path(self, path):
+        if not path:
+            print("None or empty path is specified for workspace."
+                  " Please specify correct workspace path.")
+            sys.exit(1)
         if not os.path.exists(path):
             print("Path does not exist.")
             sys.exit(1)
@@ -144,7 +149,7 @@
     def register_new_workspace(self, name, path, init=False):
         """Adds the new workspace and writes out the new workspace config"""
         self._populate()
-        path = os.path.abspath(os.path.expanduser(path))
+        path = os.path.abspath(os.path.expanduser(path)) if path else path
         # This only happens when register is called from outside of init
         if not init:
             self._validate_path(path)
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index f2730b3..1489e60 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -44,15 +44,14 @@
 def is_scheduler_filter_enabled(filter_name):
     """Check the list of enabled compute scheduler filters from config.
 
-    This function checks whether the given compute scheduler filter is
-    available and configured in the config file. If the
-    scheduler_available_filters option is set to 'all' (Default value. which
-    means default filters are configured in nova) in tempest.conf then, this
-    function returns True with assumption that requested filter 'filter_name'
-    is one of available filter in nova ("nova.scheduler.filters.all_filters").
+    This function checks whether the given compute scheduler filter is enabled
+    in the nova config file. If the scheduler_enabled_filters option is set to
+    'all' in tempest.conf then, this function returns True with assumption that
+    requested filter 'filter_name' is one of the enabled filters in nova
+    ("nova.scheduler.filters.all_filters").
     """
 
-    filters = CONF.compute_feature_enabled.scheduler_available_filters
+    filters = CONF.compute_feature_enabled.scheduler_enabled_filters
     if not filters:
         return False
     if 'all' in filters:
@@ -79,23 +78,22 @@
     :param wait_until: Server status to wait for the server to reach after
         its creation.
     :param volume_backed: Whether the server is volume backed or not.
-                          If this is true, a volume will be created and
-                          create server will be requested with
-                          'block_device_mapping_v2' populated with below
-                          values:
-                          --------------------------------------------
-                          bd_map_v2 = [{
-                              'uuid': volume['volume']['id'],
-                              'source_type': 'volume',
-                              'destination_type': 'volume',
-                              'boot_index': 0,
-                              'delete_on_termination': True}]
-                          kwargs['block_device_mapping_v2'] = bd_map_v2
-                          ---------------------------------------------
-                          If server needs to be booted from volume with other
-                          combination of bdm inputs than mentioned above, then
-                          pass the bdm inputs explicitly as kwargs and image_id
-                          as empty string ('').
+        If this is true, a volume will be created and create server will be
+        requested with 'block_device_mapping_v2' populated with below values:
+
+        .. code-block:: python
+
+            bd_map_v2 = [{
+                'uuid': volume['volume']['id'],
+                'source_type': 'volume',
+                'destination_type': 'volume',
+                'boot_index': 0,
+                'delete_on_termination': True}]
+            kwargs['block_device_mapping_v2'] = bd_map_v2
+
+        If server needs to be booted from volume with other combination of bdm
+        inputs than mentioned above, then pass the bdm inputs explicitly as
+        kwargs and image_id as empty string ('').
     :param name: Name of the server to be provisioned. If not defined a random
         string ending with '-instance' will be generated.
     :param flavor: Flavor of the server to be provisioned. If not defined,
@@ -170,10 +168,19 @@
                   'imageRef': image_id,
                   'size': CONF.volume.volume_size}
         volume = volumes_client.create_volume(**params)
-        waiters.wait_for_volume_resource_status(volumes_client,
-                                                volume['volume']['id'],
-                                                'available')
-
+        try:
+            waiters.wait_for_volume_resource_status(volumes_client,
+                                                    volume['volume']['id'],
+                                                    'available')
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                try:
+                    volumes_client.delete_volume(volume['volume']['id'])
+                    volumes_client.wait_for_resource_deletion(
+                        volume['volume']['id'])
+                except Exception as exc:
+                    LOG.exception("Deleting volume %s failed, exception %s",
+                                  volume['volume']['id'], exc)
         bd_map_v2 = [{
             'uuid': volume['volume']['id'],
             'source_type': 'volume',
diff --git a/tempest/common/identity.py b/tempest/common/identity.py
index eaf651b..cd6d058 100644
--- a/tempest/common/identity.py
+++ b/tempest/common/identity.py
@@ -26,7 +26,7 @@
         if project['name'] == project_name:
             return project
     raise lib_exc.NotFound('No such project(%s) in %s' % (project_name,
-                           projects))
+                                                          projects))
 
 
 def get_tenant_by_name(client, tenant_name):
@@ -64,7 +64,8 @@
     should not be used for testing identity features.
 
     :param clients: a client manager.
-    :return
+    :return: v2 or v3 of CredsClient
+    :rtype: V2CredsClient or V3CredsClient
     """
     if CONF.identity.auth_version == 'v2':
         client = clients.identity_client
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index 52ccfa9..49d9742 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -98,6 +98,7 @@
     def get_nic_name_by_ip(self, address):
         cmd = "ip -o addr | awk '/%s/ {print $2}'" % address
         nic = self.exec_command(cmd)
+        LOG.debug('(get_nic_name_by_ip) Command result: %s', nic)
         return nic.strip().strip(":").split('@')[0].lower()
 
     def get_dns_servers(self):
diff --git a/tempest/common/utils/net_utils.py b/tempest/common/utils/net_utils.py
index 867b3dd..b697ef1 100644
--- a/tempest/common/utils/net_utils.py
+++ b/tempest/common/utils/net_utils.py
@@ -19,7 +19,6 @@
 
 def get_unused_ip_addresses(ports_client, subnets_client,
                             network_id, subnet_id, count):
-
     """Return a list with the specified number of unused IP addresses
 
     This method uses the given ports_client to find the specified number of
diff --git a/tempest/config.py b/tempest/config.py
index c0a2d60..24ae3ae 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -20,6 +20,7 @@
 
 from oslo_concurrency import lockutils
 from oslo_config import cfg
+from oslo_config import types
 from oslo_log import log as logging
 
 from tempest.lib import exceptions
@@ -101,7 +102,7 @@
                deprecated_group='identity'),
     cfg.StrOpt('admin_domain_name',
                default='Default',
-               help="Admin domain name for authentication (Keystone V3)."
+               help="Admin domain name for authentication (Keystone V3). "
                     "The same domain applies to user and project",
                deprecated_group='identity'),
 ]
@@ -170,17 +171,28 @@
     cfg.IntOpt('user_lockout_failure_attempts',
                default=2,
                help="The number of unsuccessful login attempts the user is "
-                    "allowed before having the account locked."),
+                    "allowed before having the account locked. This only "
+                    "takes effect when identity-feature-enabled."
+                    "security_compliance is set to 'True'. For more details, "
+                    "refer to keystone config options keystone.conf:"
+                    "security_compliance.lockout_failure_attempts. "
+                    "This feature is disabled by default in keystone."),
     cfg.IntOpt('user_lockout_duration',
                default=5,
                help="The number of seconds a user account will remain "
-                    "locked."),
+                    "locked. This only takes "
+                    "effect when identity-feature-enabled.security_compliance "
+                    "is set to 'True'. For more details, refer to "
+                    "keystone config options "
+                    "keystone.conf:security_compliance.lockout_duration. "
+                    "Setting this option will have no effect unless you also "
+                    "set identity.user_lockout_failure_attempts."),
     cfg.IntOpt('user_unique_last_password_count',
                default=2,
                help="The number of passwords for a user that must be unique "
                     "before an old password can be reused. This only takes "
                     "effect when identity-feature-enabled.security_compliance "
-                    "is set to 'True'."
+                    "is set to 'True'. "
                     "This config option corresponds to keystone.conf: "
                     "security_compliance.unique_last_password_count, whose "
                     "default value is 0 meaning disabling this feature. "
@@ -251,7 +263,13 @@
     cfg.BoolOpt('application_credentials',
                 default=False,
                 help='Does the environment have application credentials '
-                     'enabled?')
+                     'enabled?'),
+    cfg.BoolOpt('immutable_user_source',
+                default=False,
+                help='Set to True if the environment has a read-only '
+                     'user source. This will skip all tests that attempt to '
+                     'create, delete, or modify users. This should not be set '
+                     'to True if using dynamic credentials')
 ]
 
 compute_group = cfg.OptGroup(name='compute',
@@ -348,6 +366,38 @@
                     "with format 'X.Y' or string 'latest'"),
 ]
 
+placement_group = cfg.OptGroup(name='placement',
+                               title='Placement Service Options')
+
+PlacementGroup = [
+    cfg.StrOpt('endpoint_type',
+               default='public',
+               choices=['public', 'admin', 'internal'],
+               help="The endpoint type to use for the placement service."),
+    cfg.StrOpt('catalog_type',
+               default='placement',
+               help="Catalog type of the Placement service."),
+    cfg.StrOpt('region',
+               default='RegionOne',
+               help="The placement region name to use. If empty, the value "
+                    "of [identity]/region is used instead. If no such region "
+                    "is found in the service catalog, the first region found "
+                    "is used."),
+    cfg.StrOpt('min_microversion',
+               default=None,
+               help="Lower version of the test target microversion range. "
+                    "The format is 'X.Y', where 'X' and 'Y' are int values. "
+                    "Valid values are string with format 'X.Y' or string "
+                    "'latest'"),
+    cfg.StrOpt('max_microversion',
+               default=None,
+               help="Upper version of the test target microversion range. "
+                    "The format is 'X.Y', where 'X' and 'Y' are int values. "
+                    "Valid values are string with format 'X.Y' or string "
+                    "'latest'"),
+]
+
+
 compute_features_group = cfg.OptGroup(name='compute-feature-enabled',
                                       title="Enabled Compute Service Features")
 
@@ -472,20 +522,30 @@
     cfg.BoolOpt('config_drive',
                 default=True,
                 help='Enable special configuration drive with metadata.'),
-    cfg.ListOpt('scheduler_available_filters',
-                default=['all'],
-                help="A list of enabled filters that nova will accept as hints"
-                     " to the scheduler when creating a server. A special "
-                     "entry 'all' indicates all filters that are included "
-                     "with nova are enabled. Empty list indicates all filters "
-                     "are disabled. The full list of available filters is in "
-                     "nova.conf: filter_scheduler.enabled_filters. If the "
+    cfg.ListOpt('scheduler_enabled_filters',
+                default=["RetryFilter", "AvailabilityZoneFilter",
+                         "ComputeFilter", "ComputeCapabilitiesFilter",
+                         "ImagePropertiesFilter",
+                         "ServerGroupAntiAffinityFilter",
+                         "ServerGroupAffinityFilter"],
+                help="A list of enabled filters that Nova will accept as "
+                     "hints to the scheduler when creating a server. If the "
                      "default value is overridden in nova.conf by the test "
                      "environment (which means that a different set of "
                      "filters is enabled than what is included in Nova by "
-                     "default) then, this option must be configured to "
+                     "default), then this option must be configured to "
                      "contain the same filters that Nova uses in the test "
-                     "environment."),
+                     "environment. A special entry 'all' indicates all "
+                     "filters that are included with Nova are enabled. If "
+                     "using 'all', be sure to enable all filters in "
+                     "nova.conf, as tests can fail in unpredictable ways if "
+                     "Nova's and Tempest's enabled filters don't match. "
+                     "Empty list indicates all filters are disabled. The "
+                     "full list of enabled filters is in nova.conf: "
+                     "filter_scheduler.enabled_filters.",
+                deprecated_opts=[cfg.DeprecatedOpt(
+                    'scheduler_available_filters',
+                    group='compute-feature-enabled')]),
     cfg.BoolOpt('swap_volume',
                 default=False,
                 help='Does the test environment support in-place swapping of '
@@ -568,6 +628,7 @@
 network_group = cfg.OptGroup(name='network',
                              title='Network Service Options')
 
+ProfileType = types.Dict(types.List(types.String(), bounds=True))
 NetworkGroup = [
     cfg.StrOpt('catalog_type',
                default='network',
@@ -631,10 +692,11 @@
                     " with pre-configured ports."
                     " Supported ports are:"
                     " ['normal','direct','macvtap']"),
-    cfg.DictOpt('port_profile',
-                default={},
-                help="port profile to use when launching instances"
-                     " with pre-configured ports."),
+    cfg.Opt('port_profile',
+            type=ProfileType,
+            default={},
+            help="port profile to use when launching instances"
+                 " with pre-configured ports."),
     cfg.ListOpt('default_network',
                 default=["1.0.0.0/16", "2.0.0.0/16"],
                 help="List of ip pools"
@@ -1038,6 +1100,18 @@
 """)
 ]
 
+
+profiler_group = cfg.OptGroup(name="profiler",
+                              title="OpenStack Profiler")
+
+ProfilerGroup = [
+    cfg.StrOpt('key',
+               help="The secret key to enable OpenStack Profiler. The value "
+                    "should match the one configured in OpenStack services "
+                    "under `[profiler]/hmac_keys` property. The default empty "
+                    "value keeps profiling disabled"),
+]
+
 DefaultGroup = [
     cfg.BoolOpt('pause_teardown',
                 default=False,
@@ -1069,6 +1143,8 @@
     (scenario_group, ScenarioGroup),
     (service_available_group, ServiceAvailableGroup),
     (debug_group, DebugGroup),
+    (placement_group, PlacementGroup),
+    (profiler_group, ProfilerGroup),
     (None, DefaultGroup)
 ]
 
@@ -1186,7 +1262,7 @@
 
         logging_cfg_path = "%s/logging.conf" % os.path.dirname(path)
         if ((not hasattr(_CONF, 'log_config_append') or
-            _CONF.log_config_append is None) and
+             _CONF.log_config_append is None) and
             os.path.isfile(logging_cfg_path)):
             # if logging conf is in place we need to set log_config_append
             _CONF.log_config_append = logging_cfg_path
diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py
index 2dd9d00..8e6d3d5 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -324,7 +324,7 @@
                 pass
         if expiry is None:
             raise ValueError(
-                "time data '{data}' does not match any of the"
+                "time data '{data}' does not match any of the "
                 "expected formats: {formats}".format(
                     data=expiry_string, formats=self.EXPIRY_DATE_FORMATS))
         return expiry
diff --git a/tempest/lib/cmd/check_uuid.py b/tempest/lib/cmd/check_uuid.py
index 82fcd0b..71ecb32 100755
--- a/tempest/lib/cmd/check_uuid.py
+++ b/tempest/lib/cmd/check_uuid.py
@@ -110,7 +110,7 @@
             for item in files:
                 if item.endswith('.py'):
                     module_name = '.'.join((root_package,
-                                           os.path.splitext(item)[0]))
+                                            os.path.splitext(item)[0]))
                     if not module_name.startswith(UNIT_TESTS_EXCLUDE):
                         modules.append(module_name)
         return modules
@@ -233,8 +233,8 @@
                           if self._is_test_case(module, node))
             for node in test_cases:
                 for subnode in filter(self._is_test_method, node.body):
-                        test_name = '%s.%s' % (node.name, subnode.name)
-                        tests[module_name]['tests'][test_name] = subnode
+                    test_name = '%s.%s' % (node.name, subnode.name)
+                    tests[module_name]['tests'][test_name] = subnode
         return tests
 
     @staticmethod
diff --git a/tempest/lib/common/api_version_utils.py b/tempest/lib/common/api_version_utils.py
index bcb076b..709c319 100644
--- a/tempest/lib/common/api_version_utils.py
+++ b/tempest/lib/common/api_version_utils.py
@@ -54,7 +54,7 @@
     config_min_version = api_version_request.APIVersionRequest(cfg_min_version)
     config_max_version = api_version_request.APIVersionRequest(cfg_max_version)
     if ((min_version > max_version) or
-       (config_min_version > config_max_version)):
+        (config_min_version > config_max_version)):
         msg = ("Test Class versions [%s - %s]. "
                "Configuration versions [%s - %s]."
                % (min_version.get_string(),
diff --git a/tempest/lib/common/fixed_network.py b/tempest/lib/common/fixed_network.py
index 875a79d..926c3a4 100644
--- a/tempest/lib/common/fixed_network.py
+++ b/tempest/lib/common/fixed_network.py
@@ -24,7 +24,7 @@
     """Get a full network dict from just a network name
 
     :param str name: the name of the network to use
-    :param NetworksClient compute_networks_client: The network client
+    :param network.NetworksClient compute_networks_client: The network client
         object to use for making the network lists api request
     :return: The full dictionary for the network in question
     :rtype: dict
diff --git a/tempest/lib/common/jsonschema_validator.py b/tempest/lib/common/jsonschema_validator.py
index 9a35b76..bbf5e89 100644
--- a/tempest/lib/common/jsonschema_validator.py
+++ b/tempest/lib/common/jsonschema_validator.py
@@ -12,9 +12,8 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
-import base64
-
 import jsonschema
+from oslo_serialization import base64
 from oslo_utils import timeutils
 import six
 
@@ -46,9 +45,7 @@
     try:
         if isinstance(instance, six.text_type):
             instance = instance.encode('utf-8')
-        base64.decodestring(instance)
-    except base64.binascii.Error:
-        return False
+        base64.decode_as_bytes(instance)
     except TypeError:
         # The name must be string type. If instance isn't string type, the
         # TypeError will be raised at here.
diff --git a/tempest/lib/common/preprov_creds.py b/tempest/lib/common/preprov_creds.py
index fcdeb17..1011504 100644
--- a/tempest/lib/common/preprov_creds.py
+++ b/tempest/lib/common/preprov_creds.py
@@ -273,7 +273,7 @@
             # NOTE(andreaf) Not all fields may be available on all credentials
             # so defaulting to None for that case.
             if all([getattr(creds, k, None) == hash_attributes.get(k, None) for
-                   k in init_attributes]):
+                    k in init_attributes]):
                 return _hash
         raise AttributeError('Invalid credentials %s' % creds)
 
diff --git a/tempest/lib/common/profiler.py b/tempest/lib/common/profiler.py
new file mode 100644
index 0000000..1544337
--- /dev/null
+++ b/tempest/lib/common/profiler.py
@@ -0,0 +1,64 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import base64
+import hashlib
+import hmac
+import json
+
+from oslo_utils import encodeutils
+from oslo_utils import uuidutils
+
+_profiler = {}
+
+
+def enable(profiler_key, trace_id=None):
+    """Enable global profiler instance
+
+    :param profiler_key: the secret key used to enable profiling in services
+    :param trace_id: unique id of the trace, if empty the id is generated
+        automatically
+    """
+    _profiler['key'] = profiler_key
+    _profiler['uuid'] = trace_id or uuidutils.generate_uuid()
+
+
+def disable():
+    """Disable global profiler instance"""
+    _profiler.clear()
+
+
+def serialize_as_http_headers():
+    """Serialize profiler state as HTTP headers
+
+    This function corresponds to the one from osprofiler library.
+    :return: dictionary with 2 keys `X-Trace-Info` and `X-Trace-HMAC`.
+    """
+    p = _profiler
+    if not p:  # profiler is not enabled
+        return {}
+
+    info = {'base_id': p['uuid'], 'parent_id': p['uuid']}
+    trace_info = base64.urlsafe_b64encode(
+        encodeutils.to_utf8(json.dumps(info)))
+    trace_hmac = _sign(trace_info, p['key'])
+
+    return {
+        'X-Trace-Info': trace_info,
+        'X-Trace-HMAC': trace_hmac,
+    }
+
+
+def _sign(trace_info, key):
+    h = hmac.new(encodeutils.to_utf8(key), digestmod=hashlib.sha1)
+    h.update(trace_info)
+    return h.hexdigest()
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index ec46caf..f076727 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -27,6 +27,7 @@
 
 from tempest.lib.common import http
 from tempest.lib.common import jsonschema_validator
+from tempest.lib.common import profiler
 from tempest.lib.common.utils import test_utils
 from tempest.lib import exceptions
 
@@ -131,8 +132,10 @@
             accept_type = 'json'
         if send_type is None:
             send_type = 'json'
-        return {'Content-Type': 'application/%s' % send_type,
-                'Accept': 'application/%s' % accept_type}
+        headers = {'Content-Type': 'application/%s' % send_type,
+                   'Accept': 'application/%s' % accept_type}
+        headers.update(profiler.serialize_as_http_headers())
+        return headers
 
     def __str__(self):
         STRING_LIMIT = 80
@@ -282,7 +285,7 @@
     def get(self, url, headers=None, extra_headers=False):
         """Send a HTTP GET request using keystone service catalog and auth
 
-        :param str url: the relative url to send the post request to
+        :param str url: the relative url to send the get request to
         :param dict headers: The headers to use for the request
         :param bool extra_headers: Boolean value than indicates if the headers
                                    returned by the get_headers() method are to
@@ -297,7 +300,7 @@
     def delete(self, url, headers=None, body=None, extra_headers=False):
         """Send a HTTP DELETE request using keystone service catalog and auth
 
-        :param str url: the relative url to send the post request to
+        :param str url: the relative url to send the delete request to
         :param dict headers: The headers to use for the request
         :param dict body: the request body
         :param bool extra_headers: Boolean value than indicates if the headers
@@ -313,7 +316,7 @@
     def patch(self, url, body, headers=None, extra_headers=False):
         """Send a HTTP PATCH request using keystone service catalog and auth
 
-        :param str url: the relative url to send the post request to
+        :param str url: the relative url to send the patch request to
         :param dict body: the request body
         :param dict headers: The headers to use for the request
         :param bool extra_headers: Boolean value than indicates if the headers
@@ -329,7 +332,7 @@
     def put(self, url, body, headers=None, extra_headers=False, chunked=False):
         """Send a HTTP PUT request using keystone service catalog and auth
 
-        :param str url: the relative url to send the post request to
+        :param str url: the relative url to send the put request to
         :param dict body: the request body
         :param dict headers: The headers to use for the request
         :param bool extra_headers: Boolean value than indicates if the headers
@@ -346,7 +349,7 @@
     def head(self, url, headers=None, extra_headers=False):
         """Send a HTTP HEAD request using keystone service catalog and auth
 
-        :param str url: the relative url to send the post request to
+        :param str url: the relative url to send the head request to
         :param dict headers: The headers to use for the request
         :param bool extra_headers: Boolean value than indicates if the headers
                                    returned by the get_headers() method are to
@@ -361,7 +364,7 @@
     def copy(self, url, headers=None, extra_headers=False):
         """Send a HTTP COPY request using keystone service catalog and auth
 
-        :param str url: the relative url to send the post request to
+        :param str url: the relative url to send the copy request to
         :param dict headers: The headers to use for the request
         :param bool extra_headers: Boolean value than indicates if the headers
                                    returned by the get_headers() method are to
@@ -374,7 +377,7 @@
         return self.request('COPY', url, extra_headers, headers)
 
     def get_versions(self):
-        """Get the versions on a endpoint from the keystone catalog
+        """Get the versions on an endpoint from the keystone catalog
 
         This method will make a GET request on the baseurl from the keystone
         catalog to return a list of API versions. It is expected that a GET
@@ -526,7 +529,7 @@
         if (resp.status == 205 and
             0 != len(set(resp.keys()) - set(('status',)) -
                      self.response_header_lc - self.general_header_lc)):
-                        raise exceptions.ResponseWithEntity()
+            raise exceptions.ResponseWithEntity()
         # NOTE(afazekas)
         # Now the swift sometimes (delete not empty container)
         # returns with non json error response, we can create new rest class
diff --git a/tempest/lib/common/utils/data_utils.py b/tempest/lib/common/utils/data_utils.py
index c5df590..7f94612 100644
--- a/tempest/lib/common/utils/data_utils.py
+++ b/tempest/lib/common/utils/data_utils.py
@@ -50,8 +50,7 @@
              (e.g. 'prefixfoo-namebar-154876201')
     :rtype: string
     """
-    randbits = str(random.randint(1, 0x7fffffff))
-    rand_name = randbits
+    rand_name = str(random.randint(1, 0x7fffffff))
     if name:
         rand_name = name + '-' + rand_name
     if prefix:
@@ -65,9 +64,9 @@
     :param int length: The length of password that you expect to set
                        (If it's smaller than 3, it's same as 3.)
     :return: a random password. The format is
-             '<random upper letter>-<random number>-<random special character>
-              -<random ascii letters or digit characters or special symbols>'
-             (e.g. 'G2*ac8&lKFFgh%2')
+        ``'<random upper letter>-<random number>-<random special character>
+        -<random ascii letters or digit characters or special symbols>'``
+        (e.g. ``G2*ac8&lKFFgh%2``)
     :rtype: string
     """
     upper = random.choice(string.ascii_uppercase)
@@ -171,7 +170,7 @@
     :rtype: string
     """
     return b''.join([six.int2byte(random.randint(0, 255))
-                    for i in range(size)])
+                     for i in range(size)])
 
 
 # Courtesy of http://stackoverflow.com/a/312464
diff --git a/tempest/lib/common/utils/misc.py b/tempest/lib/common/utils/misc.py
index 2b0fcd5..a0b0c0a 100644
--- a/tempest/lib/common/utils/misc.py
+++ b/tempest/lib/common/utils/misc.py
@@ -12,9 +12,6 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
-from oslo_log import log as logging
-
-LOG = logging.getLogger(__name__)
 
 
 def singleton(cls):
diff --git a/tempest/lib/decorators.py b/tempest/lib/decorators.py
index b399aa0..4064401 100644
--- a/tempest/lib/decorators.py
+++ b/tempest/lib/decorators.py
@@ -136,10 +136,17 @@
 
     This decorator applies the testtools.testcase.attr if it is in the list of
     attributes to testtools we want to apply.
+
+    :param condition: Optional condition which if true will apply the attr. If
+        a condition is specified which is false the attr will not be applied to
+        the test function. If not specified, the attr is always applied.
     """
 
     def decorator(f):
-        if 'type' in kwargs and isinstance(kwargs['type'], str):
+        # Check to see if the attr should be conditional applied.
+        if 'condition' in kwargs and not kwargs.get('condition'):
+            return f
+        if 'type' in kwargs and isinstance(kwargs['type'], six.string_types):
             f = testtools.testcase.attr(kwargs['type'])(f)
         elif 'type' in kwargs and isinstance(kwargs['type'], list):
             for attr in kwargs['type']:
diff --git a/tempest/lib/services/clients.py b/tempest/lib/services/clients.py
index 833cfd6..90debd9 100644
--- a/tempest/lib/services/clients.py
+++ b/tempest/lib/services/clients.py
@@ -18,7 +18,6 @@
 import importlib
 import inspect
 import sys
-import warnings
 
 from debtcollector import removals
 from oslo_log import log as logging
@@ -32,9 +31,9 @@
 from tempest.lib.services import image
 from tempest.lib.services import network
 from tempest.lib.services import object_storage
+from tempest.lib.services import placement
 from tempest.lib.services import volume
 
-warnings.simplefilter("once")
 LOG = logging.getLogger(__name__)
 
 
@@ -46,6 +45,7 @@
     """
     return {
         'compute': compute,
+        'placement': placement,
         'identity.v2': identity.v2,
         'identity.v3': identity.v3,
         'image.v1': image.v1,
diff --git a/tempest/lib/services/compute/flavors_client.py b/tempest/lib/services/compute/flavors_client.py
index 2fad0a4..5d2dd46 100644
--- a/tempest/lib/services/compute/flavors_client.py
+++ b/tempest/lib/services/compute/flavors_client.py
@@ -172,7 +172,7 @@
         https://developer.openstack.org/api-ref/compute/#show-an-extra-spec-for-a-flavor
         """
         resp, body = self.get('flavors/%s/os-extra_specs/%s' % (flavor_id,
-                              key))
+                                                                key))
         body = json.loads(body)
         self.validate_response(
             schema_extra_specs.set_get_flavor_extra_specs_key,
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index 9eed4b3..18e08cc 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -636,7 +636,7 @@
     def list_virtual_interfaces(self, server_id):
         """List the virtual interfaces used in an instance."""
         resp, body = self.get('/'.join(['servers', server_id,
-                              'os-virtual-interfaces']))
+                                        'os-virtual-interfaces']))
         body = json.loads(body)
         self.validate_response(schema.list_virtual_interfaces, resp, body)
         return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/inherited_roles_client.py b/tempest/lib/services/identity/v3/inherited_roles_client.py
index 691c7fd..3949437 100644
--- a/tempest/lib/services/identity/v3/inherited_roles_client.py
+++ b/tempest/lib/services/identity/v3/inherited_roles_client.py
@@ -114,8 +114,7 @@
 
     def check_user_has_flag_on_inherited_to_project(
             self, project_id, user_id, role_id):
-        """Checks whether a user has a role assignment"""
-        """with the inherited_to_projects flag on a project."""
+        """Check if user has an inherited project role on project"""
         resp, body = self.head(
             "OS-INHERIT/projects/%s/users/%s/roles/%s/inherited_to_projects"
             % (project_id, user_id, role_id))
@@ -142,8 +141,7 @@
 
     def check_group_has_flag_on_inherited_to_project(
             self, project_id, group_id, role_id):
-        """Checks whether a group has a role assignment"""
-        """with the inherited_to_projects flag on a project."""
+        """Check if group has an inherited project role on project"""
         resp, body = self.head(
             "OS-INHERIT/projects/%s/groups/%s/roles/%s/inherited_to_projects"
             % (project_id, group_id, role_id))
diff --git a/tempest/lib/services/identity/v3/oauth_token_client.py b/tempest/lib/services/identity/v3/oauth_token_client.py
index b1d298b..94da043 100644
--- a/tempest/lib/services/identity/v3/oauth_token_client.py
+++ b/tempest/lib/services/identity/v3/oauth_token_client.py
@@ -74,6 +74,7 @@
         scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
         scheme = scheme.lower()
         netloc = netloc.lower()
+        path = path.replace('//', '/')
         normalized_uri = urlparse.urlunparse((scheme, netloc, path,
                                               params, '', ''))
 
diff --git a/tempest/lib/services/placement/__init__.py b/tempest/lib/services/placement/__init__.py
new file mode 100644
index 0000000..5c20c57
--- /dev/null
+++ b/tempest/lib/services/placement/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2019 Ericsson
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib.services.placement.placement_client import \
+    PlacementClient
+
+__all__ = ['PlacementClient']
diff --git a/tempest/lib/services/placement/base_placement_client.py b/tempest/lib/services/placement/base_placement_client.py
new file mode 100644
index 0000000..505a515
--- /dev/null
+++ b/tempest/lib/services/placement/base_placement_client.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2019 Ericsson
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib.common import api_version_utils
+from tempest.lib.common import rest_client
+
+PLACEMENT_MICROVERSION = None
+
+
+class BasePlacementClient(rest_client.RestClient):
+
+    api_microversion_header_name = 'OpenStack-API-Version'
+    version_header_value = 'placement %s'
+
+    def get_headers(self):
+        headers = super(BasePlacementClient, self).get_headers()
+        if PLACEMENT_MICROVERSION:
+            headers[self.api_microversion_header_name] = \
+                self.version_header_value % PLACEMENT_MICROVERSION
+        return headers
+
+    def request(self, method, url, extra_headers=False, headers=None,
+                body=None, chunked=False):
+        resp, resp_body = super(BasePlacementClient, self).request(
+            method, url, extra_headers, headers, body, chunked)
+        if (PLACEMENT_MICROVERSION and
+            PLACEMENT_MICROVERSION != api_version_utils.LATEST_MICROVERSION):
+            api_version_utils.assert_version_header_matches_request(
+                self.api_microversion_header_name,
+                self.version_header_value % PLACEMENT_MICROVERSION,
+                resp)
+        return resp, resp_body
diff --git a/tempest/lib/services/placement/placement_client.py b/tempest/lib/services/placement/placement_client.py
new file mode 100644
index 0000000..2c6d919
--- /dev/null
+++ b/tempest/lib/services/placement/placement_client.py
@@ -0,0 +1,50 @@
+# Copyright (c) 2019 Ericsson
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+from tempest.lib.services.placement import base_placement_client
+
+
+class PlacementClient(base_placement_client.BasePlacementClient):
+
+    def list_allocation_candidates(self, **params):
+        """List allocation candidates.
+
+        For full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/placement/#list-allocation-candidates
+        """
+        url = '/allocation_candidates'
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+        resp, body = self.get(url)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_allocations(self, consumer_uuid):
+        """List all allocation records for the consumer.
+
+        For full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/placement/#list-allocations
+        """
+        url = '/allocations/%s' % consumer_uuid
+        resp, body = self.get(url)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v1/types_client.py b/tempest/lib/services/volume/v1/types_client.py
index 58a80b7..da9eb8b 100644
--- a/tempest/lib/services/volume/v1/types_client.py
+++ b/tempest/lib/services/volume/v1/types_client.py
@@ -149,10 +149,11 @@
                                        extra_specs):
         """Update a volume_type extra spec.
 
-        volume_type_id: Id of volume_type.
-        extra_spec_name: Name of the extra spec to be updated.
-        extra_spec: A dictionary of with key as extra_spec_name and the
-                     updated value.
+        :param volume_type_id: Id of volume_type.
+        :param extra_spec_name: Name of the extra spec to be updated.
+        :param extra_specs: A dictionary of with key as extra_spec_name and the
+                            updated value.
+
         For a full list of available parameters, please refer to the official
         API reference:
         https://developer.openstack.org/api-ref/block-storage/v2/#update-extra-specs-for-a-volume-type
diff --git a/tempest/lib/services/volume/v3/backups_client.py b/tempest/lib/services/volume/v3/backups_client.py
index f2d2d21..fb64333 100644
--- a/tempest/lib/services/volume/v3/backups_client.py
+++ b/tempest/lib/services/volume/v3/backups_client.py
@@ -104,7 +104,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def import_backup(self, **kwargs):
-        """Import backup metadata record."""
+        """Import backup metadata record.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/index.html#import-a-backup
+        """
         post_body = json.dumps({'backup-record': kwargs})
         resp, body = self.post("backups/import_record", post_body)
         body = json.loads(body)
diff --git a/tempest/lib/services/volume/v3/types_client.py b/tempest/lib/services/volume/v3/types_client.py
index 13ecd15..1405785 100644
--- a/tempest/lib/services/volume/v3/types_client.py
+++ b/tempest/lib/services/volume/v3/types_client.py
@@ -153,6 +153,7 @@
         :param extra_spec_name: Name of the extra spec to be updated.
         :param extra_specs: A dictionary of with key as extra_spec_name and the
                             updated value.
+
         For a full list of available parameters, please refer to the official
         API reference:
         https://developer.openstack.org/api-ref/block-storage/v3/index.html#update-extra-specification-for-volume-type
diff --git a/tempest/lib/services/volume/v3/volumes_client.py b/tempest/lib/services/volume/v3/volumes_client.py
index 11c5767..fec2950 100644
--- a/tempest/lib/services/volume/v3/volumes_client.py
+++ b/tempest/lib/services/volume/v3/volumes_client.py
@@ -182,7 +182,7 @@
 
         :param id: A checked resource id
         :raises lib_exc.DeleteErrorException: If the specified resource is on
-        the status the delete was failed.
+            the status the delete was failed.
         """
         try:
             volume = self.show_volume(id)
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 00d45cd..d2fd021 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -125,6 +125,27 @@
         returns a test server. The purpose of this wrapper is to minimize
         the impact on the code of the tests already using this
         function.
+
+        :param **kwargs:
+            See extra parameters below
+
+        :Keyword Arguments:
+            * *vnic_type* (``string``) --
+              used when launching instances with pre-configured ports.
+              Examples:
+                normal: a traditional virtual port that is either attached
+                        to a linux bridge or an openvswitch bridge on a
+                        compute node.
+                direct: an SR-IOV port that is directly attached to a VM
+                macvtap: an SR-IOV port that is attached to a VM via a macvtap
+                         device.
+              Defaults to ``CONF.network.port_vnic_type``.
+            * *port_profile* (``dict``) --
+              This attribute is a dictionary that can be used (with admin
+              credentials) to supply information influencing the binding of
+              the port.
+              example: port_profile = "capabilities:[switchdev]"
+              Defaults to ``CONF.network.port_profile``.
         """
 
         # NOTE(jlanoux): As a first step, ssh checks in the scenario
@@ -143,8 +164,8 @@
         if name is None:
             name = data_utils.rand_name(self.__class__.__name__ + "-server")
 
-        vnic_type = CONF.network.port_vnic_type
-        profile = CONF.network.port_profile
+        vnic_type = kwargs.pop('vnic_type', CONF.network.port_vnic_type)
+        profile = kwargs.pop('port_profile', CONF.network.port_profile)
 
         # If vnic_type or profile are configured create port for
         # every network
@@ -166,7 +187,7 @@
                         clients.security_groups_client.list_security_groups(
                         ).get('security_groups')
                     sec_dict = dict([(s['name'], s['id'])
-                                    for s in security_groups])
+                                     for s in security_groups])
 
                     sec_groups_names = [s['name'] for s in kwargs.pop(
                         'security_groups')]
@@ -378,12 +399,12 @@
                           server=None):
         """Get a SSH client to a remote server
 
-        @param ip_address the server floating or fixed IP address to use
-                          for ssh validation
-        @param username name of the Linux account on the remote server
-        @param private_key the SSH private key to use
-        @param server: server dict, used for debugging purposes
-        @return a RemoteClient object
+        :param ip_address: the server floating or fixed IP address to use
+                           for ssh validation
+        :param username: name of the Linux account on the remote server
+        :param private_key: the SSH private key to use
+        :param server: server dict, used for debugging purposes
+        :return: a RemoteClient object
         """
 
         if username is None:
diff --git a/tempest/scenario/test_encrypted_cinder_volumes.py b/tempest/scenario/test_encrypted_cinder_volumes.py
index 8c210d5..008d1ae 100644
--- a/tempest/scenario/test_encrypted_cinder_volumes.py
+++ b/tempest/scenario/test_encrypted_cinder_volumes.py
@@ -29,11 +29,12 @@
 
     For both LUKS and cryptsetup encryption types, this test performs
     the following:
-        * Creates an image in Glance
-        * Boots an instance from the image
-        * Creates an encryption type (as admin)
-        * Creates a volume of that encryption type (as a regular user)
-        * Attaches and detaches the encrypted volume to the instance
+
+    * Creates an image in Glance
+    * Boots an instance from the image
+    * Creates an encryption type (as admin)
+    * Creates a volume of that encryption type (as a regular user)
+    * Attaches and detaches the encrypted volume to the instance
     """
 
     @classmethod
diff --git a/tempest/scenario/test_minimum_basic.py b/tempest/scenario/test_minimum_basic.py
index 2b35e45..cee543b 100644
--- a/tempest/scenario/test_minimum_basic.py
+++ b/tempest/scenario/test_minimum_basic.py
@@ -48,6 +48,7 @@
     10. Check SSH connection to instance after reboot
 
     """
+
     def nova_show(self, server):
         got_server = (self.servers_client.show_server(server['id'])
                       ['server'])
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index e94ce3d..37bcd04 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -119,6 +119,7 @@
             server, keypair, floating_ip)
 
     @decorators.idempotent_id('7b6860c2-afa3-4846-9522-adeb38dfbe08')
+    @decorators.attr(type='slow')
     @utils.services('compute', 'network')
     def test_server_connectivity_reboot(self):
         keypair = self.create_keypair()
@@ -230,6 +231,32 @@
 
         self.assertNotEqual(src_host, dst_host)
 
+    @decorators.idempotent_id('03fd1562-faad-11e7-9ea0-fa163e65f5ce')
+    @testtools.skipUnless(CONF.compute_feature_enabled.live_migration,
+                          'Live migration is not available.')
+    @testtools.skipUnless(CONF.compute.min_compute_nodes > 1,
+                          'Less than 2 compute nodes, skipping multinode '
+                          'tests.')
+    @decorators.attr(type='slow')
+    @utils.services('compute', 'network')
+    def test_server_connectivity_live_migration(self):
+        keypair = self.create_keypair()
+        server = self._setup_server(keypair)
+        floating_ip = self._setup_network(server, keypair)
+        self._wait_server_status_and_check_network_connectivity(
+            server, keypair, floating_ip)
+
+        block_migration = (CONF.compute_feature_enabled.
+                           block_migration_for_live_migration)
+        self.admin_servers_client.live_migrate_server(
+            server['id'], host=None, block_migration=block_migration,
+            disk_over_commit=False)
+        waiters.wait_for_server_status(self.servers_client,
+                                       server['id'], 'ACTIVE')
+
+        self._wait_server_status_and_check_network_connectivity(
+            server, keypair, floating_ip)
+
     @decorators.skip_because(bug='1788403')
     @decorators.idempotent_id('25b188d7-0183-4b1e-a11d-15840c8e2fd6')
     @testtools.skipUnless(CONF.compute_feature_enabled.cold_migration,
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index c1132cf..7992585 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -374,39 +374,37 @@
     def test_network_basic_ops(self):
         """Basic network operation test
 
-        For a freshly-booted VM with an IP address ("port") on a given
-            network:
+        For a freshly-booted VM with an IP address ("port") on a given network:
 
         - the Tempest host can ping the IP address.  This implies, but
-         does not guarantee (see the ssh check that follows), that the
-         VM has been assigned the correct IP address and has
-         connectivity to the Tempest host.
+            does not guarantee (see the ssh check that follows), that the
+            VM has been assigned the correct IP address and has
+            connectivity to the Tempest host.
 
         - the Tempest host can perform key-based authentication to an
-         ssh server hosted at the IP address.  This check guarantees
-         that the IP address is associated with the target VM.
+            ssh server hosted at the IP address.  This check guarantees
+            that the IP address is associated with the target VM.
 
         - the Tempest host can ssh into the VM via the IP address and
-         successfully execute the following:
+            successfully execute the following:
 
-         - ping an external IP address, implying external connectivity.
+            - ping an external IP address, implying external connectivity.
 
-         - ping an external hostname, implying that dns is correctly
-           configured.
+            - ping an external hostname, implying that dns is correctly
+                configured.
 
-         - ping an internal IP address, implying connectivity to another
-           VM on the same network.
+            - ping an internal IP address, implying connectivity to another
+               VM on the same network.
 
         - detach the floating-ip from the VM and verify that it becomes
-        unreachable
+            unreachable
 
         - associate detached floating ip to a new VM and verify connectivity.
-        VMs are created with unique keypair so connectivity also asserts that
-        floating IP is associated with the new VM instead of the old one
+            VMs are created with unique keypair so connectivity also asserts
+            that floating IP is associated with the new VM instead of the old
+            one
 
         Verifies that floating IP status is updated correctly after each change
-
-
         """
         self._setup_network_and_servers()
         self._check_public_network_connectivity(should_connect=True)
@@ -445,30 +443,25 @@
     def test_connectivity_between_vms_on_different_networks(self):
         """Test connectivity between VMs on different networks
 
-        For a freshly-booted VM with an IP address ("port") on a given
-            network:
+        For a freshly-booted VM with an IP address ("port") on a given network:
 
         - the Tempest host can ping the IP address.
-
         - the Tempest host can ssh into the VM via the IP address and
-         successfully execute the following:
+            successfully execute the following:
 
-         - ping an external IP address, implying external connectivity.
-
-         - ping an external hostname, implying that dns is correctly
-           configured.
-
-         - ping an internal IP address, implying connectivity to another
-           VM on the same network.
+            - ping an external IP address, implying external connectivity.
+            - ping an external hostname, implying that dns is correctly
+               configured.
+            - ping an internal IP address, implying connectivity to another
+               VM on the same network.
 
         - Create another network on the same tenant with subnet, create
-        an VM on the new network.
+            an VM on the new network.
 
-         - Ping the new VM from previous VM failed since the new network
-         was not attached to router yet.
-
-         - Attach the new network to the router, Ping the new VM from
-         previous VM succeed.
+            - Ping the new VM from previous VM failed since the new network
+                was not attached to router yet.
+            - Attach the new network to the router, Ping the new VM from
+                previous VM succeed.
 
         """
         self._setup_network_and_servers()
@@ -476,9 +469,14 @@
         self._check_network_internal_connectivity(network=self.network)
         self._check_network_external_connectivity()
         self._create_new_network(create_gateway=True)
-        self._create_server(self.new_net)
-        self._check_network_internal_connectivity(network=self.new_net,
-                                                  should_connect=False)
+        new_server = self._create_server(self.new_net)
+        new_server_ips = [addr['addr'] for addr in
+                          new_server['addresses'][self.new_net['name']]]
+
+        # Assert that pinging the new VM fails since the new network is not
+        # connected to a router
+        self._check_server_connectivity(self.floating_ip_tuple.floating_ip,
+                                        new_server_ips, should_connect=False)
         router_id = self.router['id']
         self.routers_client.add_router_interface(
             router_id, subnet_id=self.new_subnet['id'])
@@ -486,8 +484,9 @@
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
                         self.routers_client.remove_router_interface,
                         router_id, subnet_id=self.new_subnet['id'])
-        self._check_network_internal_connectivity(network=self.new_net,
-                                                  should_connect=True)
+
+        self._check_server_connectivity(self.floating_ip_tuple.floating_ip,
+                                        new_server_ips, should_connect=True)
 
     @decorators.idempotent_id('c5adff73-e961-41f1-b4a9-343614f18cfa')
     @testtools.skipUnless(CONF.compute_feature_enabled.interface_attach,
@@ -555,34 +554,42 @@
     def test_subnet_details(self):
         """Tests that subnet's extra configuration details are affecting VMs.
 
-         This test relies on non-shared, isolated tenant networks.
+        This test relies on non-shared, isolated tenant networks.
 
-         NOTE: Neutron subnets push data to servers via dhcp-agent, so any
-         update in subnet requires server to actively renew its DHCP lease.
+        NOTE: Neutron subnets push data to servers via dhcp-agent, so any
+        update in subnet requires server to actively renew its DHCP lease.
 
-         1. Configure subnet with dns nameserver
-         2. retrieve the VM's configured dns and verify it matches the one
-         configured for the subnet.
-         3. update subnet's dns
-         4. retrieve the VM's configured dns and verify it matches the new one
-         configured for the subnet.
+        1. Configure subnet with dns nameserver
+        2. retrieve the VM's configured dns and verify it matches the one
+           configured for the subnet.
+        3. update subnet's dns
+        4. retrieve the VM's configured dns and verify it matches the new one
+           configured for the subnet.
 
-         TODO(yfried): add host_routes
+        TODO(yfried): add host_routes
 
-         any resolution check would be testing either:
-            * l3 forwarding (tested in test_network_basic_ops)
-            * Name resolution of an external DNS nameserver - out of scope for
-            Tempest
+        any resolution check would be testing either:
+
+        * l3 forwarding (tested in test_network_basic_ops)
+        * Name resolution of an external DNS nameserver - out of scope for
+          Tempest
         """
         # this test check only updates (no actual resolution) so using
         # arbitrary ip addresses as nameservers, instead of parsing CONF
         initial_dns_server = '1.2.3.4'
         alt_dns_server = '9.8.7.6'
 
-        # renewal should be immediate.
-        # Timeouts are suggested by salvatore-orlando in
+        # Original timeouts are suggested by salvatore-orlando in
         # https://bugs.launchpad.net/neutron/+bug/1412325/comments/3
-        renew_delay = CONF.network.build_interval
+        #
+        # Compared to that renew_delay was increased, because
+        # busybox's udhcpc accepts SIGUSR1 as a renew request. Internally
+        # it goes into RENEW_REQUESTED state. If it receives a 2nd SIGUSR1
+        # signal while in that state then it calls the deconfig script
+        # ("/sbin/cirros-dhcpc deconfig" in sufficiently new cirros versions)
+        # which leads to the address being transiently deconfigured which
+        # for our case is unwanted.
+        renew_delay = 3 * CONF.network.build_interval
         renew_timeout = CONF.network.build_timeout
 
         self._setup_network_and_servers(dns_nameservers=[initial_dns_server])
@@ -742,7 +749,7 @@
         2. Remove router from all l3-agents
         3. Verify connectivity is down
         4. Assign router to new l3-agent (or old one if no new agent is
-         available)
+           available)
         5. Verify connectivity
         """
 
@@ -823,7 +830,8 @@
         prevents traffic to pass through the VM. Anti-spoof rules are not
         required in cases where the VM routes traffic through it.
 
-        The test steps are :
+        The test steps are:
+
         1. Create a new network.
         2. Connect (hotplug) the VM to a new network.
         3. Check the VM can ping a server on the new network ("peer")
@@ -832,7 +840,7 @@
            spoofed interface (VM cannot ping the peer).
         6. Disable port-security of the spoofed port- set the flag to false.
         7. Retest 3rd step and check that the Security Group allows pings via
-        the spoofed interface.
+           the spoofed interface.
         """
 
         spoof_mac = "00:00:00:00:00:01"
diff --git a/tempest/scenario/test_network_v6.py b/tempest/scenario/test_network_v6.py
index 57a560c..438ee01 100644
--- a/tempest/scenario/test_network_v6.py
+++ b/tempest/scenario/test_network_v6.py
@@ -280,6 +280,7 @@
                                dualnet=True)
 
     @decorators.idempotent_id('9178ad42-10e4-47e9-8987-e02b170cc5cd')
+    @decorators.attr(type='slow')
     @utils.services('compute', 'network')
     def test_dualnet_multi_prefix_slaac(self):
         self._prepare_and_test(address6_mode='slaac', n_subnets6=2,
diff --git a/tempest/scenario/test_object_storage_basic_ops.py b/tempest/scenario/test_object_storage_basic_ops.py
index cbe321e..b635ca0 100644
--- a/tempest/scenario/test_object_storage_basic_ops.py
+++ b/tempest/scenario/test_object_storage_basic_ops.py
@@ -24,15 +24,15 @@
     def test_swift_basic_ops(self):
         """Test swift basic ops.
 
-         * get swift stat.
-         * create container.
-         * upload a file to the created container.
-         * list container's objects and assure that the uploaded file is
-         present.
-         * download the object and check the content
-         * delete object from container.
-         * list container's objects and assure that the deleted file is gone.
-         * delete a container.
+        * get swift stat.
+        * create container.
+        * upload a file to the created container.
+        * list container's objects and assure that the uploaded file is
+          present.
+        * download the object and check the content
+        * delete object from container.
+        * list container's objects and assure that the deleted file is gone.
+        * delete a container.
         """
         self.get_swift_stat()
         container_name = self.create_container()
diff --git a/tempest/scenario/test_security_groups_basic_ops.py b/tempest/scenario/test_security_groups_basic_ops.py
index 28a2d64..2b7926a 100644
--- a/tempest/scenario/test_security_groups_basic_ops.py
+++ b/tempest/scenario/test_security_groups_basic_ops.py
@@ -63,28 +63,28 @@
                 a. a security group open to incoming ssh connection
                 b. a VM with a floating ip
             5. create a general empty security group (same as "default", but
-            without rules allowing in-tenant traffic)
+               without rules allowing in-tenant traffic)
 
     tests:
         1. _verify_network_details
         2. _verify_mac_addr: for each access point verify that
-        (subnet, fix_ip, mac address) are as defined in the port list
+           (subnet, fix_ip, mac address) are as defined in the port list
         3. _test_in_tenant_block: test that in-tenant traffic is disabled
-        without rules allowing it
+           without rules allowing it
         4. _test_in_tenant_allow: test that in-tenant traffic is enabled
-        once an appropriate rule has been created
+           once an appropriate rule has been created
         5. _test_cross_tenant_block: test that cross-tenant traffic is disabled
-        without a rule allowing it on destination tenant
+           without a rule allowing it on destination tenant
         6. _test_cross_tenant_allow:
             * test that cross-tenant traffic is enabled once an appropriate
-            rule has been created on destination tenant.
+              rule has been created on destination tenant.
             * test that reverse traffic is still blocked
             * test than reverse traffic is enabled once an appropriate rule has
-            been created on source tenant
-        7._test_port_update_new_security_group:
-           * test that traffic is blocked with default security group
-           * test that traffic is enabled after updating port with new security
-           group having appropriate rule
+              been created on source tenant
+        7. _test_port_update_new_security_group:
+            * test that traffic is blocked with default security group
+            * test that traffic is enabled after updating port with new
+              security group having appropriate rule
         8. _test_multiple_security_groups: test multiple security groups can be
            associated with the vm
 
@@ -93,11 +93,13 @@
         2. Public network is defined and reachable from the Tempest host
         3. Public router can either be:
             * defined, in which case all tenants networks can connect directly
-            to it, and cross tenant check will be done on the private IP of the
-            destination tenant
+              to it, and cross tenant check will be done on the private IP of
+              the destination tenant
+
             or
+
             * not defined (empty string), in which case each tenant will have
-            its own router connected to the public network
+              its own router connected to the public network
     """
 
     credentials = ['primary', 'alt', 'admin']
diff --git a/tempest/scenario/test_server_basic_ops.py b/tempest/scenario/test_server_basic_ops.py
index 1671216..02bc692 100644
--- a/tempest/scenario/test_server_basic_ops.py
+++ b/tempest/scenario/test_server_basic_ops.py
@@ -13,7 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import json
+from oslo_serialization import jsonutils as json
 
 from tempest.common import utils
 from tempest.common import waiters
diff --git a/tempest/scenario/test_volume_boot_pattern.py b/tempest/scenario/test_volume_boot_pattern.py
index 810480b..6ed7e30 100644
--- a/tempest/scenario/test_volume_boot_pattern.py
+++ b/tempest/scenario/test_volume_boot_pattern.py
@@ -11,7 +11,7 @@
 #    under the License.
 
 from oslo_log import log as logging
-from oslo_serialization import jsonutils
+from oslo_serialization import jsonutils as json
 import testtools
 
 from tempest.common import utils
@@ -76,6 +76,7 @@
         self.snapshots_client.wait_for_resource_deletion(snapshot_id)
 
     @decorators.idempotent_id('557cd2c2-4eb8-4dce-98be-f86765ff311b')
+    @decorators.attr(type='slow')
     # Note: This test is being skipped based on 'public_network_id'.
     # It is being used in create_floating_ip() method which gets called
     # from get_server_ip() method
@@ -85,7 +86,6 @@
                           'Cinder volume snapshots are disabled')
     @utils.services('compute', 'volume', 'image')
     def test_volume_boot_pattern(self):
-
         """This test case attempts to reproduce the following steps:
 
         * Create in Cinder some bootable volume importing a Glance image
@@ -264,7 +264,7 @@
         bdms = image.get('block_device_mapping')
         if not bdms:
             bdms = image['properties']['block_device_mapping']
-        bdms = jsonutils.loads(bdms)
+        bdms = json.loads(bdms)
         snapshot_id = bdms[0]['snapshot_id']
         self._delete_snapshot(snapshot_id)
 
diff --git a/tempest/test.py b/tempest/test.py
index f2babbb..85000b6 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -28,6 +28,7 @@
 from tempest.common import utils
 from tempest import config
 from tempest.lib.common import fixed_network
+from tempest.lib.common import profiler
 from tempest.lib.common import validation_resources as vr
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
@@ -231,6 +232,9 @@
         if CONF.pause_teardown:
             BaseTestCase.insert_pdb_breakpoint()
 
+        if CONF.profiler.key:
+            profiler.disable()
+
     @classmethod
     def insert_pdb_breakpoint(cls):
         """Add pdb breakpoint.
@@ -259,6 +263,7 @@
         based on the result of an API call are discouraged.
 
         The following checks are implemented in `test.py` already:
+
         - check that alt credentials are available when requested by the test
         - check that admin credentials are available when requested by the test
         - check that the identity version specified by the test is marked as
@@ -310,6 +315,7 @@
         `os_[type]`:
 
         Valid values in `credentials` are:
+
         - 'primary':
             A normal user is provisioned.
             It can be used only once. Multiple entries will be ignored.
@@ -581,7 +587,7 @@
     def setUp(self):
         super(BaseTestCase, self).setUp()
         if not self.__setupclass_called:
-            raise RuntimeError("setUpClass does not calls the super's"
+            raise RuntimeError("setUpClass does not calls the super's "
                                "setUpClass in the " +
                                self.__class__.__name__)
         at_exit_set.add(self.__class__)
@@ -606,6 +612,8 @@
             self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
                                                    format=self.log_format,
                                                    level=None))
+        if CONF.profiler.key:
+            profiler.enable(CONF.profiler.key)
 
     @property
     def credentials_provider(self):
diff --git a/tempest/test_discover/plugins.py b/tempest/test_discover/plugins.py
index 9c18052..7a037eb 100644
--- a/tempest/test_discover/plugins.py
+++ b/tempest/test_discover/plugins.py
@@ -179,6 +179,7 @@
     This class is used to manage the lifecycle of external tempest test
     plugins. It provides functions for getting set
     """
+
     def __init__(self):
         self.ext_plugins = stevedore.ExtensionManager(
             'tempest.test_plugins', invoke_on_load=True,
diff --git a/tempest/test_discover/test_discover.py b/tempest/test_discover/test_discover.py
index 330f370..143c6e1 100644
--- a/tempest/test_discover/test_discover.py
+++ b/tempest/test_discover/test_discover.py
@@ -37,7 +37,7 @@
                                            top_level_dir=base_path))
         else:
             suite.addTests(loader.discover(full_test_dir, pattern=pattern,
-                           top_level_dir=base_path))
+                                           top_level_dir=base_path))
 
     plugin_load_tests = ext_plugins.get_plugin_load_tests_tuple()
     if not plugin_load_tests:
diff --git a/tempest/tests/cmd/test_account_generator.py b/tempest/tests/cmd/test_account_generator.py
index a1d3a40..b349bba 100644
--- a/tempest/tests/cmd/test_account_generator.py
+++ b/tempest/tests/cmd/test_account_generator.py
@@ -106,6 +106,8 @@
         cp = account_generator.get_credential_provider(self.opts)
         admin_creds = cp.default_admin_creds
         self.assertEqual(self.opts.os_tenant_name, admin_creds.tenant_name)
+        self.assertEqual(self.opts.os_username, admin_creds.username)
+        self.assertEqual(self.opts.os_password, admin_creds.password)
 
 
 class TestAccountGeneratorV3(TestAccountGeneratorV2):
@@ -222,6 +224,30 @@
             self.assertIsNotNone(resource[1].router)
             self.assertIsNotNone(resource[1].subnet)
 
+    def test_generate_resources_swift_no_admin(self):
+        cfg.CONF.set_default('swift', True, group='service_available')
+        cfg.CONF.set_default('operator_role', 'fake_operator',
+                             group='object-storage')
+        cfg.CONF.set_default('reseller_admin_role', 'fake_reseller',
+                             group='object-storage')
+        resources = account_generator.generate_resources(
+            self.cred_provider, admin=False)
+        resource_types = [k for k, _ in resources]
+        # No Admin, swift, expect four credentials only
+        self.assertEqual(4, len(resources))
+        # Ensure create_user was invoked 4 times (4 distinct users)
+        self.assertEqual(4, self.user_create_fixture.mock.call_count)
+        self.assertIn('primary', resource_types)
+        self.assertIn('alt', resource_types)
+        self.assertNotIn('admin', resource_types)
+        self.assertIn(['fake_operator'], resource_types)
+        self.assertIn(['fake_reseller'], resource_types)
+        self.assertNotIn(['fake_owner'], resource_types)
+        for resource in resources:
+            self.assertIsNotNone(resource[1].network)
+            self.assertIsNotNone(resource[1].router)
+            self.assertIsNotNone(resource[1].subnet)
+
 
 class TestGenerateResourcesV3(TestGenerateResourcesV2):
 
diff --git a/tempest/tests/cmd/test_cleanup.py b/tempest/tests/cmd/test_cleanup.py
new file mode 100644
index 0000000..b47da0b
--- /dev/null
+++ b/tempest/tests/cmd/test_cleanup.py
@@ -0,0 +1,26 @@
+# Copyright 2018 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from tempest.cmd import cleanup
+from tempest.tests import base
+
+
+class TestTempestCleanup(base.TestCase):
+
+    def test_load_json(self):
+        # instantiate "empty" TempestCleanup
+        c = cleanup.TempestCleanup(None, None, 'test')
+        test_saved_json = 'tempest/tests/cmd/test_saved_state_json.json'
+        # test if the file is loaded without any issues/exceptions
+        c._load_json(test_saved_json)
diff --git a/tempest/tests/cmd/test_cleanup_services.py b/tempest/tests/cmd/test_cleanup_services.py
index 495d127..3262b1c 100644
--- a/tempest/tests/cmd/test_cleanup_services.py
+++ b/tempest/tests/cmd/test_cleanup_services.py
@@ -25,6 +25,24 @@
 from tempest.tests.lib import fake_http
 
 
+class TestBaseService(base.TestCase):
+
+    def test_base_service_init(self):
+        kwargs = {'data': {'data': 'test'},
+                  'is_dry_run': False,
+                  'saved_state_json': {'saved': 'data'},
+                  'is_preserve': False,
+                  'is_save_state': True,
+                  'tenant_id': 'project_id'}
+        base = cleanup_service.BaseService(kwargs)
+        self.assertEqual(base.data, kwargs['data'])
+        self.assertFalse(base.is_dry_run)
+        self.assertEqual(base.saved_state_json, kwargs['saved_state_json'])
+        self.assertFalse(base.is_preserve)
+        self.assertTrue(base.is_save_state)
+        self.assertEqual(base.tenant_filter['project_id'], kwargs['tenant_id'])
+
+
 class MockFunctionsBase(base.TestCase):
 
     def _create_response(self, body, status, headers):
@@ -81,16 +99,50 @@
                             "images": cleanup_service.CONF_IMAGES[0],
                             "projects": cleanup_service.CONF_PROJECTS[0],
                             "users": cleanup_service.CONF_USERS[0],
+                            "networks": cleanup_service.CONF_PUB_NETWORK,
+                            "security_groups":
+                                cleanup_service.CONF_PROJECTS[0],
+                            "ports": cleanup_service.CONF_PUB_NETWORK,
+                            "routers": cleanup_service.CONF_PUB_ROUTER,
+                            "subnetpools": cleanup_service.CONF_PROJECTS[0],
                             }
 
-    # Static list to ensure global service saved items are not deleted
-    saved_state = {"users": {u'32rwef64245tgr20121qw324bgg': u'Lightning'},
-                   "flavors": {u'42': u'm1.tiny'},
-                   "images": {u'34yhwr-4t3q': u'stratus-0.3.2-x86_64-disk'},
-                   "roles": {u'3efrt74r45hn': u'president'},
-                   "projects": {u'f38ohgp93jj032': u'manhattan'},
-                   "domains": {u'default': u'Default'}
-                   }
+    saved_state = {
+        # Static list to ensure global service saved items are not deleted
+        "users": {u'32rwef64245tgr20121qw324bgg': u'Lightning'},
+        "flavors": {u'42': u'm1.tiny'},
+        "images": {u'34yhwr-4t3q': u'stratus-0.3.2-x86_64-disk'},
+        "roles": {u'3efrt74r45hn': u'president'},
+        "projects": {u'f38ohgp93jj032': u'manhattan'},
+        "domains": {u'default': u'Default'},
+        # Static list to ensure project service saved items are not deleted
+        "snapshots": {u'1ad4c789-7e8w-4dwg-afc5': u'saved-snapshot'},
+        "servers": {u'7a6d4v7w-36ds-4216': u'saved-server'},
+        "server_groups": {u'as6d5f7g-46ca-475e': u'saved-server-group'},
+        "keypairs": {u'saved-key-pair': {
+            u'fingerprint': u'7e:eb:ab:24',
+            u'name': u'saved-key-pair'
+        }},
+        "volumes": {u'aa77asdf-1234': u'saved-volume'},
+        "networks": {u'6722fc13-4319': {
+            u'id': u'6722fc13-4319',
+            u'name': u'saved-network'
+        }},
+        "floatingips": {u'9e82d248-408a': {
+            u'id': u'9e82d248-408a',
+            u'status': u'ACTIVE'
+        }},
+        "routers": {u'4s5w34hj-id44': u'saved-router'},
+        "metering_label_rules": {u'93a973ce-4dc5': {
+            u'direction': u'ingress',
+            u'id': u'93a973ce-4dc5'
+        }},
+        "metering_labels": {u'723b346ce866-4c7q': u'saved-label'},
+        "ports": {u'aa74aa4v-741a': u'saved-port'},
+        "security_groups": {u'7q844add-3697': u'saved-sec-group'},
+        "subnets": {u'55ttda4a-2584': u'saved-subnet'},
+        "subnetpools": {u'8acf64c1-43fc': u'saved-subnet-pool'}
+    }
     # Mocked methods
     get_method = 'tempest.lib.common.rest_client.RestClient.get'
     delete_method = 'tempest.lib.common.rest_client.RestClient.delete'
@@ -120,7 +172,9 @@
             mocked_fixture_tuple_list,
         )
         for fixture in fixtures:
-            if fail is False and fixture.mock.return_value == 'exception':
+            if fixture.mock.return_value == 'validate':
+                fixture.mock.assert_called()
+            elif fail is False and fixture.mock.return_value == 'exception':
                 fixture.mock.assert_not_called()
             elif self.service_name in self.saved_state.keys():
                 fixture.mock.assert_called_once()
@@ -172,6 +226,948 @@
             self.assertNotIn(rsp['name'], self.conf_values.values())
 
 
+class TestSnapshotService(BaseCmdServiceTests):
+
+    service_class = 'SnapshotService'
+    service_name = 'snapshots'
+    response = {
+        "snapshots": [
+            {
+                "status": "available",
+                "metadata": {
+                    "name": "test"
+                },
+                "name": "test-volume-snapshot",
+                "user_id": "40c2102f4a554b848d96b14f3eec39ed",
+                "volume_id": "173f7b48-c4c1-4e70-9acc-086b39073506",
+                "created_at": "2015-11-29T02:25:51.000000",
+                "size": 1,
+                "updated_at": "2015-11-20T05:36:40.000000",
+                "os-extended-snapshot-attributes:progress": "100%",
+                "id": "b1323cda-8e4b-41c1-afc5-2fc791809c8c",
+                "description": "volume snapshot"
+            },
+            {
+                "status": "available",
+                "name": "saved-snapshot",
+                "id": "1ad4c789-7e8w-4dwg-afc5",
+                "description": "snapshot in saved state"
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 202),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+
+class TestServerService(BaseCmdServiceTests):
+
+    service_class = 'ServerService'
+    service_name = 'servers'
+    response = {
+        "servers": [
+            {
+                "id": "22c91117-08de-4894-9aa9-6ef382400985",
+                "links": [
+                    {
+                        "href": "http://openstack.example.com/v2/6f70-6ef0985",
+                        "rel": "self"
+                    },
+                    {
+                        "href": "http://openstack.example.com/6f70656e7-6ef35",
+                        "rel": "bookmark"
+                    }
+                ],
+                "name": "new-server-test"
+            },
+            {
+                "id": "7a6d4v7w-36ds-4216",
+                "links": [
+                    {
+                        "href": "http://openstack.example.com/v2/6f70-6ef0985",
+                        "rel": "self"
+                    },
+                    {
+                        "href": "http://openstack.example.com/6f70656e7-6ef35",
+                        "rel": "bookmark"
+                    }
+                ],
+                "name": "saved-server"
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+
+class TestServerGroupService(BaseCmdServiceTests):
+
+    service_class = 'ServerGroupService'
+    service_name = 'server_groups'
+    validate_response = ('tempest.lib.services.compute.server_groups_client'
+                         '.ServerGroupsClient.validate_response')
+
+    response = {
+        "server_groups": [
+            {
+                "id": "616fb98f-46ca-475e-917e-2563e5a8cd19",
+                "name": "test",
+                "policy": "anti-affinity",
+                "rules": {"max_server_per_host": 3},
+                "members": [],
+                "project_id": "6f70656e737461636b20342065766572",
+                "user_id": "fake"
+            },
+            {
+                "id": "as6d5f7g-46ca-475e",
+                "name": "saved-server-group"
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.validate_response, 'validate', None),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.validate_response, 'validate', None),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.validate_response, 'validate', None),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200),
+                                     (self.validate_response, 'validate', None)
+                                     ])
+
+
+class TestKeyPairService(BaseCmdServiceTests):
+
+    service_class = 'KeyPairService'
+    service_name = 'keypairs'
+    validate_response = ('tempest.lib.services.compute.keypairs_client'
+                         '.KeyPairsClient.validate_response')
+    response = {
+        "keypairs": [
+            {
+                "keypair": {
+                    "fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:bd",
+                    "name": "keypair-5d935425-31d5-48a7-a0f1-e76e9813f2c3",
+                    "type": "ssh",
+                    "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF\n"
+                }
+            },
+            {
+                "keypair": {
+                    "fingerprint": "7e:eb:ab:24",
+                    "name": "saved-key-pair"
+                }
+            }
+        ]
+    }
+
+    def _test_saved_state_true(self, mocked_fixture_tuple_list):
+        serv = self._create_cmd_service(self.service_class, is_save_state=True)
+        _, fixtures = self.run_function_with_mocks(
+            serv.run,
+            mocked_fixture_tuple_list
+        )
+        for item in self.response[self.service_name]:
+            self.assertTrue(item['keypair']['name'],
+                            serv.data[self.service_name])
+        for fixture in fixtures:
+            fixture.mock.assert_called_once()
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.validate_response, 'validate', None),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.validate_response, 'validate', None),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.validate_response, 'validate', None),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([
+            (self.get_method, self.response, 200),
+            (self.validate_response, 'validate', None)
+        ])
+
+
+class TestVolumeService(BaseCmdServiceTests):
+
+    service_class = 'VolumeService'
+    service_name = 'volumes'
+    response = {
+        "volumes": [
+            {
+                "id": "efa54464-8fab-47cd-a05a-be3e6b396188",
+                "links": [
+                    {
+                        "href": "http://127.0.0.1:37097/v3/89af/volumes/efa54",
+                        "rel": "self"
+                    },
+                    {
+                        "href": "http://127.0.0.1:37097/89af/volumes/efa54464",
+                        "rel": "bookmark"
+                    }
+                ],
+                "name": "volume-name"
+            },
+            {
+                "id": "aa77asdf-1234",
+                "name": "saved-volume"
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 202),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+
+# Begin network service classes
+class TestNetworkService(BaseCmdServiceTests):
+
+    service_class = 'NetworkService'
+    service_name = 'networks'
+    response = {
+        "networks": [
+            {
+                "admin_state_up": True,
+                "availability_zone_hints": [],
+                "availability_zones": [
+                    "nova"
+                ],
+                "created_at": "2016-03-08T20:19:41",
+                "dns_domain": "my-domain.org.",
+                "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+                "l2_adjacency": False,
+                "mtu": 1500,
+                "name": "net1",
+                "port_security_enabled": True,
+                "project_id": "4fd44f30292945e481c7b8a0c8908869",
+                "qos_policy_id": "6a8454ade84346f59e8d40665f878b2e",
+                "revision_number": 1,
+                "router:external": False,
+                "shared": False,
+                "status": "ACTIVE",
+                "subnets": [
+                    "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+                ],
+                "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+                "updated_at": "2016-03-08T20:19:41",
+                "vlan_transparent": True,
+                "description": "",
+                "is_default": False
+            },
+            {
+                "id": "6722fc13-4319",
+                "name": "saved-network"
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+    def test_preserve_list(self):
+        self.response['networks'].append(
+            {
+                "admin_state_up": True,
+                "availability_zone_hints": [],
+                "availability_zones": [
+                    "nova"
+                ],
+                "created_at": "2017-03-08T20:19:41",
+                "dns_domain": "my-domain.org.",
+                "id": cleanup_service.CONF_PUB_NETWORK,
+                "name": "net2",
+                "port_security_enabled": True,
+                "project_id": "4fd44f30292945e481c7b8a0c8908869",
+                "qos_policy_id": "6a8454ade84346f59e8d40665f878b2e",
+                "revision_number": 1,
+                "status": "ACTIVE",
+                "subnets": [
+                    "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+                ],
+                "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+                "updated_at": "2018-03-08T20:19:41",
+                "vlan_transparent": True,
+                "is_default": False
+            })
+        self._test_is_preserve_true([(self.get_method, self.response, 200)])
+
+
+class TestNetworkFloatingIpService(BaseCmdServiceTests):
+
+    service_class = 'NetworkFloatingIpService'
+    service_name = 'floatingips'
+    response = {
+        "floatingips": [
+            {
+                "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+                "description": "for test",
+                "dns_domain": "my-domain.org.",
+                "dns_name": "myfip",
+                "created_at": "2016-12-21T10:55:50Z",
+                "updated_at": "2016-12-21T10:55:53Z",
+                "revision_number": 1,
+                "project_id": "4969c491a3c74ee4af974e6d800c62de",
+                "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+                "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+                "fixed_ip_address": "10.0.0.3",
+                "floating_ip_address": "172.24.4.228",
+                "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab",
+                "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7",
+                "status": "ACTIVE",
+                "port_details": {
+                    "status": "ACTIVE",
+                    "name": "",
+                    "admin_state_up": True,
+                    "network_id": "02dd8479-ef26-4398-a102-d19d0a7b3a1f",
+                    "device_owner": "compute:nova",
+                    "mac_address": "fa:16:3e:b1:3b:30",
+                    "device_id": "8e3941b4-a6e9-499f-a1ac-2a4662025cba"
+                },
+                "tags": ["tag1,tag2"],
+                "port_forwardings": []
+            },
+            {
+                "id": "9e82d248-408a",
+                "status": "ACTIVE"
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+
+class TestNetworkRouterService(BaseCmdServiceTests):
+
+    service_class = 'NetworkRouterService'
+    service_name = 'routers'
+    validate_response = ('tempest.lib.services.network.routers_client'
+                         '.RoutersClient.validate_response')
+    response = {
+        "routers": [
+            {
+                "admin_state_up": True,
+                "availability_zone_hints": [],
+                "availability_zones": [
+                    "nova"
+                ],
+                "created_at": "2018-03-19T19:17:04Z",
+                "description": "",
+                "distributed": False,
+                "external_gateway_info": {
+                    "enable_snat": True,
+                    "external_fixed_ips": [
+                        {
+                            "ip_address": "172.24.4.3",
+                            "subnet_id": "b930d7f6-ceb7-40a0-8b81-a425dd994ccf"
+                        },
+                        {
+                            "ip_address": "2001:db8::c",
+                            "subnet_id": "0c56df5d-ace5-46c8-8f4c-45fa4e334d18"
+                        }
+                    ],
+                    "network_id": "ae34051f-aa6c-4c75-abf5-50dc9ac99ef3"
+                },
+                "flavor_id": "f7b14d9a-b0dc-4fbe-bb14-a0f4970a69e0",
+                "ha": False,
+                "id": "915a14a6-867b-4af7-83d1-70efceb146f9",
+                "name": "router2",
+                "revision_number": 1,
+                "routes": [
+                    {
+                        "destination": "179.24.1.0/24",
+                        "nexthop": "172.24.3.99"
+                    }
+                ],
+                "status": "ACTIVE",
+                "updated_at": "2018-03-19T19:17:22Z",
+                "project_id": "0bd18306d801447bb457a46252d82d13",
+                "tenant_id": "0bd18306d801447bb457a46252d82d13",
+                "tags": ["tag1,tag2"]
+            },
+            {
+                "id": "4s5w34hj-id44",
+                "name": "saved-router"
+            }
+        ],
+        # "ports" key is added to the response in order to simplify unit
+        # testing - it's because NetworkRouterService's delete method lists
+        # ports before deleting any router
+        "ports": []
+    }
+
+    def _test_delete(self, mocked_fixture_tuple_list, fail=False):
+        serv = self._create_cmd_service(self.service_class)
+        resp, fixtures = self.run_function_with_mocks(
+            serv.run,
+            mocked_fixture_tuple_list,
+        )
+        for fixture in fixtures:
+            if fail is False and fixture.mock.return_value == 'exception':
+                fixture.mock.assert_not_called()
+            elif self.service_name in self.saved_state.keys():
+                fixture.mock.assert_called()
+                for key in self.saved_state[self.service_name].keys():
+                    self.assertNotIn(key, fixture.mock.call_args[0][0])
+        self.assertFalse(serv.data)
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+    def test_preserve_list(self):
+        self.response['routers'].append(
+            {
+                "admin_state_up": True,
+                "availability_zone_hints": [],
+                "availability_zones": [
+                    "nova"
+                ],
+                "created_at": "2018-03-19T19:17:04Z",
+                "id": cleanup_service.CONF_PUB_ROUTER,
+                "name": "router-preserve",
+                "status": "ACTIVE",
+                "updated_at": "2018-03-19T19:17:22Z",
+                "project_id": "0bd18306d801447bb457a46252d82d13",
+                "tenant_id": "0bd18306d801447bb457a46252d82d13",
+                "tags": ["tag1,tag2"]
+            })
+        self._test_is_preserve_true([(self.get_method, self.response, 200)])
+
+
+class TestNetworkMeteringLabelRuleService(BaseCmdServiceTests):
+
+    service_class = 'NetworkMeteringLabelRuleService'
+    service_name = 'metering_label_rules'
+    response = {
+        "metering_label_rules": [
+            {
+                "remote_ip_prefix": "20.0.0.0/24",
+                "direction": "ingress",
+                "metering_label_id": "e131d186-b02d-4c0b-83d5-0c0725c4f812",
+                "id": "9536641a-7d14-4dc5-afaf-93a973ce0eb8",
+                "excluded": False
+            },
+            {
+                "direction": "ingress",
+                "id": "93a973ce-4dc5"
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+
+class TestNetworkMeteringLabelService(BaseCmdServiceTests):
+
+    service_class = 'NetworkMeteringLabelService'
+    service_name = 'metering_labels'
+    response = {
+        "metering_labels": [
+            {
+                "project_id": "45345b0ee1ea477fac0f541b2cb79cd4",
+                "tenant_id": "45345b0ee1ea477fac0f541b2cb79cd4",
+                "description": "label1 description",
+                "name": "label1",
+                "id": "a6700594-5b7a-4105-8bfe-723b346ce866",
+                "shared": False
+            },
+            {
+                "name": "saved-label",
+                "id": "723b346ce866-4c7q",
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+
+class TestNetworkPortService(BaseCmdServiceTests):
+
+    service_class = 'NetworkPortService'
+    service_name = 'ports'
+    response = {
+        "ports": [
+            {
+                "admin_state_up": True,
+                "allowed_address_pairs": [],
+                "created_at": "2016-03-08T20:19:41",
+                "description": "",
+                "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824",
+                "device_owner": "",
+                "dns_assignment": {
+                    "hostname": "myport",
+                    "ip_address": "172.24.4.2",
+                    "fqdn": "myport.my-domain.org"
+                },
+                "dns_domain": "my-domain.org.",
+                "dns_name": "myport",
+                "extra_dhcp_opts": [
+                    {
+                        "opt_value": "pxelinux.0",
+                        "ip_version": 4,
+                        "opt_name": "bootfile-name"
+                    }
+                ],
+                "fixed_ips": [
+                    {
+                        "ip_address": "172.24.4.2",
+                        "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062"
+                    }
+                ],
+                "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+                "ip_allocation": "immediate",
+                "mac_address": "fa:16:3e:58:42:ed",
+                "name": "test_port",
+                "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+                "project_id": "",
+                "revision_number": 1,
+                "security_groups": [],
+                "status": "ACTIVE",
+                "tags": ["tag1,tag2"],
+                "tenant_id": "",
+                "updated_at": "2016-03-08T20:19:41",
+                "qos_policy_id": "29d5e02e-d5ab-4929-bee4-4a9fc12e22ae",
+                "port_security_enabled": False
+            },
+            {
+                "id": "aa74aa4v-741a",
+                "name": "saved-port",
+                "device_owner": ""
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+    def test_preserve_list(self):
+        self.response['ports'].append(
+            {
+                "created_at": "2018-03-08T20:19:41",
+                "description": "",
+                "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824",
+                "device_owner": "compute:router_gateway",
+                "id": "d80b1a3b-4fc1-49f3-952e-1fdy1ws542",
+                "ip_allocation": "immediate",
+                "mac_address": "fa:16:3e:58:42:ed",
+                "name": "preserve_port",
+                "network_id": cleanup_service.CONF_PUB_NETWORK,
+                "project_id": "",
+                "security_groups": [],
+                "status": "ACTIVE",
+                "tags": ["tag1,tag2"],
+                "tenant_id": "",
+                "updated_at": "2018-03-08T20:19:41",
+            })
+        self._test_is_preserve_true([(self.get_method, self.response, 200)])
+
+
+class TestNetworkSecGroupService(BaseCmdServiceTests):
+
+    service_class = 'NetworkSecGroupService'
+    service_name = 'security_groups'
+    response = {
+        "security_groups": [
+            {
+                "description": "default",
+                "id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+                "name": "test",
+                "security_group_rules": [
+                    {
+                        "direction": "egress",
+                        "ethertype": "IPv6",
+                        "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+                        "security_group_id": "85cc3048-abc3-43cc-89b3-3773414",
+                        "project_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+                        "revision_number": 1,
+                        "tags": ["tag1,tag2"],
+                        "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+                        "created_at": "2018-03-19T19:16:56Z",
+                        "updated_at": "2018-03-19T19:16:56Z",
+                        "description": ""
+                    }
+                ]
+            },
+            {
+                "id": "7q844add-3697",
+                "name": "saved-sec-group"
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+    def test_preserve_list(self):
+        self.response['security_groups'].append(
+            {
+                "description": "default",
+                "id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+                "name": "test",
+                "security_group_rules": [
+                    {
+                        "direction": "egress",
+                        "ethertype": "IPv6",
+                        "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+                        "security_group_id": "85cc3048-abc3-43cc-89b3-3773414",
+                        "project_id": cleanup_service.CONF_PROJECTS[0],
+                        "revision_number": 1,
+                        "tags": ["tag1,tag2"],
+                        "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550",
+                        "created_at": "2018-03-19T19:16:56Z",
+                        "updated_at": "2018-03-19T19:16:56Z",
+                        "description": ""
+                    }
+                ]
+            })
+        self._test_is_preserve_true([(self.get_method, self.response, 200)])
+
+
+class TestNetworkSubnetService(BaseCmdServiceTests):
+
+    service_class = 'NetworkSubnetService'
+    service_name = 'subnets'
+    response = {
+        "subnets": [
+            {
+                "name": "private-subnet",
+                "enable_dhcp": True,
+                "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+                "project_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+                "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+                "dns_nameservers": [],
+                "allocation_pools": [
+                    {
+                        "start": "10.0.0.2",
+                        "end": "10.0.0.254"
+                    }
+                ],
+                "host_routes": [],
+                "ip_version": 4,
+                "gateway_ip": "10.0.0.1",
+                "cidr": "10.0.0.0/24",
+                "id": "08eae331-0402-425a-923c-34f7cfe39c1b",
+                "created_at": "2016-10-10T14:35:34Z",
+                "revision_number": 2,
+                "service_types": [],
+                "tags": ["tag1,tag2"],
+                "updated_at": "2016-10-10T14:35:34Z"
+            },
+            {
+                "id": "55ttda4a-2584",
+                "name": "saved-subnet"
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+    def test_preserve_list(self):
+        self.response['subnets'].append(
+            {
+                "name": "public-subnet",
+                "network_id": cleanup_service.CONF_PUB_NETWORK,
+                "project_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+                "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+                "ip_version": 4,
+                "gateway_ip": "10.0.0.1",
+                "cidr": "10.0.0.0/24",
+                "id": "08eae331-0402-425a-923c-34f7cfe39c1b",
+                "created_at": "2018-10-10T14:35:34Z",
+                "service_types": [],
+                "tags": ["tag1,tag2"],
+                "updated_at": "2018-10-10T14:35:34Z"
+            })
+        self._test_is_preserve_true([(self.get_method, self.response, 200)])
+
+
+class TestNetworkSubnetPoolsService(BaseCmdServiceTests):
+
+    service_class = 'NetworkSubnetPoolsService'
+    service_name = 'subnetpools'
+    response = {
+        "subnetpools": [
+            {
+                "min_prefixlen": "64",
+                "default_prefixlen": "64",
+                "id": "03f761e6-eee0-43fc-a921-8acf64c14988",
+                "max_prefixlen": "64",
+                "name": "my-subnet-pool-ipv6",
+                "is_default": False,
+                "project_id": "9fadcee8aa7c40cdb2114fff7d569c08",
+                "tenant_id": "9fadcee8aa7c40cdb2114fff7d569c08",
+                "prefixes": [
+                    "2001:db8:0:2::/64",
+                    "2001:db8::/63"
+                ],
+                "ip_version": 6,
+                "shared": False,
+                "description": "",
+                "created_at": "2016-03-08T20:19:41",
+                "updated_at": "2016-03-08T20:19:41",
+                "revision_number": 2,
+                "tags": ["tag1,tag2"]
+            },
+            {
+                "id": "8acf64c1-43fc",
+                "name": "saved-subnet-pool"
+            }
+        ]
+    }
+
+    def test_delete_fail(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, 'error', None),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock, fail=True)
+
+    def test_delete_pass(self):
+        delete_mock = [(self.get_method, self.response, 200),
+                       (self.delete_method, None, 204),
+                       (self.log_method, 'exception', None)]
+        self._test_delete(delete_mock)
+
+    def test_dry_run(self):
+        dry_mock = [(self.get_method, self.response, 200),
+                    (self.delete_method, "delete", None)]
+        self._test_dry_run_true(dry_mock)
+
+    def test_save_state(self):
+        self._test_saved_state_true([(self.get_method, self.response, 200)])
+
+    def test_preserve_list(self):
+        self.response['subnetpools'].append(
+            {
+                "min_prefixlen": "64",
+                "default_prefixlen": "64",
+                "id": "9acf64c1-43fc",
+                "name": "preserve-pool",
+                "project_id": cleanup_service.CONF_PROJECTS[0],
+                "created_at": "2016-03-08T20:19:41",
+                "updated_at": "2016-03-08T20:19:41"
+            })
+        self._test_is_preserve_true([(self.get_method, self.response, 200)])
+
+
+# begin global services
 class TestDomainService(BaseCmdServiceTests):
 
     service_class = 'DomainService'
diff --git a/tempest/tests/cmd/test_run.py b/tempest/tests/cmd/test_run.py
index 9a6be4e..0e00d94 100644
--- a/tempest/tests/cmd/test_run.py
+++ b/tempest/tests/cmd/test_run.py
@@ -24,9 +24,15 @@
 import six
 
 from tempest.cmd import run
+from tempest.cmd import workspace
 from tempest import config
+from tempest.lib.common.utils import data_utils
 from tempest.tests import base
 
+if six.PY2:
+    # Python 2 has not FileNotFoundError exception
+    FileNotFoundError = IOError
+
 DEVNULL = open(os.devnull, 'wb')
 atexit.register(DEVNULL.close)
 
@@ -58,6 +64,12 @@
         self.assertEqual(['i_am_a_fun_little_regex'],
                          self.run_cmd._build_regex(args))
 
+    def test__build_regex_smoke_regex(self):
+        args = mock.Mock(spec=argparse.Namespace)
+        setattr(args, "smoke", True)
+        setattr(args, 'regex', 'i_am_a_fun_little_regex')
+        self.assertEqual(['smoke'], self.run_cmd._build_regex(args))
+
 
 class TestRunReturnCode(base.TestCase):
     def setUp(self):
@@ -100,6 +112,27 @@
         subprocess.call(['stestr', 'init'])
         self.assertRunExit(['tempest', 'run', '--regex', 'passing'], 0)
 
+    def test_tempest_run_failing(self):
+        self.assertRunExit(['tempest', 'run', '--regex', 'failing'], 1)
+
+    def test_tempest_run_failing_with_stestr_repository(self):
+        subprocess.call(['stestr', 'init'])
+        self.assertRunExit(['tempest', 'run', '--regex', 'failing'], 1)
+
+    def test_tempest_run_blackregex_failing(self):
+        self.assertRunExit(['tempest', 'run', '--black-regex', 'failing'], 0)
+
+    def test_tempest_run_blackregex_failing_with_stestr_repository(self):
+        subprocess.call(['stestr', 'init'])
+        self.assertRunExit(['tempest', 'run', '--black-regex', 'failing'], 0)
+
+    def test_tempest_run_blackregex_passing(self):
+        self.assertRunExit(['tempest', 'run', '--black-regex', 'passing'], 1)
+
+    def test_tempest_run_blackregex_passing_with_stestr_repository(self):
+        subprocess.call(['stestr', 'init'])
+        self.assertRunExit(['tempest', 'run', '--black-regex', 'passing'], 1)
+
     def test_tempest_run_fails(self):
         self.assertRunExit(['tempest', 'run'], 1)
 
@@ -128,7 +161,7 @@
         whitelist_file.write('passing'.encode('utf-8'))
         self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path], 0)
 
-    def test_tempest_run_with_whitelist_with_regex(self):
+    def test_tempest_run_with_whitelist_regex_include_pass_check_fail(self):
         fd, path = tempfile.mkstemp()
         self.addCleanup(os.remove, path)
         whitelist_file = os.fdopen(fd, 'wb', 0)
@@ -137,11 +170,72 @@
         self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path,
                             '--regex', 'fail'], 1)
 
+    def test_tempest_run_with_whitelist_regex_include_pass_check_pass(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        whitelist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(whitelist_file.close)
+        whitelist_file.write('passing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path,
+                            '--regex', 'passing'], 0)
+
+    def test_tempest_run_with_whitelist_regex_include_fail_check_pass(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        whitelist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(whitelist_file.close)
+        whitelist_file.write('failing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--whitelist-file=%s' % path,
+                            '--regex', 'pass'], 1)
+
     def test_tempest_run_passes_with_config_file(self):
         self.assertRunExit(['tempest', 'run',
                             '--config-file', self.stestr_conf_file,
                             '--regex', 'passing'], 0)
 
+    def test_tempest_run_with_blacklist_failing(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        blacklist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(blacklist_file.close)
+        blacklist_file.write('failing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--blacklist-file=%s' % path], 0)
+
+    def test_tempest_run_with_blacklist_passing(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        blacklist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(blacklist_file.close)
+        blacklist_file.write('passing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--blacklist-file=%s' % path], 1)
+
+    def test_tempest_run_with_blacklist_regex_exclude_fail_check_pass(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        blacklist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(blacklist_file.close)
+        blacklist_file.write('failing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--blacklist-file=%s' % path,
+                            '--regex', 'pass'], 0)
+
+    def test_tempest_run_with_blacklist_regex_exclude_pass_check_pass(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        blacklist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(blacklist_file.close)
+        blacklist_file.write('passing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--blacklist-file=%s' % path,
+                            '--regex', 'pass'], 1)
+
+    def test_tempest_run_with_blacklist_regex_exclude_pass_check_fail(self):
+        fd, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        blacklist_file = os.fdopen(fd, 'wb', 0)
+        self.addCleanup(blacklist_file.close)
+        blacklist_file.write('passing'.encode('utf-8'))
+        self.assertRunExit(['tempest', 'run', '--blacklist-file=%s' % path,
+                            '--regex', 'fail'], 1)
+
 
 class TestConfigPathCheck(base.TestCase):
     def setUp(self):
@@ -154,14 +248,22 @@
         # getting set in os environment when some data has passed to
         # set the environment.
 
-        self.run_cmd._set_env("/fakedir/fakefile")
-        self.assertEqual("/fakedir/fakefile", CONF._path)
-        self.assertIn('TEMPEST_CONFIG_DIR', os.environ)
-        self.assertEqual("/fakedir/fakefile",
-                         os.path.join(os.environ['TEMPEST_CONFIG_DIR'],
-                                      os.environ['TEMPEST_CONFIG']))
+        _, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
 
-    def test_tempest_run_set_config_no_path(self):
+        self.run_cmd._set_env(path)
+        self.assertEqual(path, CONF._path)
+        self.assertIn('TEMPEST_CONFIG_DIR', os.environ)
+        self.assertEqual(path, os.path.join(os.environ['TEMPEST_CONFIG_DIR'],
+                                            os.environ['TEMPEST_CONFIG']))
+
+    def test_tempest_run_set_config_no_exist_path(self):
+        path = "fake/path"
+        self.assertRaisesRegex(FileNotFoundError,
+                               'Config file: .* doesn\'t exist',
+                               self.run_cmd._set_env, path)
+
+    def test_tempest_run_no_config_path(self):
         # Note: (mbindlish) This test is created for the bug id: 1783751
         # Checking TEMPEST_CONFIG_DIR and TEMPEST_CONFIG should have no value
         # in os environment when no data has passed to set the environment.
@@ -173,6 +275,27 @@
 
 
 class TestTakeAction(base.TestCase):
+    def setUp(self):
+        super(TestTakeAction, self).setUp()
+        self.name = data_utils.rand_name('workspace')
+        self.path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.path, ignore_errors=True)
+        store_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, store_dir, ignore_errors=True)
+        self.store_file = os.path.join(store_dir, 'workspace.yaml')
+        self.workspace_manager = workspace.WorkspaceManager(
+            path=self.store_file)
+        self.workspace_manager.register_new_workspace(self.name, self.path)
+
+    def _setup_test_dirs(self):
+        self.directory = tempfile.mkdtemp(prefix='tempest-unit')
+        self.addCleanup(shutil.rmtree, self.directory, ignore_errors=True)
+        self.test_dir = os.path.join(self.directory, 'tests')
+        os.mkdir(self.test_dir)
+        # Change directory, run wrapper and check result
+        self.addCleanup(os.chdir, os.path.abspath(os.curdir))
+        os.chdir(self.directory)
+
     def test_workspace_not_registered(self):
         class Exception_(Exception):
             pass
@@ -201,24 +324,116 @@
         self.assertIn(workspace, exit_msg)
 
     def test_config_file_specified(self):
-        # Setup test dirs
-        self.directory = tempfile.mkdtemp(prefix='tempest-unit')
-        self.addCleanup(shutil.rmtree, self.directory)
-        self.test_dir = os.path.join(self.directory, 'tests')
-        os.mkdir(self.test_dir)
-        # Change directory, run wrapper and check result
-        self.addCleanup(os.chdir, os.path.abspath(os.curdir))
-        os.chdir(self.directory)
-
+        self._setup_test_dirs()
+        _, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
         tempest_run = run.TempestRun(app=mock.Mock(), app_args=mock.Mock())
         parsed_args = mock.Mock()
 
         parsed_args.workspace = None
         parsed_args.state = None
         parsed_args.list_tests = False
-        parsed_args.config_file = '.stestr.conf'
+        parsed_args.config_file = path
 
         with mock.patch('stestr.commands.run_command') as m:
             m.return_value = 0
             self.assertEqual(0, tempest_run.take_action(parsed_args))
             m.assert_called()
+
+    def test_no_config_file_no_workspace_no_state(self):
+        self._setup_test_dirs()
+        tempest_run = run.TempestRun(app=mock.Mock(), app_args=mock.Mock())
+        parsed_args = mock.Mock()
+
+        parsed_args.workspace = None
+        parsed_args.state = None
+        parsed_args.list_tests = False
+        parsed_args.config_file = ''
+
+        with mock.patch('stestr.commands.run_command'):
+            self.assertRaises(SystemExit, tempest_run.take_action, parsed_args)
+
+    def test_config_file_workspace_registered(self):
+        self._setup_test_dirs()
+        _, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        tempest_run = run.TempestRun(app=mock.Mock(), app_args=mock.Mock())
+        parsed_args = mock.Mock()
+        parsed_args.workspace = self.name
+        parsed_args.workspace_path = self.store_file
+        parsed_args.state = None
+        parsed_args.list_tests = False
+        parsed_args.config_file = path
+
+        with mock.patch('stestr.commands.run_command') as m:
+            m.return_value = 0
+            self.assertEqual(0, tempest_run.take_action(parsed_args))
+            m.assert_called()
+
+    @mock.patch('tempest.cmd.run.TempestRun._init_state')
+    def test_workspace_registered_no_config_no_state(self, mock_init_state):
+        self._setup_test_dirs()
+        tempest_run = run.TempestRun(app=mock.Mock(), app_args=mock.Mock())
+        parsed_args = mock.Mock()
+        parsed_args.workspace = self.name
+        parsed_args.workspace_path = self.store_file
+        parsed_args.state = None
+        parsed_args.list_tests = False
+        parsed_args.config_file = ''
+
+        with mock.patch('stestr.commands.run_command') as m:
+            m.return_value = 0
+            self.assertEqual(0, tempest_run.take_action(parsed_args))
+            m.assert_called()
+        mock_init_state.assert_not_called()
+
+    @mock.patch('tempest.cmd.run.TempestRun._init_state')
+    def test_no_config_file_no_workspace_state_true(self, mock_init_state):
+        self._setup_test_dirs()
+        tempest_run = run.TempestRun(app=mock.Mock(), app_args=mock.Mock())
+        parsed_args = mock.Mock()
+
+        parsed_args.workspace = None
+        parsed_args.state = True
+        parsed_args.list_tests = False
+        parsed_args.config_file = ''
+
+        with mock.patch('stestr.commands.run_command'):
+            self.assertRaises(SystemExit, tempest_run.take_action, parsed_args)
+        mock_init_state.assert_not_called()
+
+    @mock.patch('tempest.cmd.run.TempestRun._init_state')
+    def test_workspace_registered_no_config_state_true(self, mock_init_state):
+        self._setup_test_dirs()
+        tempest_run = run.TempestRun(app=mock.Mock(), app_args=mock.Mock())
+        parsed_args = mock.Mock()
+        parsed_args.workspace = self.name
+        parsed_args.workspace_path = self.store_file
+        parsed_args.state = True
+        parsed_args.list_tests = False
+        parsed_args.config_file = ''
+
+        with mock.patch('stestr.commands.run_command') as m:
+            m.return_value = 0
+            self.assertEqual(0, tempest_run.take_action(parsed_args))
+            m.assert_called()
+        mock_init_state.assert_called()
+
+    @mock.patch('tempest.cmd.run.TempestRun._init_state')
+    def test_no_workspace_config_file_state_true(self, mock_init_state):
+        self._setup_test_dirs()
+        _, path = tempfile.mkstemp()
+        self.addCleanup(os.remove, path)
+        tempest_run = run.TempestRun(app=mock.Mock(), app_args=mock.Mock())
+        parsed_args = mock.Mock()
+        parsed_args.workspace = None
+        parsed_args.workspace_path = self.store_file
+        parsed_args.state = True
+        parsed_args.list_tests = False
+        parsed_args.config_file = path
+
+        with mock.patch('stestr.commands.run_command') as m:
+            m.return_value = 0
+            self.assertEqual(0, tempest_run.take_action(parsed_args))
+            m.assert_called()
+        mock_init_state.assert_called()
diff --git a/tempest/tests/cmd/test_saved_state_json.json b/tempest/tests/cmd/test_saved_state_json.json
new file mode 100644
index 0000000..5c55331
--- /dev/null
+++ b/tempest/tests/cmd/test_saved_state_json.json
@@ -0,0 +1,16 @@
+{
+  "domains": {
+    "default": "Default"
+  },
+  "flavors": {
+    "1": "m1.tiny"
+  },
+  "images": {},
+  "projects": {
+    "268bcb63488b4aa2942ecaac0f85ed62": "demo"
+  },
+  "roles": {},
+  "users": {
+    "023e65a5922a454585a91c6af8310968": "demo"
+  }
+}
diff --git a/tempest/tests/cmd/test_workspace.py b/tempest/tests/cmd/test_workspace.py
index a256368..7a6b576 100644
--- a/tempest/tests/cmd/test_workspace.py
+++ b/tempest/tests/cmd/test_workspace.py
@@ -48,7 +48,7 @@
         stdout, stderr = process.communicate()
         return_code = process.returncode
         msg = ("%s failed with:\nstdout: %s\nstderr: %s" % (' '.join(cmd),
-               stdout, stderr))
+                                                            stdout, stderr))
         self.assertEqual(return_code, expected, msg)
 
     def test_run_workspace_list(self):
@@ -133,22 +133,146 @@
                          "None or empty name is specified."
                          " Please specify correct name for workspace.\n")
 
+    def test_workspace_manager_rename_with_existing_name(self):
+        new_name = self.name
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.rename_workspace,
+                                   self.name, new_name)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace already exists with name: %s.\n"
+                         % new_name)
+
+    def test_workspace_manager_rename_no_exist_old_name(self):
+        old_name = ""
+        new_name = data_utils.rand_uuid()
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.rename_workspace,
+                                   old_name, new_name)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace was not found with name: %s\n"
+                         % old_name)
+
+    def test_workspace_manager_rename_integer_data(self):
+        old_name = self.name
+        new_name = 12345
+        self.workspace_manager.rename_workspace(old_name, new_name)
+        self.assertIsNone(self.workspace_manager.get_workspace(old_name))
+        self.assertIsNotNone(self.workspace_manager.get_workspace(new_name))
+
+    def test_workspace_manager_rename_alphanumeric_data(self):
+        old_name = self.name
+        new_name = 'abc123'
+        self.workspace_manager.rename_workspace(old_name, new_name)
+        self.assertIsNone(self.workspace_manager.get_workspace(old_name))
+        self.assertIsNotNone(self.workspace_manager.get_workspace(new_name))
+
     def test_workspace_manager_move(self):
         new_path = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
         self.workspace_manager.move_workspace(self.name, new_path)
         self.assertEqual(
             self.workspace_manager.get_workspace(self.name), new_path)
+        # NOTE(mbindlish): Also checking for the workspace that it
+        # shouldn't exist in old path
+        self.assertNotEqual(
+            self.workspace_manager.get_workspace(self.name), self.path)
+
+    def test_workspace_manager_move_wrong_path(self):
+        new_path = 'wrong/path'
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.move_workspace,
+                                   self.name, new_path)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "Path does not exist.\n")
+
+    def test_workspace_manager_move_wrong_workspace(self):
+        workspace_name = "wrong_workspace_name"
+        new_path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.move_workspace,
+                                   workspace_name, new_path)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace was not found with name: %s\n"
+                         % workspace_name)
+
+    def test_workspace_manager_move_no_workspace_name(self):
+        workspace_name = ""
+        new_path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, new_path, ignore_errors=True)
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.move_workspace,
+                                   workspace_name, new_path)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace was not found with name: %s\n"
+                         % workspace_name)
+
+    def test_workspace_manager_move_no_workspace_path(self):
+        new_path = ""
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.move_workspace,
+                                   self.name, new_path)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "None or empty path is specified for workspace."
+                         " Please specify correct workspace path.\n")
 
     def test_workspace_manager_remove_entry(self):
         self.workspace_manager.remove_workspace_entry(self.name)
         self.assertIsNone(self.workspace_manager.get_workspace(self.name))
 
+    def test_workspace_manager_remove_entry_no_name(self):
+        no_name = ""
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.
+                                   remove_workspace_entry,
+                                   no_name)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace was not found with name: %s\n"
+                         % no_name)
+
+    def test_workspace_manager_remove_entry_wrong_name(self):
+        wrong_name = "wrong_name"
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.
+                                   remove_workspace_entry,
+                                   wrong_name)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "A workspace was not found with name: %s\n"
+                         % wrong_name)
+
     def test_workspace_manager_remove_directory(self):
         path = self.workspace_manager.remove_workspace_entry(self.name)
         self.workspace_manager.remove_workspace_directory(path)
         self.assertIsNone(self.workspace_manager.get_workspace(self.name))
 
+    def test_workspace_manager_remove_directory_no_path(self):
+        no_path = ""
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.
+                                   remove_workspace_directory,
+                                   no_path)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "None or empty path is specified for workspace."
+                         " Please specify correct workspace path.\n")
+
     def test_path_expansion(self):
         name = data_utils.rand_uuid()
         path = os.path.join("~", name)
@@ -165,8 +289,11 @@
                                    nonexistent_name)
         self.assertEqual(1, ex.code)
         self.assertEqual(mock_stdout.getvalue(),
-                         "A workspace was not found with name: %s\n" %
-                         nonexistent_name)
+                         "A workspace was not found with name: %s\n"
+                         % nonexistent_name)
+
+    def test_workspace_name_exists(self):
+        self.assertIsNone(self.workspace_manager._name_exists(self.name))
 
     def test_workspace_name_already_exists(self):
         duplicate_name = self.name
@@ -180,6 +307,11 @@
                          "A workspace already exists with name: %s.\n"
                          % duplicate_name)
 
+    def test_workspace_name_exists_check_new_name(self):
+        new_name = "fake_name"
+        self.assertIsNone(self.workspace_manager.
+                          _workspace_name_exists(new_name))
+
     def test_workspace_manager_path_not_exist(self):
         fake_path = "fake_path"
         with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
@@ -190,6 +322,11 @@
         self.assertEqual(mock_stdout.getvalue(),
                          "Path does not exist.\n")
 
+    def test_validate_path_exists(self):
+        new_path = self.path
+        self.assertIsNone(self.workspace_manager.
+                          _validate_path(new_path))
+
     def test_workspace_manager_list_workspaces(self):
         listed = self.workspace_manager.list_workspaces()
         self.assertEqual(1, len(listed))
@@ -207,3 +344,33 @@
         self.assertEqual(mock_stdout.getvalue(),
                          "None or empty name is specified."
                          " Please specify correct name for workspace.\n")
+
+    def test_register_new_workspace_no_path(self):
+        no_path = ""
+        with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+            ex = self.assertRaises(SystemExit,
+                                   self.workspace_manager.
+                                   register_new_workspace,
+                                   self.name, no_path)
+        self.assertEqual(1, ex.code)
+        self.assertEqual(mock_stdout.getvalue(),
+                         "None or empty path is specified for workspace."
+                         " Please specify correct workspace path.\n")
+
+    def test_register_new_workspace_integer_data(self):
+        workspace_name = 12345
+        self.workspace_manager.register_new_workspace(
+            workspace_name, self.path)
+        self.assertIsNotNone(
+            self.workspace_manager.get_workspace(workspace_name))
+        self.assertEqual(
+            self.workspace_manager.get_workspace(workspace_name), self.path)
+
+    def test_register_new_workspace_alphanumeric_data(self):
+        workspace_name = 'abc123'
+        self.workspace_manager.register_new_workspace(
+            workspace_name, self.path)
+        self.assertIsNotNone(
+            self.workspace_manager.get_workspace(workspace_name))
+        self.assertEqual(
+            self.workspace_manager.get_workspace(workspace_name), self.path)
diff --git a/tempest/tests/files/setup.cfg b/tempest/tests/files/setup.cfg
index bd68708..a81d31e 100644
--- a/tempest/tests/files/setup.cfg
+++ b/tempest/tests/files/setup.cfg
@@ -3,7 +3,7 @@
 version = 1
 summary = Fake Project for testing wrapper scripts
 author = OpenStack
-author-email = openstack-dev@lists.openstack.org
+author-email = openstack-discuss@lists.openstack.org
 home-page = https://docs.openstack.org/tempest/latest/
 classifier =
     Intended Audience :: Information Technology
diff --git a/tempest/tests/lib/common/test_dynamic_creds.py b/tempest/tests/lib/common/test_dynamic_creds.py
index ebcf5d1..4723458 100644
--- a/tempest/tests/lib/common/test_dynamic_creds.py
+++ b/tempest/tests/lib/common/test_dynamic_creds.py
@@ -109,8 +109,8 @@
             return_value=(rest_client.ResponseBody
                           (200,
                            {'roles': [{'id': id, 'name': name},
-                            {'id': '1', 'name': 'FakeRole'},
-                            {'id': '2', 'name': 'Member'}]}))))
+                                      {'id': '1', 'name': 'FakeRole'},
+                                      {'id': '2', 'name': 'Member'}]}))))
         return roles_fix
 
     def _mock_list_2_roles(self):
@@ -120,8 +120,8 @@
             return_value=(rest_client.ResponseBody
                           (200,
                            {'roles': [{'id': '1234', 'name': 'role1'},
-                            {'id': '1', 'name': 'FakeRole'},
-                            {'id': '12345', 'name': 'role2'}]}))))
+                                      {'id': '1', 'name': 'FakeRole'},
+                                      {'id': '12345', 'name': 'role2'}]}))))
         return roles_fix
 
     def _mock_assign_user_role(self):
diff --git a/tempest/tests/lib/common/test_http.py b/tempest/tests/lib/common/test_http.py
index 336ef4a..a19153f 100644
--- a/tempest/tests/lib/common/test_http.py
+++ b/tempest/tests/lib/common/test_http.py
@@ -170,9 +170,6 @@
 
 
 class TestClosingHttpRedirects(base.TestCase):
-    def setUp(self):
-        super(TestClosingHttpRedirects, self).setUp()
-
     def test_redirect_default(self):
         connection = http.ClosingHttp()
         self.assertTrue(connection.follow_redirects)
@@ -183,9 +180,6 @@
 
 
 class TestClosingProxyHttpRedirects(base.TestCase):
-    def setUp(self):
-        super(TestClosingProxyHttpRedirects, self).setUp()
-
     def test_redirect_default(self):
         connection = http.ClosingProxyHttp(proxy_url=PROXY_URL)
         self.assertTrue(connection.follow_redirects)
diff --git a/tempest/tests/lib/common/test_profiler.py b/tempest/tests/lib/common/test_profiler.py
new file mode 100644
index 0000000..59fa0364
--- /dev/null
+++ b/tempest/tests/lib/common/test_profiler.py
@@ -0,0 +1,63 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import mock
+import testtools
+
+from tempest.lib.common import profiler
+
+
+class TestProfiler(testtools.TestCase):
+
+    def test_serialize(self):
+        key = 'SECRET_KEY'
+        pm = {'key': key, 'uuid': 'ID'}
+
+        with mock.patch('tempest.lib.common.profiler._profiler', pm):
+            with mock.patch('json.dumps') as jdm:
+                jdm.return_value = '{"base_id": "ID", "parent_id": "ID"}'
+
+                expected = {
+                    'X-Trace-HMAC':
+                        '887292df9f13b8b5ecd6bbbd2e16bfaaa4d914b0',
+                    'X-Trace-Info':
+                        b'eyJiYXNlX2lkIjogIklEIiwgInBhcmVudF9pZCI6ICJJRCJ9'
+                }
+
+                self.assertEqual(expected,
+                                 profiler.serialize_as_http_headers())
+
+    def test_profiler_lifecycle(self):
+        key = 'SECRET_KEY'
+        uuid = 'ID'
+
+        self.assertEqual({}, profiler._profiler)
+
+        profiler.enable(key, uuid)
+        self.assertEqual({'key': key, 'uuid': uuid}, profiler._profiler)
+
+        profiler.disable()
+        self.assertEqual({}, profiler._profiler)
+
+    @mock.patch('oslo_utils.uuidutils.generate_uuid')
+    def test_profiler_lifecycle_generate_trace_id(self, generate_uuid_mock):
+        key = 'SECRET_KEY'
+        uuid = 'ID'
+        generate_uuid_mock.return_value = uuid
+
+        self.assertEqual({}, profiler._profiler)
+
+        profiler.enable(key)
+        self.assertEqual({'key': key, 'uuid': uuid}, profiler._profiler)
+
+        profiler.disable()
+        self.assertEqual({}, profiler._profiler)
diff --git a/tempest/tests/lib/common/test_rest_client.py b/tempest/tests/lib/common/test_rest_client.py
index 4c0bb57..b861582 100644
--- a/tempest/tests/lib/common/test_rest_client.py
+++ b/tempest/tests/lib/common/test_rest_client.py
@@ -13,10 +13,10 @@
 #    under the License.
 
 import copy
-import json
 
 import fixtures
 import jsonschema
+from oslo_serialization import jsonutils as json
 import six
 
 from tempest.lib.common import http
diff --git a/tempest/tests/lib/services/compute/test_images_client.py b/tempest/tests/lib/services/compute/test_images_client.py
index c2c3b76..d1500e5 100644
--- a/tempest/tests/lib/services/compute/test_images_client.py
+++ b/tempest/tests/lib/services/compute/test_images_client.py
@@ -186,15 +186,19 @@
     def _test_resource_deleted(self, bytes_body=False):
         params = {"id": self.FAKE_IMAGE_ID}
         expected_op = self.FAKE_IMAGE_DATA['show']
-        self.useFixture(fixtures.MockPatch('tempest.lib.services.compute'
-                        '.images_client.ImagesClient.show_image',
-                                           side_effect=lib_exc.NotFound))
+        self.useFixture(
+            fixtures.MockPatch(
+                'tempest.lib.services.compute'
+                '.images_client.ImagesClient.show_image',
+                side_effect=lib_exc.NotFound))
         self.assertEqual(True, self.client.is_resource_deleted(**params))
         tempdata = copy.deepcopy(self.FAKE_IMAGE_DATA['show'])
         tempdata['image']['id'] = None
-        self.useFixture(fixtures.MockPatch('tempest.lib.services.compute'
-                        '.images_client.ImagesClient.show_image',
-                                           return_value=expected_op))
+        self.useFixture(
+            fixtures.MockPatch(
+                'tempest.lib.services.compute'
+                '.images_client.ImagesClient.show_image',
+                return_value=expected_op))
         self.assertEqual(False, self.client.is_resource_deleted(**params))
 
     def test_list_images_with_str_body(self):
diff --git a/tempest/tests/lib/services/identity/v2/test_token_client.py b/tempest/tests/lib/services/identity/v2/test_token_client.py
index dfce9b3..a592ada 100644
--- a/tempest/tests/lib/services/identity/v2/test_token_client.py
+++ b/tempest/tests/lib/services/identity/v2/test_token_client.py
@@ -12,9 +12,8 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-import json
-
 import mock
+from oslo_serialization import jsonutils as json
 
 from tempest.lib.common import rest_client
 from tempest.lib import exceptions
diff --git a/tempest/tests/lib/services/identity/v3/test_token_client.py b/tempest/tests/lib/services/identity/v3/test_token_client.py
index 38e8c4a..a9c58df 100644
--- a/tempest/tests/lib/services/identity/v3/test_token_client.py
+++ b/tempest/tests/lib/services/identity/v3/test_token_client.py
@@ -12,9 +12,8 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-import json
-
 import mock
+from oslo_serialization import jsonutils as json
 
 from tempest.lib.common import rest_client
 from tempest.lib import exceptions
diff --git a/tempest/tests/lib/services/image/v2/test_resource_types_client.py b/tempest/tests/lib/services/image/v2/test_resource_types_client.py
index 2e3b117..741b4eb 100644
--- a/tempest/tests/lib/services/image/v2/test_resource_types_client.py
+++ b/tempest/tests/lib/services/image/v2/test_resource_types_client.py
@@ -17,7 +17,7 @@
 from tempest.tests.lib.services import base
 
 
-class TestResouceTypesClient(base.BaseServiceTest):
+class TestResourceTypesClient(base.BaseServiceTest):
     FAKE_LIST_RESOURCETYPES = {
         "resource_types": [
             {
@@ -49,21 +49,21 @@
     }
 
     def setUp(self):
-        super(TestResouceTypesClient, self).setUp()
+        super(TestResourceTypesClient, self).setUp()
         fake_auth = fake_auth_provider.FakeAuthProvider()
         self.client = resource_types_client.ResourceTypesClient(fake_auth,
                                                                 'image',
                                                                 'regionOne')
 
-    def _test_list_resouce_types(self, bytes_body=False):
+    def _test_list_resource_types(self, bytes_body=False):
         self.check_service_client_function(
             self.client.list_resource_types,
             'tempest.lib.common.rest_client.RestClient.get',
             self.FAKE_LIST_RESOURCETYPES,
             bytes_body)
 
-    def test_list_resouce_types_with_str_body(self):
-        self._test_list_resouce_types()
+    def test_list_resource_types_with_str_body(self):
+        self._test_list_resource_types()
 
-    def test_list_resouce_types_with_bytes_body(self):
-        self._test_list_resouce_types(bytes_body=True)
+    def test_list_resource_types_with_bytes_body(self):
+        self._test_list_resource_types(bytes_body=True)
diff --git a/tempest/tests/lib/services/placement/__init__.py b/tempest/tests/lib/services/placement/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/lib/services/placement/__init__.py
diff --git a/tempest/tests/lib/services/placement/test_placement_client.py b/tempest/tests/lib/services/placement/test_placement_client.py
new file mode 100644
index 0000000..1396a85
--- /dev/null
+++ b/tempest/tests/lib/services/placement/test_placement_client.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2019 Ericsson
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib.services.placement import placement_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestPlacementClient(base.BaseServiceTest):
+    FAKE_ALLOCATION_CANDIDATES = {
+        'allocation_requests': [
+            {'allocations': {
+                'rp-uuid': {'resources': {'VCPU': 42}}
+            }}
+        ],
+        'provider_summaries': {
+            'rp-uuid': {
+                'resources': {
+                    'VCPU': {'used': 0, 'capacity': 64},
+                    'MEMORY_MB': {'capacity': 11196, 'used': 0},
+                    'DISK_GB': {'capacity': 19, 'used': 0}
+                },
+                'traits': ["HW_CPU_X86_SVM"],
+            }
+        }
+    }
+
+    FAKE_ALLOCATIONS = {
+        'allocations': {
+            'rp-uuid-1': {
+                'resources': {
+                    'NET_BW_IGR_KILOBIT_PER_SEC': 1
+                },
+                'generation': 14
+            },
+            'rp-uuid2': {
+                'resources': {
+                    'MEMORY_MB': 256,
+                    'VCPU': 1
+                },
+                'generation': 9
+            }
+        }
+    }
+
+    def setUp(self):
+        super(TestPlacementClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = placement_client.PlacementClient(
+            fake_auth, 'placement', 'regionOne')
+
+    def _test_list_allocation_candidates(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_allocation_candidates,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_ALLOCATION_CANDIDATES,
+            to_utf=bytes_body,
+            **{'resources1': 'NET_BW_IGR_KILOBIT_PER_SEC:1'})
+
+    def test_list_allocation_candidates_with_str_body(self):
+        self._test_list_allocation_candidates()
+
+    def test_list_allocation_candidates_with_bytes_body(self):
+        self._test_list_allocation_candidates(bytes_body=True)
+
+    def _test_list_allocations(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_allocations,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_ALLOCATIONS,
+            to_utf=bytes_body,
+            **{'consumer_uuid': 'foo-bar'})
+
+    def test_list_allocations_with_str_body(self):
+        self._test_list_allocations()
+
+    def test_list_allocations_with_bytes_body(self):
+        self._test_list_allocations(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v3/test_scheduler_stats_client.py b/tempest/tests/lib/services/volume/v3/test_scheduler_stats_client.py
index e0f5566..84c7589 100644
--- a/tempest/tests/lib/services/volume/v3/test_scheduler_stats_client.py
+++ b/tempest/tests/lib/services/volume/v3/test_scheduler_stats_client.py
@@ -62,7 +62,7 @@
             resp_body = self.FAKE_POOLS_LIST
         else:
             resp_body = {'pools': [{'name': pool['name']}
-                         for pool in self.FAKE_POOLS_LIST['pools']]}
+                                   for pool in self.FAKE_POOLS_LIST['pools']]}
         self.check_service_client_function(
             self.client.list_pools,
             'tempest.lib.common.rest_client.RestClient.get',
diff --git a/tempest/tests/lib/services/volume/v3/test_types_client.py b/tempest/tests/lib/services/volume/v3/test_types_client.py
new file mode 100644
index 0000000..7021a3f
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_types_client.py
@@ -0,0 +1,281 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib.services.volume.v3 import types_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestTypesClient(base.BaseServiceTest):
+    FAKE_CREATE_VOLUME_TYPE = {
+        'volume_type': {
+            'id': '6685584b-1eac-4da6-b5c3-555430cf68ff',
+            'name': 'vol-type-001',
+            'description': 'volume type 0001',
+            'is_public': True,
+            'os-volume-type-access:is_public': True,
+            'extra_specs': {
+                'volume_backend_name': 'rbd'
+            }
+        }
+    }
+
+    FAKE_DEFAULT_VOLUME_TYPE_INFO = {
+        'volume_type': {
+            'id': '6685584b-1eac-4da6-b5c3-555430cf68ff',
+            'qos_specs_id': None,
+            'name': 'volume-type-test',
+            'description': 'default volume type',
+            'is_public': True,
+            'os-volume-type-access:is_public': True,
+            'extra_specs': {
+                'volume_backend_name': 'rbd'
+            }
+        }
+    }
+
+    FAKE_UPDATE_VOLUME_TYPE = {
+        'volume_type': {
+            'id': '6685584b-1eac-4da6-b5c3-555430cf68ff',
+            'qos_specs_id': None,
+            'name': 'volume-type-test',
+            'description': 'default volume type',
+            'os-volume-type-access:is_public': True,
+            'is_public': True,
+            'extra_specs': {
+                'volume_backend_name': 'rbd'
+            }
+        }
+    }
+
+    FAKE_VOLUME_TYPES = {
+        'volume_types': [
+            {
+                'name': 'volume_type01',
+                'qos_specs_id': None,
+                'extra_specs': {
+                    'volume_backend_name': 'lvmdriver-1'
+                },
+                'os-volume-type-access:is_public': True,
+                'is_public': True,
+                'id': '6685584b-1eac-4da6-b5c3-555430cf68ff',
+                'description': None
+            },
+            {
+                'name': 'volume_type02',
+                'qos_specs_id': None,
+                'extra_specs': {
+                    'volume_backend_name': 'lvmdriver-1'
+                },
+                'os-volume-type-access:is_public': True,
+                'is_public': True,
+                'id': '8eb69a46-df97-4e41-9586-9a40a7533803',
+                'description': None
+            }
+        ]
+    }
+
+    FAKE_VOLUME_TYPE_EXTRA_SPECS = {
+        'extra_specs': {
+            'capabilities': 'gpu'
+        }
+    }
+
+    FAKE_SHOW_VOLUME_TYPE_EXTRA_SPECS = {
+        'capabilities': 'gpu'
+    }
+
+    FAKE_VOLUME_TYPE_ACCESS = {
+        'volume_type_access': [{
+            'volume_type_id': '3c67e124-39ad-4ace-a507-8bb7bf510c26',
+            'project_id': 'f270b245cb11498ca4031deb7e141cfa'
+        }]
+    }
+
+    def setUp(self):
+        super(TestTypesClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = types_client.TypesClient(fake_auth,
+                                               'volume',
+                                               'regionOne')
+
+    def _test_list_volume_types(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_volume_types,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_VOLUME_TYPES,
+            bytes_body)
+
+    def _test_show_volume_type(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_volume_type,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_DEFAULT_VOLUME_TYPE_INFO,
+            to_utf=bytes_body,
+            volume_type_id="6685584b-1eac-4da6-b5c3-555430cf68ff")
+
+    def _test_create_volume_type(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_volume_type,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_CREATE_VOLUME_TYPE,
+            to_utf=bytes_body,
+            name='volume-type-test')
+
+    def _test_delete_volume_type(self):
+        self.check_service_client_function(
+            self.client.delete_volume_type,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {}, status=202,
+            volume_type_id='6685584b-1eac-4da6-b5c3-555430cf68ff')
+
+    def _test_list_volume_types_extra_specs(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_volume_types_extra_specs,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_VOLUME_TYPE_EXTRA_SPECS,
+            to_utf=bytes_body,
+            volume_type_id='6685584b-1eac-4da6-b5c3-555430cf68ff')
+
+    def _test_show_volume_type_extra_specs(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_volume_type_extra_specs,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_SHOW_VOLUME_TYPE_EXTRA_SPECS,
+            volume_type_id='6685584b-1eac-4da6-b5c3-555430cf68ff',
+            extra_specs_name='capabilities',
+            to_utf=bytes_body)
+
+    def _test_create_volume_type_extra_specs(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_volume_type_extra_specs,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_VOLUME_TYPE_EXTRA_SPECS,
+            volume_type_id="6685584b-1eac-4da6-b5c3-555430cf68ff",
+            extra_specs=self.FAKE_VOLUME_TYPE_EXTRA_SPECS,
+            to_utf=bytes_body)
+
+    def _test_delete_volume_type_extra_specs(self):
+        self.check_service_client_function(
+            self.client.delete_volume_type_extra_specs,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {}, status=202,
+            volume_type_id='6685584b-1eac-4da6-b5c3-555430cf68ff',
+            extra_spec_name='volume_backend_name')
+
+    def _test_update_volume_type(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.update_volume_type,
+            'tempest.lib.common.rest_client.RestClient.put',
+            self.FAKE_UPDATE_VOLUME_TYPE,
+            volume_type_id='6685584b-1eac-4da6-b5c3-555430cf68ff',
+            to_utf=bytes_body,
+            name='update-volume-type-test',
+            description='test update volume type description')
+
+    def _test_update_volume_type_extra_specs(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.update_volume_type_extra_specs,
+            'tempest.lib.common.rest_client.RestClient.put',
+            self.FAKE_SHOW_VOLUME_TYPE_EXTRA_SPECS,
+            extra_spec_name='capabilities',
+            volume_type_id='6685584b-1eac-4da6-b5c3-555430cf68ff',
+            extra_specs=self.FAKE_SHOW_VOLUME_TYPE_EXTRA_SPECS,
+            to_utf=bytes_body)
+
+    def _test_add_type_access(self):
+        self.check_service_client_function(
+            self.client.add_type_access,
+            'tempest.lib.common.rest_client.RestClient.post',
+            {}, status=202,
+            volume_type_id='6685584b-1eac-4da6-b5c3-555430cf68ff')
+
+    def _test_remove_type_access(self):
+        self.check_service_client_function(
+            self.client.remove_type_access,
+            'tempest.lib.common.rest_client.RestClient.post',
+            {}, status=202,
+            volume_type_id='6685584b-1eac-4da6-b5c3-555430cf68ff')
+
+    def _test_list_type_access(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_type_access,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_VOLUME_TYPE_ACCESS,
+            volume_type_id='3c67e124-39ad-4ace-a507-8bb7bf510c26',
+            to_utf=bytes_body)
+
+    def test_list_volume_types_with_str_body(self):
+        self._test_list_volume_types()
+
+    def test_list_volume_types_with_bytes_body(self):
+        self._test_list_volume_types(bytes_body=True)
+
+    def test_show_volume_type_with_str_body(self):
+        self._test_show_volume_type()
+
+    def test_show_volume_type_with_bytes_body(self):
+        self._test_show_volume_type(bytes_body=True)
+
+    def test_create_volume_type_str_body(self):
+        self._test_create_volume_type()
+
+    def test_create_volume_type_with_bytes_body(self):
+        self._test_create_volume_type(bytes_body=True)
+
+    def test_list_volume_types_extra_specs_with_str_body(self):
+        self._test_list_volume_types_extra_specs()
+
+    def test_list_volume_types_extra_specs_with_bytes_body(self):
+        self._test_list_volume_types_extra_specs(bytes_body=True)
+
+    def test_show_volume_type_extra_specs_with_str_body(self):
+        self._test_show_volume_type_extra_specs()
+
+    def test_show_volume_type_extra_specs_with_bytes_body(self):
+        self._test_show_volume_type_extra_specs(bytes_body=True)
+
+    def test_create_volume_type_extra_specs_with_str_body(self):
+        self._test_create_volume_type_extra_specs()
+
+    def test_create_volume_type_extra_specs_with_bytes_body(self):
+        self._test_create_volume_type_extra_specs(bytes_body=True)
+
+    def test_delete_volume_type_extra_specs(self):
+        self._test_delete_volume_type_extra_specs()
+
+    def test_update_volume_type_with_str_body(self):
+        self._test_update_volume_type()
+
+    def test_update_volume_type_with_bytes_body(self):
+        self._test_update_volume_type(bytes_body=True)
+
+    def test_delete_volume_type(self):
+        self._test_delete_volume_type()
+
+    def test_update_volume_type_extra_specs_with_str_body(self):
+        self._test_update_volume_type_extra_specs()
+
+    def test_update_volume_type_extra_specs_with_bytes_body(self):
+        self._test_update_volume_type_extra_specs(bytes_body=True)
+
+    def test_add_type_access(self):
+        self._test_add_type_access()
+
+    def test_remove_type_access(self):
+        self._test_remove_type_access()
+
+    def test_list_type_access_with_str_body(self):
+        self._test_list_type_access()
+
+    def test_list_type_access_with_bytes_body(self):
+        self._test_list_type_access(bytes_body=True)
diff --git a/tempest/tests/lib/test_decorators.py b/tempest/tests/lib/test_decorators.py
index 0b1a599..3e6160e 100644
--- a/tempest/tests/lib/test_decorators.py
+++ b/tempest/tests/lib/test_decorators.py
@@ -32,9 +32,17 @@
         # By our decorators.attr decorator the attribute __testtools_attrs
         # will be set only for 'type' argument, so we test it first.
         if 'type' in decorator_args:
-            # this is what testtools sets
-            self.assertEqual(getattr(foo, '__testtools_attrs'),
-                             set(expected_attrs))
+            if 'condition' in decorator_args:
+                if decorator_args['condition']:
+                    # The expected attrs should be in the function.
+                    self.assertEqual(set(expected_attrs),
+                                     getattr(foo, '__testtools_attrs'))
+                else:
+                    # The expected attrs should not be in the function.
+                    self.assertNotIn('__testtools_attrs', foo)
+            else:
+                self.assertEqual(set(expected_attrs),
+                                 getattr(foo, '__testtools_attrs'))
 
     def test_attr_without_type(self):
         self._test_attr_helper(expected_attrs='baz', bar='baz')
@@ -50,6 +58,13 @@
     def test_attr_decorator_with_duplicated_type(self):
         self._test_attr_helper(expected_attrs=['foo'], type=['foo', 'foo'])
 
+    def test_attr_decorator_condition_false(self):
+        self._test_attr_helper(None, type='slow', condition=False)
+
+    def test_attr_decorator_condition_true(self):
+        self._test_attr_helper(expected_attrs=['slow'], type='slow',
+                               condition=True)
+
 
 class TestSkipBecauseDecorator(base.TestCase):
     def _test_skip_because_helper(self, expected_to_skip=True,
diff --git a/tempest/tests/test_hacking.py b/tempest/tests/test_hacking.py
index 9534ce8..83c1abb 100644
--- a/tempest/tests/test_hacking.py
+++ b/tempest/tests/test_hacking.py
@@ -48,6 +48,7 @@
     just assertTrue if the check is expected to fail and assertFalse if it
     should pass.
     """
+
     def test_no_setup_teardown_class_for_tests(self):
         self.assertTrue(checks.no_setup_teardown_class_for_tests(
             "  def setUpClass(cls):", './tempest/tests/fake_test.py'))
diff --git a/tools/format.sh b/tools/format.sh
new file mode 100755
index 0000000..adffb8c
--- /dev/null
+++ b/tools/format.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+cd $(dirname "$(readlink -f "$0")")
+
+autopep8 --exit-code --max-line-length=79 --experimental --in-place -r ../tempest ../setup.py && echo Formatting was not needed. >&2
+
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index 4eb78fb..3772774 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -74,7 +74,8 @@
 # Gerrit prepends 4 garbage octets to the JSON, in order to counter
 # cross-site scripting attacks.  Therefore we must discard it so the
 # json library won't choke.
-projects = sorted(filter(is_in_openstack_namespace, json.loads(r.read()[4:])))
+content = r.read().decode('utf-8')[4:]
+projects = sorted(filter(is_in_openstack_namespace, json.loads(content)))
 
 # Retrieve projects having no deb, puppet, ui or spec namespace as those
 # namespaces do not contains tempest plugins.
diff --git a/tools/generate-tempest-plugins-list.sh b/tools/generate-tempest-plugins-list.sh
index b27b23a..17a4059 100755
--- a/tools/generate-tempest-plugins-list.sh
+++ b/tools/generate-tempest-plugins-list.sh
@@ -41,8 +41,6 @@
 set -ex
 
 (
-declare -A plugins
-
 if [[ -r doc/source/data/tempest-plugins-registry.header ]]; then
     cat doc/source/data/tempest-plugins-registry.header
 fi
@@ -71,7 +69,7 @@
 i=0
 for plugin in ${sorted_plugins}; do
     i=$((i+1))
-    giturl="git://git.openstack.org/openstack/${plugin}"
+    giturl="https://git.openstack.org/openstack/${plugin}"
     gitlink="https://git.openstack.org/cgit/openstack/${plugin}"
     printf "%-3s %-${name_col_len}s %s\n" "$i" "${plugin}" "\`${giturl} <${gitlink}>\`__"
 done
diff --git a/tools/tempest-plugin-sanity.sh b/tools/tempest-plugin-sanity.sh
index 8b4f913..703dce2 100644
--- a/tools/tempest-plugin-sanity.sh
+++ b/tools/tempest-plugin-sanity.sh
@@ -18,20 +18,17 @@
 # This script is intended to check the sanity of tempest plugins against
 # tempest master.
 # What it does:
-# * Creates the virtualenv
-# * Install tempest
 # * Retrieve the project lists having tempest plugin if project name is
 #   given.
-# * For each project in a list, It does:
+# * For each project in a list, it does:
+#   * Create virtualenv and install tempest in it
 #   * Clone the Project
 #   * Install the Project and also installs dependencies from
 #     test-requirements.txt.
 #   * Create Tempest workspace
 #   * List tempest plugins
 #   * List tempest plugins tests
-#   * Uninstall the project and its dependencies
-#   * Again Install tempest
-#   * Again repeat the step from cloning project
+#   * Delete virtualenv and project repo
 #
 # If one of the step fails, The script will exit with failure.
 
@@ -46,31 +43,64 @@
 
 # retrieve a list of projects having tempest plugins
 PROJECT_LIST="$(python tools/generate-tempest-plugins-list.py)"
-# List of projects having tempest plugin stale or unmaintained from long time
-BLACKLIST="trio2o"
+# List of projects having tempest plugin stale or unmaintained for a long time
+# (6 months or more)
+# TODO(masayukig): Some of these can be removed from BLACKLIST in the future.
+# airship-tempest-plugin: https://review.openstack.org/#/c/634387/
+# barbican-tempest-plugin: https://review.openstack.org/#/c/634631/
+# intel-nfv-ci-tests: https://review.openstack.org/#/c/634640/
+# networking-ansible: https://review.openstack.org/#/c/634647/
+# networking-generic-switch: https://review.openstack.org/#/c/634846/
+# networking-l2gw-tempest-plugin: https://review.openstack.org/#/c/635093/
+# networking-midonet: https://review.openstack.org/#/c/635096/
+# networking-plumgrid: https://review.openstack.org/#/c/635096/
+# networking-spp: https://review.openstack.org/#/c/635098/
+# neutron-dynamic-routing: https://review.openstack.org/#/c/637718/
+# neutron-vpnaas: https://review.openstack.org/#/c/637719/
+# nova-lxd: https://review.openstack.org/#/c/638334/
+# valet: https://review.openstack.org/#/c/638339/
+# vitrage-tempest-plugin: https://review.openstack.org/#/c/639003/
+BLACKLIST="
+airship-tempest-plugin
+barbican-tempest-plugin
+intel-nfv-ci-tests
+networking-ansible
+networking-generic-switch
+networking-l2gw-tempest-plugin
+networking-midonet
+networking-plumgrid
+networking-spp
+neutron-dynamic-routing
+neutron-vpnaas
+nova-lxd
+valet
+vitrage-tempest-plugin
+"
 
 # Function to clone project using zuul-cloner or from git
 function clone_project() {
     if [ -e /usr/zuul-env/bin/zuul-cloner ]; then
         /usr/zuul-env/bin/zuul-cloner --cache-dir /opt/git \
-        git://git.openstack.org \
+        https://git.openstack.org \
         openstack/"$1"
 
     elif [ -e /usr/bin/git ]; then
-        /usr/bin/git clone git://git.openstack.org/openstack/"$1" \
+        /usr/bin/git clone https://git.openstack.org/openstack/"$1" \
         openstack/"$1"
 
     fi
 }
 
-# Create virtualenv to perform sanity operation
-SANITY_DIR=$(pwd)
-virtualenv "$SANITY_DIR"/.venv
-export TVENV="$SANITY_DIR/tools/with_venv.sh"
-cd "$SANITY_DIR"
+# function to create virtualenv to perform sanity operation
+function prepare_workspace() {
+    SANITY_DIR=$(pwd)
+    virtualenv --clear "$SANITY_DIR"/.venv
+    export TVENV="$SANITY_DIR/tools/with_venv.sh"
+    cd "$SANITY_DIR"
 
-# Install tempest in a venv
-"$TVENV" pip install .
+    # Install tempest with test dependencies in a venv
+    "$TVENV" pip install -e . -r test-requirements.txt
+}
 
 # Function to install project
 function install_project() {
@@ -83,30 +113,31 @@
 
 # Function to perform sanity checking on Tempest plugin
 function tempest_sanity() {
-    "$TVENV" tempest init "$SANITY_DIR"/tempest_sanity
-    cd "$SANITY_DIR"/tempest_sanity
-    "$TVENV" tempest list-plugins
+    "$TVENV" tempest init "$SANITY_DIR"/tempest_sanity && \
+    cd "$SANITY_DIR"/tempest_sanity && \
+    "$TVENV" tempest list-plugins && \
     "$TVENV" tempest run -l
+    retval=$?
     # Delete tempest workspace
+    # NOTE: Cleaning should be done even if an error occurs.
     "$TVENV" tempest workspace remove --name tempest_sanity --rmdir
     cd "$SANITY_DIR"
-}
-
-# Function to uninstall project
-function uninstall_project() {
-    "$TVENV" pip uninstall -y "$SANITY_DIR"/openstack/"$1"
-    # Check for *requirements.txt file in a project then uninstall it.
-    if [ -e "$SANITY_DIR"/openstack/"$1"/*requirements.txt ]; then
-        "$TVENV" pip uninstall -y -r "$SANITY_DIR"/openstack/"$1"/*requirements.txt
-    fi
+    # Remove the sanity workspace in case of remaining
+    rm -fr "$SANITY_DIR"/tempest_sanity
     # Remove the project directory after sanity run
     rm -fr "$SANITY_DIR"/openstack/"$1"
+
+    return $retval
 }
 
 # Function to run sanity check on each project
 function plugin_sanity_check() {
-        clone_project "$1"  &&  install_project "$1"  &&  tempest_sanity "$1" \
-        &&  uninstall_project "$1"  &&  "$TVENV" pip install .
+    prepare_workspace && \
+    clone_project "$1" && \
+    install_project "$1" && \
+    tempest_sanity "$1"
+
+    return $?
 }
 
 # Log status
@@ -117,11 +148,12 @@
     # Remove blacklisted tempest plugins
     if ! [[ `echo $BLACKLIST | grep -c $project ` -gt 0 ]]; then
         plugin_sanity_check $project && passed_plugin+=", $project" || \
-        failed_plugin+=", $project"
+        failed_plugin+="$project, " > $SANITY_DIR/$project.txt
     fi
 done
 
 # Check for failed status
 if [[ -n $failed_plugin ]]; then
+    echo "Failed Plugins: $failed_plugin"
     exit 1
 fi
diff --git a/tox.ini b/tox.ini
index c0eb416..433f168 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = pep8,py36,py27,pip-check-reqs
+envlist = pep8,py36,py37,py27,pip-check-reqs
 minversion = 2.3.1
 skipsdist = True
 
@@ -20,7 +20,7 @@
     OS_STDERR_CAPTURE=1
     OS_TEST_TIMEOUT=160
     PYTHONWARNINGS=default::DeprecationWarning,ignore::DeprecationWarning:distutils,ignore::DeprecationWarning:site
-passenv = OS_STDOUT_CAPTURE OS_STDERR_CAPTURE OS_TEST_TIMEOUT OS_TEST_LOCK_PATH TEMPEST_CONFIG TEMPEST_CONFIG_DIR http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION GENERATE_TEMPEST_PLUGIN_LIST
+passenv = OS_STDOUT_CAPTURE OS_STDERR_CAPTURE OS_TEST_TIMEOUT OS_TEST_LOCK_PATH TEMPEST_CONFIG TEMPEST_CONFIG_DIR http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION GENERATE_TEMPEST_PLUGIN_LIST GABBI_TEMPEST_PATH
 usedevelop = True
 install_command = pip install {opts} {packages}
 whitelist_externals = *
@@ -48,6 +48,10 @@
   coverage xml -o cover/coverage.xml
   coverage report
 
+[testenv:debug]
+basepython = python3
+commands = oslo_debug_helper -t tempest/tests {posargs}
+
 [testenv:all]
 envdir = .tox/tempest
 sitepackages = {[tempestenv]sitepackages}
@@ -193,11 +197,21 @@
 whitelist_externals = rm
 
 [testenv:pep8]
+deps =
+    -r test-requirements.txt
+    autopep8
 basepython = python3
 commands =
+    autopep8 --exit-code --max-line-length=79 --experimental --diff -r tempest setup.py
     flake8 {posargs}
     check-uuid
 
+[testenv:autopep8]
+deps = autopep8
+basepython = python3
+commands =
+    autopep8 --max-line-length=79  --experimental --in-place -r tempest setup.py
+
 [testenv:uuidgen]
 commands =
     check-uuid --fix