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:
parent
0a187905c0
commit
75cba9d1cb
@ -17,6 +17,8 @@ List quotas for all projects with non-default quota values
|
||||
|
||||
openstack quota list
|
||||
--compute | --network | --volume
|
||||
[--project <project>]
|
||||
[--detail]
|
||||
|
||||
.. option:: --network
|
||||
|
||||
@ -30,6 +32,14 @@ List quotas for all projects with non-default quota values
|
||||
|
||||
List volume quotas
|
||||
|
||||
.. option:: --project <project>
|
||||
|
||||
List quotas for this project <project> (name or ID)
|
||||
|
||||
.. option:: --detail
|
||||
|
||||
Show details about quotas usage
|
||||
|
||||
quota set
|
||||
---------
|
||||
|
||||
|
@ -97,12 +97,164 @@ def _xform_get_quota(data, value, keys):
|
||||
return res
|
||||
|
||||
|
||||
class ListQuota(command.Lister):
|
||||
_description = _("List quotas for all projects "
|
||||
"with non-default quota values")
|
||||
class BaseQuota(object):
|
||||
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_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):
|
||||
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.add_argument(
|
||||
'--compute',
|
||||
@ -130,6 +282,8 @@ class ListQuota(command.Lister):
|
||||
project_ids = [getattr(p, 'id', '') for p in projects]
|
||||
|
||||
if parsed_args.compute:
|
||||
if parsed_args.detail:
|
||||
return self._get_detailed_quotas(parsed_args)
|
||||
compute_client = self.app.client_manager.compute
|
||||
for p in project_ids:
|
||||
try:
|
||||
@ -193,6 +347,9 @@ class ListQuota(command.Lister):
|
||||
) for s in result))
|
||||
|
||||
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
|
||||
for p in project_ids:
|
||||
try:
|
||||
@ -243,6 +400,8 @@ class ListQuota(command.Lister):
|
||||
) for s in result))
|
||||
|
||||
if parsed_args.network:
|
||||
if parsed_args.detail:
|
||||
return self._get_detailed_quotas(parsed_args)
|
||||
client = self.app.client_manager.network
|
||||
for p in project_ids:
|
||||
try:
|
||||
@ -410,7 +569,7 @@ class SetQuota(command.Command):
|
||||
**network_kwargs)
|
||||
|
||||
|
||||
class ShowQuota(command.ShowOne):
|
||||
class ShowQuota(command.ShowOne, BaseQuota):
|
||||
_description = _("Show quotas for project or class")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
@ -438,62 +597,6 @@ class ShowQuota(command.ShowOne):
|
||||
)
|
||||
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):
|
||||
|
||||
compute_client = self.app.client_manager.compute
|
||||
@ -504,10 +607,10 @@ class ShowQuota(command.ShowOne):
|
||||
# does not exist. If this is determined to be the
|
||||
# intended behaviour of the API we will validate
|
||||
# the argument with Identity ourselves later.
|
||||
compute_quota_info = self.get_compute_volume_quota(compute_client,
|
||||
parsed_args)
|
||||
volume_quota_info = self.get_compute_volume_quota(volume_client,
|
||||
parsed_args)
|
||||
compute_quota_info = self.get_compute_quota(compute_client,
|
||||
parsed_args)
|
||||
volume_quota_info = self.get_volume_quota(volume_client,
|
||||
parsed_args)
|
||||
network_quota_info = self.get_network_quota(parsed_args)
|
||||
# NOTE(reedip): Remove the below check once requirement for
|
||||
# Openstack SDK is fixed to version 0.9.12 and above
|
||||
|
@ -31,6 +31,38 @@ class QuotaTests(base.TestCase):
|
||||
cls.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):
|
||||
if not self.haz_network:
|
||||
self.skipTest("No Network service present")
|
||||
|
@ -197,6 +197,85 @@ class TestQuotaList(TestQuota):
|
||||
|
||||
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):
|
||||
# Two projects with non-default quotas
|
||||
self.compute.quotas.get = mock.Mock(
|
||||
@ -827,13 +906,13 @@ class TestQuotaShow(TestQuota):
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
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.projects[0].id,
|
||||
)
|
||||
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)
|
||||
|
||||
@ -889,12 +968,12 @@ class TestQuotaShow(TestQuota):
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
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(
|
||||
identity_fakes.project_id,
|
||||
)
|
||||
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)
|
||||
|
@ -1400,6 +1400,38 @@ class FakeQuota(object):
|
||||
|
||||
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):
|
||||
"""Fake limits"""
|
||||
|
@ -1700,3 +1700,26 @@ class FakeQuota(object):
|
||||
info=copy.deepcopy(quota_attrs),
|
||||
loaded=True)
|
||||
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
|
||||
|
@ -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>`_]
|
Loading…
Reference in New Issue
Block a user