Merge "quota: Simplify logic used to list, show quotas"

This commit is contained in:
Zuul 2022-10-12 11:14:20 +00:00 committed by Gerrit Code Review
commit 351b2b1074
2 changed files with 265 additions and 221 deletions

View File

@ -25,7 +25,6 @@ from osc_lib import utils
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.network import common from openstackclient.network import common
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
# List the quota items, map the internal argument name to the option # List the quota items, map the internal argument name to the option
@ -78,9 +77,17 @@ NETWORK_QUOTAS = {
'subnetpool': 'subnetpools', 'subnetpool': 'subnetpools',
} }
NETWORK_KEYS = ['floating_ips', 'networks', 'rbac_policies', 'routers', NETWORK_KEYS = [
'ports', 'security_group_rules', 'security_groups', 'floating_ips',
'subnet_pools', 'subnets'] 'networks',
'rbac_policies',
'routers',
'ports',
'security_group_rules',
'security_groups',
'subnet_pools',
'subnets',
]
def _xform_get_quota(data, value, keys): def _xform_get_quota(data, value, keys):
@ -94,181 +101,151 @@ def _xform_get_quota(data, value, keys):
return res return res
class BaseQuota(object): def get_project(app, project):
def _get_project(self, parsed_args): if project is not None:
if parsed_args.project is not None: identity_client = app.client_manager.identity
identity_client = self.app.client_manager.identity project = utils.find_resource(
project = utils.find_resource( identity_client.projects,
identity_client.projects, project,
parsed_args.project, )
) project_id = project.id
project_id = project.id project_name = project.name
project_name = project.name elif app.client_manager.auth_ref:
elif self.app.client_manager.auth_ref: # Get the project from the current auth
# Get the project from the current auth project = app.client_manager.auth_ref
project = self.app.client_manager.auth_ref project_id = project.project_id
project_id = project.project_id project_name = project.project_name
project_name = project.project_name else:
project_id = None
project_name = None
return {
'id': project_id,
'name': project_name,
}
def get_compute_quotas(
app,
project_id,
*,
quota_class=False,
detail=False,
default=False,
):
try:
client = app.client_manager.compute
if quota_class:
# NOTE(stephenfin): The 'project' argument here could be anything
# as the nova API doesn't care what you pass in. We only pass the
# project in to avoid weirding people out :)
quota = client.quota_classes.get(project_id)
elif default:
quota = client.quotas.defaults(project_id)
else: else:
project = None quota = client.quotas.get(project_id, detail=detail)
project_id = None except Exception as e:
project_name = None if type(e).__name__ == 'EndpointNotFound':
project_info = {} return {}
project_info['id'] = project_id raise
project_info['name'] = project_name return quota._info
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): def get_volume_quotas(
quota_class = ( app,
parsed_args.quota_class if 'quota_class' in parsed_args else False) project_id,
detail = parsed_args.detail if 'detail' in parsed_args else False *,
default = parsed_args.default if 'default' in parsed_args else False quota_class=False,
try: detail=False,
if quota_class: default=False,
quota = client.quota_classes.get(parsed_args.project) ):
else: try:
project_info = self._get_project(parsed_args) client = app.client_manager.volume
project = project_info['id'] if quota_class:
if default: quota = client.quota_classes.get(project_id)
quota = client.quotas.defaults(project) elif default:
else: quota = client.quotas.defaults(project_id)
quota = client.quotas.get(project, usage=detail) else:
except Exception as e: quota = client.quotas.get(project_id, usage=detail)
if type(e).__name__ == 'EndpointNotFound': except Exception as e:
return {} if type(e).__name__ == 'EndpointNotFound':
else: return {}
raise else:
return quota._info raise
return quota._info
def _network_quota_to_dict(self, network_quota):
def get_network_quotas(
app,
project_id,
*,
quota_class=False,
detail=False,
default=False,
):
def _network_quota_to_dict(network_quota, detail=False):
if type(network_quota) is not dict: if type(network_quota) is not dict:
dict_quota = network_quota.to_dict() dict_quota = network_quota.to_dict()
else: else:
dict_quota = network_quota dict_quota = network_quota
return {k: v for k, v in dict_quota.items() if v is not None}
def get_network_quota(self, parsed_args): result = {}
quota_class = (
parsed_args.quota_class if 'quota_class' in parsed_args else False) for key, values in dict_quota.items():
detail = parsed_args.detail if 'detail' in parsed_args else False if values is None:
default = parsed_args.default if 'default' in parsed_args else False continue
if quota_class:
return {} # NOTE(slaweq): Neutron returns values with key "used" but Nova for
if self.app.client_manager.is_network_endpoint_enabled(): # example returns same data with key "in_use" instead. Because of
project_info = self._get_project(parsed_args) # that we need to convert Neutron key to the same as is returned
project = project_info['id'] # from Nova to make result more consistent
client = self.app.client_manager.network if isinstance(values, dict) and 'used' in values:
if default: values['in_use'] = values.pop("used")
network_quota = client.get_quota_default(project)
network_quota = self._network_quota_to_dict(network_quota) result[key] = values
else:
network_quota = client.get_quota(project, return result
details=detail)
network_quota = self._network_quota_to_dict(network_quota) # neutron doesn't have the concept of quota classes and if we're using
if detail: # nova-network we already fetched this
# NOTE(slaweq): Neutron returns values with key "used" but if quota_class:
# Nova for example returns same data with key "in_use" return {}
# instead.
# Because of that we need to convert Neutron key to # we have nothing to return if we are not using neutron
# the same as is returned from Nova to make result if not app.client_manager.is_network_endpoint_enabled():
# more consistent return {}
for key, values in network_quota.items():
if type(values) is dict and "used" in values: client = app.client_manager.network
values['in_use'] = values.pop("used") if default:
network_quota[key] = values network_quota = client.get_quota_default(project_id)
return network_quota network_quota = _network_quota_to_dict(network_quota)
else: else:
return {} network_quota = client.get_quota(project_id, details=detail)
network_quota = _network_quota_to_dict(network_quota, detail=detail)
return network_quota
class ListQuota(command.Lister, BaseQuota): class ListQuota(command.Lister):
_description = _( _description = _(
"List quotas for all projects with non-default quota values or " "List quotas for all projects with non-default quota values or "
"list detailed quota information for requested project" "list detailed quota information 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))
if parsed_args.volume:
quotas.update(
self.get_volume_quota(
self.app.client_manager.volume,
parsed_args,
),
)
result = []
for resource, values in quotas.items():
# NOTE(slaweq): there is no detailed quotas info for some resources
# and it shouldn'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().get_parser(prog_name)
parser.add_argument( parser.add_argument(
'--project', '--project',
metavar='<project>', metavar='<project>',
help=_('List quotas for this project <project> (name or ID)'), help=_('List quotas for this project <project> (name or ID)'),
) )
# TODO(stephenfin): This doesn't belong here. We should put it into the
# '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=_('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(
@ -291,6 +268,69 @@ class ListQuota(command.Lister, BaseQuota):
) )
return parser return parser
def _get_detailed_quotas(self, parsed_args):
project_info = get_project(self.app, parsed_args.project)
project = project_info['id']
quotas = {}
if parsed_args.compute:
quotas.update(
get_compute_quotas(
self.app,
project,
detail=parsed_args.detail,
)
)
if parsed_args.network:
quotas.update(
get_network_quotas(
self.app,
project,
detail=parsed_args.detail,
)
)
if parsed_args.volume:
quotas.update(
get_volume_quotas(
self.app,
parsed_args,
detail=parsed_args.detail,
),
)
result = []
for resource, values in quotas.items():
# NOTE(slaweq): there is no detailed quotas info for some resources
# and it shouldn't be displayed here
if isinstance(values, dict):
result.append({
'resource': resource,
'in_use': values.get('in_use'),
'reserved': values.get('reserved'),
'limit': values.get('limit'),
})
columns = (
'resource',
'in_use',
'reserved',
'limit',
)
column_headers = (
'Resource',
'In Use',
'Reserved',
'Limit',
)
return (
column_headers,
(utils.get_dict_properties(s, columns) for s in result),
)
def take_action(self, parsed_args): def take_action(self, parsed_args):
result = [] result = []
project_ids = [] project_ids = []
@ -308,6 +348,7 @@ class ListQuota(command.Lister, BaseQuota):
if parsed_args.compute: if parsed_args.compute:
if parsed_args.detail: if parsed_args.detail:
return self._get_detailed_quotas(parsed_args) 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:
@ -365,14 +406,15 @@ class ListQuota(command.Lister, BaseQuota):
'Server Groups', 'Server Groups',
'Server Group Members', 'Server Group Members',
) )
return (column_headers, return (
(utils.get_dict_properties( column_headers,
s, columns, (utils.get_dict_properties(s, columns) for s in result),
) for s in result)) )
if parsed_args.volume: if parsed_args.volume:
if parsed_args.detail: if parsed_args.detail:
return self._get_detailed_quotas(parsed_args) return self._get_detailed_quotas(parsed_args)
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:
@ -417,14 +459,16 @@ class ListQuota(command.Lister, BaseQuota):
'Snapshots', 'Snapshots',
'Volumes', 'Volumes',
) )
return (column_headers,
(utils.get_dict_properties( return (
s, columns, column_headers,
) for s in result)) (utils.get_dict_properties(s, columns) for s in result),
)
if parsed_args.network: if parsed_args.network:
if parsed_args.detail: if parsed_args.detail:
return self._get_detailed_quotas(parsed_args) 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:
@ -475,10 +519,11 @@ class ListQuota(command.Lister, BaseQuota):
'Subnets', 'Subnets',
'Subnet Pools' 'Subnet Pools'
) )
return (column_headers,
(utils.get_dict_properties( return (
s, columns, column_headers,
) for s in result)) (utils.get_dict_properties(s, columns) for s in result),
)
return ((), ()) return ((), ())
@ -545,14 +590,16 @@ class SetQuota(common.NetDetectionMixin, command.Command):
parser.add_argument( parser.add_argument(
'--force', '--force',
action='store_true', action='store_true',
help=_('Force quota update (only supported by compute and ' help=_(
'network)') 'Force quota update (only supported by compute and network)'
),
) )
parser.add_argument( parser.add_argument(
'--check-limit', '--check-limit',
action='store_true', action='store_true',
help=_('Check quota limit when updating (only supported by ' help=_(
'network)') 'Check quota limit when updating (only supported by network)'
),
) )
return parser return parser
@ -631,14 +678,16 @@ class SetQuota(common.NetDetectionMixin, command.Command):
**network_kwargs) **network_kwargs)
class ShowQuota(command.ShowOne, BaseQuota): class ShowQuota(command.ShowOne):
_description = _( _description = _(
"Show quotas for project or class. Specify " "Show quotas for project or class. "
"``--os-compute-api-version 2.50`` or higher to see ``server-groups`` " "Specify ``--os-compute-api-version 2.50`` or higher to see "
"and ``server-group-members`` output for a given quota class.") "``server-groups`` and ``server-group-members`` output for a given "
"quota class."
)
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super(ShowQuota, self).get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument( parser.add_argument(
'project', 'project',
metavar='<project/class>', metavar='<project/class>',
@ -658,29 +707,40 @@ class ShowQuota(command.ShowOne, BaseQuota):
dest='default', dest='default',
action='store_true', action='store_true',
default=False, default=False,
help=_('Show default quotas for <project>') help=_('Show default quotas for <project>'),
) )
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
project = parsed_args.project
compute_client = self.app.client_manager.compute if not parsed_args.quota_class:
volume_client = self.app.client_manager.volume project_info = get_project(self.app, parsed_args.project)
# NOTE(dtroyer): These quota API calls do not validate the project project = project_info['id']
# or class arguments and return what appears to be
# the default quota values if the project or class # NOTE(dtroyer): These quota API calls do not validate the project or
# does not exist. If this is determined to be the # class arguments and return what appears to be the default quota
# intended behaviour of the API we will validate # values if the project or class does not exist. If this is determined
# the argument with Identity ourselves later. # to be the intended behaviour of the API we will validate the argument
compute_quota_info = self.get_compute_quota(compute_client, # with Identity ourselves later.
parsed_args) compute_quota_info = get_compute_quotas(
volume_quota_info = self.get_volume_quota(volume_client, self.app,
parsed_args) project,
network_quota_info = self.get_network_quota(parsed_args) quota_class=parsed_args.quota_class,
# NOTE(reedip): Remove the below check once requirement for default=parsed_args.default,
# Openstack SDK is fixed to version 0.9.12 and above )
if type(network_quota_info) is not dict: volume_quota_info = get_volume_quotas(
network_quota_info = network_quota_info.to_dict() self.app,
project,
quota_class=parsed_args.quota_class,
default=parsed_args.default,
)
network_quota_info = get_network_quotas(
self.app,
project,
quota_class=parsed_args.quota_class,
default=parsed_args.default,
)
info = {} info = {}
info.update(compute_quota_info) info.update(compute_quota_info)
@ -693,20 +753,27 @@ class ShowQuota(command.ShowOne, BaseQuota):
# neutron is enabled, quotas of these three resources # neutron is enabled, quotas of these three resources
# in nova will be replaced by neutron's. # in nova will be replaced by neutron's.
for k, v in itertools.chain( for k, v in itertools.chain(
COMPUTE_QUOTAS.items(), NOVA_NETWORK_QUOTAS.items(), COMPUTE_QUOTAS.items(),
VOLUME_QUOTAS.items(), NETWORK_QUOTAS.items()): NOVA_NETWORK_QUOTAS.items(),
VOLUME_QUOTAS.items(),
NETWORK_QUOTAS.items(),
):
if not k == v and info.get(k) is not None: if not k == v and info.get(k) is not None:
info[v] = info[k] info[v] = info[k]
info.pop(k) info.pop(k)
# Handle project ID special as it only appears in output # Remove the 'location' field for resources from openstacksdk
if 'id' in info: if 'location' in info:
del info['location']
# Handle class or project ID specially as they only appear in output
if parsed_args.quota_class:
info.pop('id', None)
elif 'id' in info:
info['project'] = info.pop('id') info['project'] = info.pop('id')
if 'project_id' in info: if 'project_id' in info:
del info['project_id'] del info['project_id']
project_info = self._get_project(parsed_args) info['project_name'] = project_info['name']
project_name = project_info['name']
info['project_name'] = project_name
return zip(*sorted(info.items())) return zip(*sorted(info.items()))

View File

@ -1166,29 +1166,6 @@ class TestQuotaShow(TestQuota):
) )
self.assertNotCalled(self.network.get_quota_default) self.assertNotCalled(self.network.get_quota_default)
def test_network_quota_show_remove_empty(self):
arglist = [
self.projects[0].name,
]
verifylist = [
('project', self.projects[0].name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# First check that all regular values are returned
result = self.cmd.get_network_quota(parsed_args)
self.assertEqual(len(network_fakes.QUOTA), len(result))
# set 1 of the values to None, and verify it is not returned
orig_get_quota = self.network.get_quota
network_quotas = copy.copy(network_fakes.QUOTA)
network_quotas['healthmonitor'] = None
self.network.get_quota = mock.Mock(return_value=network_quotas)
result = self.cmd.get_network_quota(parsed_args)
self.assertEqual(len(network_fakes.QUOTA) - 1, len(result))
# Go back to default mock
self.network.get_quota = orig_get_quota
class TestQuotaDelete(TestQuota): class TestQuotaDelete(TestQuota):
"""Test cases for quota delete command""" """Test cases for quota delete command"""