blob: 6444ccc37aae73211ad5ead7fd6b7e94ccb00d93 [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
Rodrigo Barbierib7137ad2015-09-06 22:53:16 -0300307 def migrate_share(cls, share_id, dest_host, client=None):
308 client = client or cls.shares_client
309 client.migrate_share(share_id, dest_host)
310 share = client.wait_for_migration_completed(share_id, dest_host)
311 return share
312
313 @classmethod
Marc Koderer0abc93b2015-07-15 09:18:35 +0200314 def create_share(cls, *args, **kwargs):
315 """Create one share and wait for available state. Retry if allowed."""
316 result = cls.create_shares([{"args": args, "kwargs": kwargs}])
317 return result[0]
318
319 @classmethod
320 def create_shares(cls, share_data_list):
321 """Creates several shares in parallel with retries.
322
323 Use this method when you want to create more than one share at same
324 time. Especially if config option 'share.share_creation_retry_number'
325 has value more than zero (0).
326 All shares will be expected to have 'available' status with or without
327 recreation else error will be raised.
328
329 :param share_data_list: list -- list of dictionaries with 'args' and
330 'kwargs' for '_create_share' method of this base class.
331 example of data:
332 share_data_list=[{'args': ['quuz'], 'kwargs': {'foo': 'bar'}}}]
333 :returns: list -- list of shares created using provided data.
334 """
335
336 data = [copy.deepcopy(d) for d in share_data_list]
337 for d in data:
338 if not isinstance(d, dict):
339 raise exceptions.TempestException(
340 "Expected 'dict', got '%s'" % type(d))
341 if "args" not in d:
342 d["args"] = []
343 if "kwargs" not in d:
344 d["kwargs"] = {}
345 if len(d) > 2:
346 raise exceptions.TempestException(
347 "Expected only 'args' and 'kwargs' keys. "
348 "Provided %s" % list(d))
349 d["kwargs"]["client"] = d["kwargs"].get(
350 "client", cls.shares_client)
351 d["share"] = cls._create_share(*d["args"], **d["kwargs"])
352 d["cnt"] = 0
353 d["available"] = False
354
355 while not all(d["available"] for d in data):
356 for d in data:
357 if d["available"]:
358 continue
359 try:
360 d["kwargs"]["client"].wait_for_share_status(
361 d["share"]["id"], "available")
362 d["available"] = True
363 except (share_exceptions.ShareBuildErrorException,
364 exceptions.TimeoutException) as e:
365 if CONF.share.share_creation_retry_number > d["cnt"]:
366 d["cnt"] += 1
367 msg = ("Share '%s' failed to be built. "
368 "Trying create another." % d["share"]["id"])
369 LOG.error(msg)
370 LOG.error(e)
371 d["share"] = cls._create_share(
372 *d["args"], **d["kwargs"])
373 else:
374 raise e
375
376 return [d["share"] for d in data]
377
378 @classmethod
379 def create_snapshot_wait_for_active(cls, share_id, name=None,
380 description=None, force=False,
381 client=None, cleanup_in_class=True):
382 if client is None:
383 client = cls.shares_client
384 if description is None:
385 description = "Tempest's snapshot"
386 snapshot = client.create_snapshot(share_id, name, description, force)
387 resource = {
388 "type": "snapshot",
389 "id": snapshot["id"],
390 "client": client,
391 }
392 if cleanup_in_class:
393 cls.class_resources.insert(0, resource)
394 else:
395 cls.method_resources.insert(0, resource)
396 client.wait_for_snapshot_status(snapshot["id"], "available")
397 return snapshot
398
399 @classmethod
400 def create_share_network(cls, client=None,
401 cleanup_in_class=False, **kwargs):
402 if client is None:
403 client = cls.shares_client
404 share_network = client.create_share_network(**kwargs)
405 resource = {
406 "type": "share_network",
407 "id": share_network["id"],
408 "client": client,
409 }
410 if cleanup_in_class:
411 cls.class_resources.insert(0, resource)
412 else:
413 cls.method_resources.insert(0, resource)
414 return share_network
415
416 @classmethod
417 def create_security_service(cls, ss_type="ldap", client=None,
418 cleanup_in_class=False, **kwargs):
419 if client is None:
420 client = cls.shares_client
421 security_service = client.create_security_service(ss_type, **kwargs)
422 resource = {
423 "type": "security_service",
424 "id": security_service["id"],
425 "client": client,
426 }
427 if cleanup_in_class:
428 cls.class_resources.insert(0, resource)
429 else:
430 cls.method_resources.insert(0, resource)
431 return security_service
432
433 @classmethod
434 def create_share_type(cls, name, is_public=True, client=None,
435 cleanup_in_class=True, **kwargs):
436 if client is None:
437 client = cls.shares_client
438 share_type = client.create_share_type(name, is_public, **kwargs)
439 resource = {
440 "type": "share_type",
441 "id": share_type["share_type"]["id"],
442 "client": client,
443 }
444 if cleanup_in_class:
445 cls.class_resources.insert(0, resource)
446 else:
447 cls.method_resources.insert(0, resource)
448 return share_type
449
450 @staticmethod
451 def add_required_extra_specs_to_dict(extra_specs=None):
452 value = six.text_type(CONF.share.multitenancy_enabled)
453 required = {
454 "driver_handles_share_servers": value,
455 "snapshot_support": 'True',
456 }
457 if extra_specs:
458 required.update(extra_specs)
459 return required
460
461 @classmethod
462 def clear_isolated_creds(cls, creds=None):
463 if creds is None:
464 creds = cls.method_isolated_creds
465 for ic in creds:
466 if "deleted" not in ic.keys():
467 ic["deleted"] = False
468 if not ic["deleted"]:
469 with handle_cleanup_exceptions():
470 ic["method"]()
471 ic["deleted"] = True
472
473 @classmethod
474 def clear_resources(cls, resources=None):
475 """Deletes resources, that were created in test suites.
476
477 This method tries to remove resources from resource list,
478 if it is not found, assumed it was deleted in test itself.
479 It is expected, that all resources were added as LIFO
480 due to restriction of deletion resources, that is in the chain.
481
482 :param resources: dict with keys 'type','id','client' and 'deleted'
483 """
484
485 if resources is None:
486 resources = cls.method_resources
487 for res in resources:
488 if "deleted" not in res.keys():
489 res["deleted"] = False
490 if "client" not in res.keys():
491 res["client"] = cls.shares_client
492 if not(res["deleted"]):
493 res_id = res['id']
494 client = res["client"]
495 with handle_cleanup_exceptions():
496 if res["type"] is "share":
497 client.delete_share(res_id)
498 client.wait_for_resource_deletion(share_id=res_id)
499 elif res["type"] is "snapshot":
500 client.delete_snapshot(res_id)
501 client.wait_for_resource_deletion(snapshot_id=res_id)
502 elif res["type"] is "share_network":
503 client.delete_share_network(res_id)
504 client.wait_for_resource_deletion(sn_id=res_id)
505 elif res["type"] is "security_service":
506 client.delete_security_service(res_id)
507 client.wait_for_resource_deletion(ss_id=res_id)
508 elif res["type"] is "share_type":
509 client.delete_share_type(res_id)
510 client.wait_for_resource_deletion(st_id=res_id)
511 else:
512 LOG.warn("Provided unsupported resource type for "
513 "cleanup '%s'. Skipping." % res["type"])
514 res["deleted"] = True
515
516 @classmethod
517 def generate_share_network_data(self):
518 data = {
519 "name": data_utils.rand_name("sn-name"),
520 "description": data_utils.rand_name("sn-desc"),
521 "neutron_net_id": data_utils.rand_name("net-id"),
522 "neutron_subnet_id": data_utils.rand_name("subnet-id"),
523 }
524 return data
525
526 @classmethod
527 def generate_security_service_data(self):
528 data = {
529 "name": data_utils.rand_name("ss-name"),
530 "description": data_utils.rand_name("ss-desc"),
531 "dns_ip": data_utils.rand_name("ss-dns_ip"),
532 "server": data_utils.rand_name("ss-server"),
533 "domain": data_utils.rand_name("ss-domain"),
534 "user": data_utils.rand_name("ss-user"),
535 "password": data_utils.rand_name("ss-password"),
536 }
537 return data
538
539 # Useful assertions
540 def assertDictMatch(self, d1, d2, approx_equal=False, tolerance=0.001):
541 """Assert two dicts are equivalent.
542
543 This is a 'deep' match in the sense that it handles nested
544 dictionaries appropriately.
545
546 NOTE:
547
548 If you don't care (or don't know) a given value, you can specify
549 the string DONTCARE as the value. This will cause that dict-item
550 to be skipped.
551
552 """
553 def raise_assertion(msg):
554 d1str = str(d1)
555 d2str = str(d2)
556 base_msg = ('Dictionaries do not match. %(msg)s d1: %(d1str)s '
557 'd2: %(d2str)s' %
558 {"msg": msg, "d1str": d1str, "d2str": d2str})
559 raise AssertionError(base_msg)
560
561 d1keys = set(d1.keys())
562 d2keys = set(d2.keys())
563 if d1keys != d2keys:
564 d1only = d1keys - d2keys
565 d2only = d2keys - d1keys
566 raise_assertion('Keys in d1 and not d2: %(d1only)s. '
567 'Keys in d2 and not d1: %(d2only)s' %
568 {"d1only": d1only, "d2only": d2only})
569
570 for key in d1keys:
571 d1value = d1[key]
572 d2value = d2[key]
573 try:
574 error = abs(float(d1value) - float(d2value))
575 within_tolerance = error <= tolerance
576 except (ValueError, TypeError):
577 # If both values aren't convertable to float, just ignore
578 # ValueError if arg is a str, TypeError if it's something else
579 # (like None)
580 within_tolerance = False
581
582 if hasattr(d1value, 'keys') and hasattr(d2value, 'keys'):
583 self.assertDictMatch(d1value, d2value)
584 elif 'DONTCARE' in (d1value, d2value):
585 continue
586 elif approx_equal and within_tolerance:
587 continue
588 elif d1value != d2value:
589 raise_assertion("d1['%(key)s']=%(d1value)s != "
590 "d2['%(key)s']=%(d2value)s" %
591 {
592 "key": key,
593 "d1value": d1value,
594 "d2value": d2value
595 })
596
597
598class BaseSharesAltTest(BaseSharesTest):
599 """Base test case class for all Shares Alt API tests."""
600
601 @classmethod
602 def resource_setup(cls):
603 cls.username = CONF.identity.alt_username
604 cls.password = CONF.identity.alt_password
605 cls.tenant_name = CONF.identity.alt_tenant_name
606 cls.verify_nonempty(cls.username, cls.password, cls.tenant_name)
607 cls.os = clients.AltManager()
608 alt_share_network_id = CONF.share.alt_share_network_id
609 cls.os.shares_client.share_network_id = alt_share_network_id
610 super(BaseSharesAltTest, cls).resource_setup()
611
612
613class BaseSharesAdminTest(BaseSharesTest):
614 """Base test case class for all Shares Admin API tests."""
615
616 @classmethod
617 def resource_setup(cls):
618 cls.username = CONF.identity.admin_username
619 cls.password = CONF.identity.admin_password
620 cls.tenant_name = CONF.identity.admin_tenant_name
621 cls.verify_nonempty(cls.username, cls.password, cls.tenant_name)
622 cls.os = clients.AdminManager()
623 admin_share_network_id = CONF.share.admin_share_network_id
624 cls.os.shares_client.share_network_id = admin_share_network_id
625 super(BaseSharesAdminTest, cls).resource_setup()