From 75cba9d1cbdd7b14b0d507af27f896c6c45e713e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 19 Oct 2018 12:46:21 +0300 Subject: [PATCH] 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 --- doc/source/cli/command-objects/quota.rst | 10 + openstackclient/common/quota.py | 231 +++++++++++++----- .../tests/functional/common/test_quota.py | 32 +++ .../tests/unit/common/test_quota.py | 87 ++++++- .../tests/unit/compute/v2/fakes.py | 32 +++ .../tests/unit/network/v2/fakes.py | 23 ++ ...d-quota-informations-1755129e1c68a252.yaml | 6 + 7 files changed, 353 insertions(+), 68 deletions(-) create mode 100644 releasenotes/notes/list-detailed-quota-informations-1755129e1c68a252.yaml diff --git a/doc/source/cli/command-objects/quota.rst b/doc/source/cli/command-objects/quota.rst index f39536af8e..c538cfb3dd 100644 --- a/doc/source/cli/command-objects/quota.rst +++ b/doc/source/cli/command-objects/quota.rst @@ -17,6 +17,8 @@ List quotas for all projects with non-default quota values openstack quota list --compute | --network | --volume + [--project ] + [--detail] .. option:: --network @@ -30,6 +32,14 @@ List quotas for all projects with non-default quota values List volume quotas +.. option:: --project + + List quotas for this project (name or ID) + +.. option:: --detail + + Show details about quotas usage + quota set --------- diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 282ea4284d..dba6873ff9 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -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='', + help=_('List quotas for this 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 diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 76c69a4d03..859422812b 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -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") diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 1a3da31d78..4f9e321b92 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -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) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 234bbd9b80..37535aa017 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -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""" diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 28e92d1196..ee0919fdd5 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -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 diff --git a/releasenotes/notes/list-detailed-quota-informations-1755129e1c68a252.yaml b/releasenotes/notes/list-detailed-quota-informations-1755129e1c68a252.yaml new file mode 100644 index 0000000000..7dbe202cdd --- /dev/null +++ b/releasenotes/notes/list-detailed-quota-informations-1755129e1c68a252.yaml @@ -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 `_]