Add support for get details of Quota

With passing "--detail" argument to "openstack quota list", details
about current usage should be returned.
It is currently supported by Nova and Neutron so details of
resources from those projects can be returned.

Change-Id: I48fda15b34283bb7c66ea18ed28262f48b9229fe
Related-Bug: #1716043
This commit is contained in:
Sławek Kapłoński 2018-10-19 12:46:21 +03:00 committed by Vlad Gusev
parent 0a187905c0
commit 75cba9d1cb
7 changed files with 353 additions and 68 deletions

View File

@ -17,6 +17,8 @@ List quotas for all projects with non-default quota values
openstack quota list openstack quota list
--compute | --network | --volume --compute | --network | --volume
[--project <project>]
[--detail]
.. option:: --network .. option:: --network
@ -30,6 +32,14 @@ List quotas for all projects with non-default quota values
List volume quotas List volume quotas
.. option:: --project <project>
List quotas for this project <project> (name or ID)
.. option:: --detail
Show details about quotas usage
quota set quota set
--------- ---------

View File

@ -97,12 +97,164 @@ def _xform_get_quota(data, value, keys):
return res return res
class ListQuota(command.Lister): class BaseQuota(object):
_description = _("List quotas for all projects " def _get_project(self, parsed_args):
"with non-default quota values") if parsed_args.project is not None:
identity_client = self.app.client_manager.identity
project = utils.find_resource(
identity_client.projects,
parsed_args.project,
)
project_id = project.id
project_name = project.name
elif self.app.client_manager.auth_ref:
# Get the project from the current auth
project = self.app.client_manager.auth_ref
project_id = project.project_id
project_name = project.project_name
else:
project = None
project_id = None
project_name = None
project_info = {}
project_info['id'] = project_id
project_info['name'] = project_name
return project_info
def get_compute_quota(self, client, parsed_args):
quota_class = (
parsed_args.quota_class if 'quota_class' in parsed_args else False)
detail = parsed_args.detail if 'detail' in parsed_args else False
default = parsed_args.default if 'default' in parsed_args else False
try:
if quota_class:
quota = client.quota_classes.get(parsed_args.project)
else:
project_info = self._get_project(parsed_args)
project = project_info['id']
if default:
quota = client.quotas.defaults(project)
else:
quota = client.quotas.get(project, detail=detail)
except Exception as e:
if type(e).__name__ == 'EndpointNotFound':
return {}
else:
raise
return quota._info
def get_volume_quota(self, client, parsed_args):
quota_class = (
parsed_args.quota_class if 'quota_class' in parsed_args else False)
default = parsed_args.default if 'default' in parsed_args else False
try:
if quota_class:
quota = client.quota_classes.get(parsed_args.project)
else:
project_info = self._get_project(parsed_args)
project = project_info['id']
if default:
quota = client.quotas.defaults(project)
else:
quota = client.quotas.get(project)
except Exception as e:
if type(e).__name__ == 'EndpointNotFound':
return {}
else:
raise
return quota._info
def get_network_quota(self, parsed_args):
quota_class = (
parsed_args.quota_class if 'quota_class' in parsed_args else False)
detail = parsed_args.detail if 'detail' in parsed_args else False
default = parsed_args.default if 'default' in parsed_args else False
if quota_class:
return {}
if self.app.client_manager.is_network_endpoint_enabled():
project_info = self._get_project(parsed_args)
project = project_info['id']
client = self.app.client_manager.network
if default:
network_quota = client.get_quota_default(project)
if type(network_quota) is not dict:
network_quota = network_quota.to_dict()
else:
network_quota = client.get_quota(project,
details=detail)
if type(network_quota) is not dict:
network_quota = network_quota.to_dict()
if detail:
# NOTE(slaweq): Neutron returns values with key "used" but
# Nova for example returns same data with key "in_use"
# instead.
# Because of that we need to convert Neutron key to
# the same as is returned from Nova to make result
# more consistent
for key, values in network_quota.items():
if type(values) is dict and "used" in values:
values[u'in_use'] = values.pop("used")
network_quota[key] = values
return network_quota
else:
return {}
class ListQuota(command.Lister, BaseQuota):
_description = _(
"List quotas for all projects with non-default quota values or "
"list detailed quota informations for requested project")
def _get_detailed_quotas(self, parsed_args):
columns = (
'resource',
'in_use',
'reserved',
'limit'
)
column_headers = (
'Resource',
'In Use',
'Reserved',
'Limit'
)
quotas = {}
if parsed_args.compute:
quotas.update(self.get_compute_quota(
self.app.client_manager.compute, parsed_args))
if parsed_args.network:
quotas.update(self.get_network_quota(parsed_args))
result = []
for resource, values in quotas.items():
# NOTE(slaweq): there is no detailed quotas info for some resources
# and it should't be displayed here
if type(values) is dict:
result.append({
'resource': resource,
'in_use': values.get('in_use'),
'reserved': values.get('reserved'),
'limit': values.get('limit')
})
return (column_headers,
(utils.get_dict_properties(
s, columns,
) for s in result))
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super(ListQuota, self).get_parser(prog_name) parser = super(ListQuota, self).get_parser(prog_name)
parser.add_argument(
'--project',
metavar='<project>',
help=_('List quotas for this project <project> (name or ID)'),
)
parser.add_argument(
'--detail',
dest='detail',
action='store_true',
default=False,
help=_('Show details about quotas usage')
)
option = parser.add_mutually_exclusive_group(required=True) option = parser.add_mutually_exclusive_group(required=True)
option.add_argument( option.add_argument(
'--compute', '--compute',
@ -130,6 +282,8 @@ class ListQuota(command.Lister):
project_ids = [getattr(p, 'id', '') for p in projects] project_ids = [getattr(p, 'id', '') for p in projects]
if parsed_args.compute: if parsed_args.compute:
if parsed_args.detail:
return self._get_detailed_quotas(parsed_args)
compute_client = self.app.client_manager.compute compute_client = self.app.client_manager.compute
for p in project_ids: for p in project_ids:
try: try:
@ -193,6 +347,9 @@ class ListQuota(command.Lister):
) for s in result)) ) for s in result))
if parsed_args.volume: if parsed_args.volume:
if parsed_args.detail:
LOG.warning("Volume service doesn't provide detailed quota"
" information")
volume_client = self.app.client_manager.volume volume_client = self.app.client_manager.volume
for p in project_ids: for p in project_ids:
try: try:
@ -243,6 +400,8 @@ class ListQuota(command.Lister):
) for s in result)) ) for s in result))
if parsed_args.network: if parsed_args.network:
if parsed_args.detail:
return self._get_detailed_quotas(parsed_args)
client = self.app.client_manager.network client = self.app.client_manager.network
for p in project_ids: for p in project_ids:
try: try:
@ -410,7 +569,7 @@ class SetQuota(command.Command):
**network_kwargs) **network_kwargs)
class ShowQuota(command.ShowOne): class ShowQuota(command.ShowOne, BaseQuota):
_description = _("Show quotas for project or class") _description = _("Show quotas for project or class")
def get_parser(self, prog_name): def get_parser(self, prog_name):
@ -438,62 +597,6 @@ class ShowQuota(command.ShowOne):
) )
return parser return parser
def _get_project(self, parsed_args):
if parsed_args.project is not None:
identity_client = self.app.client_manager.identity
project = utils.find_resource(
identity_client.projects,
parsed_args.project,
)
project_id = project.id
project_name = project.name
elif self.app.client_manager.auth_ref:
# Get the project from the current auth
project = self.app.client_manager.auth_ref
project_id = project.project_id
project_name = project.project_name
else:
project = None
project_id = None
project_name = None
project_info = {}
project_info['id'] = project_id
project_info['name'] = project_name
return project_info
def get_compute_volume_quota(self, client, parsed_args):
try:
if parsed_args.quota_class:
quota = client.quota_classes.get(parsed_args.project)
else:
project_info = self._get_project(parsed_args)
project = project_info['id']
if parsed_args.default:
quota = client.quotas.defaults(project)
else:
quota = client.quotas.get(project)
except Exception as e:
if type(e).__name__ == 'EndpointNotFound':
return {}
else:
raise
return quota._info
def get_network_quota(self, parsed_args):
if parsed_args.quota_class:
return {}
if self.app.client_manager.is_network_endpoint_enabled():
project_info = self._get_project(parsed_args)
project = project_info['id']
client = self.app.client_manager.network
if parsed_args.default:
network_quota = client.get_quota_default(project)
else:
network_quota = client.get_quota(project)
return network_quota
else:
return {}
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute compute_client = self.app.client_manager.compute
@ -504,9 +607,9 @@ class ShowQuota(command.ShowOne):
# does not exist. If this is determined to be the # does not exist. If this is determined to be the
# intended behaviour of the API we will validate # intended behaviour of the API we will validate
# the argument with Identity ourselves later. # the argument with Identity ourselves later.
compute_quota_info = self.get_compute_volume_quota(compute_client, compute_quota_info = self.get_compute_quota(compute_client,
parsed_args) parsed_args)
volume_quota_info = self.get_compute_volume_quota(volume_client, volume_quota_info = self.get_volume_quota(volume_client,
parsed_args) parsed_args)
network_quota_info = self.get_network_quota(parsed_args) network_quota_info = self.get_network_quota(parsed_args)
# NOTE(reedip): Remove the below check once requirement for # NOTE(reedip): Remove the below check once requirement for

View File

@ -31,6 +31,38 @@ class QuotaTests(base.TestCase):
cls.PROJECT_NAME =\ cls.PROJECT_NAME =\
cls.get_openstack_configuration_value('auth.project_name') cls.get_openstack_configuration_value('auth.project_name')
def test_quota_list_details_compute(self):
expected_headers = ["Resource", "In Use", "Reserved", "Limit"]
cmd_output = json.loads(self.openstack(
'quota list -f json --detail --compute'
))
self.assertIsNotNone(cmd_output)
resources = []
for row in cmd_output:
row_headers = [str(r) for r in row.keys()]
self.assertEqual(sorted(expected_headers), sorted(row_headers))
resources.append(row['Resource'])
# Ensure that returned quota is compute quota
self.assertIn("instances", resources)
# and that there is no network quota here
self.assertNotIn("networks", resources)
def test_quota_list_details_network(self):
expected_headers = ["Resource", "In Use", "Reserved", "Limit"]
cmd_output = json.loads(self.openstack(
'quota list -f json --detail --network'
))
self.assertIsNotNone(cmd_output)
resources = []
for row in cmd_output:
row_headers = [str(r) for r in row.keys()]
self.assertEqual(sorted(expected_headers), sorted(row_headers))
resources.append(row['Resource'])
# Ensure that returned quota is network quota
self.assertIn("networks", resources)
# and that there is no compute quota here
self.assertNotIn("instances", resources)
def test_quota_list_network_option(self): def test_quota_list_network_option(self):
if not self.haz_network: if not self.haz_network:
self.skipTest("No Network service present") self.skipTest("No Network service present")

View File

@ -197,6 +197,85 @@ class TestQuotaList(TestQuota):
self.cmd = quota.ListQuota(self.app, None) self.cmd = quota.ListQuota(self.app, None)
@staticmethod
def _get_detailed_reference_data(quota):
reference_data = []
for name, values in quota.to_dict().items():
if type(values) is dict:
if 'used' in values:
# For network quota it's "used" key instead of "in_use"
in_use = values['used']
else:
in_use = values['in_use']
resource_values = [
in_use,
values['reserved'],
values['limit']]
reference_data.append(tuple([name] + resource_values))
return reference_data
def test_quota_list_details_compute(self):
detailed_quota = (
compute_fakes.FakeQuota.create_one_comp_detailed_quota())
detailed_column_header = (
'Resource',
'In Use',
'Reserved',
'Limit',
)
detailed_reference_data = (
self._get_detailed_reference_data(detailed_quota))
self.compute.quotas.get = mock.Mock(return_value=detailed_quota)
arglist = [
'--detail', '--compute',
]
verifylist = [
('detail', True),
('compute', True),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
ret_quotas = list(data)
self.assertEqual(detailed_column_header, columns)
self.assertEqual(
sorted(detailed_reference_data), sorted(ret_quotas))
def test_quota_list_details_network(self):
detailed_quota = (
network_fakes.FakeQuota.create_one_net_detailed_quota())
detailed_column_header = (
'Resource',
'In Use',
'Reserved',
'Limit',
)
detailed_reference_data = (
self._get_detailed_reference_data(detailed_quota))
self.network.get_quota = mock.Mock(return_value=detailed_quota)
arglist = [
'--detail', '--network',
]
verifylist = [
('detail', True),
('network', True),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
ret_quotas = list(data)
self.assertEqual(detailed_column_header, columns)
self.assertEqual(
sorted(detailed_reference_data), sorted(ret_quotas))
def test_quota_list_compute(self): def test_quota_list_compute(self):
# Two projects with non-default quotas # Two projects with non-default quotas
self.compute.quotas.get = mock.Mock( self.compute.quotas.get = mock.Mock(
@ -827,13 +906,13 @@ 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, 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, self.projects[0].id,
) )
self.network.get_quota.assert_called_once_with( self.network.get_quota.assert_called_once_with(
self.projects[0].id, self.projects[0].id, details=False
) )
self.assertNotCalled(self.network.get_quota_default) self.assertNotCalled(self.network.get_quota_default)
@ -889,12 +968,12 @@ 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(
identity_fakes.project_id, identity_fakes.project_id, detail=False
) )
self.volume_quotas_mock.get.assert_called_once_with( self.volume_quotas_mock.get.assert_called_once_with(
identity_fakes.project_id, identity_fakes.project_id,
) )
self.network.get_quota.assert_called_once_with( self.network.get_quota.assert_called_once_with(
identity_fakes.project_id, identity_fakes.project_id, details=False
) )
self.assertNotCalled(self.network.get_quota_default) self.assertNotCalled(self.network.get_quota_default)

View File

@ -1400,6 +1400,38 @@ class FakeQuota(object):
return quota return quota
@staticmethod
def create_one_comp_detailed_quota(attrs=None):
"""Create one quota"""
attrs = attrs or {}
quota_attrs = {
'id': 'project-id-' + uuid.uuid4().hex,
'cores': {'reserved': 0, 'in_use': 0, 'limit': 20},
'fixed_ips': {'reserved': 0, 'in_use': 0, 'limit': 30},
'injected_files': {'reserved': 0, 'in_use': 0, 'limit': 100},
'injected_file_content_bytes': {
'reserved': 0, 'in_use': 0, 'limit': 10240},
'injected_file_path_bytes': {
'reserved': 0, 'in_use': 0, 'limit': 255},
'instances': {'reserved': 0, 'in_use': 0, 'limit': 50},
'key_pairs': {'reserved': 0, 'in_use': 0, 'limit': 20},
'metadata_items': {'reserved': 0, 'in_use': 0, 'limit': 10},
'ram': {'reserved': 0, 'in_use': 0, 'limit': 51200},
'server_groups': {'reserved': 0, 'in_use': 0, 'limit': 10},
'server_group_members': {'reserved': 0, 'in_use': 0, 'limit': 10}
}
quota_attrs.update(attrs)
quota = fakes.FakeResource(
info=copy.deepcopy(quota_attrs),
loaded=True)
quota.project_id = quota_attrs['id']
return quota
class FakeLimits(object): class FakeLimits(object):
"""Fake limits""" """Fake limits"""

View File

@ -1700,3 +1700,26 @@ class FakeQuota(object):
info=copy.deepcopy(quota_attrs), info=copy.deepcopy(quota_attrs),
loaded=True) loaded=True)
return quota return quota
@staticmethod
def create_one_net_detailed_quota(attrs=None):
"""Create one quota"""
attrs = attrs or {}
quota_attrs = {
'floating_ips': {'used': 0, 'reserved': 0, 'limit': 20},
'networks': {'used': 0, 'reserved': 0, 'limit': 25},
'ports': {'used': 0, 'reserved': 0, 'limit': 11},
'rbac_policies': {'used': 0, 'reserved': 0, 'limit': 15},
'routers': {'used': 0, 'reserved': 0, 'limit': 40},
'security_groups': {'used': 0, 'reserved': 0, 'limit': 10},
'security_group_rules': {'used': 0, 'reserved': 0, 'limit': 100},
'subnets': {'used': 0, 'reserved': 0, 'limit': 20},
'subnet_pools': {'used': 0, 'reserved': 0, 'limit': 30}}
quota_attrs.update(attrs)
quota = fakes.FakeResource(
info=copy.deepcopy(quota_attrs),
loaded=True)
return quota

View File

@ -0,0 +1,6 @@
---
features:
- |
Add support for list detailed ``quota`` usage for project.
This can be done by passing ``--detail`` parameter to `quota list` command.
[Bug `1716043 <https://bugs.launchpad.net/neutron/+bug/1716043>`_]