blob: adf666bfa661a2c90597d807801fda9277adaee8 [file] [log] [blame]
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +01001# Copyright 2012 OpenStack Foundation
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +01002# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010017import copy
18import importlib
19import inspect
20import logging
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010021
22from tempest.lib import auth
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +010023from tempest.lib.common.utils import misc
24from tempest.lib import exceptions
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010025from tempest.lib.services import compute
26from tempest.lib.services import image
27from tempest.lib.services import network
28
29
30LOG = logging.getLogger(__name__)
31
32
33def tempest_modules():
34 """Dict of service client modules available in Tempest.
35
36 Provides a dict of stable service modules available in Tempest, with
37 ``service_version`` as key, and the module object as value.
38 """
39 return {
40 'compute': compute,
41 'image.v1': image.v1,
42 'image.v2': image.v2,
43 'network': network
44 }
45
46
47def _tempest_internal_modules():
48 # Set of unstable service clients available in Tempest
49 # NOTE(andreaf) This list will exists only as long the remain clients
50 # are migrated to tempest.lib, and it will then be deleted without
51 # deprecation or advance notice
52 return set(['identity.v2', 'identity.v3', 'object-storage', 'volume.v1',
53 'volume.v2', 'volume.v3'])
54
55
56def available_modules():
57 """Set of service client modules available in Tempest and plugins
58
59 Set of stable service clients from Tempest and service clients exposed
60 by plugins. This set of available modules can be used for automatic
61 configuration.
62
63 :raise PluginRegistrationException: if a plugin exposes a service_version
64 already defined by Tempest or another plugin.
65
66 Examples:
67
68 >>> from tempest import config
69 >>> params = {}
70 >>> for service_version in available_modules():
71 >>> service = service_version.split('.')[0]
72 >>> params[service] = config.service_client_config(service)
73 >>> service_clients = ServiceClients(creds, identity_uri,
74 >>> client_parameters=params)
75 """
76 extra_service_versions = set([])
77 _tempest_modules = set(tempest_modules())
78 plugin_services = ClientsRegistry().get_service_clients()
79 for plugin_name in plugin_services:
80 plug_service_versions = set([x['service_version'] for x in
81 plugin_services[plugin_name]])
82 # If a plugin exposes a duplicate service_version raise an exception
83 if plug_service_versions:
84 if not plug_service_versions.isdisjoint(extra_service_versions):
85 detailed_error = (
86 'Plugin %s is trying to register a service %s already '
87 'claimed by another one' % (plugin_name,
88 extra_service_versions &
89 plug_service_versions))
90 raise exceptions.PluginRegistrationException(
91 name=plugin_name, detailed_error=detailed_error)
Andrea Frittoli (andreaf)8420abe2016-07-27 11:47:43 +010092 # NOTE(andreaf) Once all tempest clients are stable, the following
93 # if will have to be removed.
94 if not plug_service_versions.isdisjoint(
95 _tempest_internal_modules()):
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010096 detailed_error = (
97 'Plugin %s is trying to register a service %s already '
98 'claimed by a Tempest one' % (plugin_name,
Andrea Frittoli (andreaf)8420abe2016-07-27 11:47:43 +010099 _tempest_internal_modules() &
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100100 plug_service_versions))
101 raise exceptions.PluginRegistrationException(
102 name=plugin_name, detailed_error=detailed_error)
103 extra_service_versions |= plug_service_versions
104 return _tempest_modules | extra_service_versions
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +0100105
106
107@misc.singleton
108class ClientsRegistry(object):
109 """Registry of all service clients available from plugins"""
110
111 def __init__(self):
112 self._service_clients = {}
113
114 def register_service_client(self, plugin_name, service_client_data):
115 if plugin_name in self._service_clients:
116 detailed_error = 'Clients for plugin %s already registered'
117 raise exceptions.PluginRegistrationException(
118 name=plugin_name,
119 detailed_error=detailed_error % plugin_name)
120 self._service_clients[plugin_name] = service_client_data
121
122 def get_service_clients(self):
123 return self._service_clients
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100124
125
126class ClientsFactory(object):
127 """Builds service clients for a service client module
128
129 This class implements the logic of feeding service client parameters
130 to service clients from a specific module. It allows setting the
131 parameters once and obtaining new instances of the clients without the
132 need of passing any parameter.
133
134 ClientsFactory can be used directly, or consumed via the `ServiceClients`
135 class, which manages the authorization part.
136 """
137
138 def __init__(self, module_path, client_names, auth_provider, **kwargs):
139 """Initialises the client factory
140
141 :param module_path: Path to module that includes all service clients.
142 All service client classes must be exposed by a single module.
143 If they are separated in different modules, defining __all__
144 in the root module can help, similar to what is done by service
145 clients in tempest.
146 :param client_names: List or set of names of the service client
147 classes.
148 :param auth_provider: The auth provider used to initialise client.
149 :param kwargs: Parameters to be passed to all clients. Parameters
150 values can be overwritten when clients are initialised, but
151 parameters cannot be deleted.
152 :raise ImportError if the specified module_path cannot be imported
153
154 Example:
155
156 >>> # Get credentials and an auth_provider
157 >>> clients = ClientsFactory(
158 >>> module_path='my_service.my_service_clients',
159 >>> client_names=['ServiceClient1', 'ServiceClient2'],
160 >>> auth_provider=auth_provider,
161 >>> service='my_service',
162 >>> region='region1')
163 >>> my_api_client = clients.MyApiClient()
164 >>> my_api_client_region2 = clients.MyApiClient(region='region2')
165
166 """
167 # Import the module. If it's not importable, the raised exception
168 # provides good enough information about what happened
169 _module = importlib.import_module(module_path)
170 # If any of the classes is not in the module we fail
171 for class_name in client_names:
172 # TODO(andreaf) This always passes all parameters to all clients.
173 # In future to allow clients to specify the list of parameters
174 # that they accept based out of a list of standard ones.
175
176 # Obtain the class
177 klass = self._get_class(_module, class_name)
178 final_kwargs = copy.copy(kwargs)
179
180 # Set the function as an attribute of the factory
181 setattr(self, class_name, self._get_partial_class(
182 klass, auth_provider, final_kwargs))
183
184 def _get_partial_class(self, klass, auth_provider, kwargs):
185
186 # Define a function that returns a new class instance by
187 # combining default kwargs with extra ones
188 def partial_class(alias=None, **later_kwargs):
189 """Returns a callable the initialises a service client
190
191 Builds a callable that accepts kwargs, which are passed through
192 to the __init__ of the service client, along with a set of defaults
193 set in factory at factory __init__ time.
194 Original args in the service client can only be passed as kwargs.
195
196 It accepts one extra parameter 'alias' compared to the original
197 service client. When alias is provided, the returned callable will
198 also set an attribute called with a name defined in 'alias', which
199 contains the instance of the service client.
200
201 :param alias: str Name of the attribute set on the factory once
202 the callable is invoked which contains the initialised
203 service client. If None, no attribute is set.
204 :param later_kwargs: kwargs passed through to the service client
205 __init__ on top of defaults set at factory level.
206 """
207 kwargs.update(later_kwargs)
208 _client = klass(auth_provider=auth_provider, **kwargs)
209 if alias:
210 setattr(self, alias, _client)
211 return _client
212
213 return partial_class
214
215 @classmethod
216 def _get_class(cls, module, class_name):
217 klass = getattr(module, class_name, None)
218 if not klass:
219 msg = 'Invalid class name, %s is not found in %s'
220 raise AttributeError(msg % (class_name, module))
221 if not inspect.isclass(klass):
222 msg = 'Expected a class, got %s of type %s instead'
223 raise TypeError(msg % (klass, type(klass)))
224 return klass
225
226
227class ServiceClients(object):
228 """Service client provider class
229
230 The ServiceClients object provides a useful means for tests to access
231 service clients configured for a specified set of credentials.
232 It hides some of the complexity from the authorization and configuration
233 layers.
234
235 Examples:
236
237 >>> from tempest.lib.services import clients
238 >>> johndoe = cred_provider.get_creds_by_role(['johndoe'])
239 >>> johndoe_clients = clients.ServiceClients(johndoe,
240 >>> identity_uri)
241 >>> johndoe_servers = johndoe_clients.servers_client.list_servers()
242
243 """
244 # NOTE(andreaf) This class does not depend on tempest configuration
245 # and its meant for direct consumption by external clients such as tempest
246 # plugins. Tempest provides a wrapper class, `clients.Manager`, that
247 # initialises this class using values from tempest CONF object. The wrapper
248 # class should only be used by tests hosted in Tempest.
249
250 def __init__(self, credentials, identity_uri, region=None, scope='project',
251 disable_ssl_certificate_validation=True, ca_certs=None,
252 trace_requests='', client_parameters=None):
253 """Service Clients provider
254
255 Instantiate a `ServiceClients` object, from a set of credentials and an
256 identity URI. The identity version is inferred from the credentials
257 object. Optionally auth scope can be provided.
258
259 A few parameters can be given a value which is applied as default
260 for all service clients: region, dscv, ca_certs, trace_requests.
261
262 Parameters dscv, ca_certs and trace_requests all apply to the auth
263 provider as well as any service clients provided by this manager.
264
265 Any other client parameter must be set via client_parameters.
266 The list of available parameters is defined in the service clients
267 interfaces. For reference, most clients will accept 'region',
268 'service', 'endpoint_type', 'build_timeout' and 'build_interval', which
269 are all inherited from RestClient.
270
271 The `config` module in Tempest exposes an helper function
272 `service_client_config` that can be used to extract from configuration
273 a dictionary ready to be injected in kwargs.
274
275 Exceptions are:
276 - Token clients for 'identity' have a very different interface
277 - Volume client for 'volume' accepts 'default_volume_size'
278 - Servers client from 'compute' accepts 'enable_instance_password'
279
280 Examples:
281
282 >>> identity_params = config.service_client_config('identity')
283 >>> params = {
284 >>> 'identity': identity_params,
285 >>> 'compute': {'region': 'region2'}}
286 >>> manager = lib_manager.Manager(
287 >>> my_creds, identity_uri, client_parameters=params)
288
289 :param credentials: An instance of `auth.Credentials`
290 :param identity_uri: URI of the identity API. This should be a
291 mandatory parameter, and it will so soon.
292 :param region: Default value of region for service clients.
293 :param scope: default scope for tokens produced by the auth provider
294 :param disable_ssl_certificate_validation: Applies to auth and to all
295 service clients.
296 :param ca_certs: Applies to auth and to all service clients.
297 :param trace_requests: Applies to auth and to all service clients.
298 :param client_parameters: Dictionary with parameters for service
299 clients. Keys of the dictionary are the service client service
300 name, as declared in `service_clients.available_modules()` except
301 for the version. Values are dictionaries of parameters that are
302 going to be passed to all clients in the service client module.
303
304 Examples:
305
306 >>> params_service_x = {'param_name': 'param_value'}
307 >>> client_parameters = { 'service_x': params_service_x }
308
309 >>> params_service_y = config.service_client_config('service_y')
310 >>> client_parameters['service_y'] = params_service_y
311
312 """
313 self._registered_services = set([])
314 self.credentials = credentials
315 self.identity_uri = identity_uri
316 if not identity_uri:
317 raise exceptions.InvalidCredentials(
318 'ServiceClients requires a non-empty identity_uri.')
319 self.region = region
320 # Check if passed or default credentials are valid
321 if not self.credentials.is_valid():
322 raise exceptions.InvalidCredentials()
323 # Get the identity classes matching the provided credentials
324 # TODO(andreaf) Define a new interface in Credentials to get
325 # the API version from an instance
326 identity = [(k, auth.IDENTITY_VERSION[k][1]) for k in
327 auth.IDENTITY_VERSION.keys() if
328 isinstance(self.credentials, auth.IDENTITY_VERSION[k][0])]
329 # Zero matches or more than one are both not valid.
330 if len(identity) != 1:
331 raise exceptions.InvalidCredentials()
332 self.auth_version, auth_provider_class = identity[0]
333 self.dscv = disable_ssl_certificate_validation
334 self.ca_certs = ca_certs
335 self.trace_requests = trace_requests
336 # Creates an auth provider for the credentials
337 self.auth_provider = auth_provider_class(
338 self.credentials, self.identity_uri, scope=scope,
339 disable_ssl_certificate_validation=self.dscv,
340 ca_certs=self.ca_certs, trace_requests=self.trace_requests)
341 # Setup some defaults for client parameters of registered services
342 client_parameters = client_parameters or {}
343 self.parameters = {}
344 # Parameters are provided for unversioned services
345 all_modules = available_modules() | _tempest_internal_modules()
346 unversioned_services = set(
347 [x.split('.')[0] for x in all_modules])
348 for service in unversioned_services:
349 self.parameters[service] = self._setup_parameters(
350 client_parameters.pop(service, {}))
351 # Check that no client parameters was supplied for unregistered clients
352 if client_parameters:
353 raise exceptions.UnknownServiceClient(
354 services=list(client_parameters.keys()))
355
Andrea Frittoli (andreaf)8420abe2016-07-27 11:47:43 +0100356 # Register service clients from the registry (__tempest__ and plugins)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100357 clients_registry = ClientsRegistry()
358 plugin_service_clients = clients_registry.get_service_clients()
359 for plugin in plugin_service_clients:
360 service_clients = plugin_service_clients[plugin]
361 # Each plugin returns a list of service client parameters
362 for service_client in service_clients:
363 # NOTE(andreaf) If a plugin cannot register, stop the
364 # registration process, log some details to help
365 # troubleshooting, and re-raise
366 try:
367 self.register_service_client_module(**service_client)
368 except Exception:
369 LOG.exception(
370 'Failed to register service client from plugin %s '
371 'with parameters %s' % (plugin, service_client))
372 raise
373
374 def register_service_client_module(self, name, service_version,
375 module_path, client_names, **kwargs):
376 """Register a service client module
377
378 Initiates a client factory for the specified module, using this
379 class auth_provider, and accessible via a `name` attribute in the
380 service client.
381
382 :param name: Name used to access the client
383 :param service_version: Name of the service complete with version.
384 Used to track registered services. When a plugin implements it,
385 it can be used by other plugins to obtain their configuration.
386 :param module_path: Path to module that includes all service clients.
387 All service client classes must be exposed by a single module.
388 If they are separated in different modules, defining __all__
389 in the root module can help, similar to what is done by service
390 clients in tempest.
391 :param client_names: List or set of names of service client classes.
392 :param kwargs: Extra optional parameters to be passed to all clients.
393 ServiceClient provides defaults for region, dscv, ca_certs and
394 trace_requests.
395 :raise ServiceClientRegistrationException: if the provided name is
396 already in use or if service_version is already registered.
397 :raise ImportError: if module_path cannot be imported.
398 """
399 if hasattr(self, name):
400 using_name = getattr(self, name)
401 detailed_error = 'Module name already in use: %s' % using_name
402 raise exceptions.ServiceClientRegistrationException(
403 name=name, service_version=service_version,
404 module_path=module_path, client_names=client_names,
405 detailed_error=detailed_error)
406 if service_version in self.registered_services:
407 detailed_error = 'Service %s already registered.' % service_version
408 raise exceptions.ServiceClientRegistrationException(
409 name=name, service_version=service_version,
410 module_path=module_path, client_names=client_names,
411 detailed_error=detailed_error)
412 params = dict(region=self.region,
413 disable_ssl_certificate_validation=self.dscv,
414 ca_certs=self.ca_certs,
415 trace_requests=self.trace_requests)
416 params.update(kwargs)
417 # Instantiate the client factory
418 _factory = ClientsFactory(module_path=module_path,
419 client_names=client_names,
420 auth_provider=self.auth_provider,
421 **params)
422 # Adds the client factory to the service_client
423 setattr(self, name, _factory)
424 # Add the name of the new service in self.SERVICES for discovery
425 self._registered_services.add(service_version)
426
427 @property
428 def registered_services(self):
Andrea Frittoli (andreaf)8420abe2016-07-27 11:47:43 +0100429 # NOTE(andreaf) Once all tempest modules are stable this needs to
430 # be updated to remove _tempest_internal_modules
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100431 return self._registered_services | _tempest_internal_modules()
432
433 def _setup_parameters(self, parameters):
434 """Setup default values for client parameters
435
436 Region by default is the region passed as an __init__ parameter.
437 Checks that no parameter for an unknown service is provided.
438 """
439 _parameters = {}
440 # Use region from __init__
441 if self.region:
442 _parameters['region'] = self.region
443 # Update defaults with specified parameters
444 _parameters.update(parameters)
445 # If any parameter is left, parameters for an unknown service were
446 # provided as input. Fail rather than ignore silently.
447 return _parameters