blob: 8b5c758aac99efb940ce11dc60aad83f6d31cd88 [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
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)ff50cc52016-08-08 10:34:31 +010024import testtools
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010025
26from tempest.lib import auth
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +010027from tempest.lib.common.utils import misc
28from tempest.lib import exceptions
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010029from tempest.lib.services import compute
ghanshyam5163a7d2016-11-22 14:10:39 +090030from tempest.lib.services import identity
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010031from tempest.lib.services import image
32from tempest.lib.services import network
Andrea Frittoli986407d2017-10-11 10:23:17 +000033from tempest.lib.services import object_storage
Lajos Katonaceb88212018-11-30 14:54:12 +010034from tempest.lib.services import placement
lkuchlan3fce7fb2016-10-31 15:40:35 +020035from tempest.lib.services import volume
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010036
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010037LOG = logging.getLogger(__name__)
38
39
40def tempest_modules():
41 """Dict of service client modules available in Tempest.
42
43 Provides a dict of stable service modules available in Tempest, with
44 ``service_version`` as key, and the module object as value.
45 """
46 return {
47 'compute': compute,
Lajos Katonaceb88212018-11-30 14:54:12 +010048 'placement': placement,
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,
Benny Kopilov37b2bee2016-11-06 09:07:19 +020055 'volume.v2': volume.v2,
56 'volume.v3': volume.v3
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010057 }
58
59
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010060def available_modules():
61 """Set of service client modules available in Tempest and plugins
62
63 Set of stable service clients from Tempest and service clients exposed
64 by plugins. This set of available modules can be used for automatic
65 configuration.
66
67 :raise PluginRegistrationException: if a plugin exposes a service_version
68 already defined by Tempest or another plugin.
69
Masayuki Igawa683abe22017-04-11 16:06:46 +090070 Examples::
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010071
Masayuki Igawa683abe22017-04-11 16:06:46 +090072 from tempest import config
73 params = {}
74 for service_version in available_modules():
75 service = service_version.split('.')[0]
76 params[service] = config.service_client_config(service)
77 service_clients = ServiceClients(creds, identity_uri,
78 client_parameters=params)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010079 """
80 extra_service_versions = set([])
81 _tempest_modules = set(tempest_modules())
82 plugin_services = ClientsRegistry().get_service_clients()
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +010083 name_conflicts = []
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010084 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))
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +010095 name_conflicts.append(exceptions.PluginRegistrationException(
96 name=plugin_name, detailed_error=detailed_error))
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +010097 extra_service_versions |= plug_service_versions
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +010098 if name_conflicts:
99 LOG.error(
100 'Failed to list available modules due to name conflicts: %s',
101 name_conflicts)
102 raise testtools.MultipleExceptions(*name_conflicts)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100103 return _tempest_modules | extra_service_versions
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +0100104
105
106@misc.singleton
107class ClientsRegistry(object):
108 """Registry of all service clients available from plugins"""
109
110 def __init__(self):
111 self._service_clients = {}
112
113 def register_service_client(self, plugin_name, service_client_data):
114 if plugin_name in self._service_clients:
115 detailed_error = 'Clients for plugin %s already registered'
116 raise exceptions.PluginRegistrationException(
117 name=plugin_name,
118 detailed_error=detailed_error % plugin_name)
119 self._service_clients[plugin_name] = service_client_data
Andrea Frittoli6a36e3d2017-03-08 16:05:59 +0000120 LOG.debug("Successfully registered plugin %s in the service client "
121 "registry with configuration: %s", plugin_name,
122 service_client_data)
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +0100123
124 def get_service_clients(self):
125 return self._service_clients
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100126
127
128class ClientsFactory(object):
129 """Builds service clients for a service client module
130
131 This class implements the logic of feeding service client parameters
132 to service clients from a specific module. It allows setting the
133 parameters once and obtaining new instances of the clients without the
134 need of passing any parameter.
135
136 ClientsFactory can be used directly, or consumed via the `ServiceClients`
137 class, which manages the authorization part.
138 """
139
140 def __init__(self, module_path, client_names, auth_provider, **kwargs):
141 """Initialises the client factory
142
143 :param module_path: Path to module that includes all service clients.
144 All service client classes must be exposed by a single module.
145 If they are separated in different modules, defining __all__
146 in the root module can help, similar to what is done by service
147 clients in tempest.
148 :param client_names: List or set of names of the service client
149 classes.
150 :param auth_provider: The auth provider used to initialise client.
151 :param kwargs: Parameters to be passed to all clients. Parameters
152 values can be overwritten when clients are initialised, but
153 parameters cannot be deleted.
junboli872ca872017-07-21 13:24:38 +0800154 :raise ImportError: if the specified module_path cannot be imported
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100155
Masayuki Igawa683abe22017-04-11 16:06:46 +0900156 Example::
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100157
Masayuki Igawa683abe22017-04-11 16:06:46 +0900158 # Get credentials and an auth_provider
159 clients = ClientsFactory(
160 module_path='my_service.my_service_clients',
161 client_names=['ServiceClient1', 'ServiceClient2'],
162 auth_provider=auth_provider,
163 service='my_service',
164 region='region1')
165 my_api_client = clients.MyApiClient()
166 my_api_client_region2 = clients.MyApiClient(region='region2')
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100167
168 """
169 # Import the module. If it's not importable, the raised exception
170 # provides good enough information about what happened
171 _module = importlib.import_module(module_path)
172 # If any of the classes is not in the module we fail
173 for class_name in client_names:
174 # TODO(andreaf) This always passes all parameters to all clients.
175 # In future to allow clients to specify the list of parameters
176 # that they accept based out of a list of standard ones.
177
178 # Obtain the class
179 klass = self._get_class(_module, class_name)
180 final_kwargs = copy.copy(kwargs)
181
182 # Set the function as an attribute of the factory
183 setattr(self, class_name, self._get_partial_class(
184 klass, auth_provider, final_kwargs))
185
186 def _get_partial_class(self, klass, auth_provider, kwargs):
187
188 # Define a function that returns a new class instance by
189 # combining default kwargs with extra ones
190 def partial_class(alias=None, **later_kwargs):
191 """Returns a callable the initialises a service client
192
193 Builds a callable that accepts kwargs, which are passed through
194 to the __init__ of the service client, along with a set of defaults
195 set in factory at factory __init__ time.
196 Original args in the service client can only be passed as kwargs.
197
198 It accepts one extra parameter 'alias' compared to the original
199 service client. When alias is provided, the returned callable will
200 also set an attribute called with a name defined in 'alias', which
201 contains the instance of the service client.
202
203 :param alias: str Name of the attribute set on the factory once
204 the callable is invoked which contains the initialised
205 service client. If None, no attribute is set.
206 :param later_kwargs: kwargs passed through to the service client
207 __init__ on top of defaults set at factory level.
208 """
209 kwargs.update(later_kwargs)
210 _client = klass(auth_provider=auth_provider, **kwargs)
211 if alias:
212 setattr(self, alias, _client)
213 return _client
214
215 return partial_class
216
217 @classmethod
218 def _get_class(cls, module, class_name):
219 klass = getattr(module, class_name, None)
220 if not klass:
221 msg = 'Invalid class name, %s is not found in %s'
222 raise AttributeError(msg % (class_name, module))
223 if not inspect.isclass(klass):
224 msg = 'Expected a class, got %s of type %s instead'
225 raise TypeError(msg % (klass, type(klass)))
226 return klass
227
228
229class ServiceClients(object):
230 """Service client provider class
231
232 The ServiceClients object provides a useful means for tests to access
233 service clients configured for a specified set of credentials.
234 It hides some of the complexity from the authorization and configuration
235 layers.
236
Masayuki Igawa683abe22017-04-11 16:06:46 +0900237 Examples::
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100238
Masayuki Igawa683abe22017-04-11 16:06:46 +0900239 # johndoe is a tempest.lib.auth.Credentials type instance
240 johndoe_clients = clients.ServiceClients(johndoe, identity_uri)
241
242 # List servers in default region
243 johndoe_servers_client = johndoe_clients.compute.ServersClient()
244 johndoe_servers = johndoe_servers_client.list_servers()
245
246 # List servers in Region B
247 johndoe_servers_client_B = johndoe_clients.compute.ServersClient(
248 region='B')
249 johndoe_servers = johndoe_servers_client_B.list_servers()
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100250
251 """
252 # NOTE(andreaf) This class does not depend on tempest configuration
253 # and its meant for direct consumption by external clients such as tempest
254 # plugins. Tempest provides a wrapper class, `clients.Manager`, that
255 # initialises this class using values from tempest CONF object. The wrapper
256 # class should only be used by tests hosted in Tempest.
257
Andrea Frittoli3b6d5992017-04-09 18:57:16 +0200258 @removals.removed_kwarg('client_parameters')
Colleen Murphy06374e22019-10-02 14:28:22 -0700259 def __init__(self, credentials, identity_uri, region=None, scope=None,
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100260 disable_ssl_certificate_validation=True, ca_certs=None,
Matthew Treinish74514402016-09-01 11:44:57 -0400261 trace_requests='', client_parameters=None, proxy_url=None):
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100262 """Service Clients provider
263
264 Instantiate a `ServiceClients` object, from a set of credentials and an
265 identity URI. The identity version is inferred from the credentials
266 object. Optionally auth scope can be provided.
267
268 A few parameters can be given a value which is applied as default
269 for all service clients: region, dscv, ca_certs, trace_requests.
270
271 Parameters dscv, ca_certs and trace_requests all apply to the auth
272 provider as well as any service clients provided by this manager.
273
Andrea Frittoli3b6d5992017-04-09 18:57:16 +0200274 Any other client parameter should be set via ClientsRegistry.
275
276 Client parameter used to be set via client_parameters, but this is
277 deprecated, and it is actually already not honoured
278 anymore: https://launchpad.net/bugs/1680915.
279
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100280 The list of available parameters is defined in the service clients
281 interfaces. For reference, most clients will accept 'region',
282 'service', 'endpoint_type', 'build_timeout' and 'build_interval', which
283 are all inherited from RestClient.
284
285 The `config` module in Tempest exposes an helper function
286 `service_client_config` that can be used to extract from configuration
287 a dictionary ready to be injected in kwargs.
288
289 Exceptions are:
Andrea Frittoli8b8db532016-12-22 11:21:47 +0000290 - Token clients for 'identity' must be given an 'auth_url' parameter
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100291 - Volume client for 'volume' accepts 'default_volume_size'
292 - Servers client from 'compute' accepts 'enable_instance_password'
293
Andrea Frittoli3b6d5992017-04-09 18:57:16 +0200294 If Tempest configuration is used, parameters will be loaded in the
295 Registry automatically for all service client (Tempest stable ones
296 and plugins).
297
Masayuki Igawa683abe22017-04-11 16:06:46 +0900298 Examples::
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100299
Masayuki Igawa683abe22017-04-11 16:06:46 +0900300 identity_params = config.service_client_config('identity')
301 params = {
302 'identity': identity_params,
303 'compute': {'region': 'region2'}}
304 manager = lib_manager.Manager(
305 my_creds, identity_uri, client_parameters=params)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100306
307 :param credentials: An instance of `auth.Credentials`
308 :param identity_uri: URI of the identity API. This should be a
309 mandatory parameter, and it will so soon.
310 :param region: Default value of region for service clients.
311 :param scope: default scope for tokens produced by the auth provider
312 :param disable_ssl_certificate_validation: Applies to auth and to all
313 service clients.
314 :param ca_certs: Applies to auth and to all service clients.
315 :param trace_requests: Applies to auth and to all service clients.
316 :param client_parameters: Dictionary with parameters for service
317 clients. Keys of the dictionary are the service client service
318 name, as declared in `service_clients.available_modules()` except
319 for the version. Values are dictionaries of parameters that are
320 going to be passed to all clients in the service client module.
Matthew Treinish74514402016-09-01 11:44:57 -0400321 :param proxy_url: Applies to auth and to all service clients, set a
322 proxy url for the clients to use.
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100323 """
324 self._registered_services = set([])
325 self.credentials = credentials
326 self.identity_uri = identity_uri
327 if not identity_uri:
328 raise exceptions.InvalidCredentials(
329 'ServiceClients requires a non-empty identity_uri.')
330 self.region = region
331 # Check if passed or default credentials are valid
332 if not self.credentials.is_valid():
Masayuki Igawa4803e292018-07-04 16:06:07 +0900333 raise exceptions.InvalidCredentials(credentials)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100334 # Get the identity classes matching the provided credentials
335 # TODO(andreaf) Define a new interface in Credentials to get
336 # the API version from an instance
337 identity = [(k, auth.IDENTITY_VERSION[k][1]) for k in
338 auth.IDENTITY_VERSION.keys() if
339 isinstance(self.credentials, auth.IDENTITY_VERSION[k][0])]
340 # Zero matches or more than one are both not valid.
341 if len(identity) != 1:
Masayuki Igawa4803e292018-07-04 16:06:07 +0900342 msg = "Zero or %d ambiguous auth provider found. identity: %s, " \
343 "credentials: %s" % (len(identity), identity, credentials)
344 raise exceptions.InvalidCredentials(msg)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100345 self.auth_version, auth_provider_class = identity[0]
346 self.dscv = disable_ssl_certificate_validation
347 self.ca_certs = ca_certs
348 self.trace_requests = trace_requests
Matthew Treinish74514402016-09-01 11:44:57 -0400349 self.proxy_url = proxy_url
Colleen Murphy06374e22019-10-02 14:28:22 -0700350 if self.credentials.project_id or self.credentials.project_name:
351 scope = 'project'
352 elif self.credentials.system:
353 scope = 'system'
354 elif self.credentials.domain_id or self.credentials.domain_name:
355 scope = 'domain'
356 else:
357 scope = 'project'
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100358 # Creates an auth provider for the credentials
359 self.auth_provider = auth_provider_class(
360 self.credentials, self.identity_uri, scope=scope,
361 disable_ssl_certificate_validation=self.dscv,
Matthew Treinish74514402016-09-01 11:44:57 -0400362 ca_certs=self.ca_certs, trace_requests=self.trace_requests,
363 proxy_url=proxy_url)
364
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100365 # Setup some defaults for client parameters of registered services
366 client_parameters = client_parameters or {}
367 self.parameters = {}
Matthew Treinish74514402016-09-01 11:44:57 -0400368
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100369 # Parameters are provided for unversioned services
Andrea Frittoli986407d2017-10-11 10:23:17 +0000370 all_modules = available_modules()
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100371 unversioned_services = set(
372 [x.split('.')[0] for x in all_modules])
373 for service in unversioned_services:
374 self.parameters[service] = self._setup_parameters(
375 client_parameters.pop(service, {}))
376 # Check that no client parameters was supplied for unregistered clients
377 if client_parameters:
378 raise exceptions.UnknownServiceClient(
379 services=list(client_parameters.keys()))
380
Andrea Frittoli (andreaf)8420abe2016-07-27 11:47:43 +0100381 # Register service clients from the registry (__tempest__ and plugins)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100382 clients_registry = ClientsRegistry()
383 plugin_service_clients = clients_registry.get_service_clients()
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +0100384 registration_errors = []
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100385 for plugin in plugin_service_clients:
386 service_clients = plugin_service_clients[plugin]
387 # Each plugin returns a list of service client parameters
388 for service_client in service_clients:
389 # NOTE(andreaf) If a plugin cannot register, stop the
390 # registration process, log some details to help
391 # troubleshooting, and re-raise
392 try:
393 self.register_service_client_module(**service_client)
394 except Exception:
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +0100395 registration_errors.append(sys.exc_info())
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100396 LOG.exception(
397 'Failed to register service client from plugin %s '
Jordan Pittier525ec712016-12-07 17:51:26 +0100398 'with parameters %s', plugin, service_client)
Andrea Frittoli (andreaf)ff50cc52016-08-08 10:34:31 +0100399 if registration_errors:
400 raise testtools.MultipleExceptions(*registration_errors)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100401
402 def register_service_client_module(self, name, service_version,
403 module_path, client_names, **kwargs):
404 """Register a service client module
405
406 Initiates a client factory for the specified module, using this
407 class auth_provider, and accessible via a `name` attribute in the
408 service client.
409
410 :param name: Name used to access the client
411 :param service_version: Name of the service complete with version.
412 Used to track registered services. When a plugin implements it,
413 it can be used by other plugins to obtain their configuration.
414 :param module_path: Path to module that includes all service clients.
415 All service client classes must be exposed by a single module.
416 If they are separated in different modules, defining __all__
417 in the root module can help, similar to what is done by service
418 clients in tempest.
419 :param client_names: List or set of names of service client classes.
420 :param kwargs: Extra optional parameters to be passed to all clients.
Matthew Treinish74514402016-09-01 11:44:57 -0400421 ServiceClient provides defaults for region, dscv, ca_certs, http
422 proxies and trace_requests.
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100423 :raise ServiceClientRegistrationException: if the provided name is
424 already in use or if service_version is already registered.
425 :raise ImportError: if module_path cannot be imported.
426 """
427 if hasattr(self, name):
428 using_name = getattr(self, name)
429 detailed_error = 'Module name already in use: %s' % using_name
430 raise exceptions.ServiceClientRegistrationException(
431 name=name, service_version=service_version,
432 module_path=module_path, client_names=client_names,
433 detailed_error=detailed_error)
434 if service_version in self.registered_services:
435 detailed_error = 'Service %s already registered.' % service_version
436 raise exceptions.ServiceClientRegistrationException(
437 name=name, service_version=service_version,
438 module_path=module_path, client_names=client_names,
439 detailed_error=detailed_error)
440 params = dict(region=self.region,
441 disable_ssl_certificate_validation=self.dscv,
442 ca_certs=self.ca_certs,
Matthew Treinish74514402016-09-01 11:44:57 -0400443 trace_requests=self.trace_requests,
444 proxy_url=self.proxy_url)
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100445 params.update(kwargs)
446 # Instantiate the client factory
447 _factory = ClientsFactory(module_path=module_path,
448 client_names=client_names,
449 auth_provider=self.auth_provider,
450 **params)
451 # Adds the client factory to the service_client
452 setattr(self, name, _factory)
453 # Add the name of the new service in self.SERVICES for discovery
454 self._registered_services.add(service_version)
455
456 @property
457 def registered_services(self):
Andrea Frittoli986407d2017-10-11 10:23:17 +0000458 return self._registered_services
Andrea Frittoli (andreaf)e07579c2016-08-05 07:27:02 +0100459
460 def _setup_parameters(self, parameters):
461 """Setup default values for client parameters
462
463 Region by default is the region passed as an __init__ parameter.
464 Checks that no parameter for an unknown service is provided.
465 """
466 _parameters = {}
467 # Use region from __init__
468 if self.region:
469 _parameters['region'] = self.region
470 # Update defaults with specified parameters
471 _parameters.update(parameters)
472 # If any parameter is left, parameters for an unknown service were
473 # provided as input. Fail rather than ignore silently.
474 return _parameters