python-manilaclient/manilaclient/tests/functional/client.py

700 lines
27 KiB
Python

# Copyright 2014 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.
import re
import time
from oslo_utils import strutils
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 import config
from manilaclient.tests.functional import exceptions
from manilaclient.tests.functional import utils
CONF = config.CONF
SHARE = 'share'
SHARE_TYPE = 'share_type'
SHARE_NETWORK = 'share_network'
def not_found_wrapper(f):
def wrapped_func(self, *args, **kwargs):
try:
return f(self, *args, **kwargs)
except tempest_lib_exc.CommandFailed as e:
if re.search('No (\w+) with a name or ID', e.stderr):
# Raise appropriate 'NotFound' error
raise tempest_lib_exc.NotFound()
raise
return wrapped_func
def forbidden_wrapper(f):
def wrapped_func(self, *args, **kwargs):
try:
return f(self, *args, **kwargs)
except tempest_lib_exc.CommandFailed as e:
if re.search('HTTP 403', e.stderr):
# Raise appropriate 'Forbidden' error.
raise tempest_lib_exc.Forbidden()
raise
return wrapped_func
class ManilaCLIClient(base.CLIClient):
def __init__(self, *args, **kwargs):
super(ManilaCLIClient, self).__init__(*args, **kwargs)
if CONF.enable_protocols:
self.share_protocol = CONF.enable_protocols[0]
else:
msg = "Configuration option 'enable_protocols' is not defined."
raise exceptions.InvalidConfiguration(reason=msg)
self.build_interval = CONF.build_interval
self.build_timeout = CONF.build_timeout
def manila(self, action, flags='', params='', fail_ok=False,
endpoint_type='publicURL', merge_stderr=False):
"""Executes manila command for the given action.
:param action: the cli command to run using manila
:type action: string
:param flags: any optional cli flags to use
:type flags: string
:param params: any optional positional args to use
:type params: string
:param fail_ok: if True an exception is not raised when the
cli return code is non-zero
:type fail_ok: boolean
:param endpoint_type: the type of endpoint for the service
:type endpoint_type: string
:param merge_stderr: if True the stderr buffer is merged into stdout
:type merge_stderr: boolean
"""
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
elif res_type == SHARE_NETWORK:
func = self.is_share_network_deleted
elif res_type == SHARE:
func = self.is_share_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)
# Share types
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
@not_found_wrapper
def delete_share_type(self, share_type):
"""Deletes share type by its Name or ID."""
return self.manila('type-delete %s' % share_type)
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 get_share_type(self, share_type):
"""Get share type.
:param share_type: str -- Name or ID of share type
"""
share_types = self.list_share_types(True)
for st in share_types:
if share_type in (st['ID'], st['Name']):
return st
raise tempest_lib_exc.NotFound()
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)
def get_project_id(self, name_or_id):
project_id = self.openstack(
'project show -f value -c id %s' % name_or_id)
return project_id.strip()
@not_found_wrapper
def add_share_type_access(self, share_type_name_or_id, project_id):
data = dict(st=share_type_name_or_id, project=project_id)
self.manila('type-access-add %(st)s %(project)s' % data)
@not_found_wrapper
def remove_share_type_access(self, share_type_name_or_id, project_id):
data = dict(st=share_type_name_or_id, project=project_id)
self.manila('type-access-remove %(st)s %(project)s' % data)
@not_found_wrapper
def list_share_type_access(self, share_type_id):
projects_raw = self.manila('type-access-list %s' % share_type_id)
projects = output_parser.listing(projects_raw)
project_ids = [pr['Project_ID'] for pr in projects]
return project_ids
@not_found_wrapper
def set_share_type_extra_specs(self, share_type_name_or_id, extra_specs):
"""Set key-value pair for share type."""
if not (isinstance(extra_specs, dict) and extra_specs):
raise exceptions.InvalidData(
message='Provided invalid extra specs - %s' % extra_specs)
cmd = 'type-key %s set ' % share_type_name_or_id
for key, value in extra_specs.items():
cmd += '%(key)s=%(value)s ' % {'key': key, 'value': value}
return self.manila(cmd)
@not_found_wrapper
def unset_share_type_extra_specs(self, share_type_name_or_id,
extra_specs_keys):
"""Unset key-value pair for share type."""
if not (isinstance(extra_specs_keys, list) and extra_specs_keys):
raise exceptions.InvalidData(
message='Provided invalid extra specs - %s' % extra_specs_keys)
cmd = 'type-key %s unset ' % share_type_name_or_id
for key in extra_specs_keys:
cmd += '%s ' % key
return self.manila(cmd)
def list_all_share_type_extra_specs(self):
"""List extra specs for all share types."""
extra_specs_raw = self.manila('extra-specs-list')
extra_specs = utils.listing(extra_specs_raw)
return extra_specs
def list_share_type_extra_specs(self, share_type_name_or_id):
"""List extra specs for specific share type by its Name or ID."""
all_share_types = self.list_all_share_type_extra_specs()
for share_type in all_share_types:
if share_type_name_or_id in (share_type['ID'], share_type['Name']):
return share_type['all_extra_specs']
raise exceptions.ShareTypeNotFound(share_type=share_type_name_or_id)
# Share networks
def create_share_network(self, name=None, description=None,
nova_net_id=None, neutron_net_id=None,
neutron_subnet_id=None):
"""Creates share network.
:param name: text -- desired name of new share network
:param description: text -- desired description of new share network
:param nova_net_id: text -- ID of Nova network
:param neutron_net_id: text -- ID of Neutron network
:param neutron_subnet_id: text -- ID of Neutron subnet
NOTE: 'nova_net_id' and 'neutron_net_id'/'neutron_subnet_id' are
mutually exclusive.
"""
params = self._combine_share_network_data(
name=name,
description=description,
nova_net_id=nova_net_id,
neutron_net_id=neutron_net_id,
neutron_subnet_id=neutron_subnet_id)
share_network_raw = self.manila('share-network-create %s' % params)
share_network = output_parser.details(share_network_raw)
return share_network
def _combine_share_network_data(self, name=None, description=None,
nova_net_id=None, neutron_net_id=None,
neutron_subnet_id=None):
"""Combines params for share network operations 'create' and 'update'.
:returns: text -- set of CLI parameters
"""
data = dict()
if name is not None:
data['--name'] = name
if description is not None:
data['--description'] = description
if nova_net_id is not None:
data['--nova_net_id'] = nova_net_id
if neutron_net_id is not None:
data['--neutron_net_id'] = neutron_net_id
if neutron_subnet_id is not None:
data['--neutron_subnet_id'] = neutron_subnet_id
cmd = ''
for key, value in data.items():
cmd += "%(k)s=%(v)s " % dict(k=key, v=value)
return cmd
@not_found_wrapper
def get_share_network(self, share_network):
"""Returns share network by its Name or ID."""
share_network_raw = self.manila(
'share-network-show %s' % share_network)
share_network = output_parser.details(share_network_raw)
return share_network
@not_found_wrapper
def update_share_network(self, share_network, name=None, description=None,
nova_net_id=None, neutron_net_id=None,
neutron_subnet_id=None):
"""Updates share-network by its name or ID.
:param name: text -- new name for share network
:param description: text -- new description for share network
:param nova_net_id: text -- ID of some Nova network
:param neutron_net_id: text -- ID of some Neutron network
:param neutron_subnet_id: text -- ID of some Neutron subnet
NOTE: 'nova_net_id' and 'neutron_net_id'/'neutron_subnet_id' are
mutually exclusive.
"""
sn_params = self._combine_share_network_data(
name=name,
description=description,
nova_net_id=nova_net_id,
neutron_net_id=neutron_net_id,
neutron_subnet_id=neutron_subnet_id)
share_network_raw = self.manila(
'share-network-update %(sn)s %(params)s' % dict(
sn=share_network, params=sn_params))
share_network = output_parser.details(share_network_raw)
return share_network
@not_found_wrapper
def delete_share_network(self, share_network):
"""Deletes share network by its Name or ID."""
return self.manila('share-network-delete %s' % share_network)
@staticmethod
def _stranslate_to_cli_optional_param(param):
if len(param) < 1 or not isinstance(param, six.string_types):
raise exceptions.InvalidData(
'Provided wrong parameter for translation.')
while not param[0:2] == '--':
param = '-' + param
return param.replace('_', '-')
def list_share_networks(self, all_tenants=False, filters=None):
"""List share networks.
:param all_tenants: bool -- whether to list share-networks that belong
only to current project or for all projects.
:param filters: dict -- filters for listing of share networks.
Example, input:
{'project_id': 'foo'}
{'-project_id': 'foo'}
{'--project_id': 'foo'}
{'project-id': 'foo'}
will be transformed to filter parameter "--project-id=foo"
"""
cmd = 'share-network-list '
if all_tenants:
cmd += '--all-tenants '
if filters and isinstance(filters, dict):
for k, v in filters.items():
cmd += '%(k)s=%(v)s ' % {
'k': self._stranslate_to_cli_optional_param(k), 'v': v}
share_networks_raw = self.manila(cmd)
share_networks = utils.listing(share_networks_raw)
return share_networks
def is_share_network_deleted(self, share_network):
"""Says whether share network is deleted or not.
:param share_network: text -- Name or ID of share network
"""
share_types = self.list_share_networks(True)
for list_element in share_types:
if share_network in (list_element['id'], list_element['name']):
return False
return True
def wait_for_share_network_deletion(self, share_network):
"""Wait for share network deletion by its Name or ID.
:param share_network: text -- Name or ID of share network
"""
self.wait_for_resource_deletion(
SHARE_NETWORK, res_id=share_network, interval=2, timeout=6)
# Shares
def create_share(self, share_protocol, size, share_network=None,
share_type=None, name=None, description=None,
public=False, snapshot=None, metadata=None):
"""Creates a share.
:param share_protocol: str -- share protocol of a share.
:param size: int/str -- desired size of a share.
:param share_network: str -- Name or ID of share network to use.
:param share_type: str -- Name or ID of share type to use.
:param name: str -- desired name of new share.
:param description: str -- desired description of new share.
:param public: bool -- should a share be public or not.
Default is False.
:param snapshot: str -- Name or ID of a snapshot to use as source.
:param metadata: dict -- key-value data to provide with share creation.
"""
cmd = 'create %(share_protocol)s %(size)s ' % {
'share_protocol': share_protocol, 'size': size}
if share_network is not None:
cmd += '--share-network %s ' % share_network
if share_type is not None:
cmd += '--share-type %s ' % share_type
if name is None:
name = data_utils.rand_name('autotest_share_name')
cmd += '--name %s ' % name
if description is None:
description = data_utils.rand_name('autotest_share_description')
cmd += '--description %s ' % description
if public:
cmd += '--public'
if snapshot is not None:
cmd += '--snapshot %s ' % snapshot
if metadata:
metadata_cli = ''
for k, v in metadata.items():
metadata_cli += '%(k)s=%(v)s ' % {'k': k, 'v': v}
if metadata_cli:
cmd += '--metadata %s ' % metadata_cli
share_raw = self.manila(cmd)
share = output_parser.details(share_raw)
return share
@not_found_wrapper
def get_share(self, share):
"""Returns a share by its Name or ID."""
share_raw = self.manila('show %s' % share)
share = output_parser.details(share_raw)
return share
@not_found_wrapper
def update_share(self, share, name=None, description=None,
is_public=False):
"""Updates a share.
:param share: str -- name or ID of a share that should be updated.
:param name: str -- desired name of new share.
:param description: str -- desired description of new share.
:param is_public: bool -- should a share be public or not.
Default is False.
"""
cmd = 'update %s ' % share
if name:
cmd += '--name %s ' % name
if description:
cmd += '--description %s ' % description
is_public = strutils.bool_from_string(is_public, strict=True)
cmd += '--is-public %s ' % is_public
return self.manila(cmd)
@not_found_wrapper
@forbidden_wrapper
def delete_share(self, shares):
"""Deletes share[s] by Names or IDs.
:param shares: either str or list of str that can be either Name
or ID of a share(s) that should be deleted.
"""
if not isinstance(shares, list):
shares = [shares]
cmd = 'delete '
for share in shares:
cmd += '%s ' % share
return self.manila(cmd)
def list_shares(self, all_tenants=False, filters=None):
"""List shares.
:param all_tenants: bool -- whether to list shares that belong
only to current project or for all projects.
:param filters: dict -- filters for listing of shares.
Example, input:
{'project_id': 'foo'}
{-'project_id': 'foo'}
{--'project_id': 'foo'}
{'project-id': 'foo'}
will be transformed to filter parameter "--project-id=foo"
"""
cmd = 'list '
if all_tenants:
cmd += '--all-tenants '
if filters and isinstance(filters, dict):
for k, v in filters.items():
cmd += '%(k)s=%(v)s ' % {
'k': self._stranslate_to_cli_optional_param(k), 'v': v}
shares_raw = self.manila(cmd)
shares = utils.listing(shares_raw)
return shares
def is_share_deleted(self, share):
"""Says whether share is deleted or not.
:param share: str -- Name or ID of share
"""
try:
self.get_share(share)
return False
except tempest_lib_exc.NotFound:
return True
def wait_for_share_deletion(self, share):
"""Wait for share deletion by its Name or ID.
:param share: str -- Name or ID of share
"""
self.wait_for_resource_deletion(
SHARE, res_id=share, interval=5, timeout=300)
def wait_for_share_status(self, share, status):
"""Waits for a share to reach a given status."""
body = self.get_share(share)
share_name = body['name']
share_status = body['status']
start = int(time.time())
while share_status != status:
time.sleep(self.build_interval)
body = self.get_share(share)
share_status = body['status']
if share_status == status:
return
elif 'error' in share_status.lower():
raise exceptions.ShareBuildErrorException(share=share)
if int(time.time()) - start >= self.build_timeout:
message = (
"Share %(share_name)s failed to reach %(status)s status "
"within the required time (%(build_timeout)s s)." % {
"share_name": share_name, "status": status,
"build_timeout": self.build_timeout})
raise tempest_lib_exc.TimeoutException(message)
@not_found_wrapper
def _set_share_metadata(self, share, data, update_all=False):
"""Sets a share metadata.
:param share: str -- Name or ID of a share.
:param data: dict -- key-value pairs to set as metadata.
:param update_all: bool -- if set True then all keys except provided
will be deleted.
"""
if not (isinstance(data, dict) and data):
msg = ('Provided invalid data for setting of share metadata - '
'%s' % data)
raise exceptions.InvalidData(message=msg)
if update_all:
cmd = 'metadata-update-all %s ' % share
else:
cmd = 'metadata %s set ' % share
for k, v in data.items():
cmd += '%(k)s=%(v)s ' % {'k': k, 'v': v}
return self.manila(cmd)
def update_all_share_metadata(self, share, data):
metadata_raw = self._set_share_metadata(share, data, True)
metadata = output_parser.details(metadata_raw)
return metadata
def set_share_metadata(self, share, data):
return self._set_share_metadata(share, data, False)
@not_found_wrapper
def unset_share_metadata(self, share, keys):
"""Unsets some share metadata by keys.
:param share: str -- Name or ID of a share
:param keys: str/list -- key or list of keys to unset.
"""
if not (isinstance(keys, list) and keys):
msg = ('Provided invalid data for unsetting of share metadata - '
'%s' % keys)
raise exceptions.InvalidData(message=msg)
cmd = 'metadata %s unset ' % share
for key in keys:
cmd += '%s ' % key
return self.manila(cmd)
@not_found_wrapper
def get_share_metadata(self, share):
"""Returns list of all share metadata.
:param share: str -- Name or ID of a share.
"""
metadata_raw = self.manila('metadata-show %s' % share)
metadata = output_parser.details(metadata_raw)
return metadata
@not_found_wrapper
def list_access(self, share_id):
access_list_raw = self.manila('access-list %s' % share_id)
return output_parser.listing(access_list_raw)
@not_found_wrapper
def get_access(self, share_id, access_id):
for access in self.list_access(share_id):
if access['id'] == access_id:
return access
raise tempest_lib_exc.NotFound()
@not_found_wrapper
def access_allow(self, share_id, access_type, access_to, access_level):
raw_access = self.manila(
'access-allow --access-level %(level)s %(id)s %(type)s '
'%(access_to)s' % {
'level': access_level,
'id': share_id,
'type': access_type,
'access_to': access_to,
})
return output_parser.details(raw_access)
@not_found_wrapper
def access_deny(self, share_id, access_id):
self.manila('access-deny %(share_id)s %(access_id)s' % {
'share_id': share_id,
'access_id': access_id,
})
def wait_for_access_rule_status(self, share_id, access_id, state='active'):
access = self.get_access(share_id, access_id)
start = int(time.time())
while access['state'] != state:
time.sleep(self.build_interval)
access = self.get_access(share_id, access_id)
if access['state'] == state:
return
elif access['state'] == 'error':
raise exceptions.AccessRuleCreateErrorException(
access=access_id)
if int(time.time()) - start >= self.build_timeout:
message = (
"Access rule %(access)s failed to reach %(state)s state "
"within the required time (%(build_timeout)s s)." % {
"access": access_id, "state": state,
"build_timeout": self.build_timeout})
raise tempest_lib_exc.TimeoutException(message)
def wait_for_access_rule_deletion(self, share_id, access_id):
try:
access = self.get_access(share_id, access_id)
except tempest_lib_exc.NotFound:
return
start = int(time.time())
while True:
time.sleep(self.build_interval)
try:
access = self.get_access(share_id, access_id)
except tempest_lib_exc.NotFound:
return
if access['state'] == 'error':
raise exceptions.AccessRuleDeleteErrorException(
access=access_id)
if int(time.time()) - start >= self.build_timeout:
message = (
"Access rule %(access)s failed to reach deleted state "
"within the required time (%s s)." % self.build_timeout)
raise tempest_lib_exc.TimeoutException(message)