diff --git a/doc/source/command-objects/quota.rst b/doc/source/command-objects/quota.rst index 70e28dd8c..f39536af8 100644 --- a/doc/source/command-objects/quota.rst +++ b/doc/source/command-objects/quota.rst @@ -7,6 +7,29 @@ single object with multiple properties. Block Storage v1, v2, Compute v2, Network v2 +quota list +---------- + +List quotas for all projects with non-default quota values + +.. program:: quota list +.. code:: bash + + openstack quota list + --compute | --network | --volume + +.. option:: --network + + List network quotas + +.. option:: --compute + + List compute quotas + +.. option:: --volume + + List volume quotas + quota set --------- diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 82fbf2bb0..ec4c8b51b 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -16,6 +16,7 @@ """Quota action implementations""" import itertools +import logging import sys from osc_lib.command import command @@ -25,6 +26,8 @@ import six from openstackclient.i18n import _ +LOG = logging.getLogger(__name__) + # List the quota items, map the internal argument name to the option # name that the user sees. @@ -78,6 +81,176 @@ NETWORK_QUOTAS = { 'l7policy': 'l7policies', } +NETWORK_KEYS = ['floating_ips', 'networks', 'rbac_policies', 'routers', + 'ports', 'security_group_rules', 'security_groups', + 'subnet_pools', 'subnets'] + + +def _xform_get_quota(data, value, keys): + res = [] + res_info = {} + for key in keys: + res_info[key] = getattr(data, key, '') + + res_info['id'] = value + res.append(res_info) + return res + + +class ListQuota(command.Lister): + _description = _("List quotas for all projects " + "with non-default quota values") + + def get_parser(self, prog_name): + parser = super(ListQuota, self).get_parser(prog_name) + option = parser.add_mutually_exclusive_group(required=True) + option.add_argument( + '--compute', + action='store_true', + default=False, + help=_('List compute quota'), + ) + option.add_argument( + '--volume', + action='store_true', + default=False, + help=_('List volume quota'), + ) + option.add_argument( + '--network', + action='store_true', + default=False, + help=_('List network quota'), + ) + return parser + + def take_action(self, parsed_args): + projects = self.app.client_manager.identity.projects.list() + result = [] + project_ids = [getattr(p, 'id', '') for p in projects] + + if parsed_args.compute: + compute_client = self.app.client_manager.compute + for p in project_ids: + data = compute_client.quotas.get(p) + result_data = _xform_get_quota(data, p, + COMPUTE_QUOTAS.keys()) + default_data = compute_client.quotas.defaults(p) + result_default = _xform_get_quota(default_data, + p, + COMPUTE_QUOTAS.keys()) + if result_default != result_data: + result += result_data + + columns = ( + 'id', + 'cores', + 'fixed_ips', + 'injected_files', + 'injected_file_content_bytes', + 'injected_file_path_bytes', + 'instances', + 'key_pairs', + 'metadata_items', + 'ram', + 'server_groups', + 'server_group_members', + ) + column_headers = ( + 'Project ID', + 'Cores', + 'Fixed IPs', + 'Injected Files', + 'Injected File Content Bytes', + 'Injected File Path Bytes', + 'Instances', + 'Key Pairs', + 'Metadata Items', + 'Ram', + 'Server Groups', + 'Server Group Members', + ) + return (column_headers, + (utils.get_dict_properties( + s, columns, + ) for s in result)) + if parsed_args.volume: + volume_client = self.app.client_manager.volume + for p in project_ids: + data = volume_client.quotas.get(p) + result_data = _xform_get_quota(data, p, + VOLUME_QUOTAS.keys()) + default_data = volume_client.quotas.defaults(p) + result_default = _xform_get_quota(default_data, + p, + VOLUME_QUOTAS.keys()) + if result_default != result_data: + result += result_data + + columns = ( + 'id', + 'backups', + 'backup_gigabytes', + 'gigabytes', + 'per_volume_gigabytes', + 'snapshots', + 'volumes', + ) + column_headers = ( + 'Project ID', + 'Backups', + 'Backup Gigabytes', + 'Gigabytes', + 'Per Volume Gigabytes', + 'Snapshots', + 'Volumes', + ) + return (column_headers, + (utils.get_dict_properties( + s, columns, + ) for s in result)) + if parsed_args.network: + client = self.app.client_manager.network + for p in project_ids: + data = client.get_quota(p) + result_data = _xform_get_quota(data, p, NETWORK_KEYS) + default_data = client.get_quota_default(p) + result_default = _xform_get_quota(default_data, + p, NETWORK_KEYS) + if result_default != result_data: + result += result_data + + columns = ( + 'id', + 'floating_ips', + 'networks', + 'ports', + 'rbac_policies', + 'routers', + 'security_groups', + 'security_group_rules', + 'subnets', + 'subnet_pools', + ) + column_headers = ( + 'Project ID', + 'Floating IPs', + 'Networks', + 'Ports', + 'RBAC Policies', + 'Routers', + 'Security Groups', + 'Security Group Rules', + 'Subnets', + 'Subnet Pools' + ) + return (column_headers, + (utils.get_dict_properties( + s, columns, + ) for s in result)) + + return ((), ()) + class SetQuota(command.Command): _description = _("Set quotas for project or class") diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index c1de9aa92..8092b3cee 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -25,6 +25,27 @@ class QuotaTests(base.TestCase): cls.PROJECT_NAME =\ cls.get_openstack_configuration_value('auth.project_name') + def test_quota_list_network_option(self): + self.openstack('quota set --networks 40 ' + + self.PROJECT_NAME) + raw_output = self.openstack('quota list --network') + self.assertIsNotNone(raw_output) + self.assertIn("40", raw_output) + + def test_quota_list_compute_option(self): + self.openstack('quota set --instances 40 ' + + self.PROJECT_NAME) + raw_output = self.openstack('quota list --compute') + self.assertIsNotNone(raw_output) + self.assertIn("40", raw_output) + + def test_quota_list_volume_option(self): + self.openstack('quota set --backups 40 ' + + self.PROJECT_NAME) + raw_output = self.openstack('quota list --volume') + self.assertIsNotNone(raw_output) + self.assertIn("40", raw_output) + def test_quota_set(self): self.openstack('quota set --instances 11 --volumes 11 --networks 11 ' + self.PROJECT_NAME) diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 7dd233732..63f6435f4 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -17,6 +17,7 @@ from openstackclient.common import quota from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes @@ -518,3 +519,161 @@ class TestQuotaShow(TestQuota): self.network.get_quota.assert_called_once_with( identity_fakes.project_id) self.assertNotCalled(self.network.get_quota_default) + + +class TestQuotaList(TestQuota): + """Test cases for quota list command""" + + project = identity_fakes_v3.FakeProject.create_one_project() + + quota_list = network_fakes.FakeQuota.create_one_net_quota() + quota_list1 = compute_fakes.FakeQuota.create_one_comp_quota() + quota_list2 = volume_fakes.FakeQuota.create_one_vol_quota() + + default_quota = network_fakes.FakeQuota.create_one_default_net_quota() + default_quota1 = compute_fakes.FakeQuota.create_one_default_comp_quota() + default_quota2 = volume_fakes.FakeQuota.create_one_default_vol_quota() + + reference_data = (project.id, + quota_list.floating_ips, + quota_list.networks, + quota_list.ports, + quota_list.rbac_policies, + quota_list.routers, + quota_list.security_groups, + quota_list.security_group_rules, + quota_list.subnets, + quota_list.subnet_pools) + + comp_reference_data = (project.id, + quota_list1.cores, + quota_list1.fixed_ips, + quota_list1.injected_files, + quota_list1.injected_file_content_bytes, + quota_list1.injected_file_path_bytes, + quota_list1.instances, + quota_list1.key_pairs, + quota_list1.metadata_items, + quota_list1.ram, + quota_list1.server_groups, + quota_list1.server_group_members) + + vol_reference_data = (project.id, + quota_list2.backups, + quota_list2.backup_gigabytes, + quota_list2.gigabytes, + quota_list2.per_volume_gigabytes, + quota_list2.snapshots, + quota_list2.volumes) + + net_column_header = ( + 'Project ID', + 'Floating IPs', + 'Networks', + 'Ports', + 'RBAC Policies', + 'Routers', + 'Security Groups', + 'Security Group Rules', + 'Subnets', + 'Subnet Pools' + ) + + comp_column_header = ( + 'Project ID', + 'Cores', + 'Fixed IPs', + 'Injected Files', + 'Injected File Content Bytes', + 'Injected File Path Bytes', + 'Instances', + 'Key Pairs', + 'Metadata Items', + 'Ram', + 'Server Groups', + 'Server Group Members', + ) + + vol_column_header = ( + 'Project ID', + 'Backups', + 'Backup Gigabytes', + 'Gigabytes', + 'Per Volume Gigabytes', + 'Snapshots', + 'Volumes', + ) + + def setUp(self): + super(TestQuotaList, self).setUp() + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + self.identity = self.app.client_manager.identity + self.identity.tenants.list = mock.Mock(return_value=[self.project]) + + self.network = self.app.client_manager.network + self.compute = self.app.client_manager.compute + self.volume = self.app.client_manager.volume + + self.network.get_quota = mock.Mock(return_value=self.quota_list) + self.compute.quotas.get = mock.Mock(return_value=self.quota_list1) + self.volume.quotas.get = mock.Mock(return_value=self.quota_list2) + + self.network.get_quota_default = mock.Mock( + return_value=self.default_quota) + self.compute.quotas.defaults = mock.Mock( + return_value=self.default_quota1) + self.volume.quotas.defaults = mock.Mock( + return_value=self.default_quota2) + + self.cmd = quota.ListQuota(self.app, None) + + def test_quota_list_network(self): + arglist = [ + '--network' + ] + verifylist = [ + ('network', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.net_column_header, columns) + + self.assertEqual(self.reference_data, list(data)[0]) + + def test_quota_list_compute(self): + arglist = [ + '--compute' + ] + verifylist = [ + ('compute', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.comp_column_header, columns) + + self.assertEqual(self.comp_reference_data, list(data)[0]) + + def test_quota_list_volume(self): + arglist = [ + '--volume' + ] + verifylist = [ + ('volume', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.vol_column_header, columns) + + self.assertEqual(self.vol_reference_data, list(data)[0]) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index bbb770bbb..4a1948592 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1357,3 +1357,67 @@ class FakeUsage(object): usages.append(FakeUsage.create_one_usage(attrs)) return usages + + +class FakeQuota(object): + """Fake quota""" + + @staticmethod + def create_one_comp_quota(attrs=None): + """Create one quota""" + + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'cores': 20, + 'fixed_ips': 30, + 'injected_files': 100, + 'injected_file_content_bytes': 10240, + 'injected_file_path_bytes': 255, + 'instances': 50, + 'key_pairs': 20, + 'metadata_items': 10, + 'ram': 51200, + 'server_groups': 10, + 'server_group_members': 10 + } + + quota_attrs.update(attrs) + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + + quota.project_id = quota_attrs['id'] + + return quota + + @staticmethod + def create_one_default_comp_quota(attrs=None): + """Crate one quota""" + + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'cores': 10, + 'fixed_ips': 10, + 'injected_files': 100, + 'injected_file_content_bytes': 10240, + 'injected_file_path_bytes': 255, + 'instances': 20, + 'key_pairs': 20, + 'metadata_items': 10, + 'ram': 51200, + 'server_groups': 10, + 'server_group_members': 10 + } + + quota_attrs.update(attrs) + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + + quota.project_id = quota_attrs['id'] + + return quota diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index fed4b788b..d3685409a 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1629,3 +1629,53 @@ class FakeNetworkServiceProvider(object): create_one_network_service_provider( attrs)) return service_providers + + +class FakeQuota(object): + """Fake quota""" + + @staticmethod + def create_one_net_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'floating_ips': 20, + 'networks': 25, + 'ports': 11, + 'rbac_policies': 15, + 'routers': 40, + 'security_groups': 10, + 'security_group_rules': 100, + 'subnets': 20, + 'subnet_pools': 30} + + quota_attrs.update(attrs) + + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + return quota + + @staticmethod + def create_one_default_net_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'floatingip': 30, + 'network': 20, + 'port': 10, + 'rbac_policy': 25, + 'router': 30, + 'security_group': 30, + 'security_group_rule': 200, + 'subnet': 10, + 'subnetpool': 20} + + quota_attrs.update(attrs) + + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + return quota diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index d54faec7b..d321c71a2 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -954,3 +954,53 @@ class FakeType(object): info=copy.deepcopy(encryption_info), loaded=True) return encryption_type + + +class FakeQuota(object): + """Fake quota""" + + @staticmethod + def create_one_vol_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'backups': 100, + 'backup_gigabytes': 100, + 'gigabytes': 10, + 'per_volume_gigabytes': 10, + 'snapshots': 0, + 'volumes': 10} + + quota_attrs.update(attrs) + + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + quota.project_id = quota_attrs['id'] + + return quota + + @staticmethod + def create_one_default_vol_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'backups': 100, + 'backup_gigabytes': 100, + 'gigabytes': 100, + 'per_volume_gigabytes': 100, + 'snapshots': 100, + 'volumes': 100} + + quota_attrs.update(attrs) + + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + quota.project_id = quota_attrs['id'] + + return quota diff --git a/releasenotes/notes/add-quota-list-command-0d865fac61db2430.yaml b/releasenotes/notes/add-quota-list-command-0d865fac61db2430.yaml new file mode 100644 index 000000000..49a917b57 --- /dev/null +++ b/releasenotes/notes/add-quota-list-command-0d865fac61db2430.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``quota list`` command with ``--compute``, ``--volume`` + and ``--network`` options. + [Blueprint `quota-list `_] diff --git a/setup.cfg b/setup.cfg index da5107b57..996af462e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ openstack.common = configuration_show = openstackclient.common.configuration:ShowConfiguration extension_list = openstackclient.common.extension:ListExtension limits_show = openstackclient.common.limits:ShowLimits + quota_list = openstackclient.common.quota:ListQuota quota_set = openstackclient.common.quota:SetQuota quota_show = openstackclient.common.quota:ShowQuota