Merge "Modified into a more appropriate function"
diff --git a/releasenotes/notes/plugin-service-client-registration-00b19a2dd4935ba0.yaml b/releasenotes/notes/plugin-service-client-registration-00b19a2dd4935ba0.yaml
new file mode 100644
index 0000000..64f729a
--- /dev/null
+++ b/releasenotes/notes/plugin-service-client-registration-00b19a2dd4935ba0.yaml
@@ -0,0 +1,12 @@
+---
+features:
+  - A new optional interface `TempestPlugin.get_service_clients`
+    is available to plugins. It allows them to declare
+    any service client they implement. For now this is used by
+    tempest only, for auto-registration of service clients
+    in the new class `ServiceClients`.
+  - A new singleton class `clients.ClientsRegistry` is
+    available. It holds the service clients registration data
+    from all plugins. It is used by `ServiceClients` for
+    auto-registration of the service clients implemented
+    in plugins.
diff --git a/tempest/api/compute/security_groups/test_security_groups_negative.py b/tempest/api/compute/security_groups/test_security_groups_negative.py
index 5125e2b..e6abf28 100644
--- a/tempest/api/compute/security_groups/test_security_groups_negative.py
+++ b/tempest/api/compute/security_groups/test_security_groups_negative.py
@@ -44,9 +44,11 @@
             security_group_id.append(body[i]['id'])
         # Generate a non-existent security group id
         while True:
-            non_exist_id = data_utils.rand_int_id(start=999)
-            if self.neutron_available:
+            if (self.neutron_available and
+                test.is_extension_enabled('security-group', 'network')):
                 non_exist_id = data_utils.rand_uuid()
+            else:
+                non_exist_id = data_utils.rand_int_id(start=999)
             if non_exist_id not in security_group_id:
                 break
         return non_exist_id
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index d02f86f..7c12bf9 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -21,6 +21,7 @@
 from tempest.common import waiters
 from tempest import config
 from tempest import exceptions
+from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 from tempest import test
 
@@ -272,6 +273,7 @@
                 break
         self.servers_client.remove_fixed_ip(server['id'], address=fixed_ip)
 
+    @decorators.skip_because(bug='1607714')
     @test.idempotent_id('2f3a0127-95c7-4977-92d2-bc5aec602fb4')
     def test_reassign_port_between_servers(self):
         """Tests the following:
diff --git a/tempest/api/identity/v2/test_ec2_credentials.py b/tempest/api/identity/v2/test_ec2_credentials.py
index 3c379f0..8f493aa 100644
--- a/tempest/api/identity/v2/test_ec2_credentials.py
+++ b/tempest/api/identity/v2/test_ec2_credentials.py
@@ -51,7 +51,6 @@
     def test_list_ec2_credentials(self):
         """Get the list of user ec2 credentials."""
         created_creds = []
-        fetched_creds = []
         # create first ec2 credentials
         creds1 = self.non_admin_users_client.create_user_ec2_credential(
             self.creds.user_id,
diff --git a/tempest/clients.py b/tempest/clients.py
index fd010f2..e070637 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -22,9 +22,6 @@
 from tempest import exceptions
 from tempest.lib import auth
 from tempest.lib import exceptions as lib_exc
-from tempest.lib.services import compute
-from tempest.lib.services import image
-from tempest.lib.services import network
 from tempest import service_clients
 from tempest.services import baremetal
 from tempest.services import data_processing
@@ -127,133 +124,84 @@
         return configuration
 
     def _set_network_clients(self):
-        params = self.parameters['network']
-        self.network_agents_client = network.AgentsClient(
-            self.auth_provider, **params)
-        self.network_extensions_client = network.ExtensionsClient(
-            self.auth_provider, **params)
-        self.networks_client = network.NetworksClient(
-            self.auth_provider, **params)
-        self.subnetpools_client = network.SubnetpoolsClient(
-            self.auth_provider, **params)
-        self.subnets_client = network.SubnetsClient(
-            self.auth_provider, **params)
-        self.ports_client = network.PortsClient(
-            self.auth_provider, **params)
-        self.network_quotas_client = network.QuotasClient(
-            self.auth_provider, **params)
-        self.floating_ips_client = network.FloatingIPsClient(
-            self.auth_provider, **params)
-        self.metering_labels_client = network.MeteringLabelsClient(
-            self.auth_provider, **params)
-        self.metering_label_rules_client = network.MeteringLabelRulesClient(
-            self.auth_provider, **params)
-        self.routers_client = network.RoutersClient(
-            self.auth_provider, **params)
-        self.security_group_rules_client = network.SecurityGroupRulesClient(
-            self.auth_provider, **params)
-        self.security_groups_client = network.SecurityGroupsClient(
-            self.auth_provider, **params)
-        self.network_versions_client = network.NetworkVersionsClient(
-            self.auth_provider, **params)
+        self.network_agents_client = self.network.AgentsClient()
+        self.network_extensions_client = self.network.ExtensionsClient()
+        self.networks_client = self.network.NetworksClient()
+        self.subnetpools_client = self.network.SubnetpoolsClient()
+        self.subnets_client = self.network.SubnetsClient()
+        self.ports_client = self.network.PortsClient()
+        self.network_quotas_client = self.network.QuotasClient()
+        self.floating_ips_client = self.network.FloatingIPsClient()
+        self.metering_labels_client = self.network.MeteringLabelsClient()
+        self.metering_label_rules_client = (
+            self.network.MeteringLabelRulesClient())
+        self.routers_client = self.network.RoutersClient()
+        self.security_group_rules_client = (
+            self.network.SecurityGroupRulesClient())
+        self.security_groups_client = self.network.SecurityGroupsClient()
+        self.network_versions_client = self.network.NetworkVersionsClient()
 
     def _set_image_clients(self):
         if CONF.service_available.glance:
-            params = self.parameters['image']
-            self.image_client = image.v1.ImagesClient(
-                self.auth_provider, **params)
-            self.image_member_client = image.v1.ImageMembersClient(
-                self.auth_provider, **params)
-
-            self.image_client_v2 = image.v2.ImagesClient(
-                self.auth_provider, **params)
-            self.image_member_client_v2 = image.v2.ImageMembersClient(
-                self.auth_provider, **params)
-            self.namespaces_client = image.v2.NamespacesClient(
-                self.auth_provider, **params)
-            self.resource_types_client = image.v2.ResourceTypesClient(
-                self.auth_provider, **params)
-            self.schemas_client = image.v2.SchemasClient(
-                self.auth_provider, **params)
+            self.image_client = self.image_v1.ImagesClient()
+            self.image_member_client = self.image_v1.ImageMembersClient()
+            self.image_client_v2 = self.image_v2.ImagesClient()
+            self.image_member_client_v2 = self.image_v2.ImageMembersClient()
+            self.namespaces_client = self.image_v2.NamespacesClient()
+            self.resource_types_client = self.image_v2.ResourceTypesClient()
+            self.schemas_client = self.image_v2.SchemasClient()
 
     def _set_compute_clients(self):
-        params = self.parameters['compute']
-
-        self.agents_client = compute.AgentsClient(self.auth_provider, **params)
-        self.compute_networks_client = compute.NetworksClient(
-            self.auth_provider, **params)
-        self.migrations_client = compute.MigrationsClient(self.auth_provider,
-                                                          **params)
+        self.agents_client = self.compute.AgentsClient()
+        self.compute_networks_client = self.compute.NetworksClient()
+        self.migrations_client = self.compute.MigrationsClient()
         self.security_group_default_rules_client = (
-            compute.SecurityGroupDefaultRulesClient(self.auth_provider,
-                                                    **params))
-        self.certificates_client = compute.CertificatesClient(
-            self.auth_provider, **params)
-        self.servers_client = compute.ServersClient(
-            self.auth_provider,
-            enable_instance_password=CONF.compute_feature_enabled
-                .enable_instance_password,
-            **params)
-        self.server_groups_client = compute.ServerGroupsClient(
-            self.auth_provider, **params)
-        self.limits_client = compute.LimitsClient(self.auth_provider, **params)
-        self.compute_images_client = compute.ImagesClient(self.auth_provider,
-                                                          **params)
-        self.keypairs_client = compute.KeyPairsClient(self.auth_provider,
-                                                      **params)
-        self.quotas_client = compute.QuotasClient(self.auth_provider, **params)
-        self.quota_classes_client = compute.QuotaClassesClient(
-            self.auth_provider, **params)
-        self.flavors_client = compute.FlavorsClient(self.auth_provider,
-                                                    **params)
-        self.extensions_client = compute.ExtensionsClient(self.auth_provider,
-                                                          **params)
-        self.floating_ip_pools_client = compute.FloatingIPPoolsClient(
-            self.auth_provider, **params)
-        self.floating_ips_bulk_client = compute.FloatingIPsBulkClient(
-            self.auth_provider, **params)
-        self.compute_floating_ips_client = compute.FloatingIPsClient(
-            self.auth_provider, **params)
+            self.compute.SecurityGroupDefaultRulesClient())
+        self.certificates_client = self.compute.CertificatesClient()
+        eip = CONF.compute_feature_enabled.enable_instance_password
+        self.servers_client = self.compute.ServersClient(
+            enable_instance_password=eip)
+        self.server_groups_client = self.compute.ServerGroupsClient()
+        self.limits_client = self.compute.LimitsClient()
+        self.compute_images_client = self.compute.ImagesClient()
+        self.keypairs_client = self.compute.KeyPairsClient()
+        self.quotas_client = self.compute.QuotasClient()
+        self.quota_classes_client = self.compute.QuotaClassesClient()
+        self.flavors_client = self.compute.FlavorsClient()
+        self.extensions_client = self.compute.ExtensionsClient()
+        self.floating_ip_pools_client = self.compute.FloatingIPPoolsClient()
+        self.floating_ips_bulk_client = self.compute.FloatingIPsBulkClient()
+        self.compute_floating_ips_client = self.compute.FloatingIPsClient()
         self.compute_security_group_rules_client = (
-            compute.SecurityGroupRulesClient(self.auth_provider, **params))
-        self.compute_security_groups_client = compute.SecurityGroupsClient(
-            self.auth_provider, **params)
-        self.interfaces_client = compute.InterfacesClient(self.auth_provider,
-                                                          **params)
-        self.fixed_ips_client = compute.FixedIPsClient(self.auth_provider,
-                                                       **params)
-        self.availability_zone_client = compute.AvailabilityZoneClient(
-            self.auth_provider, **params)
-        self.aggregates_client = compute.AggregatesClient(self.auth_provider,
-                                                          **params)
-        self.services_client = compute.ServicesClient(self.auth_provider,
-                                                      **params)
-        self.tenant_usages_client = compute.TenantUsagesClient(
-            self.auth_provider, **params)
-        self.hosts_client = compute.HostsClient(self.auth_provider, **params)
-        self.hypervisor_client = compute.HypervisorClient(self.auth_provider,
-                                                          **params)
+            self.compute.SecurityGroupRulesClient())
+        self.compute_security_groups_client = (
+            self.compute.SecurityGroupsClient())
+        self.interfaces_client = self.compute.InterfacesClient()
+        self.fixed_ips_client = self.compute.FixedIPsClient()
+        self.availability_zone_client = self.compute.AvailabilityZoneClient()
+        self.aggregates_client = self.compute.AggregatesClient()
+        self.services_client = self.compute.ServicesClient()
+        self.tenant_usages_client = self.compute.TenantUsagesClient()
+        self.hosts_client = self.compute.HostsClient()
+        self.hypervisor_client = self.compute.HypervisorClient()
         self.instance_usages_audit_log_client = (
-            compute.InstanceUsagesAuditLogClient(self.auth_provider, **params))
-        self.tenant_networks_client = compute.TenantNetworksClient(
-            self.auth_provider, **params)
-        self.baremetal_nodes_client = compute.BaremetalNodesClient(
-            self.auth_provider, **params)
+            self.compute.InstanceUsagesAuditLogClient())
+        self.tenant_networks_client = self.compute.TenantNetworksClient()
+        self.baremetal_nodes_client = self.compute.BaremetalNodesClient()
 
         # NOTE: The following client needs special timeout values because
         # the API is a proxy for the other component.
-        params_volume = copy.deepcopy(params)
-        # Optional parameters
+        params_volume = {}
         for _key in ('build_interval', 'build_timeout'):
             _value = self.parameters['volume'].get(_key)
             if _value:
                 params_volume[_key] = _value
-        self.volumes_extensions_client = compute.VolumesClient(
-            self.auth_provider, **params_volume)
-        self.compute_versions_client = compute.VersionsClient(
-            self.auth_provider, **params_volume)
-        self.snapshots_extensions_client = compute.SnapshotsClient(
-            self.auth_provider, **params_volume)
+        self.volumes_extensions_client = self.compute.VolumesClient(
+            **params_volume)
+        self.compute_versions_client = self.compute.VersionsClient(
+            **params_volume)
+        self.snapshots_extensions_client = self.compute.SnapshotsClient(
+            **params_volume)
 
     def _set_identity_clients(self):
         params = self.parameters['identity']
diff --git a/tempest/lib/exceptions.py b/tempest/lib/exceptions.py
index 5ca78f9..de2d713 100644
--- a/tempest/lib/exceptions.py
+++ b/tempest/lib/exceptions.py
@@ -229,3 +229,13 @@
 
 class UnknownServiceClient(TempestException):
     message = "Service clients named %(services)s are not known"
+
+
+class ServiceClientRegistrationException(TempestException):
+    message = ("Error registering module %(name)s in path %(module_path)s, "
+               "with service %(service_version)s and clients "
+               "%(client_names)s: %(detailed_error)s")
+
+
+class PluginRegistrationException(TempestException):
+    message = "Error registering plugin %(name)s: %(detailed_error)s"
diff --git a/tempest/lib/services/clients.py b/tempest/lib/services/clients.py
new file mode 100644
index 0000000..8054e62
--- /dev/null
+++ b/tempest/lib/services/clients.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+# 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.common.utils import misc
+from tempest.lib import exceptions
+
+
+@misc.singleton
+class ClientsRegistry(object):
+    """Registry of all service clients available from plugins"""
+
+    def __init__(self):
+        self._service_clients = {}
+
+    def register_service_client(self, plugin_name, service_client_data):
+        if plugin_name in self._service_clients:
+            detailed_error = 'Clients for plugin %s already registered'
+            raise exceptions.PluginRegistrationException(
+                name=plugin_name,
+                detailed_error=detailed_error % plugin_name)
+        self._service_clients[plugin_name] = service_client_data
+
+    def get_service_clients(self):
+        return self._service_clients
diff --git a/tempest/service_clients.py b/tempest/service_clients.py
index 252ebf4..f131b58 100644
--- a/tempest/service_clients.py
+++ b/tempest/service_clients.py
@@ -17,9 +17,23 @@
 import copy
 import importlib
 import inspect
+import logging
 
 from tempest.lib import auth
 from tempest.lib import exceptions
+from tempest.lib.services import clients
+from tempest.lib.services import compute
+from tempest.lib.services import image
+from tempest.lib.services import network
+
+LOG = logging.getLogger(__name__)
+
+client_modules_by_service_name = {
+    'compute': compute,
+    'image.v1': image.v1,
+    'image.v2': image.v2,
+    'network': network
+}
 
 
 def tempest_modules():
@@ -33,9 +47,48 @@
 
 
 def available_modules():
-    """List of service client modules available in Tempest and plugins"""
-    # TODO(andreaf) For now this returns only tempest_modules
-    return tempest_modules()
+    """List of service client modules available in Tempest and plugins
+
+    The list of available modules can be used for automatic configuration.
+
+    :raise PluginRegistrationException: if a plugin exposes a service_version
+        already defined by Tempest or another plugin.
+
+    Examples:
+
+        >>> from tempest import config
+        >>> params = {}
+        >>> for service_version in available_modules():
+        >>>     service = service_version.split('.')[0]
+        >>>     params[service] = config.service_client_config(service)
+        >>> service_clients = ServiceClients(creds, identity_uri,
+        >>>                                  client_parameters=params)
+    """
+    extra_service_versions = set([])
+    plugin_services = clients.ClientsRegistry().get_service_clients()
+    for plugin_name in plugin_services:
+        plug_service_versions = set([x['service_version'] for x in
+                                     plugin_services[plugin_name]])
+        # If a plugin exposes a duplicate service_version raise an exception
+        if plug_service_versions:
+            if not plug_service_versions.isdisjoint(extra_service_versions):
+                detailed_error = (
+                    'Plugin %s is trying to register a service %s already '
+                    'claimed by another one' % (plugin_name,
+                                                extra_service_versions &
+                                                plug_service_versions))
+                raise exceptions.PluginRegistrationException(
+                    name=plugin_name, detailed_error=detailed_error)
+            if not plug_service_versions.isdisjoint(tempest_modules()):
+                detailed_error = (
+                    'Plugin %s is trying to register a service %s already '
+                    'claimed by a Tempest one' % (plugin_name,
+                                                  tempest_modules() &
+                                                  plug_service_versions))
+                raise exceptions.PluginRegistrationException(
+                    name=plugin_name, detailed_error=detailed_error)
+        extra_service_versions |= plug_service_versions
+    return tempest_modules() | extra_service_versions
 
 
 class ClientsFactory(object):
@@ -49,8 +102,6 @@
     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
@@ -206,6 +257,7 @@
             >>> client_parameters['service_y'] = params_service_y
 
         """
+        self._registered_services = set([])
         self.credentials = credentials
         self.identity_uri = identity_uri
         if not identity_uri:
@@ -247,6 +299,94 @@
             raise exceptions.UnknownServiceClient(
                 services=list(client_parameters.keys()))
 
+        # Register service clients owned by tempest
+        for service in tempest_modules():
+            if service in list(client_modules_by_service_name):
+                attribute = service.replace('.', '_')
+                configs = service.split('.')[0]
+                module = client_modules_by_service_name[service]
+                self.register_service_client_module(
+                    attribute, service, module.__name__,
+                    module.__all__, **self.parameters[configs])
+
+        # Register service clients from plugins
+        clients_registry = clients.ClientsRegistry()
+        plugin_service_clients = clients_registry.get_service_clients()
+        for plugin in plugin_service_clients:
+            service_clients = plugin_service_clients[plugin]
+            # Each plugin returns a list of service client parameters
+            for service_client in service_clients:
+                # NOTE(andreaf) If a plugin cannot register, stop the
+                # registration process, log some details to help
+                # troubleshooting, and re-raise
+                try:
+                    self.register_service_client_module(**service_client)
+                except Exception:
+                    LOG.exception(
+                        'Failed to register service client from plugin %s '
+                        'with parameters %s' % (plugin, service_client))
+                    raise
+
+    def register_service_client_module(self, name, service_version,
+                                       module_path, client_names, **kwargs):
+        """Register a service client module
+
+        Initiates a client factory for the specified module, using this
+        class auth_provider, and accessible via a `name` attribute in the
+        service client.
+
+        :param name: Name used to access the client
+        :param service_version: Name of the service complete with version.
+            Used to track registered services. When a plugin implements it,
+            it can be used by other plugins to obtain their configuration.
+        :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 service client classes.
+        :param kwargs: Extra optional parameters to be passed to all clients.
+            ServiceClient provides defaults for region, dscv, ca_certs and
+            trace_requests.
+        :raise ServiceClientRegistrationException: if the provided name is
+            already in use or if service_version is already registered.
+        :raise ImportError: if module_path cannot be imported.
+        """
+        if hasattr(self, name):
+            using_name = getattr(self, name)
+            detailed_error = 'Module name already in use: %s' % using_name
+            raise exceptions.ServiceClientRegistrationException(
+                name=name, service_version=service_version,
+                module_path=module_path, client_names=client_names,
+                detailed_error=detailed_error)
+        if service_version in self.registered_services:
+            detailed_error = 'Service %s already registered.' % service_version
+            raise exceptions.ServiceClientRegistrationException(
+                name=name, service_version=service_version,
+                module_path=module_path, client_names=client_names,
+                detailed_error=detailed_error)
+        params = dict(region=self.region,
+                      disable_ssl_certificate_validation=self.dscv,
+                      ca_certs=self.ca_certs,
+                      trace_requests=self.trace_requests)
+        params.update(kwargs)
+        # Instantiate the client factory
+        _factory = ClientsFactory(module_path=module_path,
+                                  client_names=client_names,
+                                  auth_provider=self.auth_provider,
+                                  **params)
+        # Adds the client factory to the service_client
+        setattr(self, name, _factory)
+        # Add the name of the new service in self.SERVICES for discovery
+        self._registered_services.add(service_version)
+
+    @property
+    def registered_services(self):
+        # TODO(andreaf) Temporary set needed until all services are migrated
+        _non_migrated_services = tempest_modules() - set(
+            client_modules_by_service_name)
+        return self._registered_services | _non_migrated_services
+
     def _setup_parameters(self, parameters):
         """Setup default values for client parameters
 
diff --git a/tempest/services/volume/base/base_availability_zone_client.py b/tempest/services/volume/base/base_availability_zone_client.py
deleted file mode 100644
index 1c2deba..0000000
--- a/tempest/services/volume/base/base_availability_zone_client.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# 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 oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
-
-
-class BaseAvailabilityZoneClient(rest_client.RestClient):
-
-    def list_availability_zones(self):
-        resp, body = self.get('os-availability-zone')
-        body = json.loads(body)
-        self.expected_success(200, resp.status)
-        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/base_extensions_client.py b/tempest/services/volume/base/base_extensions_client.py
deleted file mode 100644
index b90fe94..0000000
--- a/tempest/services/volume/base/base_extensions_client.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# All Rights Reserved.
-#
-#    Licensed under the Apache License, Version 2.0 (the "License"); you may
-#    not use this file except in compliance with the License. You may obtain
-#    a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-#    License for the specific language governing permissions and limitations
-#    under the License.
-
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
-
-
-class BaseExtensionsClient(rest_client.RestClient):
-
-    def list_extensions(self):
-        url = 'extensions'
-        resp, body = self.get(url)
-        body = json.loads(body)
-        self.expected_success(200, resp.status)
-        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v1/json/availability_zone_client.py b/tempest/services/volume/v1/json/availability_zone_client.py
index 3a27027..be4f539 100644
--- a/tempest/services/volume/v1/json/availability_zone_client.py
+++ b/tempest/services/volume/v1/json/availability_zone_client.py
@@ -13,9 +13,16 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.services.volume.base import base_availability_zone_client
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
 
 
-class AvailabilityZoneClient(
-        base_availability_zone_client.BaseAvailabilityZoneClient):
+class AvailabilityZoneClient(rest_client.RestClient):
     """Volume V1 availability zone client."""
+
+    def list_availability_zones(self):
+        resp, body = self.get('os-availability-zone')
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v1/json/extensions_client.py b/tempest/services/volume/v1/json/extensions_client.py
index f99d0f5..7b849a8 100644
--- a/tempest/services/volume/v1/json/extensions_client.py
+++ b/tempest/services/volume/v1/json/extensions_client.py
@@ -13,8 +13,17 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.services.volume.base import base_extensions_client
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
 
 
-class ExtensionsClient(base_extensions_client.BaseExtensionsClient):
+class ExtensionsClient(rest_client.RestClient):
     """Volume V1 extensions client."""
+
+    def list_extensions(self):
+        url = 'extensions'
+        resp, body = self.get(url)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v2/json/availability_zone_client.py b/tempest/services/volume/v2/json/availability_zone_client.py
index 905ebdc..bb4a357 100644
--- a/tempest/services/volume/v2/json/availability_zone_client.py
+++ b/tempest/services/volume/v2/json/availability_zone_client.py
@@ -13,9 +13,16 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.services.volume.base import base_availability_zone_client
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
 
 
-class AvailabilityZoneClient(
-        base_availability_zone_client.BaseAvailabilityZoneClient):
+class AvailabilityZoneClient(rest_client.RestClient):
     api_version = "v2"
+
+    def list_availability_zones(self):
+        resp, body = self.get('os-availability-zone')
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v2/json/extensions_client.py b/tempest/services/volume/v2/json/extensions_client.py
index 245906f..09279d5 100644
--- a/tempest/services/volume/v2/json/extensions_client.py
+++ b/tempest/services/volume/v2/json/extensions_client.py
@@ -13,8 +13,18 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.services.volume.base import base_extensions_client
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
 
 
-class ExtensionsClient(base_extensions_client.BaseExtensionsClient):
+class ExtensionsClient(rest_client.RestClient):
+    """Volume V2 extensions client."""
     api_version = "v2"
+
+    def list_extensions(self):
+        url = 'extensions'
+        resp, body = self.get(url)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/test_discover/plugins.py b/tempest/test_discover/plugins.py
index d604b28..cfb0c7f 100644
--- a/tempest/test_discover/plugins.py
+++ b/tempest/test_discover/plugins.py
@@ -19,6 +19,7 @@
 import stevedore
 
 from tempest.lib.common.utils import misc
+from tempest.lib.services import clients
 
 LOG = logging.getLogger(__name__)
 
@@ -62,6 +63,54 @@
         """
         return []
 
+    def get_service_clients(self):
+        """Get a list of the service clients for registration
+
+        If the plugin implements service clients for one or more APIs, it
+        may return their details by this method for automatic registration
+        in any ServiceClients object instantiated by tests.
+        The default implementation returns an empty list.
+
+        :return list of dictionaries. Each element of the list represents
+            the service client for an API. Each dict must define all
+            parameters required for the invocation of
+            `service_clients.ServiceClients.register_service_client_module`.
+        :rtype: list
+
+        Example:
+
+            >>>  # Example implementation with one service client
+            >>>  myservice_config = config.service_client_config('myservice')
+            >>>  params = {
+            >>>     'name': 'myservice',
+            >>>     'service_version': 'myservice',
+            >>>     'module_path': 'myservice_tempest_tests.services',
+            >>>     'client_names': ['API1Client', 'API2Client'],
+            >>>  }
+            >>>  params.update(myservice_config)
+            >>> return [params]
+
+            >>>  # Example implementation with two service clients
+            >>>  foo1_config = config.service_client_config('foo')
+            >>>  params_foo1 = {
+            >>>     'name': 'foo_v1',
+            >>>     'service_version': 'foo.v1',
+            >>>     'module_path': 'bar_tempest_tests.services.foo.v1',
+            >>>     'client_names': ['API1Client', 'API2Client'],
+            >>>  }
+            >>>  params_foo1.update(foo_config)
+            >>>  foo2_config = config.service_client_config('foo')
+            >>>  params_foo2 = {
+            >>>     'name': 'foo_v2',
+            >>>     'service_version': 'foo.v2',
+            >>>     'module_path': 'bar_tempest_tests.services.foo.v2',
+            >>>     'client_names': ['API1Client', 'API2Client'],
+            >>>  }
+            >>>  params_foo2.update(foo2_config)
+            >>> return [params_foo1, params_foo2]
+        """
+        return []
+
 
 @misc.singleton
 class TempestTestPluginManager(object):
@@ -75,6 +124,7 @@
             'tempest.test_plugins', invoke_on_load=True,
             propagate_map_exceptions=True,
             on_load_failure_callback=self.failure_hook)
+        self._register_service_clients()
 
     @staticmethod
     def failure_hook(_, ep, err):
@@ -102,3 +152,13 @@
             if opt_list:
                 plugin_options.extend(opt_list)
         return plugin_options
+
+    def _register_service_clients(self):
+        registry = clients.ClientsRegistry()
+        for plug in self.ext_plugins:
+            try:
+                registry.register_service_client(
+                    plug.name, plug.obj.get_service_clients())
+            except Exception:
+                LOG.exception('Plugin %s raised an exception trying to run '
+                              'get_service_clients' % plug.name)
diff --git a/tempest/tests/fake_tempest_plugin.py b/tempest/tests/fake_tempest_plugin.py
index f718d0b..56aae1e 100644
--- a/tempest/tests/fake_tempest_plugin.py
+++ b/tempest/tests/fake_tempest_plugin.py
@@ -18,6 +18,7 @@
 
 class FakePlugin(plugins.TempestPlugin):
     expected_load_test = ["my/test/path", "/home/dir"]
+    expected_service_clients = [{'foo': 'bar'}]
 
     def load_tests(self):
         return self.expected_load_test
@@ -28,6 +29,9 @@
     def get_opt_lists(self):
         return []
 
+    def get_service_clients(self):
+        return self.expected_service_clients
+
 
 class FakeStevedoreObj(object):
     obj = FakePlugin()
@@ -38,3 +42,26 @@
 
     def __init__(self, name='Test1'):
         self._name = name
+
+
+class FakePluginNoServiceClients(plugins.TempestPlugin):
+
+    def load_tests(self):
+        return []
+
+    def register_opts(self, conf):
+        return
+
+    def get_opt_lists(self):
+        return []
+
+
+class FakeStevedoreObjNoServiceClients(object):
+    obj = FakePluginNoServiceClients()
+
+    @property
+    def name(self):
+        return self._name
+
+    def __init__(self, name='Test2'):
+        self._name = name
diff --git a/tempest/tests/test_service_clients.py b/tempest/tests/test_service_clients.py
index 26cc93f..3d8b360 100644
--- a/tempest/tests/test_service_clients.py
+++ b/tempest/tests/test_service_clients.py
@@ -263,3 +263,84 @@
         for _key in _params.keys():
             self.assertEqual(expected_params[_key],
                              _params[_key])
+
+    def test_register_service_client_module(self):
+        expected_params = {'fake_param1': 'fake_value1',
+                           'fake_param2': 'fake_value2'}
+        _manager = self._get_manager(init_region='fake_region_default')
+        # Mock after the _manager is setup to preserve the call count
+        factory_mock = self.useFixture(fixtures.MockPatch(
+            'tempest.service_clients.ClientsFactory')).mock
+        _manager.register_service_client_module(
+            name='fake_module',
+            service_version='fake_service',
+            module_path='fake.path.to.module',
+            client_names=[],
+            **expected_params)
+        self.assertThat(_manager, has_attribute('fake_module'))
+        # Assert called once, without check for exact parameters
+        self.assertTrue(factory_mock.called)
+        self.assertEqual(1, factory_mock.call_count)
+        # Assert expected params are in with their values
+        actual_kwargs = factory_mock.call_args[1]
+        self.assertIn('region', actual_kwargs)
+        self.assertEqual('fake_region_default', actual_kwargs['region'])
+        for param in expected_params:
+            self.assertIn(param, actual_kwargs)
+            self.assertEqual(expected_params[param], actual_kwargs[param])
+        # Assert the new service is registered
+        self.assertIn('fake_service', _manager._registered_services)
+
+    def test_register_service_client_module_override_default(self):
+        new_region = 'new_region'
+        expected_params = {'fake_param1': 'fake_value1',
+                           'fake_param2': 'fake_value2',
+                           'region': new_region}
+        _manager = self._get_manager(init_region='fake_region_default')
+        # Mock after the _manager is setup to preserve the call count
+        factory_mock = self.useFixture(fixtures.MockPatch(
+            'tempest.service_clients.ClientsFactory')).mock
+        _manager.register_service_client_module(
+            name='fake_module',
+            service_version='fake_service',
+            module_path='fake.path.to.module',
+            client_names=[],
+            **expected_params)
+        self.assertThat(_manager, has_attribute('fake_module'))
+        # Assert called once, without check for exact parameters
+        self.assertTrue(factory_mock.called)
+        self.assertEqual(1, factory_mock.call_count)
+        # Assert expected params are in with their values
+        actual_kwargs = factory_mock.call_args[1]
+        self.assertIn('region', actual_kwargs)
+        self.assertEqual(new_region, actual_kwargs['region'])
+        for param in expected_params:
+            self.assertIn(param, actual_kwargs)
+            self.assertEqual(expected_params[param], actual_kwargs[param])
+        # Assert the new service is registered
+        self.assertIn('fake_service', _manager._registered_services)
+
+    def test_register_service_client_module_duplicate_name(self):
+        self.useFixture(fixtures.MockPatch(
+            'tempest.service_clients.ClientsFactory'))
+        _manager = self._get_manager()
+        name_owner = 'this_is_a_string'
+        setattr(_manager, 'fake_module', name_owner)
+        expected_error = '.*' + name_owner
+        with testtools.ExpectedException(
+                exceptions.ServiceClientRegistrationException, expected_error):
+            _manager.register_service_client_module(
+                name='fake_module', module_path='fake.path.to.module',
+                service_version='fake_service', client_names=[])
+
+    def test_register_service_client_module_duplicate_service(self):
+        self.useFixture(fixtures.MockPatch(
+            'tempest.service_clients.ClientsFactory'))
+        _manager = self._get_manager()
+        duplicate_service = 'fake_service1'
+        expected_error = '.*' + duplicate_service
+        with testtools.ExpectedException(
+                exceptions.ServiceClientRegistrationException, expected_error):
+            _manager.register_service_client_module(
+                name='fake_module', module_path='fake.path.to.module',
+                service_version=duplicate_service, client_names=[])
diff --git a/tempest/tests/test_tempest_plugin.py b/tempest/tests/test_tempest_plugin.py
index c07e98c..dd50125 100644
--- a/tempest/tests/test_tempest_plugin.py
+++ b/tempest/tests/test_tempest_plugin.py
@@ -13,6 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from tempest.lib.services import clients
 from tempest.test_discover import plugins
 from tempest.tests import base
 from tempest.tests import fake_tempest_plugin as fake_plugin
@@ -42,3 +43,39 @@
                          result['fake01'])
         self.assertEqual(fake_plugin.FakePlugin.expected_load_test,
                          result['fake02'])
+
+    def test__register_service_clients_with_one_plugin(self):
+        registry = clients.ClientsRegistry()
+        manager = plugins.TempestTestPluginManager()
+        fake_obj = fake_plugin.FakeStevedoreObj()
+        manager.ext_plugins = [fake_obj]
+        manager._register_service_clients()
+        expected_result = fake_plugin.FakePlugin.expected_service_clients
+        registered_clients = registry.get_service_clients()
+        self.assertIn(fake_obj.name, registered_clients)
+        self.assertEqual(expected_result, registered_clients[fake_obj.name])
+
+    def test__get_service_clients_with_two_plugins(self):
+        registry = clients.ClientsRegistry()
+        manager = plugins.TempestTestPluginManager()
+        obj1 = fake_plugin.FakeStevedoreObj('fake01')
+        obj2 = fake_plugin.FakeStevedoreObj('fake02')
+        manager.ext_plugins = [obj1, obj2]
+        manager._register_service_clients()
+        expected_result = fake_plugin.FakePlugin.expected_service_clients
+        registered_clients = registry.get_service_clients()
+        self.assertIn('fake01', registered_clients)
+        self.assertIn('fake02', registered_clients)
+        self.assertEqual(expected_result, registered_clients['fake01'])
+        self.assertEqual(expected_result, registered_clients['fake02'])
+
+    def test__register_service_clients_one_plugin_no_service_clients(self):
+        registry = clients.ClientsRegistry()
+        manager = plugins.TempestTestPluginManager()
+        fake_obj = fake_plugin.FakeStevedoreObjNoServiceClients()
+        manager.ext_plugins = [fake_obj]
+        manager._register_service_clients()
+        expected_result = []
+        registered_clients = registry.get_service_clients()
+        self.assertIn(fake_obj.name, registered_clients)
+        self.assertEqual(expected_result, registered_clients[fake_obj.name])