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