blob: 833cfd6fcffd79260c7f081fe3cb840bdf978d12 [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 Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +010020import sys
Andrea Frittoli3b6d5992017-04-09 18:57:16 +020021import warnings
Anusha Raminenif3eb9472017-01-13 08:54:01 +053022
Andrea Frittoli3b6d5992017-04-09 18:57:16 +020023from debtcollector import removals
Anusha Raminenif3eb9472017-01-13 08:54:01 +053024from oslo_log import log as logging
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +010025import testtools
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010026
27from tempest.lib import auth
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +010028from tempest.lib.common.utils import misc
29from tempest.lib import exceptions
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010030from tempest.lib.services import compute
ghanshyam5163a7d2016-11-22 14:10:39 +090031from tempest.lib.services import identity
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010032from tempest.lib.services import image
33from tempest.lib.services import network
Andrea Frittoli986407d2017-10-11 10:23:17 +000034from tempest.lib.services import object_storage
lkuchlan3fce7fb2016-10-31 15:40:35 +020035from tempest.lib.services import volume
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010036
Andrea Frittoli3b6d5992017-04-09 18:57:16 +020037warnings.simplefilter("once")
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010038LOG = logging.getLogger(__name__)
39
40
41def tempest_modules():
42 """Dict of service client modules available in Tempest.
43
44 Provides a dict of stable service modules available in Tempest, with
45 ``service_version`` as key, and the module object as value.
46 """
47 return {
48 'compute': compute,
ghanshyam5163a7d2016-11-22 14:10:39 +090049 'identity.v2': identity.v2,
ghanshyam68227d62016-12-22 16:17:42 +090050 'identity.v3': identity.v3,
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010051 'image.v1': image.v1,
52 'image.v2': image.v2,
lkuchlan3fce7fb2016-10-31 15:40:35 +020053 'network': network,
Andrea Frittoli986407d2017-10-11 10:23:17 +000054 'object-storage': object_storage,
lkuchlan3fce7fb2016-10-31 15:40:35 +020055 'volume.v1': volume.v1,
Benny Kopilov37b2bee2016-11-06 09:07:19 +020056 'volume.v2': volume.v2,
57 'volume.v3': volume.v3
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010058 }
59
60
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010061def 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
Masayuki Igawa683abe22017-04-11 16:06:46 +090071 Examples::
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010072
Masayuki Igawa683abe22017-04-11 16:06:46 +090073 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)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010080 """
81 extra_service_versions = set([])
82 _tempest_modules = set(tempest_modules())
83 plugin_services = ClientsRegistry().get_service_clients()
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +010084 name_conflicts = []
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010085 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))
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +010096 name_conflicts.append(exceptions.PluginRegistrationException(
97 name=plugin_name, detailed_error=detailed_error))
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010098 extra_service_versions |= plug_service_versions
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +010099 if name_conflicts:
100 LOG.error(
101 'Failed to list available modules due to name conflicts: %s',
102 name_conflicts)
103 raise testtools.MultipleExceptions(*name_conflicts)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100104 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
Andrea Frittoli6a36e3d2017-03-08 16:05:59 +0000121 LOG.debug("Successfully registered plugin %s in the service client "
122 "registry with configuration: %s", plugin_name,
123 service_client_data)
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +0100124
125 def get_service_clients(self):
126 return self._service_clients
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100127
128
129class ClientsFactory(object):
130 """Builds service clients for a service client module
131
132 This class implements the logic of feeding service client parameters
133 to service clients from a specific module. It allows setting the
134 parameters once and obtaining new instances of the clients without the
135 need of passing any parameter.
136
137 ClientsFactory can be used directly, or consumed via the `ServiceClients`
138 class, which manages the authorization part.
139 """
140
141 def __init__(self, module_path, client_names, auth_provider, **kwargs):
142 """Initialises the client factory
143
144 :param module_path: Path to module that includes all service clients.
145 All service client classes must be exposed by a single module.
146 If they are separated in different modules, defining __all__
147 in the root module can help, similar to what is done by service
148 clients in tempest.
149 :param client_names: List or set of names of the service client
150 classes.
151 :param auth_provider: The auth provider used to initialise client.
152 :param kwargs: Parameters to be passed to all clients. Parameters
153 values can be overwritten when clients are initialised, but
154 parameters cannot be deleted.
junboli872ca872017-07-21 13:24:38 +0800155 :raise ImportError: if the specified module_path cannot be imported
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100156
Masayuki Igawa683abe22017-04-11 16:06:46 +0900157 Example::
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100158
Masayuki Igawa683abe22017-04-11 16:06:46 +0900159 # Get credentials and an auth_provider
160 clients = ClientsFactory(
161 module_path='my_service.my_service_clients',
162 client_names=['ServiceClient1', 'ServiceClient2'],
163 auth_provider=auth_provider,
164 service='my_service',
165 region='region1')
166 my_api_client = clients.MyApiClient()
167 my_api_client_region2 = clients.MyApiClient(region='region2')
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100168
169 """
170 # Import the module. If it's not importable, the raised exception
171 # provides good enough information about what happened
172 _module = importlib.import_module(module_path)
173 # If any of the classes is not in the module we fail
174 for class_name in client_names:
175 # TODO(andreaf) This always passes all parameters to all clients.
176 # In future to allow clients to specify the list of parameters
177 # that they accept based out of a list of standard ones.
178
179 # Obtain the class
180 klass = self._get_class(_module, class_name)
181 final_kwargs = copy.copy(kwargs)
182
183 # Set the function as an attribute of the factory
184 setattr(self, class_name, self._get_partial_class(
185 klass, auth_provider, final_kwargs))
186
187 def _get_partial_class(self, klass, auth_provider, kwargs):
188
189 # Define a function that returns a new class instance by
190 # combining default kwargs with extra ones
191 def partial_class(alias=None, **later_kwargs):
192 """Returns a callable the initialises a service client
193
194 Builds a callable that accepts kwargs, which are passed through
195 to the __init__ of the service client, along with a set of defaults
196 set in factory at factory __init__ time.
197 Original args in the service client can only be passed as kwargs.
198
199 It accepts one extra parameter 'alias' compared to the original
200 service client. When alias is provided, the returned callable will
201 also set an attribute called with a name defined in 'alias', which
202 contains the instance of the service client.
203
204 :param alias: str Name of the attribute set on the factory once
205 the callable is invoked which contains the initialised
206 service client. If None, no attribute is set.
207 :param later_kwargs: kwargs passed through to the service client
208 __init__ on top of defaults set at factory level.
209 """
210 kwargs.update(later_kwargs)
211 _client = klass(auth_provider=auth_provider, **kwargs)
212 if alias:
213 setattr(self, alias, _client)
214 return _client
215
216 return partial_class
217
218 @classmethod
219 def _get_class(cls, module, class_name):
220 klass = getattr(module, class_name, None)
221 if not klass:
222 msg = 'Invalid class name, %s is not found in %s'
223 raise AttributeError(msg % (class_name, module))
224 if not inspect.isclass(klass):
225 msg = 'Expected a class, got %s of type %s instead'
226 raise TypeError(msg % (klass, type(klass)))
227 return klass
228
229
230class ServiceClients(object):
231 """Service client provider class
232
233 The ServiceClients object provides a useful means for tests to access
234 service clients configured for a specified set of credentials.
235 It hides some of the complexity from the authorization and configuration
236 layers.
237
Masayuki Igawa683abe22017-04-11 16:06:46 +0900238 Examples::
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100239
Masayuki Igawa683abe22017-04-11 16:06:46 +0900240 # johndoe is a tempest.lib.auth.Credentials type instance
241 johndoe_clients = clients.ServiceClients(johndoe, identity_uri)
242
243 # List servers in default region
244 johndoe_servers_client = johndoe_clients.compute.ServersClient()
245 johndoe_servers = johndoe_servers_client.list_servers()
246
247 # List servers in Region B
248 johndoe_servers_client_B = johndoe_clients.compute.ServersClient(
249 region='B')
250 johndoe_servers = johndoe_servers_client_B.list_servers()
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100251
252 """
253 # NOTE(andreaf) This class does not depend on tempest configuration
254 # and its meant for direct consumption by external clients such as tempest
255 # plugins. Tempest provides a wrapper class, `clients.Manager`, that
256 # initialises this class using values from tempest CONF object. The wrapper
257 # class should only be used by tests hosted in Tempest.
258
Andrea Frittoli3b6d5992017-04-09 18:57:16 +0200259 @removals.removed_kwarg('client_parameters')
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100260 def __init__(self, credentials, identity_uri, region=None, scope='project',
261 disable_ssl_certificate_validation=True, ca_certs=None,
Matthew Treinish74514402016-09-01 11:44:57 -0400262 trace_requests='', client_parameters=None, proxy_url=None):
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100263 """Service Clients provider
264
265 Instantiate a `ServiceClients` object, from a set of credentials and an
266 identity URI. The identity version is inferred from the credentials
267 object. Optionally auth scope can be provided.
268
269 A few parameters can be given a value which is applied as default
270 for all service clients: region, dscv, ca_certs, trace_requests.
271
272 Parameters dscv, ca_certs and trace_requests all apply to the auth
273 provider as well as any service clients provided by this manager.
274
Andrea Frittoli3b6d5992017-04-09 18:57:16 +0200275 Any other client parameter should be set via ClientsRegistry.
276
277 Client parameter used to be set via client_parameters, but this is
278 deprecated, and it is actually already not honoured
279 anymore: https://launchpad.net/bugs/1680915.
280
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100281 The list of available parameters is defined in the service clients
282 interfaces. For reference, most clients will accept 'region',
283 'service', 'endpoint_type', 'build_timeout' and 'build_interval', which
284 are all inherited from RestClient.
285
286 The `config` module in Tempest exposes an helper function
287 `service_client_config` that can be used to extract from configuration
288 a dictionary ready to be injected in kwargs.
289
290 Exceptions are:
Andrea Frittoli8b8db532016-12-22 11:21:47 +0000291 - Token clients for 'identity' must be given an 'auth_url' parameter
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100292 - Volume client for 'volume' accepts 'default_volume_size'
293 - Servers client from 'compute' accepts 'enable_instance_password'
294
Andrea Frittoli3b6d5992017-04-09 18:57:16 +0200295 If Tempest configuration is used, parameters will be loaded in the
296 Registry automatically for all service client (Tempest stable ones
297 and plugins).
298
Masayuki Igawa683abe22017-04-11 16:06:46 +0900299 Examples::
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100300
Masayuki Igawa683abe22017-04-11 16:06:46 +0900301 identity_params = config.service_client_config('identity')
302 params = {
303 'identity': identity_params,
304 'compute': {'region': 'region2'}}
305 manager = lib_manager.Manager(
306 my_creds, identity_uri, client_parameters=params)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100307
308 :param credentials: An instance of `auth.Credentials`
309 :param identity_uri: URI of the identity API. This should be a
310 mandatory parameter, and it will so soon.
311 :param region: Default value of region for service clients.
312 :param scope: default scope for tokens produced by the auth provider
313 :param disable_ssl_certificate_validation: Applies to auth and to all
314 service clients.
315 :param ca_certs: Applies to auth and to all service clients.
316 :param trace_requests: Applies to auth and to all service clients.
317 :param client_parameters: Dictionary with parameters for service
318 clients. Keys of the dictionary are the service client service
319 name, as declared in `service_clients.available_modules()` except
320 for the version. Values are dictionaries of parameters that are
321 going to be passed to all clients in the service client module.
Matthew Treinish74514402016-09-01 11:44:57 -0400322 :param proxy_url: Applies to auth and to all service clients, set a
323 proxy url for the clients to use.
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100324 """
325 self._registered_services = set([])
326 self.credentials = credentials
327 self.identity_uri = identity_uri
328 if not identity_uri:
329 raise exceptions.InvalidCredentials(
330 'ServiceClients requires a non-empty identity_uri.')
331 self.region = region
332 # Check if passed or default credentials are valid
333 if not self.credentials.is_valid():
Masayuki Igawa4803e292018-07-04 16:06:07 +0900334 raise exceptions.InvalidCredentials(credentials)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100335 # Get the identity classes matching the provided credentials
336 # TODO(andreaf) Define a new interface in Credentials to get
337 # the API version from an instance
338 identity = [(k, auth.IDENTITY_VERSION[k][1]) for k in
339 auth.IDENTITY_VERSION.keys() if
340 isinstance(self.credentials, auth.IDENTITY_VERSION[k][0])]
341 # Zero matches or more than one are both not valid.
342 if len(identity) != 1:
Masayuki Igawa4803e292018-07-04 16:06:07 +0900343 msg = "Zero or %d ambiguous auth provider found. identity: %s, " \
344 "credentials: %s" % (len(identity), identity, credentials)
345 raise exceptions.InvalidCredentials(msg)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100346 self.auth_version, auth_provider_class = identity[0]
347 self.dscv = disable_ssl_certificate_validation
348 self.ca_certs = ca_certs
349 self.trace_requests = trace_requests
Matthew Treinish74514402016-09-01 11:44:57 -0400350 self.proxy_url = proxy_url
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100351 # Creates an auth provider for the credentials
352 self.auth_provider = auth_provider_class(
353 self.credentials, self.identity_uri, scope=scope,
354 disable_ssl_certificate_validation=self.dscv,
Matthew Treinish74514402016-09-01 11:44:57 -0400355 ca_certs=self.ca_certs, trace_requests=self.trace_requests,
356 proxy_url=proxy_url)
357
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100358 # Setup some defaults for client parameters of registered services
359 client_parameters = client_parameters or {}
360 self.parameters = {}
Matthew Treinish74514402016-09-01 11:44:57 -0400361
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100362 # Parameters are provided for unversioned services
Andrea Frittoli986407d2017-10-11 10:23:17 +0000363 all_modules = available_modules()
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100364 unversioned_services = set(
365 [x.split('.')[0] for x in all_modules])
366 for service in unversioned_services:
367 self.parameters[service] = self._setup_parameters(
368 client_parameters.pop(service, {}))
369 # Check that no client parameters was supplied for unregistered clients
370 if client_parameters:
371 raise exceptions.UnknownServiceClient(
372 services=list(client_parameters.keys()))
373
Andrea Frittoli (andreaf)8420abe2016-07-27 11:47:43 +0100374 # Register service clients from the registry (__tempest__ and plugins)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100375 clients_registry = ClientsRegistry()
376 plugin_service_clients = clients_registry.get_service_clients()
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +0100377 registration_errors = []
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100378 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:
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +0100388 registration_errors.append(sys.exc_info())
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100389 LOG.exception(
390 'Failed to register service client from plugin %s '
Jordan Pittier525ec712016-12-07 17:51:26 +0100391 'with parameters %s', plugin, service_client)
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +0100392 if registration_errors:
393 raise testtools.MultipleExceptions(*registration_errors)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100394
395 def register_service_client_module(self, name, service_version,
396 module_path, client_names, **kwargs):
397 """Register a service client module
398
399 Initiates a client factory for the specified module, using this
400 class auth_provider, and accessible via a `name` attribute in the
401 service client.
402
403 :param name: Name used to access the client
404 :param service_version: Name of the service complete with version.
405 Used to track registered services. When a plugin implements it,
406 it can be used by other plugins to obtain their configuration.
407 :param module_path: Path to module that includes all service clients.
408 All service client classes must be exposed by a single module.
409 If they are separated in different modules, defining __all__
410 in the root module can help, similar to what is done by service
411 clients in tempest.
412 :param client_names: List or set of names of service client classes.
413 :param kwargs: Extra optional parameters to be passed to all clients.
Matthew Treinish74514402016-09-01 11:44:57 -0400414 ServiceClient provides defaults for region, dscv, ca_certs, http
415 proxies and trace_requests.
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100416 :raise ServiceClientRegistrationException: if the provided name is
417 already in use or if service_version is already registered.
418 :raise ImportError: if module_path cannot be imported.
419 """
420 if hasattr(self, name):
421 using_name = getattr(self, name)
422 detailed_error = 'Module name already in use: %s' % using_name
423 raise exceptions.ServiceClientRegistrationException(
424 name=name, service_version=service_version,
425 module_path=module_path, client_names=client_names,
426 detailed_error=detailed_error)
427 if service_version in self.registered_services:
428 detailed_error = 'Service %s already registered.' % service_version
429 raise exceptions.ServiceClientRegistrationException(
430 name=name, service_version=service_version,
431 module_path=module_path, client_names=client_names,
432 detailed_error=detailed_error)
433 params = dict(region=self.region,
434 disable_ssl_certificate_validation=self.dscv,
435 ca_certs=self.ca_certs,
Matthew Treinish74514402016-09-01 11:44:57 -0400436 trace_requests=self.trace_requests,
437 proxy_url=self.proxy_url)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100438 params.update(kwargs)
439 # Instantiate the client factory
440 _factory = ClientsFactory(module_path=module_path,
441 client_names=client_names,
442 auth_provider=self.auth_provider,
443 **params)
444 # Adds the client factory to the service_client
445 setattr(self, name, _factory)
446 # Add the name of the new service in self.SERVICES for discovery
447 self._registered_services.add(service_version)
448
449 @property
450 def registered_services(self):
Andrea Frittoli986407d2017-10-11 10:23:17 +0000451 return self._registered_services
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100452
453 def _setup_parameters(self, parameters):
454 """Setup default values for client parameters
455
456 Region by default is the region passed as an __init__ parameter.
457 Checks that no parameter for an unknown service is provided.
458 """
459 _parameters = {}
460 # Use region from __init__
461 if self.region:
462 _parameters['region'] = self.region
463 # Update defaults with specified parameters
464 _parameters.update(parameters)
465 # If any parameter is left, parameters for an unknown service were
466 # provided as input. Fail rather than ignore silently.
467 return _parameters