Add quotas per share type

Following CLI interface changes are implemented:

1) update share type quotas:
   $ manila quota-update ... --share-type %share_type_name_or_id% ...

2) get share type quotas:
   $ manila quota-show ... --share-type %share_type_name_or_id% ...

3) delete/reset share type quotas:
   $ manila quota-delete ... --share-type %share_type_name_or_id% ...

'quota-defaults' stays unchanged as it is the same for all kinds of
quotas - project, user or share type.

Implements BluePrint support-quotas-per-share-type

Change-Id: I0cd2bc41193e50b330456b14c5f66134a513cc32
This commit is contained in:
Valeriy Ponomaryov 2017-03-28 16:58:03 +03:00
parent 8bf5f9b7e0
commit 8045f4d24e
7 changed files with 373 additions and 49 deletions

View File

@ -27,7 +27,7 @@ from manilaclient import utils
LOG = logging.getLogger(__name__)
MAX_VERSION = '2.37'
MAX_VERSION = '2.39'
MIN_VERSION = '2.0'
DEPRECATED_VERSION = '1.0'
_VERSIONED_METHOD_MAP = {}

View File

@ -13,13 +13,130 @@
# License for the specific language governing permissions and limitations
# under the License.
from random import randint
import ddt
from tempest.lib.cli import output_parser
from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions
from manilaclient.tests.functional import base
from manilaclient.tests.functional import utils
def _get_share_type_quota_values(project_quota_value):
project_quota_value = int(project_quota_value)
if project_quota_value == -1:
return randint(1, 999)
elif project_quota_value == 0:
return 0
else:
return project_quota_value - 1
@ddt.ddt
@utils.skip_if_microversion_not_supported("2.39")
class QuotasReadWriteTest(base.BaseTestCase):
def setUp(self):
super(self.__class__, self).setUp()
self.microversion = "2.39"
self.project_id = self.admin_client.get_project_id(
self.admin_client.tenant_name)
# Create share type
self.share_type = self.create_share_type(
name=data_utils.rand_name("manilaclient_functional_test"),
driver_handles_share_servers=False,
is_public=True,
microversion=self.microversion,
cleanup_in_class=False,
)
self.st_id = self.share_type["ID"]
def _verify_current_st_quotas_equal_to(self, quotas):
# Read share type quotas
cmd = 'quota-show --tenant-id %s --share-type %s' % (
self.project_id, self.st_id)
st_quotas_raw = self.admin_client.manila(
cmd, microversion=self.microversion)
st_quotas = output_parser.details(st_quotas_raw)
# Verify that quotas
self.assertGreater(len(st_quotas), 3)
for key, value in st_quotas.items():
if key not in ('shares', 'gigabytes', 'snapshots',
'snapshot_gigabytes'):
continue
self.assertIn(key, quotas)
self.assertEqual(int(quotas[key]), int(value))
def test_update_share_type_quotas_positive(self):
# Get project quotas
cmd = 'quota-show --tenant-id %s ' % self.project_id
quotas_raw = self.admin_client.manila(
cmd, microversion=self.microversion)
p_quotas = output_parser.details(quotas_raw)
# Define share type quotas
st_custom_quotas = {
'shares': _get_share_type_quota_values(p_quotas['shares']),
'snapshots': _get_share_type_quota_values(p_quotas['snapshots']),
'gigabytes': _get_share_type_quota_values(p_quotas['gigabytes']),
'snapshot_gigabytes': _get_share_type_quota_values(
p_quotas['snapshot_gigabytes']),
}
# Update quotas for share type
cmd = ('quota-update %s --share-type %s '
'--shares %s --gigabytes %s --snapshots %s '
'--snapshot-gigabytes %s') % (
self.project_id, self.st_id,
st_custom_quotas['shares'],
st_custom_quotas['gigabytes'],
st_custom_quotas['snapshots'],
st_custom_quotas['snapshot_gigabytes'])
self.admin_client.manila(cmd, microversion=self.microversion)
# Verify share type quotas
self._verify_current_st_quotas_equal_to(st_custom_quotas)
# Reset share type quotas
cmd = 'quota-delete --tenant-id %s --share-type %s' % (
self.project_id, self.st_id)
self.admin_client.manila(cmd, microversion=self.microversion)
# Verify share type quotas after reset
self._verify_current_st_quotas_equal_to(p_quotas)
@utils.skip_if_microversion_not_supported("2.38")
def test_read_share_type_quotas_with_too_old_microversion(self):
cmd = 'quota-show --tenant-id %s --share-type %s' % (
self.project_id, self.st_id)
self.assertRaises(
exceptions.CommandFailed,
self.admin_client.manila,
cmd, microversion='2.38')
@utils.skip_if_microversion_not_supported("2.38")
def test_update_share_type_quotas_with_too_old_microversion(self):
cmd = 'quota-update --tenant-id %s --share-type %s --shares %s' % (
self.project_id, self.st_id, '0')
self.assertRaises(
exceptions.CommandFailed,
self.admin_client.manila,
cmd, microversion='2.38')
@utils.skip_if_microversion_not_supported("2.38")
def test_delete_share_type_quotas_with_too_old_microversion(self):
cmd = 'quota-delete --tenant-id %s --share-type %s' % (
self.project_id, self.st_id)
self.assertRaises(
exceptions.CommandFailed,
self.admin_client.manila,
cmd, microversion='2.38')
@ddt.ddt
class ManilaClientTestQuotasReadOnly(base.BaseTestCase):

View File

@ -35,7 +35,7 @@ class QuotaSetsTest(utils.TestCase):
return quotas.RESOURCE_PATH
return quotas.RESOURCE_PATH_LEGACY
@ddt.data("2.6", "2.7", "2.25")
@ddt.data("2.6", "2.7", "2.25", "2.38", "2.39")
def test_tenant_quotas_get(self, microversion):
tenant_id = 'test'
manager = self._get_manager(microversion)
@ -53,7 +53,7 @@ class QuotaSetsTest(utils.TestCase):
manager._get.assert_called_once_with(expected_url, "quota_set")
@ddt.data("2.6", "2.7", "2.25")
@ddt.data("2.6", "2.7", "2.25", "2.38", "2.39")
def test_user_quotas_get(self, microversion):
tenant_id = 'test'
user_id = 'fake_user'
@ -73,7 +73,21 @@ class QuotaSetsTest(utils.TestCase):
manager._get.assert_called_once_with(expected_url, "quota_set")
@ddt.data("2.6", "2.7")
def test_share_type_quotas_get(self):
tenant_id = 'fake_tenant_id'
share_type = 'fake_share_type'
manager = self._get_manager('2.39')
resource_path = self._get_resource_path('2.39')
expected_url = ("%s/%s/detail?share_type=%s"
% (resource_path, tenant_id, share_type))
with mock.patch.object(manager, '_get',
mock.Mock(return_value='fake_get')):
manager.get(tenant_id, share_type=share_type, detail=True)
manager._get.assert_called_once_with(expected_url, "quota_set")
@ddt.data("2.6", "2.7", "2.38", "2.39")
def test_tenant_quotas_defaults(self, microversion):
tenant_id = 'test'
manager = self._get_manager(microversion)
@ -86,10 +100,10 @@ class QuotaSetsTest(utils.TestCase):
manager._get.assert_called_once_with(expected_url, "quota_set")
@ddt.data(
("2.6", {}),
("2.6", {"force": True}),
("2.7", {}),
("2.7", {"force": True}),
("2.6", {}), ("2.6", {"force": True}),
("2.7", {}), ("2.7", {"force": True}),
("2.38", {}), ("2.38", {"force": True}),
("2.39", {}), ("2.39", {"force": True}),
)
@ddt.unpack
def test_update_quota(self, microversion, extra_data):
@ -117,7 +131,7 @@ class QuotaSetsTest(utils.TestCase):
manager._update.assert_called_once_with(
expected_url, expected_body, "quota_set")
@ddt.data("2.6", "2.7")
@ddt.data("2.6", "2.7", "2.38", "2.39")
def test_update_user_quota(self, microversion):
tenant_id = 'test'
user_id = 'fake_user'
@ -143,7 +157,46 @@ class QuotaSetsTest(utils.TestCase):
manager._update.assert_called_once_with(
expected_url, expected_body, "quota_set")
@ddt.data("2.6", "2.7")
def test_update_share_type_quota(self):
tenant_id = 'fake_tenant_id'
share_type = 'fake_share_type'
manager = self._get_manager('2.39')
resource_path = self._get_resource_path('2.39')
expected_url = "%s/%s?share_type=%s" % (
resource_path, tenant_id, share_type)
expected_body = {
'quota_set': {
'tenant_id': tenant_id,
'shares': 1,
'snapshots': 2,
'gigabytes': 3,
'snapshot_gigabytes': 4,
},
}
with mock.patch.object(manager, '_update',
mock.Mock(return_value='fake_update')):
manager.update(
tenant_id, shares=1, snapshots=2, gigabytes=3,
snapshot_gigabytes=4, share_type=share_type)
manager._update.assert_called_once_with(
expected_url, expected_body, "quota_set")
def test_update_share_type_quotas_for_share_networks(self):
manager = self._get_manager("2.39")
with mock.patch.object(manager, '_update',
mock.Mock(return_value='fake_delete')):
self.assertRaises(
ValueError,
manager.update,
'fake_tenant_id', share_type='fake_share_type',
share_networks=13,
)
manager._update.assert_not_called()
@ddt.data("2.6", "2.7", "2.38", "2.39")
def test_quotas_delete(self, microversion):
tenant_id = 'test'
manager = self._get_manager(microversion)
@ -155,7 +208,7 @@ class QuotaSetsTest(utils.TestCase):
manager._delete.assert_called_once_with(expected_url)
@ddt.data("2.6", "2.7")
@ddt.data("2.6", "2.7", "2.38", "2.39")
def test_user_quotas_delete(self, microversion):
tenant_id = 'test'
user_id = 'fake_user'
@ -167,3 +220,29 @@ class QuotaSetsTest(utils.TestCase):
manager.delete(tenant_id, user_id=user_id)
manager._delete.assert_called_once_with(expected_url)
def test_share_type_quotas_delete(self):
tenant_id = 'test'
share_type = 'fake_st'
manager = self._get_manager("2.39")
resource_path = self._get_resource_path("2.39")
expected_url = "%s/test?share_type=fake_st" % resource_path
with mock.patch.object(manager, '_delete',
mock.Mock(return_value='fake_delete')):
manager.delete(tenant_id, share_type=share_type)
manager._delete.assert_called_once_with(expected_url)
@ddt.data('get', 'update', 'delete')
def test_share_type_quotas_using_old_microversion(self, operation):
manager = self._get_manager("2.38")
with mock.patch.object(manager, '_%s' % operation,
mock.Mock(return_value='fake_delete')):
self.assertRaises(
TypeError,
getattr(manager, operation),
'fake_tenant_id', share_type='fake_share_type',
)
getattr(manager, '_%s' % operation).assert_not_called()

View File

@ -1792,6 +1792,18 @@ class ShellTest(test_utils.TestCase):
)
cliutils.print_dict.assert_called_once_with(mock.ANY)
@ddt.data('1111', '0')
@mock.patch('manilaclient.common.cliutils.print_dict')
def test_quota_show_with_share_type(self, share_type_id, mock_print_dict):
self.run_command(
'quota-show --tenant 1234 --share_type %s' % share_type_id)
self.assert_called(
'GET',
'/quota-sets/1234?share_type=%s' % share_type_id,
)
mock_print_dict.assert_called_once_with(mock.ANY)
@mock.patch.object(cliutils, 'print_list', mock.Mock())
def test_pool_list_with_detail(self):
self.run_command('pool-list --detail')

View File

@ -35,8 +35,15 @@ class QuotaSet(common_base.Resource):
class QuotaSetManager(base.ManagerWithFind):
resource_class = QuotaSet
def _do_get(self, tenant_id, user_id=None, detail=False,
def _check_user_id_and_share_type_args(self, user_id, share_type):
if user_id and share_type:
raise ValueError(
"'user_id' and 'share_type' values are mutually exclusive. "
"one or both should be unset.")
def _do_get(self, tenant_id, user_id=None, share_type=None, detail=False,
resource_path=RESOURCE_PATH):
self._check_user_id_and_share_type_args(user_id, share_type)
if hasattr(tenant_id, 'tenant_id'):
tenant_id = tenant_id.tenant_id
@ -45,8 +52,13 @@ class QuotaSetManager(base.ManagerWithFind):
else:
query = ''
if user_id:
if user_id and share_type:
query = '%s?user_id=%s&share_type=%s' % (
query, user_id, share_type)
elif user_id:
query = '%s?user_id=%s' % (query, user_id)
elif share_type:
query = '%s?share_type=%s' % (query, share_type)
data = {
"resource_path": resource_path,
"tenant_id": tenant_id,
@ -65,16 +77,22 @@ class QuotaSetManager(base.ManagerWithFind):
return self._do_get(tenant_id, user_id,
resource_path=RESOURCE_PATH)
@api_versions.wraps("2.25") # noqa
@api_versions.wraps("2.25", "2.38") # noqa
def get(self, tenant_id, user_id=None, detail=False):
return self._do_get(tenant_id, user_id, detail,
return self._do_get(tenant_id, user_id, detail=detail,
resource_path=RESOURCE_PATH)
@api_versions.wraps("2.39") # noqa
def get(self, tenant_id, user_id=None, share_type=None, detail=False):
return self._do_get(
tenant_id, user_id, share_type=share_type, detail=detail,
resource_path=RESOURCE_PATH)
def _do_update(self, tenant_id, shares=None, snapshots=None,
gigabytes=None, snapshot_gigabytes=None,
share_networks=None, force=None, user_id=None,
resource_path=RESOURCE_PATH):
share_type=None, resource_path=RESOURCE_PATH):
self._check_user_id_and_share_type_args(user_id, share_type)
body = {
'quota_set': {
'tenant_id': tenant_id,
@ -94,9 +112,12 @@ class QuotaSetManager(base.ManagerWithFind):
"resource_path": resource_path,
"tenant_id": tenant_id,
"user_id": user_id,
"st": share_type,
}
if user_id:
url = '%(resource_path)s/%(tenant_id)s?user_id=%(user_id)s' % data
elif share_type:
url = '%(resource_path)s/%(tenant_id)s?share_type=%(st)s' % data
else:
url = "%(resource_path)s/%(tenant_id)s" % data
@ -108,16 +129,32 @@ class QuotaSetManager(base.ManagerWithFind):
user_id=None):
return self._do_update(
tenant_id, shares, snapshots, gigabytes, snapshot_gigabytes,
share_networks, force, user_id, RESOURCE_PATH_LEGACY,
share_networks, force, user_id, resource_path=RESOURCE_PATH_LEGACY,
)
@api_versions.wraps("2.7") # noqa
@api_versions.wraps("2.7", "2.38") # noqa
def update(self, tenant_id, shares=None, snapshots=None, gigabytes=None,
snapshot_gigabytes=None, share_networks=None, force=None,
user_id=None):
return self._do_update(
tenant_id, shares, snapshots, gigabytes, snapshot_gigabytes,
share_networks, force, user_id, RESOURCE_PATH,
share_networks, force, user_id, resource_path=RESOURCE_PATH,
)
@api_versions.wraps("2.39") # noqa
def update(self, tenant_id, user_id=None, share_type=None,
shares=None, snapshots=None, gigabytes=None,
snapshot_gigabytes=None, share_networks=None,
force=None):
if share_type and share_networks:
raise ValueError(
"'share_networks' quota can be set only for project or user, "
"not share type.")
return self._do_update(
tenant_id, shares, snapshots, gigabytes, snapshot_gigabytes,
share_networks, force, user_id,
share_type=share_type,
resource_path=RESOURCE_PATH,
)
@api_versions.wraps("1.0", "2.6")
@ -134,14 +171,19 @@ class QuotaSetManager(base.ManagerWithFind):
"resource_path": RESOURCE_PATH, "tenant_id": tenant_id},
"quota_set")
def _do_delete(self, tenant_id, user_id=None, resource_path=RESOURCE_PATH):
def _do_delete(self, tenant_id, user_id=None, share_type=None,
resource_path=RESOURCE_PATH):
self._check_user_id_and_share_type_args(user_id, share_type)
data = {
"resource_path": resource_path,
"tenant_id": tenant_id,
"user_id": user_id,
"st": share_type,
}
if user_id:
url = '%(resource_path)s/%(tenant_id)s?user_id=%(user_id)s' % data
elif share_type:
url = '%(resource_path)s/%(tenant_id)s?share_type=%(st)s' % data
else:
url = '%(resource_path)s/%(tenant_id)s' % data
self._delete(url)
@ -151,6 +193,11 @@ class QuotaSetManager(base.ManagerWithFind):
return self._do_delete(
tenant_id, user_id, resource_path=RESOURCE_PATH_LEGACY)
@api_versions.wraps("2.7") # noqa
@api_versions.wraps("2.7", "2.38") # noqa
def delete(self, tenant_id, user_id=None):
return self._do_delete(tenant_id, user_id, resource_path=RESOURCE_PATH)
@api_versions.wraps("2.39") # noqa
def delete(self, tenant_id, user_id=None, share_type=None):
return self._do_delete(
tenant_id, user_id, share_type, resource_path=RESOURCE_PATH)

View File

@ -399,50 +399,70 @@ def _quota_update(manager, identifier, args):
@cliutils.arg(
'--tenant',
'--tenant-id',
metavar='<tenant-id>',
default=None,
help='ID of tenant to list the quotas for.')
@cliutils.arg(
'--user',
'--user-id',
metavar='<user-id>',
default=None,
help='ID of user to list the quotas for.')
help="ID of user to list the quotas for. Optional. "
"Mutually exclusive with '--share-type'.")
@cliutils.arg(
'--share-type',
'--share_type',
metavar='<share-type>',
type=str,
default=None,
action='single_alias',
help="UUID or name of a share type to set the quotas for. Optional. "
"Mutually exclusive with '--user-id'. "
"Available only for microversion >= 2.39")
@cliutils.arg(
'--detail',
action='store_true',
help='Optional flag to indicate whether to show quota in detail. '
'Default false, available only for microversion >= 2.25.')
@api_versions.wraps("1.0")
def do_quota_show(cs, args):
"""List the quotas for a tenant/user."""
project = args.tenant or cs.keystone_client.project_id
qts = cs.quotas.get(project, user_id=args.user, detail=args.detail)
_quota_set_pretty_show(qts)
project = args.tenant_id or cs.keystone_client.project_id
kwargs = {
"tenant_id": project,
"user_id": args.user_id,
"detail": args.detail,
}
if args.share_type is not None:
if cs.api_version < api_versions.APIVersion("2.39"):
raise exceptions.CommandError(
"'share type' quotas are available only starting with "
"'2.39' API microversion.")
kwargs["share_type"] = args.share_type
_quota_set_pretty_show(cs.quotas.get(**kwargs))
@cliutils.arg(
'--tenant',
'--tenant-id',
metavar='<tenant-id>',
default=None,
help='ID of tenant to list the default quotas for.')
def do_quota_defaults(cs, args):
"""List the default quotas for a tenant."""
project_id = cs.keystone_client.project_id
if not args.tenant:
_quota_show(cs.quotas.defaults(project_id))
else:
_quota_show(cs.quotas.defaults(args.tenant))
project = args.tenant_id or cs.keystone_client.project_id
_quota_show(cs.quotas.defaults(project))
@cliutils.arg(
'tenant',
'tenant_id',
metavar='<tenant_id>',
help='UUID of tenant to set the quotas for.')
@cliutils.arg(
'--user',
'--user-id',
metavar='<user-id>',
default=None,
help='ID of user to set the quotas for.')
help="ID of a user to set the quotas for. Optional. "
"Mutually exclusive with '--share-type'.")
@cliutils.arg(
'--shares',
metavar='<shares>',
@ -477,6 +497,16 @@ def do_quota_defaults(cs, args):
default=None,
action='single_alias',
help='New value for the "share_networks" quota.')
@cliutils.arg(
'--share-type',
'--share_type',
metavar='<share-type>',
type=str,
default=None,
action='single_alias',
help="UUID or name of a share type to set the quotas for. Optional. "
"Mutually exclusive with '--user-id'. "
"Available only for microversion >= 2.39")
@cliutils.arg(
'--force',
dest='force',
@ -484,30 +514,66 @@ def do_quota_defaults(cs, args):
default=None,
help='Whether force update the quota even if the already used '
'and reserved exceeds the new quota.')
@api_versions.wraps("1.0")
def do_quota_update(cs, args):
"""Update the quotas for a tenant/user (Admin only)."""
_quota_update(cs.quotas, args.tenant, args)
"""Update the quotas for a project/user and/or share type (Admin only)."""
kwargs = {
"tenant_id": args.tenant_id,
"user_id": args.user_id,
"shares": args.shares,
"gigabytes": args.gigabytes,
"snapshots": args.snapshots,
"snapshot_gigabytes": args.snapshot_gigabytes,
"share_networks": args.share_networks,
"force": args.force,
}
if args.share_type is not None:
if cs.api_version < api_versions.APIVersion("2.39"):
raise exceptions.CommandError(
"'share type' quotas are available only starting with "
"'2.39' API microversion.")
kwargs["share_type"] = args.share_type
cs.quotas.update(**kwargs)
@cliutils.arg(
'--tenant',
'--tenant-id',
metavar='<tenant-id>',
help='ID of tenant to delete quota for.')
@cliutils.arg(
'--user',
'--user-id',
metavar='<user-id>',
help='ID of user to delete quota for.')
help="ID of user to delete quota for. Optional."
"Mutually exclusive with '--share-type'.")
@cliutils.arg(
'--share-type',
'--share_type',
metavar='<share-type>',
type=str,
default=None,
action='single_alias',
help="UUID or name of a share type to set the quotas for. Optional. "
"Mutually exclusive with '--user-id'. "
"Available only for microversion >= 2.39")
@api_versions.wraps("1.0")
def do_quota_delete(cs, args):
"""Delete quota for a tenant/user.
"""Delete quota for a tenant/user or tenant/share-type.
The quota will revert back to default (Admin only).
"""
if not args.tenant:
project_id = cs.keystone_client.project_id
cs.quotas.delete(project_id, user_id=args.user)
else:
cs.quotas.delete(args.tenant, user_id=args.user)
project_id = args.tenant_id or cs.keystone_client.project_id
kwargs = {
"tenant_id": project_id,
"user_id": args.user_id,
}
if args.share_type is not None:
if cs.api_version < api_versions.APIVersion("2.39"):
raise exceptions.CommandError(
"'share type' quotas are available only starting with "
"'2.39' API microversion.")
kwargs["share_type"] = args.share_type
cs.quotas.delete(**kwargs)
@cliutils.arg(

View File

@ -0,0 +1,3 @@
---
features:
- Added support for per-share-type quotas.