Introduce the ClientsFactory

Define a new class ClientsFactory, which provides lazy loading
an simple instantiation of clients by using partial to set
defaults for all client parameters.

This class will be used in upcoming patches to implement an
interface that allows plugins to register their own service
clients in the ServiceClients interface.

It will also be used to implement lazy loading of clients for
tempest own clients.

Partially-implements: bp client-manager-refactor

Change-Id: If2295d56a8e52ffce28d6a0f15517bc325aab010
This commit is contained in:
Andrea Frittoli (andreaf) 2016-06-21 17:20:31 +01:00
parent 1c984e4034
commit 73dd51dfe1
2 changed files with 225 additions and 0 deletions

View File

@ -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 @@ def available_modules():
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

View File

@ -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):