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