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