quota: Add 'quota show --usage' option

Provide an more sane way to get usage information for a particular
project's quotas. This requires using the 'Lister' command type since
the 'ShowOne' command type only allows for simple key-value pair output.

We also add a note indicating that the '<project>' argument is optional.

Change-Id: Ic7342cf08f024cc690049414c5eef5b9a7594677
Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
Stephen Finucane 2022-09-23 18:00:34 +01:00
parent 47e667e71d
commit 04e68e0d5a
5 changed files with 151 additions and 30 deletions

View File

@ -94,7 +94,7 @@ quota-defaults,quota show --default,Lists default quotas for a tenant.
quota-delete,quota delete --volume,Delete the quotas for a tenant. quota-delete,quota delete --volume,Delete the quotas for a tenant.
quota-show,quota show,Lists quotas for a tenant. quota-show,quota show,Lists quotas for a tenant.
quota-update,quota set,Updates quotas for a tenant. quota-update,quota set,Updates quotas for a tenant.
quota-usage,quota list --detail,Lists quota usage for a tenant. quota-usage,quota show --usage,Lists quota usage for a tenant.
rate-limits,limits show --rate,Lists rate limits for a user. rate-limits,limits show --rate,Lists rate limits for a user.
readonly-mode-update,volume set --read-only-mode | --read-write-mode,Updates volume read-only access-mode flag. readonly-mode-update,volume set --read-only-mode | --read-write-mode,Updates volume read-only access-mode flag.
rename,volume set --name,Renames a volume. rename,volume set --name,Renames a volume.

1 absolute-limits limits show --absolute Lists absolute limits for a user.
94 quota-delete quota delete --volume Delete the quotas for a tenant.
95 quota-show quota show Lists quotas for a tenant.
96 quota-update quota set Updates quotas for a tenant.
97 quota-usage quota list --detail quota show --usage Lists quota usage for a tenant.
98 rate-limits limits show --rate Lists rate limits for a user.
99 readonly-mode-update volume set --read-only-mode | --read-write-mode Updates volume read-only access-mode flag.
100 rename volume set --name Renames a volume.

View File

@ -233,19 +233,26 @@ class ListQuota(command.Lister):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
# TODO(stephenfin): Remove in OSC 8.0
parser.add_argument( parser.add_argument(
'--project', '--project',
metavar='<project>', metavar='<project>',
help=_('List quotas for this project <project> (name or ID)'), help=_(
"**Deprecated** List quotas for this project <project> "
"(name or ID). "
"Use 'quota show' instead."
),
) )
# TODO(stephenfin): This doesn't belong here. We should put it into the # TODO(stephenfin): Remove in OSC 8.0
# 'quota show' command and deprecate this.
parser.add_argument( parser.add_argument(
'--detail', '--detail',
dest='detail', dest='detail',
action='store_true', action='store_true',
default=False, default=False,
help=_('Show details about quotas usage'), help=_(
"**Deprecated** Show details about quotas usage. "
"Use 'quota show --usage' instead."
),
) )
option = parser.add_mutually_exclusive_group(required=True) option = parser.add_mutually_exclusive_group(required=True)
option.add_argument( option.add_argument(
@ -332,6 +339,19 @@ class ListQuota(command.Lister):
) )
def take_action(self, parsed_args): def take_action(self, parsed_args):
if parsed_args.detail:
msg = _(
"The --detail option has been deprecated. "
"Use 'openstack quota show --usage' instead."
)
self.log.warning(msg)
elif parsed_args.project: # elif to avoid being too noisy
msg = _(
"The --project option has been deprecated. "
"Use 'openstack quota show' instead."
)
self.log.warning(msg)
result = [] result = []
project_ids = [] project_ids = []
if parsed_args.project is None: if parsed_args.project is None:
@ -678,7 +698,7 @@ class SetQuota(common.NetDetectionMixin, command.Command):
**network_kwargs) **network_kwargs)
class ShowQuota(command.ShowOne): class ShowQuota(command.Lister):
_description = _( _description = _(
"Show quotas for project or class. " "Show quotas for project or class. "
"Specify ``--os-compute-api-version 2.50`` or higher to see " "Specify ``--os-compute-api-version 2.50`` or higher to see "
@ -692,7 +712,10 @@ class ShowQuota(command.ShowOne):
'project', 'project',
metavar='<project/class>', metavar='<project/class>',
nargs='?', nargs='?',
help=_('Show quotas for this project or class (name or ID)'), help=_(
'Show quotas for this project or class (name or ID) '
'(defaults to current project)'
),
) )
type_group = parser.add_mutually_exclusive_group() type_group = parser.add_mutually_exclusive_group()
type_group.add_argument( type_group.add_argument(
@ -709,6 +732,13 @@ class ShowQuota(command.ShowOne):
default=False, default=False,
help=_('Show default quotas for <project>'), help=_('Show default quotas for <project>'),
) )
type_group.add_argument(
'--usage',
dest='usage',
action='store_true',
default=False,
help=_('Show details about quotas usage'),
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
@ -726,18 +756,21 @@ class ShowQuota(command.ShowOne):
compute_quota_info = get_compute_quotas( compute_quota_info = get_compute_quotas(
self.app, self.app,
project, project,
detail=parsed_args.usage,
quota_class=parsed_args.quota_class, quota_class=parsed_args.quota_class,
default=parsed_args.default, default=parsed_args.default,
) )
volume_quota_info = get_volume_quotas( volume_quota_info = get_volume_quotas(
self.app, self.app,
project, project,
detail=parsed_args.usage,
quota_class=parsed_args.quota_class, quota_class=parsed_args.quota_class,
default=parsed_args.default, default=parsed_args.default,
) )
network_quota_info = get_network_quotas( network_quota_info = get_network_quotas(
self.app, self.app,
project, project,
detail=parsed_args.usage,
quota_class=parsed_args.quota_class, quota_class=parsed_args.quota_class,
default=parsed_args.default, default=parsed_args.default,
) )
@ -762,20 +795,46 @@ class ShowQuota(command.ShowOne):
info[v] = info[k] info[v] = info[k]
info.pop(k) info.pop(k)
# Remove the 'id' field since it's not very useful
if 'id' in info:
del info['id']
# Remove the 'location' field for resources from openstacksdk # Remove the 'location' field for resources from openstacksdk
if 'location' in info: if 'location' in info:
del info['location'] del info['location']
# Handle class or project ID specially as they only appear in output if not parsed_args.usage:
if parsed_args.quota_class: result = [
info.pop('id', None) {'resource': k, 'limit': v} for k, v in info.items()
elif 'id' in info: ]
info['project'] = info.pop('id') else:
if 'project_id' in info: result = [
del info['project_id'] {'resource': k, **v} for k, v in info.items()
info['project_name'] = project_info['name'] ]
return zip(*sorted(info.items())) columns = (
'resource',
'limit',
)
column_headers = (
'Resource',
'Limit',
)
if parsed_args.usage:
columns += (
'in_use',
'reserved',
)
column_headers += (
'In Use',
'Reserved',
)
return (
column_headers,
(utils.get_dict_properties(s, columns) for s in result),
)
class DeleteQuota(command.Command): class DeleteQuota(command.Command):

View File

@ -114,6 +114,7 @@ class QuotaTests(base.TestCase):
cmd_output = json.loads(self.openstack( cmd_output = json.loads(self.openstack(
'quota show -f json ' + self.PROJECT_NAME 'quota show -f json ' + self.PROJECT_NAME
)) ))
cmd_output = {x['Resource']: x['Limit'] for x in cmd_output}
self.assertIsNotNone(cmd_output) self.assertIsNotNone(cmd_output)
self.assertEqual( self.assertEqual(
31, 31,
@ -136,6 +137,7 @@ class QuotaTests(base.TestCase):
self.assertIsNotNone(cmd_output) self.assertIsNotNone(cmd_output)
# We don't necessarily know the default quotas, we're checking the # We don't necessarily know the default quotas, we're checking the
# returned attributes # returned attributes
cmd_output = {x['Resource']: x['Limit'] for x in cmd_output}
self.assertTrue(cmd_output["cores"] >= 0) self.assertTrue(cmd_output["cores"] >= 0)
self.assertTrue(cmd_output["backups"] >= 0) self.assertTrue(cmd_output["backups"] >= 0)
if self.haz_network: if self.haz_network:
@ -150,6 +152,7 @@ class QuotaTests(base.TestCase):
'quota show -f json --class default' 'quota show -f json --class default'
)) ))
self.assertIsNotNone(cmd_output) self.assertIsNotNone(cmd_output)
cmd_output = {x['Resource']: x['Limit'] for x in cmd_output}
self.assertEqual( self.assertEqual(
33, 33,
cmd_output["key-pairs"], cmd_output["key-pairs"],
@ -166,6 +169,7 @@ class QuotaTests(base.TestCase):
self.assertIsNotNone(cmd_output) self.assertIsNotNone(cmd_output)
# We don't necessarily know the default quotas, we're checking the # We don't necessarily know the default quotas, we're checking the
# returned attributes # returned attributes
cmd_output = {x['Resource']: x['Limit'] for x in cmd_output}
self.assertTrue(cmd_output["key-pairs"] >= 0) self.assertTrue(cmd_output["key-pairs"] >= 0)
self.assertTrue(cmd_output["snapshots"] >= 0) self.assertTrue(cmd_output["snapshots"] >= 0)

View File

@ -1094,17 +1094,20 @@ class TestQuotaShow(TestQuota):
self.cmd.take_action(parsed_args) self.cmd.take_action(parsed_args)
self.compute_quotas_mock.get.assert_called_once_with( self.compute_quotas_mock.get.assert_called_once_with(
self.projects[0].id, detail=False self.projects[0].id,
detail=False,
) )
self.volume_quotas_mock.get.assert_called_once_with( self.volume_quotas_mock.get.assert_called_once_with(
self.projects[0].id, usage=False self.projects[0].id,
usage=False,
) )
self.network.get_quota.assert_called_once_with( self.network.get_quota.assert_called_once_with(
self.projects[0].id, details=False self.projects[0].id,
details=False,
) )
self.assertNotCalled(self.network.get_quota_default) self.assertNotCalled(self.network.get_quota_default)
def test_quota_show_with_default(self): def test_quota_show__with_default(self):
arglist = [ arglist = [
'--default', '--default',
self.projects[0].name, self.projects[0].name,
@ -1128,30 +1131,67 @@ class TestQuotaShow(TestQuota):
) )
self.assertNotCalled(self.network.get_quota) self.assertNotCalled(self.network.get_quota)
def test_quota_show_with_class(self): def test_quota_show__with_class(self):
arglist = [ arglist = [
'--class', '--class',
self.projects[0].name, 'default',
] ]
verifylist = [ verifylist = [
('quota_class', True), ('quota_class', True),
('project', 'default'), # project is actually a class here
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.compute_quotas_class_mock.get.assert_called_once_with('default')
self.volume_quotas_class_mock.get.assert_called_once_with('default')
# neutron doesn't have the concept of quota classes
self.assertNotCalled(self.network.get_quota)
self.assertNotCalled(self.network.get_quota_default)
def test_quota_show__with_usage(self):
# update mocks to return detailed quota instead
self.compute_quota = \
compute_fakes.FakeQuota.create_one_comp_detailed_quota()
self.compute_quotas_mock.get.return_value = self.compute_quota
self.volume_quota = \
volume_fakes.FakeQuota.create_one_detailed_quota()
self.volume_quotas_mock.get.return_value = self.volume_quota
self.network.get_quota.return_value = \
network_fakes.FakeQuota.create_one_net_detailed_quota()
arglist = [
'--usage',
self.projects[0].name,
]
verifylist = [
('usage', True),
('project', self.projects[0].name), ('project', self.projects[0].name),
] ]
parsed_args = self.check_parser(self.cmd, arglist, verifylist) parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args) self.cmd.take_action(parsed_args)
self.compute_quotas_class_mock.get.assert_called_once_with( self.compute_quotas_mock.get.assert_called_once_with(
self.projects[0].name, self.projects[0].id,
detail=True,
) )
self.volume_quotas_class_mock.get.assert_called_once_with( self.volume_quotas_mock.get.assert_called_once_with(
self.projects[0].name, self.projects[0].id,
usage=True,
)
self.network.get_quota.assert_called_once_with(
self.projects[0].id,
details=True,
) )
self.assertNotCalled(self.network.get_quota)
self.assertNotCalled(self.network.get_quota_default)
def test_quota_show_no_project(self): def test_quota_show__no_project(self):
parsed_args = self.check_parser(self.cmd, [], []) arglist = []
verifylist = [
('project', None),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args) self.cmd.take_action(parsed_args)

View File

@ -0,0 +1,18 @@
---
features:
- |
The ``quota show`` command now supports a ``--usage`` option. When
provided, this will result in the command returning usage information for
each quota. This replaces the ``quota list --detail`` command which is now
deprecated for removal.
deprecations:
- |
The ``--detail`` option for the ``quota list`` command has been deprecated
for removal. When used without the ``--detail`` option, the ``quota list``
command returned quota information for multiple projects yet when used with
this option it only returned (detailed) quota information for a single
project. This detailed quota information is now available via the
``quota show --usage`` command.
- |
The ``--project`` option for the ``quota list`` command has been deprecated
for removal. Use the ``quota show`` command instead.