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