Merge "Increase size of subnet allocation pool"
diff --git a/README.rst b/README.rst
index 53c7de5..13f4f61 100644
--- a/README.rst
+++ b/README.rst
@@ -178,9 +178,8 @@
 is OS_TEST_PATH=./tempest/test_discover which will only run test discover on the
 Tempest suite.
 
-Alternatively, you can use the run_tests.sh script which will create a venv and
-run the unit tests. There are also the py27 and py34 tox jobs which will run
-the unit tests with the corresponding version of python.
+Alternatively, there are the py27 and py34 tox jobs which will run the unit
+tests with the corresponding version of python.
 
 Python 2.6
 ----------
@@ -224,7 +223,7 @@
 
     $ cd $TEMPEST_ROOT_DIR
     $ oslo-config-generator --config-file \
-        etc/config-generator.tempest.conf \
+        tempest/cmd/config-generator.tempest.conf \
         --output-file etc/tempest.conf
 
 After that, open up the ``etc/tempest.conf`` file and edit the
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 17059e4..5263fdd 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -217,3 +217,7 @@
  * `2.20`_
 
  .. _2.20: http://docs.openstack.org/developer/nova/api_microversion_history.html#id18
+
+ * `2.25`_
+
+ .. _2.25: http://docs.openstack.org/developer/nova/api_microversion_history.html#maximum-in-mitaka
diff --git a/releasenotes/notes/add-new-identity-clients-3c3afd674a395bde.yaml b/releasenotes/notes/add-new-identity-clients-3c3afd674a395bde.yaml
new file mode 100644
index 0000000..b8dcfce
--- /dev/null
+++ b/releasenotes/notes/add-new-identity-clients-3c3afd674a395bde.yaml
@@ -0,0 +1,10 @@
+---
+features:
+  - |
+    Define identity service clients as libraries
+    The following identity service clients are defined as library interface,
+    so the other projects can use these modules as stable libraries without
+    any maintenance changes.
+
+     * endpoints_client(v3)
+     * policies_client (v3)
diff --git a/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml b/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml
index b457ddd..092014e 100644
--- a/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml
+++ b/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml
@@ -1,4 +1,8 @@
 ---
 features:
-  - Adds subunit-describe-calls. A parser for subunit streams to determine what
+  - |
+    Adds subunit-describe-calls. A parser for subunit streams to determine what
     REST API calls are made inside of a test and in what order they are called.
+
+      * Input can be piped in or a file can be specified
+      * Output is shortened for stdout, the output file has more information
diff --git a/run_tests.sh b/run_tests.sh
index 22314b6..a856bb4 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -17,6 +17,44 @@
   echo "  -- [TESTROPTIONS]        After the first '--' you can pass arbitrary arguments to testr "
 }
 
+function deprecation_warning {
+  cat <<EOF
+-------------------------------------------------------------------------
+WARNING: run_tests.sh is deprecated and this script will be removed after
+the Newton release. All tests should be run through testr/ostestr or tox.
+
+To run style checks:
+
+ tox -e pep8
+
+To run python 2.7 unit tests
+
+ tox -e py27
+
+To run unit tests and generate coverage report
+
+ tox -e cover
+
+To run a subset of any of these tests:
+
+ tox -e py27 someregex
+
+ i.e.: tox -e py27 test_servers
+
+Additional tox targets are available in tox.ini. For more information
+see:
+http://docs.openstack.org/project-team-guide/project-setup/python.html
+
+NOTE: if you want to use testr to run tests, you can instead use:
+
+ OS_TEST_PATH=./tempest/tests testr run
+
+Documentation on using testr directly can be found at
+http://testrepository.readthedocs.org/en/latest/MANUAL.html
+-------------------------------------------------------------------------
+EOF
+}
+
 testrargs=""
 just_pep8=0
 venv=${VENV:-.venv}
@@ -32,6 +70,8 @@
 config_file=""
 update=0
 
+deprecation_warning
+
 if ! options=$(getopt -o VNnfuctphd -l virtual-env,no-virtual-env,no-site-packages,force,update,serial,coverage,pep8,help,debug -- "$@")
 then
     # parse error
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 94635ff..dd7beaa 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -27,6 +27,8 @@
 
 class LiveBlockMigrationTestJSON(base.BaseV2ComputeAdminTest):
     _host_key = 'OS-EXT-SRV-ATTR:host'
+    max_microversion = '2.24'
+    block_migration = None
 
     @classmethod
     def skip_checks(cls):
@@ -64,12 +66,16 @@
         return self._get_server_details(server_id)[self._host_key]
 
     def _migrate_server_to(self, server_id, dest_host, volume_backed=False):
-        block_migration = (CONF.compute_feature_enabled.
-                           block_migration_for_live_migration and
-                           not volume_backed)
+        kwargs = dict()
+        block_migration = getattr(self, 'block_migration', None)
+        if self.block_migration is None:
+            kwargs['disk_over_commit'] = False
+            block_migration = (CONF.compute_feature_enabled.
+                               block_migration_for_live_migration and
+                               not volume_backed)
         body = self.admin_servers_client.live_migrate_server(
             server_id, host=dest_host, block_migration=block_migration,
-            disk_over_commit=False)
+            **kwargs)
         return body
 
     def _get_host_other_than(self, host):
@@ -167,3 +173,9 @@
         waiters.wait_for_server_status(self.servers_client,
                                        server_id, 'ACTIVE')
         self.assertEqual(target_host, self._get_host_for_server(server_id))
+
+
+class LiveAutoBlockMigrationV225TestJSON(LiveBlockMigrationTestJSON):
+    min_microversion = '2.25'
+    max_microversion = 'latest'
+    block_migration = 'auto'
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 37aa5ac..5e75493 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -359,7 +359,7 @@
             for address in addresses:
                 if address['version'] == CONF.validation.ip_version_for_ssh:
                     return address['addr']
-            raise exceptions.ServerUnreachable()
+            raise exceptions.ServerUnreachable(server_id=server['id'])
         else:
             raise exceptions.InvalidConfiguration()
 
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index 60bb314..1879e46 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -1,4 +1,4 @@
-# Copyright 2013 OpenStack, LLC
+# Copyright 2013 OpenStack Foundation
 # All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
diff --git a/tempest/api/identity/admin/v3/test_projects_negative.py b/tempest/api/identity/admin/v3/test_projects_negative.py
index e661f42..c76b9ee 100644
--- a/tempest/api/identity/admin/v3/test_projects_negative.py
+++ b/tempest/api/identity/admin/v3/test_projects_negative.py
@@ -1,4 +1,4 @@
-# Copyright 2013 OpenStack, LLC
+# Copyright 2013 OpenStack Foundation
 # All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
diff --git a/tempest/api/network/test_floating_ips.py b/tempest/api/network/test_floating_ips.py
index 2abbf93..c64b01e 100644
--- a/tempest/api/network/test_floating_ips.py
+++ b/tempest/api/network/test_floating_ips.py
@@ -54,7 +54,7 @@
 
         # Create network, subnet, router and add interface
         cls.network = cls.create_network()
-        cls.subnet = cls.create_subnet(cls.network)
+        cls.subnet = cls.create_subnet(cls.network, enable_dhcp=False)
         cls.router = cls.create_router(data_utils.rand_name('router-'),
                                        external_network_id=cls.ext_net_id)
         cls.create_router_interface(cls.router['id'], cls.subnet['id'])
diff --git a/tempest/api/orchestration/stacks/templates/neutron_basic.yaml b/tempest/api/orchestration/stacks/templates/neutron_basic.yaml
index be33c94..ccb1b54 100644
--- a/tempest/api/orchestration/stacks/templates/neutron_basic.yaml
+++ b/tempest/api/orchestration/stacks/templates/neutron_basic.yaml
@@ -58,7 +58,7 @@
             #!/bin/sh -v
 
             SIGNAL_DATA='{"Status": "SUCCESS", "Reason": "SmokeServerNeutron created", "Data": "Application has completed configuration.", "UniqueId": "00000"}'
-            while ! curl --fail -X PUT -H 'Content-Type:' --data-binary "$SIGNAL_DATA" \
+            while ! curl --insecure --fail -X PUT -H 'Content-Type:' --data-binary "$SIGNAL_DATA" \
             'wait_handle' ; do sleep 3; done
           params:
             wait_handle: {get_resource: WaitHandleNeutron}
diff --git a/tempest/api/volume/admin/test_volumes_backup.py b/tempest/api/volume/admin/test_volumes_backup.py
index b6dc488..b144c7c 100644
--- a/tempest/api/volume/admin/test_volumes_backup.py
+++ b/tempest/api/volume/admin/test_volumes_backup.py
@@ -166,6 +166,23 @@
         self.admin_backups_client.wait_for_backup_status(import_backup['id'],
                                                          'available')
 
+    @test.idempotent_id('47a35425-a891-4e13-961c-c45deea21e94')
+    def test_volume_backup_reset_status(self):
+        # Create a backup
+        backup_name = data_utils.rand_name('Backup')
+        backup = self.admin_backups_client.create_backup(
+            volume_id=self.volume['id'], name=backup_name)['backup']
+        self.addCleanup(self.admin_backups_client.delete_backup,
+                        backup['id'])
+        self.assertEqual(backup_name, backup['name'])
+        self.admin_backups_client.wait_for_backup_status(backup['id'],
+                                                         'available')
+        # Reset backup status to error
+        self.admin_backups_client.reset_backup_status(backup_id=backup['id'],
+                                                      status="error")
+        self.admin_backups_client.wait_for_backup_status(backup['id'],
+                                                         'error')
+
 
 class VolumesBackupsAdminV1Test(VolumesBackupsAdminV2Test):
     _api_version = 1
diff --git a/tempest/cmd/subunit_describe_calls.py b/tempest/cmd/subunit_describe_calls.py
index da7f426..0f868a9 100644
--- a/tempest/cmd/subunit_describe_calls.py
+++ b/tempest/cmd/subunit_describe_calls.py
@@ -21,13 +21,14 @@
 Runtime Arguments
 -----------------
 
-**--subunit, -s**: (Required) The path to the subunit file being parsed
+**--subunit, -s**: (Optional) The path to the subunit file being parsed,
+defaults to stdin
 
 **--non-subunit-name, -n**: (Optional) The file_name that the logs are being
 stored in
 
-**--output-file, -o**: (Required) The path where the JSON output will be
-written to
+**--output-file, -o**: (Optional) The path where the JSON output will be
+written to. This contains more information than is present in stdout.
 
 **--ports, -p**: (Optional) The path to a JSON file describing the ports being
 used by different services
@@ -35,13 +36,14 @@
 Usage
 -----
 
-subunit-describe-calls will take in a file path via the --subunit parameter
-which contains either a subunit v1 or v2 stream. This is then parsed checking
-for details contained in the file_bytes of the --non-subunit-name parameter
-(the default is pythonlogging which is what Tempest uses to store logs). By
-default the OpenStack Kilo release port defaults (http://bit.ly/22jpF5P)
-are used unless a file is provided via the --ports option. The resulting output
-is dumped in JSON output to the path provided in the --output-file option.
+subunit-describe-calls will take in either stdin subunit v1 or v2 stream or a
+file path which contains either a subunit v1 or v2 stream passed via the
+--subunit parameter. This is then parsed checking for details contained in the
+file_bytes of the --non-subunit-name parameter (the default is pythonlogging
+which is what Tempest uses to store logs). By default the OpenStack Kilo
+release port defaults (http://bit.ly/22jpF5P) are used unless a file is
+provided via the --ports option. The resulting output is dumped in JSON output
+to the path provided in the --output-file option.
 
 Ports file JSON structure
 ^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -64,7 +66,11 @@
               "verb": "HTTP Verb",
               "service": "Name of the service",
               "url": "A shortened version of the URL called",
-              "status_code": "The status code of the response"
+              "status_code": "The status code of the response",
+              "request_headers": "The headers of the request",
+              "request_body": "The body of the request",
+              "response_headers": "The headers of the response",
+              "response_body": "The body of the response"
           }
       ]
   }
@@ -75,6 +81,7 @@
 import json
 import os
 import re
+import sys
 
 import subunit
 import testtools
@@ -91,6 +98,9 @@
                         '(?P<verb>\w*) (?P<url>.*) .*')
     port_re = re.compile(r'.*:(?P<port>\d+).*')
     path_re = re.compile(r'http[s]?://[^/]*/(?P<path>.*)')
+    request_re = re.compile(r'.* Request - Headers: (?P<headers>.*)')
+    response_re = re.compile(r'.* Response - Headers: (?P<headers>.*)')
+    body_re = re.compile(r'.*Body: (?P<body>.*)')
 
     # Based on mitaka defaults:
     # http://docs.openstack.org/mitaka/config-reference/
@@ -151,15 +161,46 @@
 
         calls = []
         for _, detail in details.items():
+            in_request = False
+            in_response = False
+            current_call = {}
             for line in detail.as_text().split("\n"):
-                match = self.url_re.match(line)
-                if match is not None:
-                    calls.append({
-                        "name": match.group("name"),
-                        "verb": match.group("verb"),
-                        "status_code": match.group("code"),
-                        "service": self.get_service(match.group("url")),
-                        "url": self.url_path(match.group("url"))})
+                url_match = self.url_re.match(line)
+                request_match = self.request_re.match(line)
+                response_match = self.response_re.match(line)
+                body_match = self.body_re.match(line)
+
+                if url_match is not None:
+                    if current_call != {}:
+                        calls.append(current_call.copy())
+                        current_call = {}
+                        in_request, in_response = False, False
+                    current_call.update({
+                        "name": url_match.group("name"),
+                        "verb": url_match.group("verb"),
+                        "status_code": url_match.group("code"),
+                        "service": self.get_service(url_match.group("url")),
+                        "url": self.url_path(url_match.group("url"))})
+                elif request_match is not None:
+                    in_request, in_response = True, False
+                    current_call.update(
+                        {"request_headers": request_match.group("headers")})
+                elif in_request and body_match is not None:
+                    in_request = False
+                    current_call.update(
+                        {"request_body": body_match.group(
+                            "body")})
+                elif response_match is not None:
+                    in_request, in_response = False, True
+                    current_call.update(
+                        {"response_headers": response_match.group(
+                            "headers")})
+                elif in_response and body_match is not None:
+                    in_response = False
+                    current_call.update(
+                        {"response_body": body_match.group("body")})
+            if current_call != {}:
+                calls.append(current_call.copy())
 
         return calls
 
@@ -203,11 +244,12 @@
         desc = "Outputs all HTTP calls a given test made that were logged."
         super(ArgumentParser, self).__init__(description=desc)
 
-        self.prog = "Argument Parser"
+        self.prog = "subunit-describe-calls"
 
         self.add_argument(
-            "-s", "--subunit", metavar="<subunit file>", required=True,
-            default=None, help="The path to the subunit output file.")
+            "-s", "--subunit", metavar="<subunit file>",
+            nargs="?", type=argparse.FileType('rb'), default=sys.stdin,
+            help="The path to the subunit output file.")
 
         self.add_argument(
             "-n", "--non-subunit-name", metavar="<non subunit name>",
@@ -216,19 +258,18 @@
 
         self.add_argument(
             "-o", "--output-file", metavar="<output file>", default=None,
-            help="The output file name for the json.", required=True)
+            help="The output file name for the json.")
 
         self.add_argument(
             "-p", "--ports", metavar="<ports file>", default=None,
             help="A JSON file describing the ports for each service.")
 
 
-def parse(subunit_file, non_subunit_name, ports):
+def parse(stream, non_subunit_name, ports):
     if ports is not None and os.path.exists(ports):
         ports = json.loads(open(ports).read())
 
     url_parser = UrlParser(ports)
-    stream = open(subunit_file, 'rb')
     suite = subunit.ByteStreamToStreamResult(
         stream, non_subunit_name=non_subunit_name)
     result = testtools.StreamToExtendedDecorator(url_parser)
@@ -248,8 +289,21 @@
 
 
 def output(url_parser, output_file):
-    with open(output_file, "w") as outfile:
-        outfile.write(json.dumps(url_parser.test_logs))
+    if output_file is not None:
+        with open(output_file, "w") as outfile:
+            outfile.write(json.dumps(url_parser.test_logs))
+        return
+
+    for test_name, items in url_parser.test_logs.iteritems():
+        sys.stdout.write('{0}\n'.format(test_name))
+        if not items:
+            sys.stdout.write('\n')
+            continue
+        for item in items:
+            sys.stdout.write('\t- {0} {1} request for {2} to {3}\n'.format(
+                item.get('status_code'), item.get('verb'),
+                item.get('service'), item.get('url')))
+        sys.stdout.write('\n')
 
 
 def entry_point():
diff --git a/tempest/common/utils/net_utils.py b/tempest/common/utils/net_utils.py
index d98fb32..fd0391d 100644
--- a/tempest/common/utils/net_utils.py
+++ b/tempest/common/utils/net_utils.py
@@ -12,7 +12,6 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-import itertools
 import netaddr
 
 from tempest.lib import exceptions as lib_exc
@@ -39,10 +38,11 @@
             alloc_set.add(fixed_ip['ip_address'])
 
     av_set = subnet_set - alloc_set
-    ip_list = [str(ip) for ip in itertools.islice(av_set, count)]
-
-    if len(ip_list) != count:
-        msg = "Insufficient IP addresses available"
-        raise lib_exc.BadRequest(message=msg)
-
-    return ip_list
+    addrs = []
+    for cidr in reversed(av_set.iter_cidrs()):
+        for ip in reversed(cidr):
+            addrs.append(str(ip))
+            if len(addrs) == count:
+                return addrs
+    msg = "Insufficient IP addresses available"
+    raise lib_exc.BadRequest(message=msg)
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index df08e30..073481c 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -141,7 +141,7 @@
     while int(time.time()) - start < client.build_timeout:
         image = show_image(image_id)
         # Compute image client returns response wrapped in 'image' element
-        # which is not case with Glance image client.
+        # which is not the case with Glance image client.
         if 'image' in image:
             image = image['image']
 
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index f534f30..272f6e3 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -63,14 +63,15 @@
 
 
 class ServerUnreachable(exceptions.TempestException):
-    message = "The server is not reachable via the configured network"
+    message = ("Server %(server_id)s is not reachable via "
+               "the configured network")
 
 
 # NOTE(andreaf) This exception is added here to facilitate the migration
 # of get_network_from_name and preprov_creds to tempest.lib, and it should
 # be migrated along with them
 class InvalidTestResource(exceptions.TempestException):
-    message = "%(name) is not a valid %(type), or the name is ambiguous"
+    message = "%(name)s is not a valid %(type)s, or the name is ambiguous"
 
 
 class RFCViolation(exceptions.RestClientException):
diff --git a/tempest/lib/api_schema/response/compute/v2_16/__init__.py b/tempest/lib/api_schema/response/compute/v2_16/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_16/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_16/servers.py b/tempest/lib/api_schema/response/compute/v2_16/servers.py
new file mode 100644
index 0000000..6868110
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_16/servers.py
@@ -0,0 +1,160 @@
+# Copyright 2014 NEC Corporation.  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.
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+from tempest.lib.api_schema.response.compute.v2_9 import servers
+
+# Compute microversion 2.16:
+# 1. New attributes in 'server' dict.
+#      'host_status'
+
+server_detail = {
+    'type': 'object',
+    'properties': {
+        'id': {'type': 'string'},
+        'name': {'type': 'string'},
+        'status': {'type': 'string'},
+        'image': {'oneOf': [
+            {'type': 'object',
+                'properties': {
+                    'id': {'type': 'string'},
+                    'links': parameter_types.links
+                },
+                'additionalProperties': False,
+                'required': ['id', 'links']},
+            {'type': ['string', 'null']}
+        ]},
+        'flavor': {
+            'type': 'object',
+            'properties': {
+                'id': {'type': 'string'},
+                'links': parameter_types.links
+            },
+            'additionalProperties': False,
+            'required': ['id', 'links']
+        },
+        'fault': {
+            'type': 'object',
+            'properties': {
+                'code': {'type': 'integer'},
+                'created': {'type': 'string'},
+                'message': {'type': 'string'},
+                'details': {'type': 'string'},
+            },
+            'additionalProperties': False,
+            # NOTE(gmann): 'details' is not necessary to be present
+            #  in the 'fault'. So it is not defined as 'required'.
+            'required': ['code', 'created', 'message']
+        },
+        'user_id': {'type': 'string'},
+        'tenant_id': {'type': 'string'},
+        'created': {'type': 'string'},
+        'updated': {'type': 'string'},
+        'progress': {'type': 'integer'},
+        'metadata': {'type': 'object'},
+        'links': parameter_types.links,
+        'addresses': parameter_types.addresses,
+        'hostId': {'type': 'string'},
+        'OS-DCF:diskConfig': {'type': 'string'},
+        'accessIPv4': parameter_types.access_ip_v4,
+        'accessIPv6': parameter_types.access_ip_v6,
+        'key_name': {'type': ['string', 'null']},
+        'security_groups': {'type': 'array'},
+        'OS-SRV-USG:launched_at': {'type': ['string', 'null']},
+        'OS-SRV-USG:terminated_at': {'type': ['string', 'null']},
+        'OS-EXT-AZ:availability_zone': {'type': 'string'},
+        'OS-EXT-STS:task_state': {'type': ['string', 'null']},
+        'OS-EXT-STS:vm_state': {'type': 'string'},
+        'OS-EXT-STS:power_state': {'type': 'integer'},
+        'OS-EXT-SRV-ATTR:host': {'type': ['string', 'null']},
+        'OS-EXT-SRV-ATTR:instance_name': {'type': 'string'},
+        'OS-EXT-SRV-ATTR:hypervisor_hostname': {'type': ['string', 'null']},
+        'config_drive': {'type': 'string'},
+        'os-extended-volumes:volumes_attached': {
+            'type': 'array',
+            'items': {
+                'type': 'object',
+                'properties': {
+                    'id': {'type': 'string'},
+                    'delete_on_termination': {'type': 'boolean'}
+                },
+                'additionalProperties': False,
+            },
+        },
+        'OS-EXT-SRV-ATTR:reservation_id': {'type': ['string', 'null']},
+        'OS-EXT-SRV-ATTR:launch_index': {'type': 'integer'},
+        'OS-EXT-SRV-ATTR:kernel_id': {'type': ['string', 'null']},
+        'OS-EXT-SRV-ATTR:ramdisk_id': {'type': ['string', 'null']},
+        'OS-EXT-SRV-ATTR:hostname': {'type': 'string'},
+        'OS-EXT-SRV-ATTR:root_device_name': {'type': ['string', 'null']},
+        'OS-EXT-SRV-ATTR:user_data': {'type': ['string', 'null']},
+        'locked': {'type': 'boolean'},
+        # NOTE(gmann): new attributes in version 2.16
+        'host_status': {'type': 'string'}
+    },
+    'additionalProperties': False,
+    # NOTE(gmann): 'progress' attribute is present in the response
+    # only when server's status is one of the progress statuses
+    # ("ACTIVE","BUILD", "REBUILD", "RESIZE","VERIFY_RESIZE")
+    # 'fault' attribute is present in the response
+    # only when server's status is one of the  "ERROR", "DELETED".
+    # OS-DCF:diskConfig and accessIPv4/v6 are API
+    # extensions, and some environments return a response
+    # without these attributes.So these are not defined as 'required'.
+    'required': ['id', 'name', 'status', 'image', 'flavor',
+                 'user_id', 'tenant_id', 'created', 'updated',
+                 'metadata', 'links', 'addresses', 'hostId']
+}
+
+server_detail['properties']['addresses']['patternProperties'][
+    '^[a-zA-Z0-9-_.]+$']['items']['properties'].update({
+        'OS-EXT-IPS:type': {'type': 'string'},
+        'OS-EXT-IPS-MAC:mac_addr': parameter_types.mac_address})
+# NOTE(gmann)dd: Update OS-EXT-IPS:type and OS-EXT-IPS-MAC:mac_addr
+# attributes in server address. Those are API extension,
+# and some environments return a response without
+# these attributes. So they are not 'required'.
+
+get_server = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'server': server_detail
+        },
+        'additionalProperties': False,
+        'required': ['server']
+    }
+}
+
+list_servers_detail = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'servers': {
+                'type': 'array',
+                'items': server_detail
+            },
+            'servers_links': parameter_types.links
+        },
+        'additionalProperties': False,
+        # NOTE(gmann): servers_links attribute is not necessary to be
+        # present always So it is not 'required'.
+        'required': ['servers']
+    }
+}
+
+list_servers = copy.deepcopy(servers.list_servers)
diff --git a/tempest/lib/api_schema/response/compute/v2_19/servers.py b/tempest/lib/api_schema/response/compute/v2_19/servers.py
index 883839e..05cc32c 100644
--- a/tempest/lib/api_schema/response/compute/v2_19/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_19/servers.py
@@ -15,15 +15,18 @@
 import copy
 
 from tempest.lib.api_schema.response.compute.v2_1 import servers as serversv21
-from tempest.lib.api_schema.response.compute.v2_9 import servers as serversv29
+from tempest.lib.api_schema.response.compute.v2_16 import servers \
+    as serversv216
 
-get_server = copy.deepcopy(serversv29.get_server)
+list_servers = copy.deepcopy(serversv216.list_servers)
+
+get_server = copy.deepcopy(serversv216.get_server)
 get_server['response_body']['properties']['server'][
     'properties'].update({'description': {'type': ['string', 'null']}})
 get_server['response_body']['properties']['server'][
     'required'].append('description')
 
-list_servers_detail = copy.deepcopy(serversv29.list_servers_detail)
+list_servers_detail = copy.deepcopy(serversv216.list_servers_detail)
 list_servers_detail['response_body']['properties']['servers']['items'][
     'properties'].update({'description': {'type': ['string', 'null']}})
 list_servers_detail['response_body']['properties']['servers']['items'][
diff --git a/tempest/lib/api_schema/response/compute/v2_23/__init__.py b/tempest/lib/api_schema/response/compute/v2_23/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_23/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_23/migrations.py b/tempest/lib/api_schema/response/compute/v2_23/migrations.py
new file mode 100644
index 0000000..3cd0f6e
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_23/migrations.py
@@ -0,0 +1,62 @@
+# Copyright 2014 NEC Corporation.  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.lib.api_schema.response.compute.v2_1 import parameter_types
+
+# Compute microversion 2.23:
+# New attributes in 'migrations' list.
+#    'migration_type'
+#    'links'
+
+list_migrations = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'migrations': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        'id': {'type': 'integer'},
+                        'status': {'type': ['string', 'null']},
+                        'instance_uuid': {'type': ['string', 'null']},
+                        'source_node': {'type': ['string', 'null']},
+                        'source_compute': {'type': ['string', 'null']},
+                        'dest_node': {'type': ['string', 'null']},
+                        'dest_compute': {'type': ['string', 'null']},
+                        'dest_host': {'type': ['string', 'null']},
+                        'old_instance_type_id': {'type': ['integer', 'null']},
+                        'new_instance_type_id': {'type': ['integer', 'null']},
+                        'created_at': {'type': 'string'},
+                        'updated_at': {'type': ['string', 'null']},
+                        # New attributes in version 2.23
+                        'migration_type': {'type': ['string', 'null']},
+                        'links': parameter_types.links
+                    },
+                    'additionalProperties': False,
+                    'required': [
+                        'id', 'status', 'instance_uuid', 'source_node',
+                        'source_compute', 'dest_node', 'dest_compute',
+                        'dest_host', 'old_instance_type_id',
+                        'new_instance_type_id', 'created_at', 'updated_at',
+                        'migration_type'
+                    ]
+                }
+            }
+        },
+        'additionalProperties': False,
+        'required': ['migrations']
+    }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_3/__init__.py b/tempest/lib/api_schema/response/compute/v2_3/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_3/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_3/servers.py b/tempest/lib/api_schema/response/compute/v2_3/servers.py
new file mode 100644
index 0000000..ee16333
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_3/servers.py
@@ -0,0 +1,166 @@
+# Copyright 2014 NEC Corporation.  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.
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+from tempest.lib.api_schema.response.compute.v2_1 import servers
+
+# Compute microversion 2.3:
+# 1. New attributes in 'os-extended-volumes:volumes_attached' dict.
+#      'delete_on_termination'
+# 2. New attributes in 'server' dict.
+#      'OS-EXT-SRV-ATTR:reservation_id'
+#      'OS-EXT-SRV-ATTR:launch_index'
+#      'OS-EXT-SRV-ATTR:kernel_id'
+#      'OS-EXT-SRV-ATTR:ramdisk_id'
+#      'OS-EXT-SRV-ATTR:hostname'
+#      'OS-EXT-SRV-ATTR:root_device_name'
+#      'OS-EXT-SRV-ATTR:user_data'
+
+server_detail = {
+    'type': 'object',
+    'properties': {
+        'id': {'type': 'string'},
+        'name': {'type': 'string'},
+        'status': {'type': 'string'},
+        'image': {'oneOf': [
+            {'type': 'object',
+                'properties': {
+                    'id': {'type': 'string'},
+                    'links': parameter_types.links
+                },
+                'additionalProperties': False,
+                'required': ['id', 'links']},
+            {'type': ['string', 'null']}
+        ]},
+        'flavor': {
+            'type': 'object',
+            'properties': {
+                'id': {'type': 'string'},
+                'links': parameter_types.links
+            },
+            'additionalProperties': False,
+            'required': ['id', 'links']
+        },
+        'fault': {
+            'type': 'object',
+            'properties': {
+                'code': {'type': 'integer'},
+                'created': {'type': 'string'},
+                'message': {'type': 'string'},
+                'details': {'type': 'string'},
+            },
+            'additionalProperties': False,
+            # NOTE(gmann): 'details' is not necessary to be present
+            #  in the 'fault'. So it is not defined as 'required'.
+            'required': ['code', 'created', 'message']
+        },
+        'user_id': {'type': 'string'},
+        'tenant_id': {'type': 'string'},
+        'created': {'type': 'string'},
+        'updated': {'type': 'string'},
+        'progress': {'type': 'integer'},
+        'metadata': {'type': 'object'},
+        'links': parameter_types.links,
+        'addresses': parameter_types.addresses,
+        'hostId': {'type': 'string'},
+        'OS-DCF:diskConfig': {'type': 'string'},
+        'accessIPv4': parameter_types.access_ip_v4,
+        'accessIPv6': parameter_types.access_ip_v6,
+        'key_name': {'type': ['string', 'null']},
+        'security_groups': {'type': 'array'},
+        'OS-SRV-USG:launched_at': {'type': ['string', 'null']},
+        'OS-SRV-USG:terminated_at': {'type': ['string', 'null']},
+        'OS-EXT-AZ:availability_zone': {'type': 'string'},
+        'OS-EXT-STS:task_state': {'type': ['string', 'null']},
+        'OS-EXT-STS:vm_state': {'type': 'string'},
+        'OS-EXT-STS:power_state': {'type': 'integer'},
+        'OS-EXT-SRV-ATTR:host': {'type': ['string', 'null']},
+        'OS-EXT-SRV-ATTR:instance_name': {'type': 'string'},
+        'OS-EXT-SRV-ATTR:hypervisor_hostname': {'type': ['string', 'null']},
+        'config_drive': {'type': 'string'},
+        # NOTE(gmann): new attributes in version 2.3
+        'os-extended-volumes:volumes_attached': {
+            'type': 'array',
+            'items': {
+                'type': 'object',
+                'properties': {
+                    'id': {'type': 'string'},
+                    'delete_on_termination': {'type': 'boolean'}
+                },
+                'additionalProperties': False,
+            },
+        },
+        'OS-EXT-SRV-ATTR:reservation_id': {'type': ['string', 'null']},
+        'OS-EXT-SRV-ATTR:launch_index': {'type': 'integer'},
+        'OS-EXT-SRV-ATTR:kernel_id': {'type': ['string', 'null']},
+        'OS-EXT-SRV-ATTR:ramdisk_id': {'type': ['string', 'null']},
+        'OS-EXT-SRV-ATTR:hostname': {'type': 'string'},
+        'OS-EXT-SRV-ATTR:root_device_name': {'type': ['string', 'null']},
+        'OS-EXT-SRV-ATTR:user_data': {'type': ['string', 'null']},
+    },
+    'additionalProperties': False,
+    # NOTE(gmann): 'progress' attribute is present in the response
+    # only when server's status is one of the progress statuses
+    # ("ACTIVE","BUILD", "REBUILD", "RESIZE","VERIFY_RESIZE")
+    # 'fault' attribute is present in the response
+    # only when server's status is one of the  "ERROR", "DELETED".
+    # OS-DCF:diskConfig and accessIPv4/v6 are API
+    # extensions, and some environments return a response
+    # without these attributes.So these are not defined as 'required'.
+    'required': ['id', 'name', 'status', 'image', 'flavor',
+                 'user_id', 'tenant_id', 'created', 'updated',
+                 'metadata', 'links', 'addresses', 'hostId']
+}
+
+server_detail['properties']['addresses']['patternProperties'][
+    '^[a-zA-Z0-9-_.]+$']['items']['properties'].update({
+        'OS-EXT-IPS:type': {'type': 'string'},
+        'OS-EXT-IPS-MAC:mac_addr': parameter_types.mac_address})
+# NOTE(gmann)dd: Update OS-EXT-IPS:type and OS-EXT-IPS-MAC:mac_addr
+# attributes in server address. Those are API extension,
+# and some environments return a response without
+# these attributes. So they are not 'required'.
+
+get_server = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'server': server_detail
+        },
+        'additionalProperties': False,
+        'required': ['server']
+    }
+}
+
+list_servers_detail = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'servers': {
+                'type': 'array',
+                'items': server_detail
+            },
+            'servers_links': parameter_types.links
+        },
+        'additionalProperties': False,
+        # NOTE(gmann): servers_links attribute is not necessary to be
+        # present always So it is not 'required'.
+        'required': ['servers']
+    }
+}
+
+list_servers = copy.deepcopy(servers.list_servers)
diff --git a/tempest/lib/api_schema/response/compute/v2_9/servers.py b/tempest/lib/api_schema/response/compute/v2_9/servers.py
index e9b7249..470190c 100644
--- a/tempest/lib/api_schema/response/compute/v2_9/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_9/servers.py
@@ -14,7 +14,9 @@
 
 import copy
 
-from tempest.lib.api_schema.response.compute.v2_1 import servers
+from tempest.lib.api_schema.response.compute.v2_3 import servers
+
+list_servers = copy.deepcopy(servers.list_servers)
 
 get_server = copy.deepcopy(servers.get_server)
 get_server['response_body']['properties']['server'][
diff --git a/tempest/lib/common/utils/data_utils.py b/tempest/lib/common/utils/data_utils.py
index 93382c0..6b6548e 100644
--- a/tempest/lib/common/utils/data_utils.py
+++ b/tempest/lib/common/utils/data_utils.py
@@ -75,7 +75,7 @@
     ascii_char = string.ascii_letters
     digits = string.digits
     digit = random.choice(string.digits)
-    puncs = '~!@#$%^&*_=+'
+    puncs = '~!@#%^&*_=+'
     punc = random.choice(puncs)
     seed = ascii_char + digits + puncs
     pre = upper + digit + punc
diff --git a/tempest/lib/services/compute/migrations_client.py b/tempest/lib/services/compute/migrations_client.py
index 62246d3..c3bdba7 100644
--- a/tempest/lib/services/compute/migrations_client.py
+++ b/tempest/lib/services/compute/migrations_client.py
@@ -16,11 +16,16 @@
 from six.moves.urllib import parse as urllib
 
 from tempest.lib.api_schema.response.compute.v2_1 import migrations as schema
+from tempest.lib.api_schema.response.compute.v2_23 import migrations \
+    as schemav223
 from tempest.lib.common import rest_client
 from tempest.lib.services.compute import base_compute_client
 
 
 class MigrationsClient(base_compute_client.BaseComputeClient):
+    schema_versions_info = [
+        {'min': None, 'max': '2.22', 'schema': schema},
+        {'min': '2.23', 'max': None, 'schema': schemav223}]
 
     def list_migrations(self, **params):
         """List all migrations.
@@ -35,5 +40,6 @@
 
         resp, body = self.get(url)
         body = json.loads(body)
+        schema = self.get_schema(self.schema_versions_info)
         self.validate_response(schema.list_migrations, resp, body)
         return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index 0d31ac7..9444e20 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -20,7 +20,9 @@
 from six.moves.urllib import parse as urllib
 
 from tempest.lib.api_schema.response.compute.v2_1 import servers as schema
+from tempest.lib.api_schema.response.compute.v2_16 import servers as schemav216
 from tempest.lib.api_schema.response.compute.v2_19 import servers as schemav219
+from tempest.lib.api_schema.response.compute.v2_3 import servers as schemav23
 from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
 from tempest.lib.common import rest_client
 from tempest.lib.services.compute import base_compute_client
@@ -28,8 +30,10 @@
 
 class ServersClient(base_compute_client.BaseComputeClient):
     schema_versions_info = [
-        {'min': None, 'max': '2.8', 'schema': schema},
-        {'min': '2.9', 'max': '2.18', 'schema': schemav29},
+        {'min': None, 'max': '2.2', 'schema': schema},
+        {'min': '2.3', 'max': '2.8', 'schema': schemav23},
+        {'min': '2.9', 'max': '2.15', 'schema': schemav29},
+        {'min': '2.16', 'max': '2.18', 'schema': schemav216},
         {'min': '2.19', 'max': None, 'schema': schemav219}]
 
     def __init__(self, auth_provider, service, region,
diff --git a/tempest/services/identity/v3/json/endpoints_client.py b/tempest/lib/services/identity/v3/endpoints_client.py
similarity index 100%
rename from tempest/services/identity/v3/json/endpoints_client.py
rename to tempest/lib/services/identity/v3/endpoints_client.py
diff --git a/tempest/services/identity/v3/json/policies_client.py b/tempest/lib/services/identity/v3/policies_client.py
similarity index 100%
rename from tempest/services/identity/v3/json/policies_client.py
rename to tempest/lib/services/identity/v3/policies_client.py
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index f889c44..b151375 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -664,7 +664,7 @@
             for address in addresses:
                 if address['version'] == CONF.validation.ip_version_for_ssh:
                     return address['addr']
-            raise exceptions.ServerUnreachable()
+            raise exceptions.ServerUnreachable(server_id=server['id'])
         else:
             raise exceptions.InvalidConfiguration()
 
diff --git a/tempest/scenario/test_snapshot_pattern.py b/tempest/scenario/test_snapshot_pattern.py
index d6528a3..47c6e8d 100644
--- a/tempest/scenario/test_snapshot_pattern.py
+++ b/tempest/scenario/test_snapshot_pattern.py
@@ -13,8 +13,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import testtools
-
 from tempest import config
 from tempest.scenario import manager
 from tempest import test
@@ -33,9 +31,13 @@
 
     """
 
+    @classmethod
+    def skip_checks(cls):
+        super(TestSnapshotPattern, cls).skip_checks()
+        if not CONF.compute_feature_enabled.snapshot:
+            raise cls.skipException("Snapshotting is not available.")
+
     @test.idempotent_id('608e604b-1d63-4a82-8e3e-91bc665c90b4')
-    @testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
-                          'Snapshotting is not available.')
     @test.services('compute', 'network', 'image')
     def test_snapshot_pattern(self):
         # prepare for booting an instance
diff --git a/tempest/service_clients.py b/tempest/service_clients.py
index 386e621..252ebf4 100644
--- a/tempest/service_clients.py
+++ b/tempest/service_clients.py
@@ -14,6 +14,10 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import copy
+import importlib
+import inspect
+
 from tempest.lib import auth
 from tempest.lib import exceptions
 
@@ -34,6 +38,88 @@
     return tempest_modules()
 
 
+class ClientsFactory(object):
+    """Builds service clients for a service client module
+
+    This class implements the logic of feeding service client parameters
+    to service clients from a specific module. It allows setting the
+    parameters once and obtaining new instances of the clients without the
+    need of passing any parameter.
+
+    ClientsFactory can be used directly, or consumed via the `ServiceClients`
+    class, which manages the authorization part.
+    """
+    # TODO(andreaf) This version includes ClientsFactory but it does not
+    # use it yet in ServiceClients
+
+    def __init__(self, module_path, client_names, auth_provider, **kwargs):
+        """Initialises the client factory
+
+        :param module_path Path to module that includes all service clients.
+            All service client classes must be exposed by a single module.
+            If they are separated in different modules, defining __all__
+            in the root module can help, similar to what is done by service
+            clients in tempest.
+        :param client_names List or set of names of the service client classes.
+        :param auth_provider The auth provider used to initialise client.
+        :param kwargs Parameters to be passed to all clients. Parameters values
+            can be overwritten when clients are initialised, but parameters
+            cannot be deleted.
+        :raise ImportError if the specified module_path cannot be imported
+
+        Example:
+
+            >>> # Get credentials and an auth_provider
+            >>> clients = ClientsFactory(
+            >>>     module_path='my_service.my_service_clients',
+            >>>     client_names=['ServiceClient1', 'ServiceClient2'],
+            >>>     auth_provider=auth_provider,
+            >>>     service='my_service',
+            >>>     region='region1')
+            >>> my_api_client = clients.MyApiClient()
+            >>> my_api_client_region2 = clients.MyApiClient(region='region2')
+
+        """
+        # Import the module. If it's not importable, the raised exception
+        # provides good enough information about what happened
+        _module = importlib.import_module(module_path)
+        # If any of the classes is not in the module we fail
+        for class_name in client_names:
+            # TODO(andreaf) This always passes all parameters to all clients.
+            # In future to allow clients to specify the list of parameters
+            # that they accept based out of a list of standard ones.
+
+            # Obtain the class
+            klass = self._get_class(_module, class_name)
+            final_kwargs = copy.copy(kwargs)
+
+            # Set the function as an attribute of the factory
+            setattr(self, class_name, self._get_partial_class(
+                klass, auth_provider, final_kwargs))
+
+    @classmethod
+    def _get_partial_class(cls, klass, auth_provider, kwargs):
+
+        # Define a function that returns a new class instance by
+        # combining default kwargs with extra ones
+        def partial_class(**later_kwargs):
+            kwargs.update(later_kwargs)
+            return klass(auth_provider=auth_provider, **kwargs)
+
+        return partial_class
+
+    @classmethod
+    def _get_class(cls, module, class_name):
+        klass = getattr(module, class_name, None)
+        if not klass:
+            msg = 'Invalid class name, %s is not found in %s'
+            raise AttributeError(msg % (class_name, module))
+        if not inspect.isclass(klass):
+            msg = 'Expected a class, got %s of type %s instead'
+            raise TypeError(msg % (klass, type(klass)))
+        return klass
+
+
 class ServiceClients(object):
     """Service client provider class
 
diff --git a/tempest/services/identity/v3/__init__.py b/tempest/services/identity/v3/__init__.py
index 144c5a9..6ad8ef2 100644
--- a/tempest/services/identity/v3/__init__.py
+++ b/tempest/services/identity/v3/__init__.py
@@ -12,14 +12,14 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+from tempest.lib.services.identity.v3.endpoints_client import EndPointsClient
+from tempest.lib.services.identity.v3.policies_client import PoliciesClient
 from tempest.lib.services.identity.v3.token_client import V3TokenClient
 from tempest.services.identity.v3.json.credentials_client import \
     CredentialsClient
 from tempest.services.identity.v3.json.domains_client import DomainsClient
-from tempest.services.identity.v3.json.endpoints_client import EndPointsClient
 from tempest.services.identity.v3.json.groups_client import GroupsClient
 from tempest.services.identity.v3.json.identity_client import IdentityClient
-from tempest.services.identity.v3.json.policies_client import PoliciesClient
 from tempest.services.identity.v3.json.projects_client import ProjectsClient
 from tempest.services.identity.v3.json.regions_client import RegionsClient
 from tempest.services.identity.v3.json.roles_client import RolesClient
@@ -27,7 +27,7 @@
 from tempest.services.identity.v3.json.trusts_client import TrustsClient
 from tempest.services.identity.v3.json.users_clients import UsersClient
 
-__all__ = ['V3TokenClient', 'CredentialsClient', 'DomainsClient',
-           'EndPointsClient', 'GroupsClient', 'IdentityClient',
-           'PoliciesClient', 'ProjectsClient', 'RegionsClient', 'RolesClient',
+__all__ = ['EndPointsClient', 'PoliciesClient', 'V3TokenClient',
+           'CredentialsClient', 'DomainsClient', 'GroupsClient',
+           'IdentityClient', 'ProjectsClient', 'RegionsClient', 'RolesClient',
            'ServicesClient', 'TrustsClient', 'UsersClient', ]
diff --git a/tempest/services/volume/base/base_backups_client.py b/tempest/services/volume/base/base_backups_client.py
index 3842d66..63c5417 100644
--- a/tempest/services/volume/base/base_backups_client.py
+++ b/tempest/services/volume/base/base_backups_client.py
@@ -89,6 +89,13 @@
         self.expected_success(201, resp.status)
         return rest_client.ResponseBody(resp, body)
 
+    def reset_backup_status(self, backup_id, status):
+        """Reset the specified backup's status."""
+        post_body = json.dumps({'os-reset_status': {"status": status}})
+        resp, body = self.post('backups/%s/action' % backup_id, post_body)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
     def wait_for_backup_status(self, backup_id, status):
         """Waits for a Backup to reach a given status."""
         body = self.show_backup(backup_id)['backup']
@@ -99,7 +106,7 @@
             time.sleep(self.build_interval)
             body = self.show_backup(backup_id)['backup']
             backup_status = body['status']
-            if backup_status == 'error':
+            if backup_status == 'error' and backup_status != status:
                 raise exceptions.VolumeBackupException(backup_id=backup_id)
 
             if int(time.time()) - start >= self.build_timeout:
diff --git a/tempest/tests/cmd/test_subunit_describe_calls.py b/tempest/tests/cmd/test_subunit_describe_calls.py
index 43b417a..1c24c37 100644
--- a/tempest/tests/cmd/test_subunit_describe_calls.py
+++ b/tempest/tests/cmd/test_subunit_describe_calls.py
@@ -38,46 +38,159 @@
             os.path.dirname(os.path.abspath(__file__)),
             'sample_streams/calls.subunit')
         parser = subunit_describe_calls.parse(
-            subunit_file, "pythonlogging", None)
+            open(subunit_file), "pythonlogging", None)
         expected_result = {
-            'bar': [{'name': 'AgentsAdminTestJSON:setUp',
-                     'service': 'Nova',
-                     'status_code': '200',
-                     'url': 'v2.1/<id>/os-agents',
-                     'verb': 'POST'},
-                    {'name': 'AgentsAdminTestJSON:test_create_agent',
-                     'service': 'Nova',
-                     'status_code': '200',
-                     'url': 'v2.1/<id>/os-agents',
-                     'verb': 'POST'},
-                    {'name': 'AgentsAdminTestJSON:tearDown',
-                     'service': 'Nova',
-                     'status_code': '200',
-                     'url': 'v2.1/<id>/os-agents/1',
-                     'verb': 'DELETE'},
-                    {'name': 'AgentsAdminTestJSON:_run_cleanups',
-                     'service': 'Nova',
-                     'status_code': '200',
-                     'url': 'v2.1/<id>/os-agents/2',
-                     'verb': 'DELETE'}],
-            'foo': [{'name': 'AgentsAdminTestJSON:setUp',
-                     'service': 'Nova',
-                     'status_code': '200',
-                     'url': 'v2.1/<id>/os-agents',
-                     'verb': 'POST'},
-                    {'name': 'AgentsAdminTestJSON:test_delete_agent',
-                     'service': 'Nova',
-                     'status_code': '200',
-                     'url': 'v2.1/<id>/os-agents/3',
-                     'verb': 'DELETE'},
-                    {'name': 'AgentsAdminTestJSON:test_delete_agent',
-                     'service': 'Nova',
-                     'status_code': '200',
-                     'url': 'v2.1/<id>/os-agents',
-                     'verb': 'GET'},
-                    {'name': 'AgentsAdminTestJSON:tearDown',
-                     'service': 'Nova',
-                     'status_code': '404',
-                     'url': 'v2.1/<id>/os-agents/3',
-                     'verb': 'DELETE'}]}
+            'bar': [{
+                'name': 'AgentsAdminTestJSON:setUp',
+                'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+                '"hypervisor": "common", "md5hash": '
+                '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+                '"architecture": "tempest-x86_64-424013832", "os": "linux"}}',
+                'request_headers': "{'Content-Type': 'application/json', "
+                "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+                'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+                '"hypervisor": "common", "md5hash": '
+                '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+                '"architecture": "tempest-x86_64-424013832", "os": "linux", '
+                '"agent_id": 1}}',
+                'response_headers': "{'status': '200', 'content-length': "
+                "'203', 'x-compute-request-id': "
+                "'req-25ddaae2-0ef1-40d1-8228-59bd64a7e75b', 'vary': "
+                "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+                "'x-openstack-nova-api-version': '2.1', 'date': "
+                "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+                "'application/json'}",
+                'service': 'Nova',
+                'status_code': '200',
+                'url': 'v2.1/<id>/os-agents',
+                'verb': 'POST'}, {
+                'name': 'AgentsAdminTestJSON:test_create_agent',
+                'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+                '"hypervisor": "kvm", "md5hash": '
+                '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+                '"architecture": "tempest-x86-252246646", "os": "win"}}',
+                'request_headers': "{'Content-Type': 'application/json', "
+                "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+                'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+                '"hypervisor": "kvm", "md5hash": '
+                '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+                '"architecture": "tempest-x86-252246646", "os": "win", '
+                '"agent_id": 2}}',
+                'response_headers': "{'status': '200', 'content-length': "
+                "'195', 'x-compute-request-id': "
+                "'req-b4136f06-c015-4e7e-995f-c43831e3ecce', 'vary': "
+                "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+                "'x-openstack-nova-api-version': '2.1', 'date': "
+                "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+                "'application/json'}",
+                'service': 'Nova',
+                'status_code': '200',
+                'url': 'v2.1/<id>/os-agents',
+                'verb': 'POST'}, {
+                'name': 'AgentsAdminTestJSON:tearDown',
+                'request_body': 'None',
+                'request_headers': "{'Content-Type': 'application/json', "
+                "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+                'response_body': '',
+                'response_headers': "{'status': '200', 'content-length': "
+                "'0', 'x-compute-request-id': "
+                "'req-ee905fd6-a5b5-4da4-8c37-5363cb25bd9d', 'vary': "
+                "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+                "'x-openstack-nova-api-version': '2.1', 'date': "
+                "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+                "'application/json'}",
+                'service': 'Nova',
+                'status_code': '200',
+                'url': 'v2.1/<id>/os-agents/1',
+                'verb': 'DELETE'}, {
+                'name': 'AgentsAdminTestJSON:_run_cleanups',
+                'request_body': 'None',
+                'request_headers': "{'Content-Type': 'application/json', "
+                "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+                'response_headers': "{'status': '200', 'content-length': "
+                "'0', 'x-compute-request-id': "
+                "'req-e912cac0-63e0-4679-a68a-b6d18ddca074', 'vary': "
+                "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+                "'x-openstack-nova-api-version': '2.1', 'date': "
+                "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+                "'application/json'}",
+                'service': 'Nova',
+                'status_code': '200',
+                'url': 'v2.1/<id>/os-agents/2',
+                'verb': 'DELETE'}],
+            'foo': [{
+                'name': 'AgentsAdminTestJSON:setUp',
+                'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+                '"hypervisor": "common", "md5hash": '
+                '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+                '"architecture": "tempest-x86_64-948635295", "os": "linux"}}',
+                'request_headers': "{'Content-Type': 'application/json', "
+                "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+                'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+                '"hypervisor": "common", "md5hash": '
+                '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+                '"architecture": "tempest-x86_64-948635295", "os": "linux", '
+                '"agent_id": 3}}',
+                'response_headers': "{'status': '200', 'content-length': "
+                "'203', 'x-compute-request-id': "
+                "'req-ccd2116d-04b1-4ffe-ae32-fb623f68bf1c', 'vary': "
+                "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+                "'x-openstack-nova-api-version': '2.1', 'date': "
+                "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+                "'application/json'}",
+                'service': 'Nova',
+                'status_code': '200',
+                'url': 'v2.1/<id>/os-agents',
+                'verb': 'POST'}, {
+                'name': 'AgentsAdminTestJSON:test_delete_agent',
+                'request_body': 'None',
+                'request_headers': "{'Content-Type': 'application/json', "
+                "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+                'response_body': '',
+                'response_headers': "{'status': '200', 'content-length': "
+                "'0', 'x-compute-request-id': "
+                "'req-6e7fa28f-ae61-4388-9a78-947c58bc0588', 'vary': "
+                "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+                "'x-openstack-nova-api-version': '2.1', 'date': "
+                "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+                "'application/json'}",
+                'service': 'Nova',
+                'status_code': '200',
+                'url': 'v2.1/<id>/os-agents/3',
+                'verb': 'DELETE'}, {
+                'name': 'AgentsAdminTestJSON:test_delete_agent',
+                'request_body': 'None',
+                'request_headers': "{'Content-Type': 'application/json', "
+                "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+                'response_body': '{"agents": []}',
+                'response_headers': "{'status': '200', 'content-length': "
+                "'14', 'content-location': "
+                "'http://23.253.76.97:8774/v2.1/"
+                "cf6b1933fe5b476fbbabb876f6d1b924/os-agents', "
+                "'x-compute-request-id': "
+                "'req-e41aa9b4-41a6-4138-ae04-220b768eb644', 'vary': "
+                "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+                "'x-openstack-nova-api-version': '2.1', 'date': "
+                "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+                "'application/json'}",
+                'service': 'Nova',
+                'status_code': '200',
+                'url': 'v2.1/<id>/os-agents',
+                'verb': 'GET'}, {
+                'name': 'AgentsAdminTestJSON:tearDown',
+                'request_body': 'None',
+                'request_headers': "{'Content-Type': 'application/json', "
+                "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+                'response_headers': "{'status': '404', 'content-length': "
+                "'82', 'x-compute-request-id': "
+                "'req-e297aeea-91cf-4f26-b49c-8f46b1b7a926', 'vary': "
+                "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+                "'x-openstack-nova-api-version': '2.1', 'date': "
+                "'Tue, 02 Feb 2016 03:27:02 GMT', 'content-type': "
+                "'application/json; charset=UTF-8'}",
+                'service': 'Nova',
+                'status_code': '404',
+                'url': 'v2.1/<id>/os-agents/3',
+                'verb': 'DELETE'}]}
+
         self.assertEqual(expected_result, parser.test_logs)
diff --git a/tempest/tests/lib/services/identity/v3/test_endpoints_client.py b/tempest/tests/lib/services/identity/v3/test_endpoints_client.py
new file mode 100644
index 0000000..f8c553f
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_endpoints_client.py
@@ -0,0 +1,100 @@
+# Copyright 2016 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.lib.services.identity.v3 import endpoints_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestEndpointsClient(base.BaseServiceTest):
+    FAKE_CREATE_ENDPOINT = {
+        "endpoint": {
+            "id": 1,
+            "tenantId": 1,
+            "region": "North",
+            "type": "compute",
+            "publicURL": "https://compute.north.public.com/v1",
+            "internalURL": "https://compute.north.internal.com/v1",
+            "adminURL": "https://compute.north.internal.com/v1"
+        }
+    }
+
+    FAKE_LIST_ENDPOINTS = {
+        "endpoints": [
+            {
+                "id": 1,
+                "tenantId": "1",
+                "region": "North",
+                "type": "compute",
+                "publicURL": "https://compute.north.public.com/v1",
+                "internalURL": "https://compute.north.internal.com/v1",
+                "adminURL": "https://compute.north.internal.com/v1"
+            },
+            {
+                "id": 2,
+                "tenantId": "1",
+                "region": "South",
+                "type": "compute",
+                "publicURL": "https://compute.north.public.com/v1",
+                "internalURL": "https://compute.north.internal.com/v1",
+                "adminURL": "https://compute.north.internal.com/v1"
+            }
+        ]
+    }
+
+    def setUp(self):
+        super(TestEndpointsClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = endpoints_client.EndPointsClient(fake_auth,
+                                                       'identity', 'regionOne')
+
+    def _test_create_endpoint(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_endpoint,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_CREATE_ENDPOINT,
+            bytes_body,
+            status=201,
+            service_id="b344506af7644f6794d9cb316600b020",
+            region="region-demo",
+            publicurl="https://compute.north.public.com/v1",
+            adminurl="https://compute.north.internal.com/v1",
+            internalurl="https://compute.north.internal.com/v1")
+
+    def _test_list_endpoints(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_endpoints,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_ENDPOINTS,
+            bytes_body)
+
+    def test_create_endpoint_with_str_body(self):
+        self._test_create_endpoint()
+
+    def test_create_endpoint_with_bytes_body(self):
+        self._test_create_endpoint(bytes_body=True)
+
+    def test_list_endpoints_with_str_body(self):
+        self._test_list_endpoints()
+
+    def test_list_endpoints_with_bytes_body(self):
+        self._test_list_endpoints(bytes_body=True)
+
+    def test_delete_endpoint(self):
+        self.check_service_client_function(
+            self.client.delete_endpoint,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            endpoint_id="b344506af7644f6794d9cb316600b020",
+            status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_policies_client.py b/tempest/tests/lib/services/identity/v3/test_policies_client.py
new file mode 100644
index 0000000..66c3d65
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_policies_client.py
@@ -0,0 +1,152 @@
+# Copyright 2016 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.lib.services.identity.v3 import policies_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestPoliciesClient(base.BaseServiceTest):
+    FAKE_CREATE_POLICY = {
+        "policy": {
+            "blob": "{'foobar_user': 'role:compute-user'}",
+            "project_id": "0426ac1e48f642ef9544c2251e07e261",
+            "type": "application/json",
+            "user_id": "0ffd248c55b443eaac5253b4e9cbf9b5"
+            }
+        }
+
+    FAKE_POLICY_INFO = {
+        "policy": {
+            "blob": {
+                "foobar_user": [
+                    "role:compute-user"
+                    ]
+                },
+            "id": "717273",
+            "links": {
+                "self": "http://example.com/identity/v3/policies/717273"
+                },
+            "project_id": "456789",
+            "type": "application/json",
+            "user_id": "616263"
+            }
+        }
+
+    FAKE_LIST_POLICIES = {
+        "links": {
+            "next": None,
+            "previous": None,
+            "self": "http://example.com/identity/v3/policies"
+            },
+        "policies": [
+            {
+                "blob": {
+                    "foobar_user": [
+                        "role:compute-user"
+                        ]
+                    },
+                "id": "717273",
+                "links": {
+                    "self": "http://example.com/identity/v3/policies/717273"
+                    },
+                "project_id": "456789",
+                "type": "application/json",
+                "user_id": "616263"
+                },
+            {
+                "blob": {
+                    "foobar_user": [
+                        "role:compute-user"
+                        ]
+                    },
+                "id": "717274",
+                "links": {
+                    "self": "http://example.com/identity/v3/policies/717274"
+                    },
+                "project_id": "456789",
+                "type": "application/json",
+                "user_id": "616263"
+                }
+            ]
+    }
+
+    def setUp(self):
+        super(TestPoliciesClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = policies_client.PoliciesClient(fake_auth,
+                                                     'identity', 'regionOne')
+
+    def _test_create_policy(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_policy,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_CREATE_POLICY,
+            bytes_body,
+            status=201)
+
+    def _test_show_policy(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_policy,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_POLICY_INFO,
+            bytes_body,
+            policy_id="717273")
+
+    def _test_list_policies(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_policies,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_POLICIES,
+            bytes_body)
+
+    def _test_update_policy(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.update_policy,
+            'tempest.lib.common.rest_client.RestClient.patch',
+            self.FAKE_POLICY_INFO,
+            bytes_body,
+            policy_id="717273")
+
+    def test_create_policy_with_str_body(self):
+        self._test_create_policy()
+
+    def test_create_policy_with_bytes_body(self):
+        self._test_create_policy(bytes_body=True)
+
+    def test_show_policy_with_str_body(self):
+        self._test_show_policy()
+
+    def test_show_policy_with_bytes_body(self):
+        self._test_show_policy(bytes_body=True)
+
+    def test_list_policies_with_str_body(self):
+        self._test_list_policies()
+
+    def test_list_policies_with_bytes_body(self):
+        self._test_list_policies(bytes_body=True)
+
+    def test_update_policy_with_str_body(self):
+        self._test_update_policy()
+
+    def test_update_policy_with_bytes_body(self):
+        self._test_update_policy(bytes_body=True)
+
+    def test_delete_policy(self):
+        self.check_service_client_function(
+            self.client.delete_policy,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            policy_id="717273",
+            status=204)
diff --git a/tempest/tests/test_service_clients.py b/tempest/tests/test_service_clients.py
index a559086..26cc93f 100644
--- a/tempest/tests/test_service_clients.py
+++ b/tempest/tests/test_service_clients.py
@@ -13,15 +13,154 @@
 # the License.
 
 import fixtures
+import mock
 import testtools
+import types
 
 from tempest.lib import auth
 from tempest.lib import exceptions
 from tempest import service_clients
 from tempest.tests import base
+from tempest.tests.lib import fake_auth_provider
 from tempest.tests.lib import fake_credentials
 
 
+has_attribute = testtools.matchers.MatchesPredicateWithParams(
+    lambda x, y: hasattr(x, y), '{0} does not have an attribute {1}')
+
+
+class TestClientsFactory(base.TestCase):
+
+    def setUp(self):
+        super(TestClientsFactory, self).setUp()
+        self.classes = []
+
+    def _setup_fake_module(self, class_names=None, extra_dict=None):
+        class_names = class_names or []
+        fake_module = types.ModuleType('fake_service_client')
+        _dict = {}
+        # Add fake classes to the fake module
+        for name in class_names:
+            _dict[name] = type(name, (object,), {})
+            # Store it for assertions
+            self.classes.append(_dict[name])
+        if extra_dict:
+            _dict[extra_dict] = extra_dict
+        fake_module.__dict__.update(_dict)
+        fixture_importlib = self.useFixture(fixtures.MockPatch(
+            'importlib.import_module', return_value=fake_module))
+        return fixture_importlib.mock
+
+    def test___init___one_class(self):
+        fake_partial = 'fake_partial'
+        partial_mock = self.useFixture(fixtures.MockPatch(
+            'tempest.service_clients.ClientsFactory._get_partial_class',
+            return_value=fake_partial)).mock
+        class_names = ['FakeServiceClient1']
+        mock_importlib = self._setup_fake_module(class_names=class_names)
+        auth_provider = fake_auth_provider.FakeAuthProvider()
+        params = {'k1': 'v1', 'k2': 'v2'}
+        factory = service_clients.ClientsFactory('fake_path', class_names,
+                                                 auth_provider, **params)
+        # Assert module has been imported
+        mock_importlib.assert_called_once_with('fake_path')
+        # All attributes have been created
+        for client in class_names:
+            self.assertThat(factory, has_attribute(client))
+        # Partial have been invoked correctly
+        partial_mock.assert_called_once_with(
+            self.classes[0], auth_provider, params)
+        # Get the clients
+        for name in class_names:
+            self.assertEqual(fake_partial, getattr(factory, name))
+
+    def test___init___two_classes(self):
+        fake_partial = 'fake_partial'
+        partial_mock = self.useFixture(fixtures.MockPatch(
+            'tempest.service_clients.ClientsFactory._get_partial_class',
+            return_value=fake_partial)).mock
+        class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+        mock_importlib = self._setup_fake_module(class_names=class_names)
+        auth_provider = fake_auth_provider.FakeAuthProvider()
+        params = {'k1': 'v1', 'k2': 'v2'}
+        factory = service_clients.ClientsFactory('fake_path', class_names,
+                                                 auth_provider, **params)
+        # Assert module has been imported
+        mock_importlib.assert_called_once_with('fake_path')
+        # All attributes have been created
+        for client in class_names:
+            self.assertThat(factory, has_attribute(client))
+        # Partial have been invoked the right number of times
+        partial_mock.call_count = len(class_names)
+        # Get the clients
+        for name in class_names:
+            self.assertEqual(fake_partial, getattr(factory, name))
+
+    def test___init___no_module(self):
+        auth_provider = fake_auth_provider.FakeAuthProvider()
+        class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+        with testtools.ExpectedException(ImportError, '.*fake_module.*'):
+            service_clients.ClientsFactory('fake_module', class_names,
+                                           auth_provider)
+
+    def test___init___not_a_class(self):
+        class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+        extended_class_names = class_names + ['not_really_a_class']
+        self._setup_fake_module(
+            class_names=class_names, extra_dict='not_really_a_class')
+        auth_provider = fake_auth_provider.FakeAuthProvider()
+        expected_msg = '.*not_really_a_class.*str.*'
+        with testtools.ExpectedException(TypeError, expected_msg):
+            service_clients.ClientsFactory('fake_module', extended_class_names,
+                                           auth_provider)
+
+    def test___init___class_not_found(self):
+        class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+        extended_class_names = class_names + ['not_really_a_class']
+        self._setup_fake_module(class_names=class_names)
+        auth_provider = fake_auth_provider.FakeAuthProvider()
+        expected_msg = '.*not_really_a_class.*fake_service_client.*'
+        with testtools.ExpectedException(AttributeError, expected_msg):
+            service_clients.ClientsFactory('fake_module', extended_class_names,
+                                           auth_provider)
+
+    def test__get_partial_class_no_later_kwargs(self):
+        expected_fake_client = 'not_really_a_client'
+        self._setup_fake_module(class_names=[])
+        auth_provider = fake_auth_provider.FakeAuthProvider()
+        params = {'k1': 'v1', 'k2': 'v2'}
+        factory = service_clients.ClientsFactory(
+            'fake_path', [], auth_provider, **params)
+        klass_mock = mock.Mock(return_value=expected_fake_client)
+        partial = factory._get_partial_class(klass_mock, auth_provider, params)
+        # Class has not be initialised yet
+        klass_mock.assert_not_called()
+        # Use partial and assert on parameters
+        client = partial()
+        self.assertEqual(expected_fake_client, client)
+        klass_mock.assert_called_once_with(auth_provider=auth_provider,
+                                           **params)
+
+    def test__get_partial_class_later_kwargs(self):
+        expected_fake_client = 'not_really_a_client'
+        self._setup_fake_module(class_names=[])
+        auth_provider = fake_auth_provider.FakeAuthProvider()
+        params = {'k1': 'v1', 'k2': 'v2'}
+        later_params = {'k2': 'v4', 'k3': 'v3'}
+        factory = service_clients.ClientsFactory(
+            'fake_path', [], auth_provider, **params)
+        klass_mock = mock.Mock(return_value=expected_fake_client)
+        partial = factory._get_partial_class(klass_mock, auth_provider, params)
+        # Class has not be initialised yet
+        klass_mock.assert_not_called()
+        # Use partial and assert on parameters
+        client = partial(**later_params)
+        params.update(later_params)
+        self.assertEqual(expected_fake_client, client)
+        klass_mock.assert_called_once_with(auth_provider=auth_provider,
+                                           **params)
+
+
 class TestServiceClients(base.TestCase):
 
     def setUp(self):