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
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
28
manilaclient/tests/functional/exceptions.py
Normal file
28
manilaclient/tests/functional/exceptions.py
Normal file
@@ -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"
|
@@ -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))
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user