blob: 136ad65ca2994f2d4fb618fb70e7af031237a5ee [file] [log] [blame]
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +01001# Copyright 2012 OpenStack Foundation
2# 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)73dd51d2016-06-21 17:20:31 +010017import copy
18import importlib
19import inspect
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +010020import logging
Andrea Frittoli (andreaf)73dd51d2016-06-21 17:20:31 +010021
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +010022from tempest.lib import auth
23from tempest.lib import exceptions
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +010024from tempest.lib.services import clients
Andrea Frittoli (andreaf)f2affcc2016-06-28 21:41:47 +010025from tempest.lib.services import compute
26from tempest.lib.services import image
27from tempest.lib.services import network
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +010028
29LOG = logging.getLogger(__name__)
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +010030
Andrea Frittoli (andreaf)f2affcc2016-06-28 21:41:47 +010031client_modules_by_service_name = {
32 'compute': compute,
33 'image.v1': image.v1,
34 'image.v2': image.v2,
35 'network': network
36}
37
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +010038
Andrea Frittoli (andreaf)de5fb0c2016-06-13 12:15:00 +010039def tempest_modules():
40 """List of service client modules available in Tempest.
41
42 Provides a list of service modules available Tempest.
43 """
44 return set(['compute', 'identity.v2', 'identity.v3', 'image.v1',
45 'image.v2', 'network', 'object-storage', 'volume.v1',
46 'volume.v2', 'volume.v3'])
47
48
49def available_modules():
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +010050 """List of service client modules available in Tempest and plugins
51
52 The list of available modules can be used for automatic configuration.
53
54 :raise PluginRegistrationException: if a plugin exposes a service_version
55 already defined by Tempest or another plugin.
56
57 Examples:
58
59 >>> from tempest import config
60 >>> params = {}
61 >>> for service_version in available_modules():
62 >>> service = service_version.split('.')[0]
63 >>> params[service] = config.service_client_config(service)
64 >>> service_clients = ServiceClients(creds, identity_uri,
65 >>> client_parameters=params)
66 """
67 extra_service_versions = set([])
68 plugin_services = clients.ClientsRegistry().get_service_clients()
69 for plugin_name in plugin_services:
70 plug_service_versions = set([x['service_version'] for x in
71 plugin_services[plugin_name]])
72 # If a plugin exposes a duplicate service_version raise an exception
73 if plug_service_versions:
74 if not plug_service_versions.isdisjoint(extra_service_versions):
75 detailed_error = (
76 'Plugin %s is trying to register a service %s already '
77 'claimed by another one' % (plugin_name,
78 extra_service_versions &
79 plug_service_versions))
80 raise exceptions.PluginRegistrationException(
81 name=plugin_name, detailed_error=detailed_error)
82 if not plug_service_versions.isdisjoint(tempest_modules()):
83 detailed_error = (
84 'Plugin %s is trying to register a service %s already '
85 'claimed by a Tempest one' % (plugin_name,
86 tempest_modules() &
87 plug_service_versions))
88 raise exceptions.PluginRegistrationException(
89 name=plugin_name, detailed_error=detailed_error)
90 extra_service_versions |= plug_service_versions
91 return tempest_modules() | extra_service_versions
Andrea Frittoli (andreaf)de5fb0c2016-06-13 12:15:00 +010092
93
Andrea Frittoli (andreaf)73dd51d2016-06-21 17:20:31 +010094class ClientsFactory(object):
95 """Builds service clients for a service client module
96
97 This class implements the logic of feeding service client parameters
98 to service clients from a specific module. It allows setting the
99 parameters once and obtaining new instances of the clients without the
100 need of passing any parameter.
101
102 ClientsFactory can be used directly, or consumed via the `ServiceClients`
103 class, which manages the authorization part.
104 """
Andrea Frittoli (andreaf)73dd51d2016-06-21 17:20:31 +0100105
106 def __init__(self, module_path, client_names, auth_provider, **kwargs):
107 """Initialises the client factory
108
Ken'ichi Ohmichi3f96aff2016-07-29 15:38:44 -0700109 :param module_path: Path to module that includes all service clients.
Andrea Frittoli (andreaf)73dd51d2016-06-21 17:20:31 +0100110 All service client classes must be exposed by a single module.
111 If they are separated in different modules, defining __all__
112 in the root module can help, similar to what is done by service
113 clients in tempest.
Ken'ichi Ohmichi3f96aff2016-07-29 15:38:44 -0700114 :param client_names: List or set of names of the service client
115 classes.
116 :param auth_provider: The auth provider used to initialise client.
117 :param kwargs: Parameters to be passed to all clients. Parameters
118 values can be overwritten when clients are initialised, but
119 parameters cannot be deleted.
Andrea Frittoli (andreaf)73dd51d2016-06-21 17:20:31 +0100120 :raise ImportError if the specified module_path cannot be imported
121
122 Example:
123
124 >>> # Get credentials and an auth_provider
125 >>> clients = ClientsFactory(
126 >>> module_path='my_service.my_service_clients',
127 >>> client_names=['ServiceClient1', 'ServiceClient2'],
128 >>> auth_provider=auth_provider,
129 >>> service='my_service',
130 >>> region='region1')
131 >>> my_api_client = clients.MyApiClient()
132 >>> my_api_client_region2 = clients.MyApiClient(region='region2')
133
134 """
135 # Import the module. If it's not importable, the raised exception
136 # provides good enough information about what happened
137 _module = importlib.import_module(module_path)
138 # If any of the classes is not in the module we fail
139 for class_name in client_names:
140 # TODO(andreaf) This always passes all parameters to all clients.
141 # In future to allow clients to specify the list of parameters
142 # that they accept based out of a list of standard ones.
143
144 # Obtain the class
145 klass = self._get_class(_module, class_name)
146 final_kwargs = copy.copy(kwargs)
147
148 # Set the function as an attribute of the factory
149 setattr(self, class_name, self._get_partial_class(
150 klass, auth_provider, final_kwargs))
151
Andrea Frittoli (andreaf)f9d9a9d2016-06-30 17:35:38 +0100152 def _get_partial_class(self, klass, auth_provider, kwargs):
Andrea Frittoli (andreaf)73dd51d2016-06-21 17:20:31 +0100153
154 # Define a function that returns a new class instance by
155 # combining default kwargs with extra ones
Andrea Frittoli (andreaf)f9d9a9d2016-06-30 17:35:38 +0100156 def partial_class(alias=None, **later_kwargs):
157 """Returns a callable the initialises a service client
158
159 Builds a callable that accepts kwargs, which are passed through
160 to the __init__ of the service client, along with a set of defaults
161 set in factory at factory __init__ time.
162 Original args in the service client can only be passed as kwargs.
163
164 It accepts one extra parameter 'alias' compared to the original
165 service client. When alias is provided, the returned callable will
166 also set an attribute called with a name defined in 'alias', which
167 contains the instance of the service client.
168
169 :param alias: str Name of the attribute set on the factory once
170 the callable is invoked which contains the initialised
171 service client. If None, no attribute is set.
172 :param later_kwargs: kwargs passed through to the service client
173 __init__ on top of defaults set at factory level.
174 """
Andrea Frittoli (andreaf)73dd51d2016-06-21 17:20:31 +0100175 kwargs.update(later_kwargs)
Andrea Frittoli (andreaf)f9d9a9d2016-06-30 17:35:38 +0100176 _client = klass(auth_provider=auth_provider, **kwargs)
177 if alias:
178 setattr(self, alias, _client)
179 return _client
Andrea Frittoli (andreaf)73dd51d2016-06-21 17:20:31 +0100180
181 return partial_class
182
183 @classmethod
184 def _get_class(cls, module, class_name):
185 klass = getattr(module, class_name, None)
186 if not klass:
187 msg = 'Invalid class name, %s is not found in %s'
188 raise AttributeError(msg % (class_name, module))
189 if not inspect.isclass(klass):
190 msg = 'Expected a class, got %s of type %s instead'
191 raise TypeError(msg % (klass, type(klass)))
192 return klass
193
194
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +0100195class ServiceClients(object):
196 """Service client provider class
197
198 The ServiceClients object provides a useful means for tests to access
199 service clients configured for a specified set of credentials.
200 It hides some of the complexity from the authorization and configuration
201 layers.
202
203 Examples:
204
205 >>> from tempest import service_clients
206 >>> johndoe = cred_provider.get_creds_by_role(['johndoe'])
Andrea Frittoli (andreaf)de5fb0c2016-06-13 12:15:00 +0100207 >>> johndoe_clients = service_clients.ServiceClients(johndoe,
208 >>> identity_uri)
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +0100209 >>> johndoe_servers = johndoe_clients.servers_client.list_servers()
210
211 """
212 # NOTE(andreaf) This class does not depend on tempest configuration
213 # and its meant for direct consumption by external clients such as tempest
214 # plugins. Tempest provides a wrapper class, `clients.Manager`, that
215 # initialises this class using values from tempest CONF object. The wrapper
216 # class should only be used by tests hosted in Tempest.
217
Andrea Frittoli (andreaf)de5fb0c2016-06-13 12:15:00 +0100218 def __init__(self, credentials, identity_uri, region=None, scope='project',
219 disable_ssl_certificate_validation=True, ca_certs=None,
220 trace_requests='', client_parameters=None):
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +0100221 """Service Clients provider
222
223 Instantiate a `ServiceClients` object, from a set of credentials and an
224 identity URI. The identity version is inferred from the credentials
225 object. Optionally auth scope can be provided.
Andrea Frittoli (andreaf)de5fb0c2016-06-13 12:15:00 +0100226
227 A few parameters can be given a value which is applied as default
228 for all service clients: region, dscv, ca_certs, trace_requests.
229
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +0100230 Parameters dscv, ca_certs and trace_requests all apply to the auth
231 provider as well as any service clients provided by this manager.
232
Andrea Frittoli (andreaf)de5fb0c2016-06-13 12:15:00 +0100233 Any other client parameter must be set via client_parameters.
234 The list of available parameters is defined in the service clients
235 interfaces. For reference, most clients will accept 'region',
236 'service', 'endpoint_type', 'build_timeout' and 'build_interval', which
237 are all inherited from RestClient.
238
239 The `config` module in Tempest exposes an helper function
240 `service_client_config` that can be used to extract from configuration
241 a dictionary ready to be injected in kwargs.
242
243 Exceptions are:
244 - Token clients for 'identity' have a very different interface
245 - Volume client for 'volume' accepts 'default_volume_size'
246 - Servers client from 'compute' accepts 'enable_instance_password'
247
248 Examples:
249
250 >>> identity_params = config.service_client_config('identity')
251 >>> params = {
252 >>> 'identity': identity_params,
253 >>> 'compute': {'region': 'region2'}}
254 >>> manager = lib_manager.Manager(
255 >>> my_creds, identity_uri, client_parameters=params)
256
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +0100257 :param credentials: An instance of `auth.Credentials`
258 :param identity_uri: URI of the identity API. This should be a
259 mandatory parameter, and it will so soon.
260 :param region: Default value of region for service clients.
261 :param scope: default scope for tokens produced by the auth provider
Ken'ichi Ohmichi3f96aff2016-07-29 15:38:44 -0700262 :param disable_ssl_certificate_validation: Applies to auth and to all
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +0100263 service clients.
Ken'ichi Ohmichi3f96aff2016-07-29 15:38:44 -0700264 :param ca_certs: Applies to auth and to all service clients.
265 :param trace_requests: Applies to auth and to all service clients.
266 :param client_parameters: Dictionary with parameters for service
Andrea Frittoli (andreaf)de5fb0c2016-06-13 12:15:00 +0100267 clients. Keys of the dictionary are the service client service
268 name, as declared in `service_clients.available_modules()` except
269 for the version. Values are dictionaries of parameters that are
270 going to be passed to all clients in the service client module.
271
272 Examples:
273
274 >>> params_service_x = {'param_name': 'param_value'}
275 >>> client_parameters = { 'service_x': params_service_x }
276
277 >>> params_service_y = config.service_client_config('service_y')
278 >>> client_parameters['service_y'] = params_service_y
279
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +0100280 """
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +0100281 self._registered_services = set([])
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +0100282 self.credentials = credentials
283 self.identity_uri = identity_uri
284 if not identity_uri:
285 raise exceptions.InvalidCredentials(
Andrea Frittoli (andreaf)de5fb0c2016-06-13 12:15:00 +0100286 'ServiceClients requires a non-empty identity_uri.')
Andrea Frittoli (andreaf)23950142016-06-13 12:39:29 +0100287 self.region = region
288 # Check if passed or default credentials are valid
289 if not self.credentials.is_valid():
290 raise exceptions.InvalidCredentials()
291 # Get the identity classes matching the provided credentials
292 # TODO(andreaf) Define a new interface in Credentials to get
293 # the API version from an instance
294 identity = [(k, auth.IDENTITY_VERSION[k][1]) for k in
295 auth.IDENTITY_VERSION.keys() if
296 isinstance(self.credentials, auth.IDENTITY_VERSION[k][0])]
297 # Zero matches or more than one are both not valid.
298 if len(identity) != 1:
299 raise exceptions.InvalidCredentials()
300 self.auth_version, auth_provider_class = identity[0]
301 self.dscv = disable_ssl_certificate_validation
302 self.ca_certs = ca_certs
303 self.trace_requests = trace_requests
304 # Creates an auth provider for the credentials
305 self.auth_provider = auth_provider_class(
306 self.credentials, self.identity_uri, scope=scope,
307 disable_ssl_certificate_validation=self.dscv,
308 ca_certs=self.ca_certs, trace_requests=self.trace_requests)
Andrea Frittoli (andreaf)de5fb0c2016-06-13 12:15:00 +0100309 # Setup some defaults for client parameters of registered services
310 client_parameters = client_parameters or {}
311 self.parameters = {}
312 # Parameters are provided for unversioned services
313 unversioned_services = set(
314 [x.split('.')[0] for x in available_modules()])
315 for service in unversioned_services:
316 self.parameters[service] = self._setup_parameters(
317 client_parameters.pop(service, {}))
318 # Check that no client parameters was supplied for unregistered clients
319 if client_parameters:
320 raise exceptions.UnknownServiceClient(
321 services=list(client_parameters.keys()))
322
Andrea Frittoli (andreaf)f2affcc2016-06-28 21:41:47 +0100323 # Register service clients owned by tempest
324 for service in tempest_modules():
325 if service in list(client_modules_by_service_name):
326 attribute = service.replace('.', '_')
327 configs = service.split('.')[0]
328 module = client_modules_by_service_name[service]
329 self.register_service_client_module(
330 attribute, service, module.__name__,
331 module.__all__, **self.parameters[configs])
332
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +0100333 # Register service clients from plugins
334 clients_registry = clients.ClientsRegistry()
335 plugin_service_clients = clients_registry.get_service_clients()
336 for plugin in plugin_service_clients:
337 service_clients = plugin_service_clients[plugin]
338 # Each plugin returns a list of service client parameters
339 for service_client in service_clients:
340 # NOTE(andreaf) If a plugin cannot register, stop the
341 # registration process, log some details to help
342 # troubleshooting, and re-raise
343 try:
344 self.register_service_client_module(**service_client)
345 except Exception:
346 LOG.exception(
347 'Failed to register service client from plugin %s '
348 'with parameters %s' % (plugin, service_client))
349 raise
350
351 def register_service_client_module(self, name, service_version,
352 module_path, client_names, **kwargs):
353 """Register a service client module
354
355 Initiates a client factory for the specified module, using this
356 class auth_provider, and accessible via a `name` attribute in the
357 service client.
358
359 :param name: Name used to access the client
360 :param service_version: Name of the service complete with version.
361 Used to track registered services. When a plugin implements it,
362 it can be used by other plugins to obtain their configuration.
363 :param module_path: Path to module that includes all service clients.
364 All service client classes must be exposed by a single module.
365 If they are separated in different modules, defining __all__
366 in the root module can help, similar to what is done by service
367 clients in tempest.
368 :param client_names: List or set of names of service client classes.
369 :param kwargs: Extra optional parameters to be passed to all clients.
370 ServiceClient provides defaults for region, dscv, ca_certs and
371 trace_requests.
372 :raise ServiceClientRegistrationException: if the provided name is
373 already in use or if service_version is already registered.
374 :raise ImportError: if module_path cannot be imported.
375 """
376 if hasattr(self, name):
377 using_name = getattr(self, name)
378 detailed_error = 'Module name already in use: %s' % using_name
379 raise exceptions.ServiceClientRegistrationException(
380 name=name, service_version=service_version,
381 module_path=module_path, client_names=client_names,
382 detailed_error=detailed_error)
383 if service_version in self.registered_services:
384 detailed_error = 'Service %s already registered.' % service_version
385 raise exceptions.ServiceClientRegistrationException(
386 name=name, service_version=service_version,
387 module_path=module_path, client_names=client_names,
388 detailed_error=detailed_error)
389 params = dict(region=self.region,
390 disable_ssl_certificate_validation=self.dscv,
391 ca_certs=self.ca_certs,
392 trace_requests=self.trace_requests)
393 params.update(kwargs)
394 # Instantiate the client factory
395 _factory = ClientsFactory(module_path=module_path,
396 client_names=client_names,
397 auth_provider=self.auth_provider,
398 **params)
399 # Adds the client factory to the service_client
400 setattr(self, name, _factory)
401 # Add the name of the new service in self.SERVICES for discovery
402 self._registered_services.add(service_version)
403
404 @property
405 def registered_services(self):
Andrea Frittoli (andreaf)f2affcc2016-06-28 21:41:47 +0100406 # TODO(andreaf) Temporary set needed until all services are migrated
407 _non_migrated_services = tempest_modules() - set(
408 client_modules_by_service_name)
409 return self._registered_services | _non_migrated_services
Andrea Frittoli (andreaf)6d4d85a2016-06-21 17:20:31 +0100410
Andrea Frittoli (andreaf)de5fb0c2016-06-13 12:15:00 +0100411 def _setup_parameters(self, parameters):
412 """Setup default values for client parameters
413
414 Region by default is the region passed as an __init__ parameter.
415 Checks that no parameter for an unknown service is provided.
416 """
417 _parameters = {}
418 # Use region from __init__
419 if self.region:
420 _parameters['region'] = self.region
421 # Update defaults with specified parameters
422 _parameters.update(parameters)
423 # If any parameter is left, parameters for an unknown service were
424 # provided as input. Fail rather than ignore silently.
425 return _parameters