blob: 554ba35c00b595bc79660279663168f17afe54b1 [file] [log] [blame]
Marc Koderer0abc93b2015-07-15 09:18:35 +02001# Copyright 2014 Mirantis Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import copy
17import inspect
18import traceback
19
20from oslo_concurrency import lockutils
21from oslo_log import log
22import six
23from tempest.common import isolated_creds # noqa
24from tempest import config # noqa
25from tempest import test # noqa
26from tempest_lib.common.utils import data_utils
27from tempest_lib import exceptions
28
29from manila_tempest_tests import clients_share as clients
30from manila_tempest_tests import share_exceptions
31
32CONF = config.CONF
33LOG = log.getLogger(__name__)
34
35
36class handle_cleanup_exceptions(object):
37 """Handle exceptions raised with cleanup operations.
38
39 Always suppress errors when exceptions.NotFound or exceptions.Forbidden
40 are raised.
41 Suppress all other exceptions only in case config opt
42 'suppress_errors_in_cleanup' in config group 'share' is True.
43 """
44
45 def __enter__(self):
46 return self
47
48 def __exit__(self, exc_type, exc_value, exc_traceback):
49 if not (isinstance(exc_value,
50 (exceptions.NotFound, exceptions.Forbidden)) or
51 CONF.share.suppress_errors_in_cleanup):
52 return False # Do not suppress error if any
53 if exc_traceback:
54 LOG.error("Suppressed cleanup error in Manila: "
55 "\n%s" % traceback.format_exc())
56 return True # Suppress error if any
57
58
59def network_synchronized(f):
60
61 def wrapped_func(self, *args, **kwargs):
62 with_isolated_creds = True if len(args) > 2 else False
63 no_lock_required = kwargs.get(
64 "isolated_creds_client", with_isolated_creds)
65 if no_lock_required:
66 # Usage of not reusable network. No need in lock.
67 return f(self, *args, **kwargs)
68
69 # Use lock assuming reusage of common network.
70 @lockutils.synchronized("manila_network_lock", external=True)
71 def source_func(self, *args, **kwargs):
72 return f(self, *args, **kwargs)
73
74 return source_func(self, *args, **kwargs)
75
76 return wrapped_func
77
78
79class BaseSharesTest(test.BaseTestCase):
80 """Base test case class for all Manila API tests."""
81
82 force_tenant_isolation = False
83 protocols = ["nfs", "cifs", "glusterfs", "hdfs"]
84
85 # Will be cleaned up in resource_cleanup
86 class_resources = []
87
88 # Will be cleaned up in tearDown method
89 method_resources = []
90
91 # Will be cleaned up in resource_cleanup
92 class_isolated_creds = []
93
94 # Will be cleaned up in tearDown method
95 method_isolated_creds = []
96
97 @classmethod
98 def get_client_with_isolated_creds(cls,
99 name=None,
100 type_of_creds="admin",
101 cleanup_in_class=False):
102 """Creates isolated creds.
103
104 :param name: name, will be used for naming ic and related stuff
105 :param type_of_creds: admin, alt or primary
106 :param cleanup_in_class: defines place where to delete
107 :returns: SharesClient -- shares client with isolated creds.
108 :returns: To client added dict attr 'creds' with
109 :returns: key elements 'tenant' and 'user'.
110 """
111 if name is None:
112 # Get name of test method
113 name = inspect.stack()[1][3]
114 if len(name) > 32:
115 name = name[0:32]
116
117 # Choose type of isolated creds
118 ic = isolated_creds.IsolatedCreds(name=name)
119 if "admin" in type_of_creds:
120 creds = ic.get_admin_creds()
121 elif "alt" in type_of_creds:
122 creds = ic.get_alt_creds()
123 else:
124 creds = ic.self.get_credentials(type_of_creds)
125 ic.type_of_creds = type_of_creds
126
127 # create client with isolated creds
128 os = clients.Manager(credentials=creds)
129 client = os.shares_client
130
131 # Set place where will be deleted isolated creds
132 ic_res = {
133 "method": ic.clear_isolated_creds,
134 "deleted": False,
135 }
136 if cleanup_in_class:
137 cls.class_isolated_creds.insert(0, ic_res)
138 else:
139 cls.method_isolated_creds.insert(0, ic_res)
140
141 # Provide share network
142 if CONF.share.multitenancy_enabled:
143 if not CONF.service_available.neutron:
144 raise cls.skipException("Neutron support is required")
145 nc = os.network_client
146 share_network_id = cls.provide_share_network(client, nc, ic)
147 client.share_network_id = share_network_id
148 resource = {
149 "type": "share_network",
150 "id": client.share_network_id,
151 "client": client,
152 }
153 if cleanup_in_class:
154 cls.class_resources.insert(0, resource)
155 else:
156 cls.method_resources.insert(0, resource)
157 return client
158
159 @classmethod
160 def verify_nonempty(cls, *args):
161 if not all(args):
162 msg = "Missing API credentials in configuration."
163 raise cls.skipException(msg)
164
165 @classmethod
166 def resource_setup(cls):
167 if not (any(p in CONF.share.enable_protocols
168 for p in cls.protocols) and
169 CONF.service_available.manila):
170 skip_msg = "Manila is disabled"
171 raise cls.skipException(skip_msg)
172 super(BaseSharesTest, cls).resource_setup()
173 if not hasattr(cls, "os"):
174 cls.username = CONF.identity.username
175 cls.password = CONF.identity.password
176 cls.tenant_name = CONF.identity.tenant_name
177 cls.verify_nonempty(cls.username, cls.password, cls.tenant_name)
178 cls.os = clients.Manager()
179 if CONF.share.multitenancy_enabled:
180 if not CONF.service_available.neutron:
181 raise cls.skipException("Neutron support is required")
182 sc = cls.os.shares_client
183 nc = cls.os.network_client
184 share_network_id = cls.provide_share_network(sc, nc)
185 cls.os.shares_client.share_network_id = share_network_id
186 cls.shares_client = cls.os.shares_client
187
188 def setUp(self):
189 super(BaseSharesTest, self).setUp()
190 self.addCleanup(self.clear_resources)
191 self.addCleanup(self.clear_isolated_creds)
192
193 @classmethod
194 def resource_cleanup(cls):
195 super(BaseSharesTest, cls).resource_cleanup()
196 cls.clear_resources(cls.class_resources)
197 cls.clear_isolated_creds(cls.class_isolated_creds)
198
199 @classmethod
200 @network_synchronized
201 def provide_share_network(cls, shares_client, network_client,
202 isolated_creds_client=None):
203 """Used for finding/creating share network for multitenant driver.
204
205 This method creates/gets entity share-network for one tenant. This
206 share-network will be used for creation of service vm.
207
208 :param shares_client: shares client, which requires share-network
209 :param network_client: network client from same tenant as shares
210 :param isolated_creds_client: IsolatedCreds instance
211 If provided, then its networking will be used if needed.
212 If not provided, then common network will be used if needed.
213 :returns: str -- share network id for shares_client tenant
214 :returns: None -- if single-tenant driver used
215 """
216
217 sc = shares_client
218
219 if not CONF.share.multitenancy_enabled:
220 # Assumed usage of a single-tenant driver
221 share_network_id = None
222 elif sc.share_network_id:
223 # Share-network already exists, use it
224 share_network_id = sc.share_network_id
225 else:
226 net_id = subnet_id = share_network_id = None
227
228 if not isolated_creds_client:
229 # Search for networks, created in previous runs
230 search_word = "reusable"
231 sn_name = "autogenerated_by_tempest_%s" % search_word
232 service_net_name = "share-service"
233 networks = network_client.list_networks()
234 if "networks" in networks.keys():
235 networks = networks["networks"]
236 for network in networks:
237 if (service_net_name in network["name"] and
238 sc.tenant_id == network['tenant_id']):
239 net_id = network["id"]
240 if len(network["subnets"]) > 0:
241 subnet_id = network["subnets"][0]
242 break
243
244 # Create suitable network
245 if (net_id is None or subnet_id is None):
246 ic = isolated_creds.IsolatedCreds(name=service_net_name)
247 net_data = ic._create_network_resources(sc.tenant_id)
248 network, subnet, router = net_data
249 net_id = network["id"]
250 subnet_id = subnet["id"]
251
252 # Try get suitable share-network
253 share_networks = sc.list_share_networks_with_detail()
254 for sn in share_networks:
255 if (net_id == sn["neutron_net_id"] and
256 subnet_id == sn["neutron_subnet_id"] and
257 sn["name"] and search_word in sn["name"]):
258 share_network_id = sn["id"]
259 break
260 else:
261 sn_name = "autogenerated_by_tempest_for_isolated_creds"
262 # Use precreated network and subnet from isolated creds
263 net_id = isolated_creds_client.get_credentials(
264 isolated_creds_client.type_of_creds).network['id']
265 subnet_id = isolated_creds_client.get_credentials(
266 isolated_creds_client.type_of_creds).subnet['id']
267
268 # Create suitable share-network
269 if share_network_id is None:
270 sn_desc = "This share-network was created by tempest"
271 sn = sc.create_share_network(name=sn_name,
272 description=sn_desc,
273 neutron_net_id=net_id,
274 neutron_subnet_id=subnet_id)
275 share_network_id = sn["id"]
276
277 return share_network_id
278
279 @classmethod
280 def _create_share(cls, share_protocol=None, size=1, name=None,
281 snapshot_id=None, description=None, metadata=None,
282 share_network_id=None, share_type_id=None,
283 client=None, cleanup_in_class=True, is_public=False):
284 client = client or cls.shares_client
285 description = description or "Tempest's share"
286 share_network_id = share_network_id or client.share_network_id or None
287 metadata = metadata or {}
288 kwargs = {
289 'share_protocol': share_protocol,
290 'size': size,
291 'name': name,
292 'snapshot_id': snapshot_id,
293 'description': description,
294 'metadata': metadata,
295 'share_network_id': share_network_id,
296 'share_type_id': share_type_id,
297 'is_public': is_public,
298 }
299 share = client.create_share(**kwargs)
300 resource = {"type": "share", "id": share["id"], "client": client}
301 cleanup_list = (cls.class_resources if cleanup_in_class else
302 cls.method_resources)
303 cleanup_list.insert(0, resource)
304 return share
305
306 @classmethod
307 def create_share(cls, *args, **kwargs):
308 """Create one share and wait for available state. Retry if allowed."""
309 result = cls.create_shares([{"args": args, "kwargs": kwargs}])
310 return result[0]
311
312 @classmethod
313 def create_shares(cls, share_data_list):
314 """Creates several shares in parallel with retries.
315
316 Use this method when you want to create more than one share at same
317 time. Especially if config option 'share.share_creation_retry_number'
318 has value more than zero (0).
319 All shares will be expected to have 'available' status with or without
320 recreation else error will be raised.
321
322 :param share_data_list: list -- list of dictionaries with 'args' and
323 'kwargs' for '_create_share' method of this base class.
324 example of data:
325 share_data_list=[{'args': ['quuz'], 'kwargs': {'foo': 'bar'}}}]
326 :returns: list -- list of shares created using provided data.
327 """
328
329 data = [copy.deepcopy(d) for d in share_data_list]
330 for d in data:
331 if not isinstance(d, dict):
332 raise exceptions.TempestException(
333 "Expected 'dict', got '%s'" % type(d))
334 if "args" not in d:
335 d["args"] = []
336 if "kwargs" not in d:
337 d["kwargs"] = {}
338 if len(d) > 2:
339 raise exceptions.TempestException(
340 "Expected only 'args' and 'kwargs' keys. "
341 "Provided %s" % list(d))
342 d["kwargs"]["client"] = d["kwargs"].get(
343 "client", cls.shares_client)
344 d["share"] = cls._create_share(*d["args"], **d["kwargs"])
345 d["cnt"] = 0
346 d["available"] = False
347
348 while not all(d["available"] for d in data):
349 for d in data:
350 if d["available"]:
351 continue
352 try:
353 d["kwargs"]["client"].wait_for_share_status(
354 d["share"]["id"], "available")
355 d["available"] = True
356 except (share_exceptions.ShareBuildErrorException,
357 exceptions.TimeoutException) as e:
358 if CONF.share.share_creation_retry_number > d["cnt"]:
359 d["cnt"] += 1
360 msg = ("Share '%s' failed to be built. "
361 "Trying create another." % d["share"]["id"])
362 LOG.error(msg)
363 LOG.error(e)
364 d["share"] = cls._create_share(
365 *d["args"], **d["kwargs"])
366 else:
367 raise e
368
369 return [d["share"] for d in data]
370
371 @classmethod
372 def create_snapshot_wait_for_active(cls, share_id, name=None,
373 description=None, force=False,
374 client=None, cleanup_in_class=True):
375 if client is None:
376 client = cls.shares_client
377 if description is None:
378 description = "Tempest's snapshot"
379 snapshot = client.create_snapshot(share_id, name, description, force)
380 resource = {
381 "type": "snapshot",
382 "id": snapshot["id"],
383 "client": client,
384 }
385 if cleanup_in_class:
386 cls.class_resources.insert(0, resource)
387 else:
388 cls.method_resources.insert(0, resource)
389 client.wait_for_snapshot_status(snapshot["id"], "available")
390 return snapshot
391
392 @classmethod
393 def create_share_network(cls, client=None,
394 cleanup_in_class=False, **kwargs):
395 if client is None:
396 client = cls.shares_client
397 share_network = client.create_share_network(**kwargs)
398 resource = {
399 "type": "share_network",
400 "id": share_network["id"],
401 "client": client,
402 }
403 if cleanup_in_class:
404 cls.class_resources.insert(0, resource)
405 else:
406 cls.method_resources.insert(0, resource)
407 return share_network
408
409 @classmethod
410 def create_security_service(cls, ss_type="ldap", client=None,
411 cleanup_in_class=False, **kwargs):
412 if client is None:
413 client = cls.shares_client
414 security_service = client.create_security_service(ss_type, **kwargs)
415 resource = {
416 "type": "security_service",
417 "id": security_service["id"],
418 "client": client,
419 }
420 if cleanup_in_class:
421 cls.class_resources.insert(0, resource)
422 else:
423 cls.method_resources.insert(0, resource)
424 return security_service
425
426 @classmethod
427 def create_share_type(cls, name, is_public=True, client=None,
428 cleanup_in_class=True, **kwargs):
429 if client is None:
430 client = cls.shares_client
431 share_type = client.create_share_type(name, is_public, **kwargs)
432 resource = {
433 "type": "share_type",
434 "id": share_type["share_type"]["id"],
435 "client": client,
436 }
437 if cleanup_in_class:
438 cls.class_resources.insert(0, resource)
439 else:
440 cls.method_resources.insert(0, resource)
441 return share_type
442
443 @staticmethod
444 def add_required_extra_specs_to_dict(extra_specs=None):
445 value = six.text_type(CONF.share.multitenancy_enabled)
446 required = {
447 "driver_handles_share_servers": value,
448 "snapshot_support": 'True',
449 }
450 if extra_specs:
451 required.update(extra_specs)
452 return required
453
454 @classmethod
455 def clear_isolated_creds(cls, creds=None):
456 if creds is None:
457 creds = cls.method_isolated_creds
458 for ic in creds:
459 if "deleted" not in ic.keys():
460 ic["deleted"] = False
461 if not ic["deleted"]:
462 with handle_cleanup_exceptions():
463 ic["method"]()
464 ic["deleted"] = True
465
466 @classmethod
467 def clear_resources(cls, resources=None):
468 """Deletes resources, that were created in test suites.
469
470 This method tries to remove resources from resource list,
471 if it is not found, assumed it was deleted in test itself.
472 It is expected, that all resources were added as LIFO
473 due to restriction of deletion resources, that is in the chain.
474
475 :param resources: dict with keys 'type','id','client' and 'deleted'
476 """
477
478 if resources is None:
479 resources = cls.method_resources
480 for res in resources:
481 if "deleted" not in res.keys():
482 res["deleted"] = False
483 if "client" not in res.keys():
484 res["client"] = cls.shares_client
485 if not(res["deleted"]):
486 res_id = res['id']
487 client = res["client"]
488 with handle_cleanup_exceptions():
489 if res["type"] is "share":
490 client.delete_share(res_id)
491 client.wait_for_resource_deletion(share_id=res_id)
492 elif res["type"] is "snapshot":
493 client.delete_snapshot(res_id)
494 client.wait_for_resource_deletion(snapshot_id=res_id)
495 elif res["type"] is "share_network":
496 client.delete_share_network(res_id)
497 client.wait_for_resource_deletion(sn_id=res_id)
498 elif res["type"] is "security_service":
499 client.delete_security_service(res_id)
500 client.wait_for_resource_deletion(ss_id=res_id)
501 elif res["type"] is "share_type":
502 client.delete_share_type(res_id)
503 client.wait_for_resource_deletion(st_id=res_id)
504 else:
505 LOG.warn("Provided unsupported resource type for "
506 "cleanup '%s'. Skipping." % res["type"])
507 res["deleted"] = True
508
509 @classmethod
510 def generate_share_network_data(self):
511 data = {
512 "name": data_utils.rand_name("sn-name"),
513 "description": data_utils.rand_name("sn-desc"),
514 "neutron_net_id": data_utils.rand_name("net-id"),
515 "neutron_subnet_id": data_utils.rand_name("subnet-id"),
516 }
517 return data
518
519 @classmethod
520 def generate_security_service_data(self):
521 data = {
522 "name": data_utils.rand_name("ss-name"),
523 "description": data_utils.rand_name("ss-desc"),
524 "dns_ip": data_utils.rand_name("ss-dns_ip"),
525 "server": data_utils.rand_name("ss-server"),
526 "domain": data_utils.rand_name("ss-domain"),
527 "user": data_utils.rand_name("ss-user"),
528 "password": data_utils.rand_name("ss-password"),
529 }
530 return data
531
532 # Useful assertions
533 def assertDictMatch(self, d1, d2, approx_equal=False, tolerance=0.001):
534 """Assert two dicts are equivalent.
535
536 This is a 'deep' match in the sense that it handles nested
537 dictionaries appropriately.
538
539 NOTE:
540
541 If you don't care (or don't know) a given value, you can specify
542 the string DONTCARE as the value. This will cause that dict-item
543 to be skipped.
544
545 """
546 def raise_assertion(msg):
547 d1str = str(d1)
548 d2str = str(d2)
549 base_msg = ('Dictionaries do not match. %(msg)s d1: %(d1str)s '
550 'd2: %(d2str)s' %
551 {"msg": msg, "d1str": d1str, "d2str": d2str})
552 raise AssertionError(base_msg)
553
554 d1keys = set(d1.keys())
555 d2keys = set(d2.keys())
556 if d1keys != d2keys:
557 d1only = d1keys - d2keys
558 d2only = d2keys - d1keys
559 raise_assertion('Keys in d1 and not d2: %(d1only)s. '
560 'Keys in d2 and not d1: %(d2only)s' %
561 {"d1only": d1only, "d2only": d2only})
562
563 for key in d1keys:
564 d1value = d1[key]
565 d2value = d2[key]
566 try:
567 error = abs(float(d1value) - float(d2value))
568 within_tolerance = error <= tolerance
569 except (ValueError, TypeError):
570 # If both values aren't convertable to float, just ignore
571 # ValueError if arg is a str, TypeError if it's something else
572 # (like None)
573 within_tolerance = False
574
575 if hasattr(d1value, 'keys') and hasattr(d2value, 'keys'):
576 self.assertDictMatch(d1value, d2value)
577 elif 'DONTCARE' in (d1value, d2value):
578 continue
579 elif approx_equal and within_tolerance:
580 continue
581 elif d1value != d2value:
582 raise_assertion("d1['%(key)s']=%(d1value)s != "
583 "d2['%(key)s']=%(d2value)s" %
584 {
585 "key": key,
586 "d1value": d1value,
587 "d2value": d2value
588 })
589
590
591class BaseSharesAltTest(BaseSharesTest):
592 """Base test case class for all Shares Alt API tests."""
593
594 @classmethod
595 def resource_setup(cls):
596 cls.username = CONF.identity.alt_username
597 cls.password = CONF.identity.alt_password
598 cls.tenant_name = CONF.identity.alt_tenant_name
599 cls.verify_nonempty(cls.username, cls.password, cls.tenant_name)
600 cls.os = clients.AltManager()
601 alt_share_network_id = CONF.share.alt_share_network_id
602 cls.os.shares_client.share_network_id = alt_share_network_id
603 super(BaseSharesAltTest, cls).resource_setup()
604
605
606class BaseSharesAdminTest(BaseSharesTest):
607 """Base test case class for all Shares Admin API tests."""
608
609 @classmethod
610 def resource_setup(cls):
611 cls.username = CONF.identity.admin_username
612 cls.password = CONF.identity.admin_password
613 cls.tenant_name = CONF.identity.admin_tenant_name
614 cls.verify_nonempty(cls.username, cls.password, cls.tenant_name)
615 cls.os = clients.AdminManager()
616 admin_share_network_id = CONF.share.admin_share_network_id
617 cls.os.shares_client.share_network_id = admin_share_network_id
618 super(BaseSharesAdminTest, cls).resource_setup()