Merge "Fix a typo of a missing letter"
diff --git a/data/tempest-plugins-registry.header b/doc/source/data/tempest-plugins-registry.header
similarity index 100%
rename from data/tempest-plugins-registry.header
rename to doc/source/data/tempest-plugins-registry.header
diff --git a/doc/source/library.rst b/doc/source/library.rst
index a461a0f..074d642 100644
--- a/doc/source/library.rst
+++ b/doc/source/library.rst
@@ -69,3 +69,4 @@
    library/auth
    library/clients
    library/credential_providers
+   library/validation_resources
diff --git a/doc/source/library/validation_resources.rst b/doc/source/library/validation_resources.rst
new file mode 100644
index 0000000..9b36476
--- /dev/null
+++ b/doc/source/library/validation_resources.rst
@@ -0,0 +1,11 @@
+.. _validation_resources:
+
+Validation Resources
+====================
+
+-------------------------------
+The validation_resources module
+-------------------------------
+
+.. automodule:: tempest.lib.common.validation_resources
+   :members:
diff --git a/releasenotes/notes/add-validation-resources-to-lib-dc2600c4324ca4d7.yaml b/releasenotes/notes/add-validation-resources-to-lib-dc2600c4324ca4d7.yaml
new file mode 100644
index 0000000..7814f4e
--- /dev/null
+++ b/releasenotes/notes/add-validation-resources-to-lib-dc2600c4324ca4d7.yaml
@@ -0,0 +1,7 @@
+---
+features:
+  - |
+    Add the `validation_resources` module to tempest.lib. The module provides
+    a set of helpers that can be used to provision and cleanup all the
+    resources required to perform ping / ssh tests against a virtual machine:
+    a keypair, a security group with targeted rules and a floating IP.
diff --git a/requirements.txt b/requirements.txt
index 36b9efa..16223d6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,23 +3,23 @@
 # process, which may cause wedges in the gate later.
 pbr!=2.1.0,>=2.0.0 # Apache-2.0
 cliff>=2.8.0 # Apache-2.0
-jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT
+jsonschema<3.0.0,>=2.6.0 # MIT
 testtools>=1.4.0 # MIT
 paramiko>=2.0.0 # LGPLv2.1+
-netaddr!=0.7.16,>=0.7.13 # BSD
+netaddr>=0.7.18 # BSD
 testrepository>=0.0.18 # Apache-2.0/BSD
-oslo.concurrency>=3.8.0 # Apache-2.0
-oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
+oslo.concurrency>=3.20.0 # Apache-2.0
+oslo.config>=4.6.0 # Apache-2.0
 oslo.log>=3.30.0 # Apache-2.0
-oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0
-oslo.utils>=3.20.0 # Apache-2.0
+oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
+oslo.utils>=3.28.0 # Apache-2.0
 six>=1.9.0 # MIT
 fixtures>=3.0.0 # Apache-2.0/BSD
 PyYAML>=3.10 # MIT
 python-subunit>=0.0.18 # Apache-2.0/BSD
 stevedore>=1.20.0 # Apache-2.0
 PrettyTable<0.8,>=0.7.1 # BSD
-os-testr>=0.8.0 # Apache-2.0
+os-testr>=1.0.0 # Apache-2.0
 urllib3>=1.21.1 # MIT
 debtcollector>=1.2.0 # Apache-2.0
 unittest2 # BSD
diff --git a/tempest/api/compute/admin/test_create_server.py b/tempest/api/compute/admin/test_create_server.py
index 66bedd9..08b2d19 100644
--- a/tempest/api/compute/admin/test_create_server.py
+++ b/tempest/api/compute/admin/test_create_server.py
@@ -17,8 +17,10 @@
 
 from tempest.api.compute import base
 from tempest.common.utils.linux import remote_client
+from tempest.common import waiters
 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
 
 CONF = config.CONF
@@ -35,12 +37,6 @@
         super(ServersWithSpecificFlavorTestJSON, cls).setup_clients()
         cls.client = cls.servers_client
 
-    @classmethod
-    def resource_setup(cls):
-        cls.set_validation_resources()
-
-        super(ServersWithSpecificFlavorTestJSON, cls).resource_setup()
-
     @decorators.idempotent_id('b3c7bcfc-bb5b-4e22-b517-c7f686b802ca')
     @testtools.skipUnless(CONF.validation.run_validation,
                           'Instance validation tests are disabled.')
@@ -67,20 +63,30 @@
 
         admin_pass = self.image_ssh_password
 
+        validation_resources = self.get_test_validation_resources(
+            self.os_primary)
         server_no_eph_disk = self.create_test_server(
             validatable=True,
+            validation_resources=validation_resources,
             wait_until='ACTIVE',
             adminPass=admin_pass,
             flavor=flavor_no_eph_disk_id)
 
+        self.addCleanup(waiters.wait_for_server_termination,
+                        self.servers_client, server_no_eph_disk['id'])
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.servers_client.delete_server,
+                        server_no_eph_disk['id'])
+
         # Get partition number of server without ephemeral disk.
         server_no_eph_disk = self.client.show_server(
             server_no_eph_disk['id'])['server']
         linux_client = remote_client.RemoteClient(
-            self.get_server_ip(server_no_eph_disk),
+            self.get_server_ip(server_no_eph_disk,
+                               validation_resources),
             self.ssh_user,
             admin_pass,
-            self.validation_resources['keypair']['private_key'],
+            validation_resources['keypair']['private_key'],
             server=server_no_eph_disk,
             servers_client=self.client)
         disks_num = len(linux_client.get_disks().split('\n'))
@@ -90,17 +96,25 @@
 
         server_with_eph_disk = self.create_test_server(
             validatable=True,
+            validation_resources=validation_resources,
             wait_until='ACTIVE',
             adminPass=admin_pass,
             flavor=flavor_with_eph_disk_id)
 
+        self.addCleanup(waiters.wait_for_server_termination,
+                        self.servers_client, server_with_eph_disk['id'])
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.servers_client.delete_server,
+                        server_with_eph_disk['id'])
+
         server_with_eph_disk = self.client.show_server(
             server_with_eph_disk['id'])['server']
         linux_client = remote_client.RemoteClient(
-            self.get_server_ip(server_with_eph_disk),
+            self.get_server_ip(server_with_eph_disk,
+                               validation_resources),
             self.ssh_user,
             admin_pass,
-            self.validation_resources['keypair']['private_key'],
+            validation_resources['keypair']['private_key'],
             server=server_with_eph_disk,
             servers_client=self.client)
         disks_num_eph = len(linux_client.get_disks().split('\n'))
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 47c5882..1a31723 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -187,7 +187,7 @@
 
     @classmethod
     def create_test_server(cls, validatable=False, volume_backed=False,
-                           **kwargs):
+                           validation_resources=None, **kwargs):
         """Wrapper utility that returns a test server.
 
         This wrapper utility calls the common create test server and
@@ -197,6 +197,10 @@
 
         :param validatable: Whether the server will be pingable or sshable.
         :param volume_backed: Whether the instance is volume backed or not.
+        :param validation_resources: Dictionary of validation resources as
+            returned by `get_class_validation_resources`.
+        :param kwargs: Extra arguments are passed down to the
+            `compute.create_test_server` call.
         """
         if 'name' not in kwargs:
             kwargs['name'] = data_utils.rand_name(cls.__name__ + "-server")
@@ -213,7 +217,7 @@
         body, servers = compute.create_test_server(
             cls.os_primary,
             validatable,
-            validation_resources=cls.validation_resources,
+            validation_resources=validation_resources,
             tenant_network=tenant_network,
             volume_backed=volume_backed,
             **kwargs)
@@ -325,13 +329,33 @@
 
     @classmethod
     def rebuild_server(cls, server_id, validatable=False, **kwargs):
-        # Destroy an existing server and creates a new one
+        """Destroy an existing class level server and creates a new one
+
+        Some test classes use a test server that can be used by multiple
+        tests. This is done to optimise runtime and test load.
+        If something goes wrong with the test server, it can be rebuilt
+        using this helper.
+
+        This helper can also be used for the initial provisioning if no
+        server_id is specified.
+
+        :param server_id: UUID of the server to be rebuilt. If None is
+            specified, a new server is provisioned.
+        :param validatable: whether to the server needs to be
+            validatable. When True, validation resources are acquired via
+            the `get_class_validation_resources` helper.
+        :param kwargs: extra paramaters are passed through to the
+            `create_test_server` call.
+        :return: the UUID of the created server.
+        """
         if server_id:
             cls.delete_server(server_id)
 
         cls.password = data_utils.rand_password()
         server = cls.create_test_server(
             validatable,
+            validation_resources=cls.get_class_validation_resources(
+                cls.os_primary),
             wait_until='ACTIVE',
             adminPass=cls.password,
             **kwargs)
@@ -362,14 +386,23 @@
         cls._delete_volume(cls.volumes_client, volume_id)
 
     @classmethod
-    def get_server_ip(cls, server):
+    def get_server_ip(cls, server, validation_resources=None):
         """Get the server fixed or floating IP.
 
         Based on the configuration we're in, return a correct ip
         address for validating that a guest is up.
+
+        :param server: The server dict as returned by the API
+        :param validation_resources: The dict of validation resources
+            provisioned for the server.
         """
         if CONF.validation.connect_method == 'floating':
-            return cls.validation_resources['floating_ip']['ip']
+            if validation_resources:
+                return validation_resources['floating_ip']['ip']
+            else:
+                msg = ('When validation.connect_method equals floating, '
+                       'validation_resources cannot be None')
+                raise exceptions.InvalidParam(invalid_param=msg)
         elif CONF.validation.connect_method == 'fixed':
             addresses = server['addresses'][CONF.validation.network_for_ssh]
             for address in addresses:
diff --git a/tempest/api/compute/images/test_image_metadata.py b/tempest/api/compute/images/test_image_metadata.py
index c1faa4b..b497626 100644
--- a/tempest/api/compute/images/test_image_metadata.py
+++ b/tempest/api/compute/images/test_image_metadata.py
@@ -72,7 +72,7 @@
         body = body['image'] if 'image' in body else body
         cls.image_id = body['id']
         cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
-                                    cls.compute_images_client.delete_image,
+                                    cls.glance_client.delete_image,
                                     cls.image_id)
         image_file = six.BytesIO((b'*' * 1024))
         if CONF.image_feature_enabled.api_v1:
diff --git a/tempest/api/compute/servers/test_create_server.py b/tempest/api/compute/servers/test_create_server.py
index d8ce7ea..c660821 100644
--- a/tempest/api/compute/servers/test_create_server.py
+++ b/tempest/api/compute/servers/test_create_server.py
@@ -42,8 +42,9 @@
 
     @classmethod
     def resource_setup(cls):
-        cls.set_validation_resources()
         super(ServersTestJSON, cls).resource_setup()
+        validation_resources = cls.get_class_validation_resources(
+            cls.os_primary)
         cls.meta = {'hello': 'world'}
         cls.accessIPv4 = '1.1.1.1'
         cls.accessIPv6 = '0000:0000:0000:0000:0000:babe:220.12.22.2'
@@ -52,6 +53,7 @@
         disk_config = cls.disk_config
         server_initial = cls.create_test_server(
             validatable=True,
+            validation_resources=validation_resources,
             wait_until='ACTIVE',
             name=cls.name,
             metadata=cls.meta,
@@ -105,11 +107,13 @@
         # Verify that the number of vcpus reported by the instance matches
         # the amount stated by the flavor
         flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
+        validation_resources = self.get_class_validation_resources(
+            self.os_primary)
         linux_client = remote_client.RemoteClient(
-            self.get_server_ip(self.server),
+            self.get_server_ip(self.server, validation_resources),
             self.ssh_user,
             self.password,
-            self.validation_resources['keypair']['private_key'],
+            validation_resources['keypair']['private_key'],
             server=self.server,
             servers_client=self.client)
         output = linux_client.exec_command('grep -c ^processor /proc/cpuinfo')
@@ -120,11 +124,13 @@
                           'Instance validation tests are disabled.')
     def test_host_name_is_same_as_server_name(self):
         # Verify the instance host name is the same as the server name
+        validation_resources = self.get_class_validation_resources(
+            self.os_primary)
         linux_client = remote_client.RemoteClient(
-            self.get_server_ip(self.server),
+            self.get_server_ip(self.server, validation_resources),
             self.ssh_user,
             self.password,
-            self.validation_resources['keypair']['private_key'],
+            validation_resources['keypair']['private_key'],
             server=self.server,
             servers_client=self.client)
         hostname = linux_client.exec_command("hostname").rstrip()
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index dbf6713..a126fd6 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -66,11 +66,6 @@
                                   dhcp=True)
         super(DeviceTaggingTest, cls).setup_credentials()
 
-    @classmethod
-    def resource_setup(cls):
-        cls.set_validation_resources()
-        super(DeviceTaggingTest, cls).resource_setup()
-
     def verify_device_metadata(self, md_json):
         md_dict = json.loads(md_json)
         for d in md_dict['devices']:
@@ -139,9 +134,12 @@
         # Create server
         admin_pass = data_utils.rand_password()
         config_drive_enabled = CONF.compute_feature_enabled.config_drive
+        validation_resources = self.get_test_validation_resources(
+            self.os_primary)
 
         server = self.create_test_server(
             validatable=True,
+            validation_resources=validation_resources,
             config_drive=config_drive_enabled,
             adminPass=admin_pass,
             name=data_utils.rand_name('device-tagging-server'),
@@ -208,10 +206,10 @@
         self.addCleanup(self.delete_server, server['id'])
 
         self.ssh_client = remote_client.RemoteClient(
-            self.get_server_ip(server),
+            self.get_server_ip(server, validation_resources),
             CONF.validation.image_ssh_user,
             admin_pass,
-            self.validation_resources['keypair']['private_key'],
+            validation_resources['keypair']['private_key'],
             server=server,
             servers_client=self.servers_client)
 
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index b5fc39c..4cfc665 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -44,8 +44,13 @@
                                            self.server_id, 'ACTIVE')
         except lib_exc.NotFound:
             # The server was deleted by previous test, create a new one
+            # Use class level validation resources to avoid them being
+            # deleted once a test is over
+            validation_resources = self.get_class_validation_resources(
+                self.os_primary)
             server = self.create_test_server(
                 validatable=True,
+                validation_resources=validation_resources,
                 wait_until='ACTIVE')
             self.__class__.server_id = server['id']
         except Exception:
@@ -69,8 +74,6 @@
 
     @classmethod
     def resource_setup(cls):
-        cls.set_validation_resources()
-
         super(ServerActionsTestJSON, cls).resource_setup()
         cls.server_id = cls.rebuild_server(None, validatable=True)
 
@@ -80,8 +83,11 @@
     def test_change_server_password(self):
         # Since this test messes with the password and makes the
         # server unreachable, it should create its own server
+        validation_resources = self.get_test_validation_resources(
+            self.os_primary)
         newserver = self.create_test_server(
             validatable=True,
+            validation_resources=validation_resources,
             wait_until='ACTIVE')
         # The server's password should be set to the provided password
         new_password = 'Newpass1234'
@@ -92,7 +98,7 @@
             # Verify that the user can authenticate with the new password
             server = self.client.show_server(newserver['id'])['server']
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server),
+                self.get_server_ip(server, validation_resources),
                 self.ssh_user,
                 new_password,
                 server=server,
@@ -101,13 +107,15 @@
 
     def _test_reboot_server(self, reboot_type):
         if CONF.validation.run_validation:
+            validation_resources = self.get_class_validation_resources(
+                self.os_primary)
             # Get the time the server was last rebooted,
             server = self.client.show_server(self.server_id)['server']
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server),
+                self.get_server_ip(server, validation_resources),
                 self.ssh_user,
                 self.password,
-                self.validation_resources['keypair']['private_key'],
+                validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.client)
             boot_time = linux_client.get_boot_time()
@@ -122,10 +130,10 @@
         if CONF.validation.run_validation:
             # Log in and verify the boot time has changed
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server),
+                self.get_server_ip(server, validation_resources),
                 self.ssh_user,
                 self.password,
-                self.validation_resources['keypair']['private_key'],
+                validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.client)
             new_boot_time = linux_client.get_boot_time()
@@ -201,6 +209,8 @@
         self.assertEqual(original_addresses, server['addresses'])
 
         if CONF.validation.run_validation:
+            validation_resources = self.get_class_validation_resources(
+                self.os_primary)
             # Authentication is attempted in the following order of priority:
             # 1.The key passed in, if one was passed in.
             # 2.Any key we can find through an SSH agent (if allowed).
@@ -208,10 +218,10 @@
             #   ~/.ssh/ (if allowed).
             # 4.Plain username/password auth, if a password was given.
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(rebuilt_server),
+                self.get_server_ip(rebuilt_server, validation_resources),
                 self.ssh_user,
                 password,
-                self.validation_resources['keypair']['private_key'],
+                validation_resources['keypair']['private_key'],
                 server=rebuilt_server,
                 servers_client=self.client)
             linux_client.validate_authentication()
diff --git a/tempest/api/compute/servers/test_server_personality.py b/tempest/api/compute/servers/test_server_personality.py
index 90b9da4..2f0f5ee 100644
--- a/tempest/api/compute/servers/test_server_personality.py
+++ b/tempest/api/compute/servers/test_server_personality.py
@@ -20,6 +20,7 @@
 from tempest.common import waiters
 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
 
@@ -34,11 +35,6 @@
         super(ServerPersonalityTestJSON, cls).setup_credentials()
 
     @classmethod
-    def resource_setup(cls):
-        cls.set_validation_resources()
-        super(ServerPersonalityTestJSON, cls).resource_setup()
-
-    @classmethod
     def skip_checks(cls):
         super(ServerPersonalityTestJSON, cls).skip_checks()
         if not CONF.compute_feature_enabled.personality:
@@ -57,16 +53,23 @@
         personality = [{'path': file_path,
                         'contents': base64.encode_as_text(file_contents)}]
         password = data_utils.rand_password()
-        created_server = self.create_test_server(personality=personality,
-                                                 adminPass=password,
-                                                 wait_until='ACTIVE',
-                                                 validatable=True)
+        validation_resources = self.get_test_validation_resources(
+            self.os_primary)
+        created_server = self.create_test_server(
+            personality=personality, adminPass=password, wait_until='ACTIVE',
+            validatable=True,
+            validation_resources=validation_resources)
+        self.addCleanup(waiters.wait_for_server_termination,
+                        self.servers_client, created_server['id'])
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.servers_client.delete_server,
+                        created_server['id'])
         server = self.client.show_server(created_server['id'])['server']
         if CONF.validation.run_validation:
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server),
+                self.get_server_ip(server, validation_resources),
                 self.ssh_user, password,
-                self.validation_resources['keypair']['private_key'],
+                validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.client)
             self.assertEqual(file_contents,
@@ -75,8 +78,16 @@
 
     @decorators.idempotent_id('128966d8-71fc-443c-8cab-08e24114ecc9')
     def test_rebuild_server_with_personality(self):
-        server = self.create_test_server(wait_until='ACTIVE', validatable=True)
+        validation_resources = self.get_test_validation_resources(
+            self.os_primary)
+        server = self.create_test_server(
+            wait_until='ACTIVE', validatable=True,
+            validation_resources=validation_resources)
         server_id = server['id']
+        self.addCleanup(waiters.wait_for_server_termination,
+                        self.servers_client, server_id)
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.servers_client.delete_server, server_id)
         file_contents = 'Test server rebuild.'
         personality = [{'path': 'rebuild.txt',
                         'contents': base64.encode_as_text(file_contents)}]
@@ -126,16 +137,22 @@
                 'contents': base64.encode_as_text(file_contents + str(i)),
             })
         password = data_utils.rand_password()
-        created_server = self.create_test_server(personality=person,
-                                                 adminPass=password,
-                                                 wait_until='ACTIVE',
-                                                 validatable=True)
+        validation_resources = self.get_test_validation_resources(
+            self.os_primary)
+        created_server = self.create_test_server(
+            personality=person, adminPass=password, wait_until='ACTIVE',
+            validatable=True, validation_resources=validation_resources)
+        self.addCleanup(waiters.wait_for_server_termination,
+                        self.servers_client, created_server['id'])
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.servers_client.delete_server,
+                        created_server['id'])
         server = self.client.show_server(created_server['id'])['server']
         if CONF.validation.run_validation:
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server),
+                self.get_server_ip(server, validation_resources),
                 self.ssh_user, password,
-                self.validation_resources['keypair']['private_key'],
+                validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.client)
             for i in person:
diff --git a/tempest/api/compute/volumes/test_attach_volume.py b/tempest/api/compute/volumes/test_attach_volume.py
index e0fed58..9bef80f 100644
--- a/tempest/api/compute/volumes/test_attach_volume.py
+++ b/tempest/api/compute/volumes/test_attach_volume.py
@@ -40,35 +40,37 @@
 
     @classmethod
     def resource_setup(cls):
-        cls.set_validation_resources()
         super(AttachVolumeTestJSON, cls).resource_setup()
         cls.device = CONF.compute.volume_device_name
 
     def _create_server(self):
         # Start a server and wait for it to become ready
+        validation_resources = self.get_test_validation_resources(
+            self.os_primary)
         server = self.create_test_server(
             validatable=True,
+            validation_resources=validation_resources,
             wait_until='ACTIVE',
             adminPass=self.image_ssh_password)
         self.addCleanup(self.delete_server, server['id'])
         # Record addresses so that we can ssh later
         server['addresses'] = self.servers_client.list_addresses(
             server['id'])['addresses']
-        return server
+        return server, validation_resources
 
     @decorators.idempotent_id('52e9045a-e90d-4c0d-9087-79d657faffff')
     def test_attach_detach_volume(self):
         # Stop and Start a server with an attached volume, ensuring that
         # the volume remains attached.
-        server = self._create_server()
+        server, validation_resources = self._create_server()
 
         # NOTE(andreaf) Create one remote client used throughout the test.
         if CONF.validation.run_validation:
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server),
+                self.get_server_ip(server, validation_resources),
                 self.image_ssh_user,
                 self.image_ssh_password,
-                self.validation_resources['keypair']['private_key'],
+                validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.servers_client)
             # NOTE(andreaf) We need to ensure the ssh key has been
@@ -111,7 +113,7 @@
     @decorators.idempotent_id('7fa563fe-f0f7-43eb-9e22-a1ece036b513')
     def test_list_get_volume_attachments(self):
         # List volume attachment of the server
-        server = self._create_server()
+        server, _ = self._create_server()
         volume_1st = self.create_volume()
         attachment_1st = self.attach_volume(server, volume_1st,
                                             device=('/dev/%s' % self.device))
@@ -163,15 +165,15 @@
         if not CONF.compute_feature_enabled.shelve:
             raise cls.skipException('Shelve is not available.')
 
-    def _count_volumes(self, server):
+    def _count_volumes(self, server, validation_resources):
         # Count number of volumes on an instance
         volumes = 0
         if CONF.validation.run_validation:
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server),
+                self.get_server_ip(server, validation_resources),
                 self.image_ssh_user,
                 self.image_ssh_password,
-                self.validation_resources['keypair']['private_key'],
+                validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.servers_client)
 
@@ -179,7 +181,7 @@
             volumes = int(linux_client.exec_command(command).strip())
         return volumes
 
-    def _shelve_server(self, server):
+    def _shelve_server(self, server, validation_resources):
         # NOTE(andreaf) If we are going to shelve a server, we should
         # check first whether the server is ssh-able. Otherwise we
         # won't be able to distinguish failures introduced by shelve
@@ -188,10 +190,10 @@
         # avoid breaking the VM
         if CONF.validation.run_validation:
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server),
+                self.get_server_ip(server, validation_resources),
                 self.image_ssh_user,
                 self.image_ssh_password,
-                self.validation_resources['keypair']['private_key'],
+                validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.servers_client)
             linux_client.validate_authentication()
@@ -199,30 +201,34 @@
         # If validation went ok, or it was skipped, shelve the server
         compute.shelve_server(self.servers_client, server['id'])
 
-    def _unshelve_server_and_check_volumes(self, server, number_of_volumes):
+    def _unshelve_server_and_check_volumes(self, server,
+                                           validation_resources,
+                                           number_of_volumes):
         # Unshelve the instance and check that there are expected volumes
         self.servers_client.unshelve_server(server['id'])
         waiters.wait_for_server_status(self.servers_client,
                                        server['id'],
                                        'ACTIVE')
         if CONF.validation.run_validation:
-            counted_volumes = self._count_volumes(server)
+            counted_volumes = self._count_volumes(
+                server, validation_resources)
             self.assertEqual(number_of_volumes, counted_volumes)
 
     @decorators.idempotent_id('13a940b6-3474-4c3c-b03f-29b89112bfee')
     def test_attach_volume_shelved_or_offload_server(self):
         # Create server, count number of volumes on it, shelve
         # server and attach pre-created volume to shelved server
-        server = self._create_server()
+        server, validation_resources = self._create_server()
         volume = self.create_volume()
-        num_vol = self._count_volumes(server)
-        self._shelve_server(server)
+        num_vol = self._count_volumes(server, validation_resources)
+        self._shelve_server(server, validation_resources)
         attachment = self.attach_volume(server, volume,
                                         device=('/dev/%s' % self.device),
                                         check_reserved=True)
 
         # Unshelve the instance and check that attached volume exists
-        self._unshelve_server_and_check_volumes(server, num_vol + 1)
+        self._unshelve_server_and_check_volumes(
+            server, validation_resources, num_vol + 1)
 
         # Get volume attachment of the server
         volume_attachment = self.servers_client.show_volume_attachment(
@@ -238,10 +244,10 @@
     def test_detach_volume_shelved_or_offload_server(self):
         # Count number of volumes on instance, shelve
         # server and attach pre-created volume to shelved server
-        server = self._create_server()
+        server, validation_resources = self._create_server()
         volume = self.create_volume()
-        num_vol = self._count_volumes(server)
-        self._shelve_server(server)
+        num_vol = self._count_volumes(server, validation_resources)
+        self._shelve_server(server, validation_resources)
 
         # Attach and then detach the volume
         self.attach_volume(server, volume, device=('/dev/%s' % self.device),
@@ -252,4 +258,5 @@
 
         # Unshelve the instance and check that we have the expected number of
         # volume(s)
-        self._unshelve_server_and_check_volumes(server, num_vol)
+        self._unshelve_server_and_check_volumes(
+            server, validation_resources, num_vol)
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index df0f5a5..86fe3f5 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -128,6 +128,8 @@
                    "this stage.")
             raise ValueError(msg)
 
+        LOG.debug("Provisioning test server with validation resources %s",
+                  validation_resources)
         if 'security_groups' in kwargs:
             kwargs['security_groups'].append(
                 {'name': validation_resources['security_group']['name']})
diff --git a/tempest/common/validation_resources.py b/tempest/lib/common/validation_resources.py
similarity index 97%
rename from tempest/common/validation_resources.py
rename to tempest/lib/common/validation_resources.py
index 0aa5ab0..c35a01a 100644
--- a/tempest/common/validation_resources.py
+++ b/tempest/lib/common/validation_resources.py
@@ -181,6 +181,9 @@
         floating_ip = resources['floating_ip']['ip']
     """
     # Create and Return the validation resources required to validate a VM
+    msg = ('Requested validation resources keypair %s, floating IP %s, '
+           'security group %s')
+    LOG.debug(msg, keypair, floating_ip, security_group)
     validation_data = {}
     try:
         if keypair:
@@ -429,6 +432,9 @@
         self._validation_resources = None
 
     def _setUp(self):
+        msg = ('Requested setup of ValidationResources keypair %s, floating '
+               'IP %s, security group %s')
+        LOG.debug(msg, self._keypair, self._floating_ip, self._security_group)
         self._validation_resources = create_validation_resources(
             self._clients, keypair=self._keypair,
             floating_ip=self._floating_ip,
@@ -441,9 +447,9 @@
         # cleanup here, so we don't need a try-finally around provisioning
         vr = self._validation_resources
         self.addCleanup(clear_validation_resources, self._clients,
-                        keypair=vr['keypair'],
-                        floating_ip=vr['floating_ip'],
-                        security_group=vr['security_group'],
+                        keypair=vr.get('keypair', None),
+                        floating_ip=vr.get('floating_ip', None),
+                        security_group=vr.get('security_group', None),
                         use_neutron=self._use_neutron)
 
     @property
diff --git a/tempest/test.py b/tempest/test.py
index 13a91fd..7d95bcf 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -26,10 +26,10 @@
 from tempest import clients
 from tempest.common import credentials_factory as credentials
 from tempest.common import utils
-import tempest.common.validation_resources as vresources
 from tempest import config
 from tempest.lib.common import cred_client
 from tempest.lib.common import fixed_network
+from tempest.lib.common import validation_resources as vr
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 
@@ -105,13 +105,17 @@
     # a list of roles - the first element of the list being a label, and the
     # rest the actual roles
     credentials = []
-    # Resources required to validate a server using ssh
-    validation_resources = {}
-    network_resources = {}
+
+    # Network resources to be provisioned for the requested test credentials.
+    # Only used with the dynamic credentials provider.
+    _network_resources = {}
 
     # Stack of resource cleanups
     _class_cleanups = []
 
+    # Resources required to validate a server using ssh
+    _validation_resources = {}
+
     # NOTE(sdague): log_format is defined inline here instead of using the oslo
     # default because going through the config path recouples config to the
     # stress tests too early, and depending on testr order will fail unit tests
@@ -126,7 +130,9 @@
 
     @classmethod
     def _reset_class(cls):
+        cls.__setup_credentials_called = False
         cls.__resource_cleaup_called = False
+        cls.__skip_checks_called = False
         cls._class_cleanups = []
 
     @classmethod
@@ -141,10 +147,16 @@
         cls.teardowns = []
         # All the configuration checks that may generate a skip
         cls.skip_checks()
+        if not cls.__skip_checks_called:
+            raise RuntimeError("skip_checks for %s did not call the super's "
+                               "skip_checks" % cls.__name__)
         try:
             # Allocation of all required credentials and client managers
             cls.teardowns.append(('credentials', cls.clear_credentials))
             cls.setup_credentials()
+            if not cls.__setup_credentials_called:
+                raise RuntimeError("setup_credentials for %s did not call the "
+                                   "super's setup_credentials" % cls.__name__)
             # Shortcuts to clients
             cls.setup_clients()
             # Additional class-wide test resources
@@ -233,13 +245,37 @@
         """Class level skip checks.
 
         Subclasses verify in here all conditions that might prevent the
-        execution of the entire test class.
-        Checks implemented here may not make use API calls, and should rely on
-        configuration alone.
-        In general skip checks that require an API call are discouraged.
-        If one is really needed it may be implemented either in the
-        resource_setup or at test level.
+        execution of the entire test class. Skipping here prevents any other
+        class fixture from being executed i.e. no credentials or other
+        resource allocation will happen.
+
+        Tests defined in the test class will no longer appear in test results.
+        The `setUpClass` for the entire test class will be marked as SKIPPED
+        instead.
+
+        At this stage no test credentials are available, so skip checks
+        should rely on configuration alone. This is deliberate since skips
+        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
+          enabled in the configuration
+
+        Overriders of skip_checks must always invoke skip_check on `super`
+        first.
+
+        Example::
+
+            @classmethod
+            def skip_checks(cls):
+                super(Example, cls).skip_checks()
+                if not CONF.service_available.my_service:
+                    skip_msg = ("%s skipped as my_service is not available")
+                    raise cls.skipException(skip_msg % cls.__name__)
         """
+        cls.__skip_checks_called = True
         identity_version = cls.get_identity_version()
         # setting force_tenant_isolation to True also needs admin credentials.
         if ('admin' in cls.credentials or
@@ -271,6 +307,7 @@
         set_network_resources() method, otherwise it will create
         network resources(network resources are created in a later step).
         """
+        cls.__setup_credentials_called = True
         for credentials_type in cls.credentials:
             # This may raise an exception in case credentials are not available
             # In that case we want to let the exception through and the test
@@ -379,29 +416,13 @@
                             servers.delete_server,
                             cls.shared_server2['id'])
         """
-        if (CONF.validation.ip_version_for_ssh not in (4, 6) and
-            CONF.service_available.neutron):
-            msg = "Invalid IP version %s in ip_version_for_ssh. Use 4 or 6"
-            raise lib_exc.InvalidConfiguration(
-                msg % CONF.validation.ip_version_for_ssh)
-        if hasattr(cls, "os_primary"):
-            vr = cls.validation_resources
-            cls.validation_resources = vresources.create_validation_resources(
-                cls.os_primary,
-                use_neutron=CONF.service_available.neutron,
-                ethertype='IPv' + str(CONF.validation.ip_version_for_ssh),
-                floating_network_id=CONF.network.public_network_id,
-                floating_network_name=CONF.network.floating_network_name,
-                **vr)
-        else:
-            LOG.warning("Client manager not found, validation resources not"
-                        " created")
+        pass
 
     @classmethod
     def resource_cleanup(cls):
         """Class level resource cleanup for test cases.
 
-        Resource cleanup processes the stack or cleanups produced by
+        Resource cleanup processes the stack of cleanups produced by
         `addClassResourceCleanup` and then cleans up validation resources
         if any were provisioned.
 
@@ -440,16 +461,6 @@
                 fn(*args, **kwargs)
             except Exception:
                 cleanup_errors.append(sys.exc_info())
-        if cls.validation_resources:
-            if hasattr(cls, "os_primary"):
-                vr = cls.validation_resources
-                vresources.clear_validation_resources(
-                    cls.os_primary,
-                    use_neutron=CONF.service_available.neutron, **vr)
-                cls.validation_resources = {}
-            else:
-                LOG.warning("Client manager not found, validation resources "
-                            "not deleted")
         if cleanup_errors:
             raise testtools.MultipleExceptions(*cleanup_errors)
 
@@ -555,7 +566,7 @@
                                              False)
 
             cls._creds_provider = credentials.get_credentials_provider(
-                name=cls.__name__, network_resources=cls.network_resources,
+                name=cls.__name__, network_resources=cls._network_resources,
                 force_tenant_isolation=force_tenant_isolation)
         return cls._creds_provider
 
@@ -610,62 +621,128 @@
         if hasattr(cls, '_creds_provider'):
             cls._creds_provider.clear_creds()
 
+    @staticmethod
+    def _validation_resources_params_from_conf():
+        return dict(
+            keypair=(CONF.validation.auth_method.lower() == "keypair"),
+            floating_ip=(CONF.validation.connect_method.lower() == "floating"),
+            security_group=CONF.validation.security_group,
+            security_group_rules=CONF.validation.security_group_rules,
+            use_neutron=CONF.service_available.neutron,
+            ethertype='IPv' + str(CONF.validation.ip_version_for_ssh),
+            floating_network_id=CONF.network.public_network_id,
+            floating_network_name=CONF.network.floating_network_name)
+
     @classmethod
-    def set_validation_resources(cls, keypair=None, floating_ip=None,
-                                 security_group=None,
-                                 security_group_rules=None):
-        """Specify which ssh server validation resources should be created.
+    def get_class_validation_resources(cls, os_clients):
+        """Provision validation resources according to configuration
 
-        Each of the argument must be set to either None, True or False, with
-        None - use default from config (security groups and security group
-               rules get created when set to None)
-        False - Do not create the validation resource
-        True - create the validation resource
+        This is a wrapper around `create_validation_resources` from
+        `tempest.common.validation_resources` that passes parameters from
+        Tempest configuration. Only one instance of class level
+        validation resources is managed by the helper, so If resources
+        were already provisioned before, existing ones will be returned.
 
-        @param keypair
-        @param security_group
-        @param security_group_rules
-        @param floating_ip
+        Resources are returned as a dictionary. They are also scheduled for
+        automatic cleanup during class teardown using
+        `addClassResourcesCleanup`.
+
+        If `CONF.validation.run_validation` is False no resource will be
+        provisioned at all.
+
+        @param os_clients: Clients to be used to provision the resources.
         """
         if not CONF.validation.run_validation:
             return
 
-        if keypair is None:
-            keypair = (CONF.validation.auth_method.lower() == "keypair")
+        if os_clients in cls._validation_resources:
+            return cls._validation_resources[os_clients]
 
-        if floating_ip is None:
-            floating_ip = (CONF.validation.connect_method.lower() ==
-                           "floating")
+        if (CONF.validation.ip_version_for_ssh not in (4, 6) and
+                CONF.service_available.neutron):
+            msg = "Invalid IP version %s in ip_version_for_ssh. Use 4 or 6"
+            raise lib_exc.InvalidConfiguration(
+                msg % CONF.validation.ip_version_for_ssh)
 
-        if security_group is None:
-            security_group = CONF.validation.security_group
+        resources = vr.create_validation_resources(
+            os_clients,
+            **cls._validation_resources_params_from_conf())
 
-        if security_group_rules is None:
-            security_group_rules = CONF.validation.security_group_rules
+        cls.addClassResourceCleanup(
+            vr.clear_validation_resources, os_clients,
+            use_neutron=CONF.service_available.neutron,
+            **resources)
+        cls._validation_resources[os_clients] = resources
+        return resources
 
-        if not cls.validation_resources:
-            cls.validation_resources = {
-                'keypair': keypair,
-                'security_group': security_group,
-                'security_group_rules': security_group_rules,
-                'floating_ip': floating_ip}
+    def get_test_validation_resources(self, os_clients):
+        """Returns a dict of validation resources according to configuration
+
+        Initialise a validation resources fixture based on configuration.
+        Start the fixture and returns the validation resources.
+
+        If `CONF.validation.run_validation` is False no resource will be
+        provisioned at all.
+
+        @param os_clients: Clients to be used to provision the resources.
+        """
+
+        params = {}
+        # Test will try to use the fixture, so for this to be useful
+        # we must return a fixture. If validation is disabled though
+        # we don't need to provision anything, which is the default
+        # behavior for the fixture.
+        if CONF.validation.run_validation:
+            params = self._validation_resources_params_from_conf()
+
+        validation = self.useFixture(
+            vr.ValidationResourcesFixture(os_clients, **params))
+        return validation.resources
 
     @classmethod
     def set_network_resources(cls, network=False, router=False, subnet=False,
                               dhcp=False):
         """Specify which network resources should be created
 
+        The dynamic credentials provider by default provisions network
+        resources for each user/project that is provisioned. This behavior
+        can be altered using this method, which allows tests to define which
+        specific network resources to be provisioned - none if no parameter
+        is specified.
+
+        Credentials are provisioned as part of the class setup fixture,
+        during the `setup_credentials` step. For this to be effective this
+        helper must be invoked before super's `setup_credentials` is executed.
+
         @param network
         @param router
         @param subnet
         @param dhcp
+
+        Example::
+
+            @classmethod
+            def setup_credentials(cls):
+                # Do not setup network resources for this test
+                cls.set_network_resources()
+                super(MyTest, cls).setup_credentials()
         """
-        # network resources should be set only once from callers
+        # If this is invoked after the credentials are setup, it won't take
+        # any effect. To avoid this situation, fail the test in case this was
+        # invoked too late in the test lifecycle.
+        if cls.__setup_credentials_called:
+            raise RuntimeError(
+                "set_network_resources invoked after setup_credentials on the "
+                "super class has been already invoked. For "
+                "set_network_resources to have effect please invoke it before "
+                "the call to super().setup_credentials")
+
+        # Network resources should be set only once from callers
         # in order to ensure that even if it's called multiple times in
         # a chain of overloaded methods, the attribute is set only
-        # in the leaf class
-        if not cls.network_resources:
-            cls.network_resources = {
+        # in the leaf class.
+        if not cls._network_resources:
+            cls._network_resources = {
                 'network': network,
                 'router': router,
                 'subnet': subnet,
diff --git a/tempest/tests/common/test_validation_resources.py b/tempest/tests/lib/common/test_validation_resources.py
similarity index 99%
rename from tempest/tests/common/test_validation_resources.py
rename to tempest/tests/lib/common/test_validation_resources.py
index f7edfc0..d5139f4 100644
--- a/tempest/tests/common/test_validation_resources.py
+++ b/tempest/tests/lib/common/test_validation_resources.py
@@ -15,7 +15,7 @@
 import mock
 import testtools
 
-from tempest.common import validation_resources as vr
+from tempest.lib.common import validation_resources as vr
 from tempest.lib import exceptions as lib_exc
 from tempest.lib.services import clients
 from tempest.tests import base
diff --git a/tempest/tests/test_test.py b/tempest/tests/test_test.py
index ed08c3a..ead0bd8 100644
--- a/tempest/tests/test_test.py
+++ b/tempest/tests/test_test.py
@@ -19,10 +19,15 @@
 from oslo_config import cfg
 import testtools
 
+from tempest import clients
 from tempest import config
+from tempest.lib.common import validation_resources as vr
+from tempest.lib import exceptions as lib_exc
 from tempest import test
 from tempest.tests import base
 from tempest.tests import fake_config
+from tempest.tests.lib import fake_credentials
+from tempest.tests.lib.services import registry_fixture
 
 
 if sys.version_info >= (2, 7):
@@ -41,6 +46,188 @@
         self.log.append((test, err, details))
 
 
+class TestValidationResources(base.TestCase):
+
+    validation_resources_module = 'tempest.lib.common.validation_resources'
+
+    def setUp(self):
+        super(TestValidationResources, self).setUp()
+        self.useFixture(fake_config.ConfigFixture())
+        self.useFixture(registry_fixture.RegistryFixture())
+        self.patchobject(config, 'TempestConfigPrivate',
+                         fake_config.FakePrivate)
+
+        class TestTestClass(test.BaseTestCase):
+            pass
+
+        self.test_test_class = TestTestClass
+
+    def test_validation_resources_no_validation(self):
+        cfg.CONF.set_default('run_validation', False, 'validation')
+        creds = fake_credentials.FakeKeystoneV3Credentials()
+        osclients = clients.Manager(creds)
+        vr = self.test_test_class.get_class_validation_resources(osclients)
+        self.assertIsNone(vr)
+
+    def test_validation_resources_exists(self):
+        cfg.CONF.set_default('run_validation', True, 'validation')
+        creds = fake_credentials.FakeKeystoneV3Credentials()
+        osclients = clients.Manager(creds)
+        expected_vr = 'expected_validation_resources'
+        self.test_test_class._validation_resources[osclients] = expected_vr
+        obtained_vr = self.test_test_class.get_class_validation_resources(
+            osclients)
+        self.assertEqual(expected_vr, obtained_vr)
+
+    @mock.patch(validation_resources_module + '.create_validation_resources',
+                autospec=True)
+    def test_validation_resources_new(self, mock_create_vr):
+        cfg.CONF.set_default('run_validation', True, 'validation')
+        cfg.CONF.set_default('neutron', True, 'service_available')
+        creds = fake_credentials.FakeKeystoneV3Credentials()
+        osclients = clients.Manager(creds)
+        expected_vr = {'expected_validation_resources': None}
+        mock_create_vr.return_value = expected_vr
+        with mock.patch.object(
+                self.test_test_class,
+                'addClassResourceCleanup') as mock_add_class_cleanup:
+            obtained_vr = self.test_test_class.get_class_validation_resources(
+                osclients)
+            self.assertEqual(1, mock_add_class_cleanup.call_count)
+            self.assertEqual(mock.call(vr.clear_validation_resources,
+                                       osclients,
+                                       use_neutron=True,
+                                       **expected_vr),
+                             mock_add_class_cleanup.call_args)
+        self.assertEqual(mock_create_vr.call_count, 1)
+        self.assertIn(osclients, mock_create_vr.call_args_list[0][0])
+        self.assertEqual(expected_vr, obtained_vr)
+        self.assertIn(osclients, self.test_test_class._validation_resources)
+        self.assertEqual(expected_vr,
+                         self.test_test_class._validation_resources[osclients])
+
+    def test_validation_resources_invalid_config(self):
+        invalid_version = 999
+        cfg.CONF.set_default('run_validation', True, 'validation')
+        cfg.CONF.set_default('ip_version_for_ssh', invalid_version,
+                             'validation')
+        cfg.CONF.set_default('neutron', True, 'service_available')
+        creds = fake_credentials.FakeKeystoneV3Credentials()
+        osclients = clients.Manager(creds)
+        with testtools.ExpectedException(
+                lib_exc.InvalidConfiguration,
+                value_re='^.*\n.*' + str(invalid_version)):
+            self.test_test_class.get_class_validation_resources(osclients)
+
+    @mock.patch(validation_resources_module + '.create_validation_resources',
+                autospec=True)
+    def test_validation_resources_invalid_config_nova_net(self,
+                                                          mock_create_vr):
+        invalid_version = 999
+        cfg.CONF.set_default('run_validation', True, 'validation')
+        cfg.CONF.set_default('ip_version_for_ssh', invalid_version,
+                             'validation')
+        cfg.CONF.set_default('neutron', False, 'service_available')
+        creds = fake_credentials.FakeKeystoneV3Credentials()
+        osclients = clients.Manager(creds)
+        expected_vr = {'expected_validation_resources': None}
+        mock_create_vr.return_value = expected_vr
+        obtained_vr = self.test_test_class.get_class_validation_resources(
+            osclients)
+        self.assertEqual(mock_create_vr.call_count, 1)
+        self.assertIn(osclients, mock_create_vr.call_args_list[0][0])
+        self.assertEqual(expected_vr, obtained_vr)
+        self.assertIn(osclients, self.test_test_class._validation_resources)
+        self.assertEqual(expected_vr,
+                         self.test_test_class._validation_resources[osclients])
+
+    @mock.patch(validation_resources_module + '.create_validation_resources',
+                autospec=True)
+    @mock.patch(validation_resources_module + '.clear_validation_resources',
+                autospec=True)
+    def test_validation_resources_fixture(self, mock_clean_vr, mock_create_vr):
+
+        class TestWithRun(self.test_test_class):
+
+            def runTest(self):
+                pass
+
+        cfg.CONF.set_default('run_validation', True, 'validation')
+        test_case = TestWithRun()
+        creds = fake_credentials.FakeKeystoneV3Credentials()
+        osclients = clients.Manager(creds)
+        test_case.get_test_validation_resources(osclients)
+        self.assertEqual(1, mock_create_vr.call_count)
+        self.assertEqual(0, mock_clean_vr.call_count)
+
+
+class TestSetNetworkResources(base.TestCase):
+
+    def setUp(self):
+        super(TestSetNetworkResources, self).setUp()
+
+        class ParentTest(test.BaseTestCase):
+
+            @classmethod
+            def setup_credentials(cls):
+                cls.set_network_resources(dhcp=True)
+                super(ParentTest, cls).setup_credentials()
+
+            def runTest(self):
+                pass
+
+        self.parent_class = ParentTest
+
+    def test_set_network_resources_child_only(self):
+
+        class ChildTest(self.parent_class):
+
+            @classmethod
+            def setup_credentials(cls):
+                cls.set_network_resources(router=True)
+                super(ChildTest, cls).setup_credentials()
+
+        child_test = ChildTest()
+        child_test.setUpClass()
+        # Assert that the parents network resources are not set
+        self.assertFalse(child_test._network_resources['dhcp'])
+        # Assert that the child network resources are set
+        self.assertTrue(child_test._network_resources['router'])
+
+    def test_set_network_resources_right_order(self):
+
+        class ChildTest(self.parent_class):
+
+            @classmethod
+            def setup_credentials(cls):
+                super(ChildTest, cls).setup_credentials()
+                cls.set_network_resources(router=True)
+
+        child_test = ChildTest()
+        with testtools.ExpectedException(RuntimeError,
+                                         value_re='set_network_resources'):
+            child_test.setUpClass()
+
+    def test_set_network_resources_children(self):
+
+        class ChildTest(self.parent_class):
+
+            @classmethod
+            def setup_credentials(cls):
+                cls.set_network_resources(router=True)
+                super(ChildTest, cls).setup_credentials()
+
+        class GrandChildTest(ChildTest):
+            pass
+
+        # Invoke setupClass on both and check that the setup_credentials
+        # call check mechanism does not report any false negative.
+        child_test = ChildTest()
+        child_test.setUpClass()
+        grandchild_test = GrandChildTest()
+        grandchild_test.setUpClass()
+
+
 class TestTempestBaseTestClass(base.TestCase):
 
     def setUp(self):
@@ -56,26 +243,16 @@
 
         self.parent_test = ParentTest
 
-    @mock.patch(
-        'tempest.common.validation_resources.clear_validation_resources',
-        autospec=True)
-    def test_resource_cleanup(self, mock_vr):
+    def test_resource_cleanup(self):
         cfg.CONF.set_default('neutron', False, 'service_available')
         exp_args = (1, 2,)
         exp_kwargs = {'a': 1, 'b': 2}
-        exp_vr = {'keypair': 'kp1', 'floating_ip': 'fip2'}
         mock1 = mock.Mock()
         mock2 = mock.Mock()
         exp_functions = [mock1, mock2]
 
         class TestWithCleanups(self.parent_test):
 
-            # set fake validation resources
-            validation_resources = exp_vr
-
-            # set fake clients
-            os_primary = 'os_primary'
-
             @classmethod
             def resource_setup(cls):
                 for fn in exp_functions:
@@ -92,34 +269,22 @@
         # All stacked resource cleanups invoked
         mock1.assert_called_once_with(*exp_args, **exp_kwargs)
         mock2.assert_called_once_with(*exp_args, **exp_kwargs)
-        self.assertEqual(1, mock_vr.call_count)
         # Cleanup stack is empty
         self.assertEqual(0, len(test_cleanups._class_cleanups))
-        # Assert vrs are cleaned up
-        self.assertIn(mock.call(TestWithCleanups.os_primary, use_neutron=False,
-                                **exp_vr), mock_vr.call_args_list)
 
-    @mock.patch(
-        'tempest.common.validation_resources.clear_validation_resources',
-        autospec=True)
-    def test_resource_cleanup_failures(self, mock_vr):
+    def test_resource_cleanup_failures(self):
         cfg.CONF.set_default('neutron', False, 'service_available')
         exp_args = (1, 2,)
         exp_kwargs = {'a': 1, 'b': 2}
-        exp_vr = {'keypair': 'kp1', 'floating_ip': 'fip2'}
         mock1 = mock.Mock()
         mock1.side_effect = Exception('mock1 resource cleanup failure')
         mock2 = mock.Mock()
-        exp_functions = [mock1, mock2]
+        mock3 = mock.Mock()
+        mock3.side_effect = Exception('mock3 resource cleanup failure')
+        exp_functions = [mock1, mock2, mock3]
 
         class TestWithFailingCleanups(self.parent_test):
 
-            # set fake validation resources
-            validation_resources = exp_vr
-
-            # set fake clients
-            os_primary = 'os_primary'
-
             @classmethod
             def resource_setup(cls):
                 for fn in exp_functions:
@@ -137,19 +302,15 @@
         # Type, Exception, traceback [1] -> MultipleException
         found_exc = log[0][1][1]
         self.assertTrue(isinstance(found_exc, testtools.MultipleExceptions))
-        self.assertEqual(1, len(found_exc.args))
+        self.assertEqual(2, len(found_exc.args))
         # Each arg is exc_info - match messages and order
-        self.assertIn('mock1 resource', str(found_exc.args[0][1]))
+        self.assertIn('mock3 resource', str(found_exc.args[0][1]))
+        self.assertIn('mock1 resource', str(found_exc.args[1][1]))
         # All stacked resource cleanups invoked
         mock1.assert_called_once_with(*exp_args, **exp_kwargs)
         mock2.assert_called_once_with(*exp_args, **exp_kwargs)
-        self.assertEqual(1, mock_vr.call_count)
         # Cleanup stack is empty
         self.assertEqual(0, len(test_cleanups._class_cleanups))
-        # Assert fake vr are cleaned up
-        self.assertIn(mock.call(TestWithFailingCleanups.os_primary,
-                                use_neutron=False, **exp_vr),
-                      mock_vr.call_args_list)
 
     def test_super_resource_cleanup_not_invoked(self):
 
@@ -171,3 +332,97 @@
         found_exc = log[0][1][1]
         self.assertTrue(isinstance(found_exc, RuntimeError))
         self.assertIn(BadResourceCleanup.__name__, str(found_exc))
+
+    def test_super_skip_checks_not_invoked(self):
+
+        class BadSkipChecks(self.parent_test):
+
+            @classmethod
+            def skip_checks(cls):
+                pass
+
+        bad_class = BadSkipChecks()
+        with testtools.ExpectedException(
+                RuntimeError,
+                value_re='^.* ' + BadSkipChecks.__name__):
+            bad_class.setUpClass()
+
+    def test_super_setup_credentials_not_invoked(self):
+
+        class BadSetupCredentials(self.parent_test):
+
+            @classmethod
+            def skip_checks(cls):
+                pass
+
+        bad_class = BadSetupCredentials()
+        with testtools.ExpectedException(
+                RuntimeError,
+                value_re='^.* ' + BadSetupCredentials.__name__):
+            bad_class.setUpClass()
+
+    def test_grandparent_skip_checks_not_invoked(self):
+
+        class BadSkipChecks(self.parent_test):
+
+            @classmethod
+            def skip_checks(cls):
+                pass
+
+        class SonOfBadSkipChecks(BadSkipChecks):
+            pass
+
+        bad_class = SonOfBadSkipChecks()
+        with testtools.ExpectedException(
+                RuntimeError,
+                value_re='^.* ' + SonOfBadSkipChecks.__name__):
+            bad_class.setUpClass()
+
+    @mock.patch('tempest.common.credentials_factory.is_admin_available',
+                autospec=True, return_value=True)
+    def test_skip_checks_admin(self, mock_iaa):
+        identity_version = 'identity_version'
+
+        class NeedAdmin(self.parent_test):
+            credentials = ['admin']
+
+            @classmethod
+            def get_identity_version(cls):
+                return identity_version
+
+        NeedAdmin().skip_checks()
+        mock_iaa.assert_called_once_with('identity_version')
+
+    @mock.patch('tempest.common.credentials_factory.is_admin_available',
+                autospec=True, return_value=False)
+    def test_skip_checks_admin_not_available(self, mock_iaa):
+        identity_version = 'identity_version'
+
+        class NeedAdmin(self.parent_test):
+            credentials = ['admin']
+
+            @classmethod
+            def get_identity_version(cls):
+                return identity_version
+
+        with testtools.ExpectedException(testtools.testcase.TestSkipped):
+            NeedAdmin().skip_checks()
+        mock_iaa.assert_called_once_with('identity_version')
+
+    def test_skip_checks_identity_v2_not_available(self):
+        cfg.CONF.set_default('api_v2', False, 'identity-feature-enabled')
+
+        class NeedV2(self.parent_test):
+            identity_version = 'v2'
+
+        with testtools.ExpectedException(testtools.testcase.TestSkipped):
+            NeedV2().skip_checks()
+
+    def test_skip_checks_identity_v3_not_available(self):
+        cfg.CONF.set_default('api_v3', False, 'identity-feature-enabled')
+
+        class NeedV3(self.parent_test):
+            identity_version = 'v3'
+
+        with testtools.ExpectedException(testtools.testcase.TestSkipped):
+            NeedV3().skip_checks()
diff --git a/test-requirements.txt b/test-requirements.txt
index 29f0865..37644d0 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -4,7 +4,7 @@
 hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
 # needed for doc build
 sphinx>=1.6.2 # BSD
-openstackdocstheme>=1.16.0 # Apache-2.0
+openstackdocstheme>=1.17.0 # Apache-2.0
 reno>=2.5.0 # Apache-2.0
 mock>=2.0.0 # BSD
 coverage!=4.4,>=4.0 # Apache-2.0
diff --git a/tools/generate-tempest-plugins-list.sh b/tools/generate-tempest-plugins-list.sh
index e6aad86..20c99b2 100755
--- a/tools/generate-tempest-plugins-list.sh
+++ b/tools/generate-tempest-plugins-list.sh
@@ -33,8 +33,8 @@
 #   * network access to https://git.openstack.org/cgit
 #   ))
 #
-# If a file named data/tempest-plugins-registry.header or
-# data/tempest-plugins-registry.footer is found relative to the
+# If a file named doc/source/data/tempest-plugins-registry.header or
+# doc/source/data/tempest-plugins-registry.footer is found relative to the
 # current working directory, it will be prepended or appended to
 # the generated reStructuredText plugins table respectively.
 
@@ -43,8 +43,8 @@
 (
 declare -A plugins
 
-if [[ -r data/tempest-plugins-registry.header ]]; then
-    cat data/tempest-plugins-registry.header
+if [[ -r doc/source/data/tempest-plugins-registry.header ]]; then
+    cat doc/source/data/tempest-plugins-registry.header
 fi
 
 sorted_plugins=$(python tools/generate-tempest-plugins-list.py)
@@ -56,8 +56,8 @@
     printf "+----------------------------+-------------------------------------------------------------------------+\n"
 done
 
-if [[ -r data/tempest-plugins-registry.footer ]]; then
-    cat data/tempest-plugins-registry.footer
+if [[ -r doc/source/data/tempest-plugins-registry.footer ]]; then
+    cat doc/source/data/tempest-plugins-registry.footer
 fi
 ) > doc/source/plugin-registry.rst