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