From 4243d8731b05e4db4759a6f9e24eaf6a1c54654a Mon Sep 17 00:00:00 2001 From: Valeriy Ponomaryov Date: Tue, 21 Apr 2015 16:42:05 +0300 Subject: [PATCH] Add rw functional tests for public share types Add base client methods for share types and use them in rw functional tests. This commit covers case of 'public' share types. Partially implements bp rw-functional-tests Change-Id: Ia35dbe7f42ada319853642b893bc5c2fa2db4175 --- contrib/ci/post_test_hook.sh | 4 + manilaclient/config.py | 11 +- manilaclient/tests/functional/base.py | 144 +++++++++++++++--- manilaclient/tests/functional/client.py | 115 ++++++++++++++ manilaclient/tests/functional/exceptions.py | 28 ++++ .../tests/functional/test_share_types.py | 52 ++++++- requirements.txt | 1 + 7 files changed, 328 insertions(+), 27 deletions(-) create mode 100644 manilaclient/tests/functional/exceptions.py diff --git a/contrib/ci/post_test_hook.sh b/contrib/ci/post_test_hook.sh index bf41310e5..95307f594 100644 --- a/contrib/ci/post_test_hook.sh +++ b/contrib/ci/post_test_hook.sh @@ -42,6 +42,10 @@ iniset $MANILACLIENT_CONF DEFAULT admin_tenant_name $OS_TENANT_NAME iniset $MANILACLIENT_CONF DEFAULT admin_password $OS_PASSWORD iniset $MANILACLIENT_CONF DEFAULT admin_auth_url $OS_AUTH_URL +# Suppress errors in cleanup of resources +SUPPRESS_ERRORS=${SUPPRESS_ERRORS_IN_CLEANUP:-True} +iniset $MANILACLIENT_CONF DEFAULT suppress_errors_in_cleanup $SUPPRESS_ERRORS + # let us control if we die or not set +o errexit diff --git a/manilaclient/config.py b/manilaclient/config.py index c1cf13fc7..41c4a402b 100644 --- a/manilaclient/config.py +++ b/manilaclient/config.py @@ -17,6 +17,7 @@ import copy import os from oslo.config import cfg +import oslo_log._options as log_options # 1. Define opts @@ -59,6 +60,10 @@ base_opts = [ 'OS_MANILA_EXEC_DIR', os.path.join(os.path.abspath('.'), '.tox/functional/bin')), help="The path to manilaclient to be executed."), + cfg.BoolOpt("suppress_errors_in_cleanup", + default=True, + help="Whether to suppress errors with clean up operation " + "or not."), ] # 2. Generate config @@ -99,8 +104,10 @@ CONF.register_opts(base_opts) def list_opts(): - """Return a list of oslo.config options available in Manilaclient.""" - return [ + """Return a list of oslo_config options available in Manilaclient.""" + opts = [ (None, copy.deepcopy(auth_opts)), (None, copy.deepcopy(base_opts)), ] + opts.extend(log_options.list_opts()) + return opts diff --git a/manilaclient/tests/functional/base.py b/manilaclient/tests/functional/base.py index d0fa25107..3658e58a1 100644 --- a/manilaclient/tests/functional/base.py +++ b/manilaclient/tests/functional/base.py @@ -13,36 +13,138 @@ # License for the specific language governing permissions and limitations # under the License. -import os +import traceback +from oslo_log import log from tempest_lib.cli import base +from tempest_lib import exceptions as lib_exc from manilaclient import config from manilaclient.tests.functional import client CONF = config.CONF +LOG = log.getLogger(__name__) + + +class handle_cleanup_exceptions(object): + """Handle exceptions raised with cleanup operations. + + Always suppress errors when lib_exc.NotFound or lib_exc.Forbidden + are raised. + Suppress all other exceptions only in case config opt + 'suppress_errors_in_cleanup' is True. + """ + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + if not (isinstance(exc_value, + (lib_exc.NotFound, lib_exc.Forbidden)) or + CONF.suppress_errors_in_cleanup): + return False # Do not suppress error if any + if exc_traceback: + LOG.error("Suppressed cleanup error: " + "\n%s" % traceback.format_exc()) + return True # Suppress error if any class BaseTestCase(base.ClientTestBase): - def _get_clients(self): - cli_dir = os.environ.get( - 'OS_MANILA_EXEC_DIR', - os.path.join(os.path.abspath('.'), '.tox/functional/bin')) - clients = { - 'admin': client.ManilaCLIClient( - username=CONF.admin_username, - password=CONF.admin_password, - tenant_name=CONF.admin_tenant_name, - uri=CONF.admin_auth_url or CONF.auth_url, - cli_dir=cli_dir, - ), - 'user': client.ManilaCLIClient( - username=CONF.username, - password=CONF.password, - tenant_name=CONF.tenant_name, - uri=CONF.auth_url, - cli_dir=cli_dir, - ), + # Will be cleaned up after test suite run + class_resources = [] + + # Will be cleaned up after single test run + method_resources = [] + + def setUp(self): + super(BaseTestCase, self).setUp() + self.addCleanup(self.clear_resources) + + @classmethod + def tearDownClass(cls): + super(BaseTestCase, cls).tearDownClass() + cls.clear_resources(cls.class_resources) + + @classmethod + def clear_resources(cls, resources=None): + """Deletes resources, that were created in test suites. + + This method tries to remove resources from resource list, + if it is not found, assume it was deleted in test itself. + It is expected, that all resources were added as LIFO + due to restriction of deletion resources, that are in the chain. + :param resources: dict with keys 'type','id','client' and 'deleted' + """ + + if resources is None: + resources = cls.method_resources + for res in resources: + if "deleted" not in res: + res["deleted"] = False + if "client" not in res: + res["client"] = cls.get_cleanup_client() + if not(res["deleted"]): + res_id = res['id'] + client = res["client"] + with handle_cleanup_exceptions(): + # TODO(vponomaryov): add support for other resources + if res["type"] is "share_type": + client.delete_share_type(res_id) + client.wait_for_share_type_deletion(res_id) + else: + LOG.warn("Provided unsupported resource type for " + "cleanup '%s'. Skipping." % res["type"]) + res["deleted"] = True + + @classmethod + def get_admin_client(cls): + return client.ManilaCLIClient( + username=CONF.admin_username, + password=CONF.admin_password, + tenant_name=CONF.admin_tenant_name, + uri=CONF.admin_auth_url or CONF.auth_url, + cli_dir=CONF.manila_exec_dir) + + @classmethod + def get_user_client(cls): + return client.ManilaCLIClient( + username=CONF.username, + password=CONF.password, + tenant_name=CONF.tenant_name, + uri=CONF.auth_url, + cli_dir=CONF.manila_exec_dir) + + @property + def admin_client(self): + if not hasattr(self, '_admin_client'): + self._admin_client = self.get_admin_client() + return self._admin_client + + @property + def user_client(self): + if not hasattr(self, '_user_client'): + self._user_client = self.get_user_client() + return self._user_client + + def _get_clients(self): + return {'admin': self.admin_client, 'user': self.user_client} + + def create_share_type(self, name=None, driver_handles_share_servers=True, + is_public=True, client=None, cleanup_in_class=True): + if client is None: + client = self.admin_client + share_type = client.create_share_type( + name=name, + driver_handles_share_servers=driver_handles_share_servers, + is_public=is_public) + resource = { + "type": "share_type", + "id": share_type["ID"], + "client": client, } - return clients + if cleanup_in_class: + self.class_resources.insert(0, resource) + else: + self.method_resources.insert(0, resource) + return share_type diff --git a/manilaclient/tests/functional/client.py b/manilaclient/tests/functional/client.py index e029e4243..2718f6b64 100644 --- a/manilaclient/tests/functional/client.py +++ b/manilaclient/tests/functional/client.py @@ -13,7 +13,17 @@ # License for the specific language governing permissions and limitations # under the License. +import time + +import six from tempest_lib.cli import base +from tempest_lib.cli import output_parser +from tempest_lib.common.utils import data_utils +from tempest_lib import exceptions as tempest_lib_exc + +from manilaclient.tests.functional import exceptions + +SHARE_TYPE = 'share_type' class ManilaCLIClient(base.CLIClient): @@ -39,3 +49,108 @@ class ManilaCLIClient(base.CLIClient): flags += ' --endpoint-type %s' % endpoint_type return self.cmd_with_auth( 'manila', action, flags, params, fail_ok, merge_stderr) + + def wait_for_resource_deletion(self, res_type, res_id, interval=3, + timeout=180): + """Resource deletion waiter. + + :param res_type: text -- type of resource. Supported only 'share_type'. + Other types support is TODO. + :param res_id: text -- ID of resource to use for deletion check + :param interval: int -- interval between requests in seconds + :param timeout: int -- total time in seconds to wait for deletion + """ + # TODO(vponomaryov): add support for other resource types + if res_type == SHARE_TYPE: + func = self.is_share_type_deleted + else: + raise exceptions.InvalidResource(message=res_type) + + end_loop_time = time.time() + timeout + deleted = func(res_id) + + while not (deleted or time.time() > end_loop_time): + time.sleep(interval) + deleted = func(res_id) + + if not deleted: + raise exceptions.ResourceReleaseFailed( + res_type=res_type, res_id=res_id) + + def create_share_type(self, name=None, driver_handles_share_servers=True, + is_public=True): + """Creates share type. + + :param name: text -- name of share type to use, if not set then + autogenerated will be used + :param driver_handles_share_servers: bool/str -- boolean or its + string alias. Default is True. + :param is_public: bool/str -- boolean or its string alias. Default is + True. + """ + if name is None: + name = data_utils.rand_name('manilaclient_functional_test') + dhss = driver_handles_share_servers + if not isinstance(dhss, six.string_types): + dhss = six.text_type(dhss) + if not isinstance(is_public, six.string_types): + is_public = six.text_type(is_public) + cmd = 'type-create %(name)s %(dhss)s --is-public %(is_public)s' % { + 'name': name, 'dhss': dhss, 'is_public': is_public} + share_type_raw = self.manila(cmd) + + # NOTE(vponomaryov): share type creation response is "list"-like with + # only one element: + # [{ + # 'ID': '%id%', + # 'Name': '%name%', + # 'Visibility': 'public', + # 'is_default': '-', + # 'required_extra_specs': 'driver_handles_share_servers : False', + # }] + share_type = output_parser.listing(share_type_raw)[0] + return share_type + + def delete_share_type(self, share_type): + """Deletes share type by its Name or ID.""" + try: + return self.manila('type-delete %s' % share_type) + except tempest_lib_exc.CommandFailed as e: + not_found_msg = 'No sharetype with a name or ID' + if not_found_msg in e.stderr: + # Assuming it was deleted in tests + raise tempest_lib_exc.NotFound() + raise + + def list_share_types(self, list_all=True): + """List share types. + + :param list_all: bool -- whether to list all share types or only public + """ + cmd = 'type-list' + if list_all: + cmd += ' --all' + share_types_raw = self.manila(cmd) + share_types = output_parser.listing(share_types_raw) + return share_types + + def is_share_type_deleted(self, share_type): + """Says whether share type is deleted or not. + + :param share_type: text -- Name or ID of share type + """ + # NOTE(vponomaryov): we use 'list' operation because there is no + # 'get/show' operation for share-types available for CLI + share_types = self.list_share_types(list_all=True) + for list_element in share_types: + if share_type in (list_element['ID'], list_element['Name']): + return False + return True + + def wait_for_share_type_deletion(self, share_type): + """Wait for share type deletion by its Name or ID. + + :param share_type: text -- Name or ID of share type + """ + self.wait_for_resource_deletion( + SHARE_TYPE, res_id=share_type, interval=2, timeout=6) diff --git a/manilaclient/tests/functional/exceptions.py b/manilaclient/tests/functional/exceptions.py new file mode 100644 index 000000000..b9957e563 --- /dev/null +++ b/manilaclient/tests/functional/exceptions.py @@ -0,0 +1,28 @@ +# Copyright 2015 Mirantis Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest_lib import exceptions + +""" +Exceptions for functional tests. +""" + + +class ResourceReleaseFailed(exceptions.TempestException): + message = "Failed to release resource '%(res_type)s' with id '%(res_id)s'." + + +class InvalidResource(exceptions.TempestException): + message = "Provided invalid resource: %(message)s" diff --git a/manilaclient/tests/functional/test_share_types.py b/manilaclient/tests/functional/test_share_types.py index 4bf446415..f3b67086b 100644 --- a/manilaclient/tests/functional/test_share_types.py +++ b/manilaclient/tests/functional/test_share_types.py @@ -14,17 +14,61 @@ # under the License. import ddt +from oslo_utils import strutils +import six +from tempest_lib.common.utils import data_utils from manilaclient.tests.functional import base @ddt.ddt -class ManilaClientTestShareTypesReadOnly(base.BaseTestCase): +class ShareTypesReadOnlyTest(base.BaseTestCase): @ddt.data('admin', 'user') def test_share_type_list(self, role): self.clients[role].manila('type-list') - @ddt.data('admin') - def test_extra_specs_list(self, role): - self.clients[role].manila('extra-specs-list') + def test_extra_specs_list(self): + self.admin_client.manila('extra-specs-list') + + +@ddt.ddt +class ShareTypesReadWriteTest(base.BaseTestCase): + + @ddt.data('false', 'False', '0', 'True', 'true', '1') + def test_create_delete_public_share_type(self, dhss): + share_type_name = data_utils.rand_name('manilaclient_functional_test') + dhss_expected = 'driver_handles_share_servers : %s' % six.text_type( + strutils.bool_from_string(dhss)) + + # Create share type + share_type = self.create_share_type( + name=share_type_name, + driver_handles_share_servers=dhss, + is_public=True) + + # Verify response body + keys = ( + 'ID', 'Name', 'Visibility', 'is_default', 'required_extra_specs') + for key in keys: + self.assertIn(key, share_type) + self.assertEqual(share_type_name, share_type['Name']) + self.assertEqual(dhss_expected, share_type['required_extra_specs']) + self.assertEqual('public', share_type['Visibility'].lower()) + self.assertEqual('-', share_type['is_default']) + + # Verify that it is listed with common 'type-list' operation. + share_types = self.admin_client.list_share_types(list_all=False) + self.assertTrue( + any(share_type['ID'] == st['ID'] for st in share_types)) + + # Delete share type + self.admin_client.delete_share_type(share_type['ID']) + + # Wait for share type deletion + self.admin_client.wait_for_share_type_deletion(share_type['ID']) + + # Verify that it is not listed with common 'type-list' operation. + share_types = self.admin_client.list_share_types(list_all=False) + self.assertFalse( + any(share_type['ID'] == st['ID'] for st in share_types)) diff --git a/requirements.txt b/requirements.txt index 64a94ffbc..3057e9eac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 oslo.config>=1.9.3 # Apache-2.0 +oslo.log>=1.0.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 oslo.utils>=1.4.0 # Apache-2.0 PrettyTable>=0.7,<0.8