From 735896eb1ad4543823008693c9d337691512f8bf Mon Sep 17 00:00:00 2001 From: Lance Bragstad Date: Wed, 13 Jun 2018 19:33:08 +0000 Subject: [PATCH] Implement support for project limits This commit let's users manage limits via the command line. bp unified-limits Change-Id: I7c44bbb60557378b66c5c43a7ba917f40dc2b633 --- doc/source/cli/command-objects/limit.rst | 128 ++++++ openstackclient/identity/v3/limit.py | 238 +++++++++++ .../tests/functional/identity/v3/common.py | 43 ++ .../functional/identity/v3/test_limit.py | 192 +++++++++ .../tests/unit/identity/v3/fakes.py | 25 ++ .../tests/unit/identity/v3/test_limit.py | 382 ++++++++++++++++++ .../bp-unified-limits-6c5fdb1c26805d86.yaml | 7 + setup.cfg | 6 + 8 files changed, 1021 insertions(+) create mode 100644 doc/source/cli/command-objects/limit.rst create mode 100644 openstackclient/identity/v3/limit.py create mode 100644 openstackclient/tests/functional/identity/v3/test_limit.py create mode 100644 openstackclient/tests/unit/identity/v3/test_limit.py create mode 100644 releasenotes/notes/bp-unified-limits-6c5fdb1c26805d86.yaml diff --git a/doc/source/cli/command-objects/limit.rst b/doc/source/cli/command-objects/limit.rst new file mode 100644 index 000000000..71cf2a420 --- /dev/null +++ b/doc/source/cli/command-objects/limit.rst @@ -0,0 +1,128 @@ +===== +limit +===== + +Identity v3 + +Limits are used to specify project-specific limits thresholds of resources. + +limit create +------------ + +Create a new limit + +.. program:: limit create +.. code:: bash + + openstack limit create + [--description ] + [--region ] + --project + --service + --resource-limit + + +.. option:: --description + + Useful description of the limit or its purpose + +.. option:: --region + + Region that the limit should be applied to + +.. describe:: --project + + The project that the limit applies to (required) + +.. describe:: --service + + The service that is responsible for the resource being limited (required) + +.. describe:: --resource-limit + + The limit to apply to the project (required) + +.. describe:: + + The name of the resource to limit (e.g. cores or volumes) + +limit delete +------------ + +Delete project-specific limit(s) + +.. program:: limit delete +.. code:: bash + + openstack limit delete + [ ...] + +.. describe:: + + Limit(s) to delete (ID) + +limit list +---------- + +List project-specific limits + +.. program:: limit list +.. code:: bash + + openstack limit list + [--service ] + [--resource-name ] + [--region ] + +.. option:: --service + + The service to filter the response by (name or ID) + +.. option:: --resource-name + + The name of the resource to filter the response by + +.. option:: --region + + The region name to filter the response by + +limit show +---------- + +Display details about a limit + +.. program:: limit show +.. code:: bash + + openstack limit show + + +.. describe:: + + Limit to display (ID) + +limit set +--------- + +Update a limit + +.. program:: limit show +.. code:: bash + + openstack limit set + [--description ] + [--resource-limit ] + + + +.. option:: --description + + Useful description of the limit or its purpose + +.. option:: --resource-limit + + The limit to apply to the project + +.. describe:: + + Limit to update (ID) diff --git a/openstackclient/identity/v3/limit.py b/openstackclient/identity/v3/limit.py new file mode 100644 index 000000000..c6f1cb1fb --- /dev/null +++ b/openstackclient/identity/v3/limit.py @@ -0,0 +1,238 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Limits action implementations.""" + +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ +from openstackclient.identity import common as common_utils + +LOG = logging.getLogger(__name__) + + +class CreateLimit(command.ShowOne): + _description = _("Create a limit") + + def get_parser(self, prog_name): + parser = super(CreateLimit, self).get_parser(prog_name) + parser.add_argument( + '--description', + metavar='', + help=_('Description of the limit'), + ) + parser.add_argument( + '--region', + metavar='', + help=_('Region for the limit to affect.'), + ) + parser.add_argument( + '--project', + metavar='', + required=True, + help=_('Project to associate the resource limit to'), + ) + parser.add_argument( + '--service', + metavar='', + required=True, + help=_('Service responsible for the resource to limit'), + ) + parser.add_argument( + '--resource-limit', + metavar='', + required=True, + type=int, + help=_('The resource limit for the project to assume'), + ) + parser.add_argument( + 'resource_name', + metavar='', + help=_('The name of the resource to limit'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + project = common_utils.find_project( + identity_client, parsed_args.project + ) + service = common_utils.find_service( + identity_client, parsed_args.service + ) + region = None + if parsed_args.region: + region = utils.find_resource( + identity_client.regions, parsed_args.region + ) + + limit = identity_client.limits.create( + project, + service, + parsed_args.resource_name, + parsed_args.resource_limit, + description=parsed_args.description, + region=region + ) + + limit._info.pop('links', None) + return zip(*sorted(six.iteritems(limit._info))) + + +class ListLimit(command.Lister): + _description = _("List limits") + + def get_parser(self, prog_name): + parser = super(ListLimit, self).get_parser(prog_name) + parser.add_argument( + '--service', + metavar='', + help=_('Service responsible for the resource to limit'), + ) + parser.add_argument( + '--resource-name', + metavar='', + dest='resource_name', + help=_('The name of the resource to limit'), + ) + parser.add_argument( + '--region', + metavar='', + help=_('Region for the registered limit to affect.'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + service = None + if parsed_args.service: + service = common_utils.find_service( + identity_client, parsed_args.service + ) + region = None + if parsed_args.region: + region = utils.find_resource( + identity_client.regions, parsed_args.region + ) + + limits = identity_client.limits.list( + service=service, + resource_name=parsed_args.resource_name, + region=region + ) + + columns = ( + 'ID', 'Project ID', 'Service ID', 'Resource Name', + 'Resource Limit', 'Description', 'Region ID' + ) + return ( + columns, + (utils.get_item_properties(s, columns) for s in limits), + ) + + +class ShowLimit(command.ShowOne): + _description = _("Display limit details") + + def get_parser(self, prog_name): + parser = super(ShowLimit, self).get_parser(prog_name) + parser.add_argument( + 'limit_id', + metavar='', + help=_('Limit to display (ID)'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + limit = identity_client.limits.get(parsed_args.limit_id) + limit._info.pop('links', None) + return zip(*sorted(six.iteritems(limit._info))) + + +class SetLimit(command.ShowOne): + _description = _("Update information about a limit") + + def get_parser(self, prog_name): + parser = super(SetLimit, self).get_parser(prog_name) + parser.add_argument( + 'limit_id', + metavar='', + help=_('Limit to update (ID)'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('Description of the limit'), + ) + parser.add_argument( + '--resource-limit', + metavar='', + dest='resource_limit', + type=int, + help=_('The resource limit for the project to assume'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + limit = identity_client.limits.update( + parsed_args.limit_id, + description=parsed_args.description, + resource_limit=parsed_args.resource_limit + ) + + limit._info.pop('links', None) + + return zip(*sorted(six.iteritems(limit._info))) + + +class DeleteLimit(command.Command): + _description = _("Delete a limit") + + def get_parser(self, prog_name): + parser = super(DeleteLimit, self).get_parser(prog_name) + parser.add_argument( + 'limit_id', + metavar='', + nargs="+", + help=_('Limit to delete (ID)'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + errors = 0 + for limit_id in parsed_args.limit_id: + try: + identity_client.limits.delete(limit_id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete limit with ID " + "'%(id)s': %(e)s"), + {'id': limit_id, 'e': e}) + + if errors > 0: + total = len(parsed_args.limit_id) + msg = (_("%(errors)s of %(total)s limits failed to " + "delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py index 525a31a21..58468bc7e 100644 --- a/openstackclient/tests/functional/identity/v3/common.py +++ b/openstackclient/tests/functional/identity/v3/common.py @@ -59,6 +59,10 @@ class IdentityTests(base.TestCase): REGISTERED_LIMIT_LIST_HEADERS = ['ID', 'Service ID', 'Resource Name', 'Default Limit', 'Description', 'Region ID'] + LIMIT_FIELDS = ['id', 'project_id', 'service_id', 'resource_name', + 'resource_limit', 'description', 'region_id'] + LIMIT_LIST_HEADERS = ['ID', 'Project ID', 'Service ID', 'Resource Name', + 'Resource Limit', 'Description', 'Region ID'] @classmethod def setUpClass(cls): @@ -356,3 +360,42 @@ class IdentityTests(base.TestCase): for k, v in d.iteritems(): if k == key: return v + + def _create_dummy_limit(self, add_clean_up=True): + registered_limit_id = self._create_dummy_registered_limit() + + raw_output = self.openstack( + 'registered limit show %s' % registered_limit_id + ) + items = self.parse_show(raw_output) + resource_name = self._extract_value_from_items('resource_name', items) + service_id = self._extract_value_from_items('service_id', items) + resource_limit = 15 + + project_name = self._create_dummy_project() + raw_output = self.openstack('project show %s' % project_name) + items = self.parse_show(raw_output) + project_id = self._extract_value_from_items('id', items) + + params = { + 'project_id': project_id, + 'service_id': service_id, + 'resource_name': resource_name, + 'resource_limit': resource_limit + } + + raw_output = self.openstack( + 'limit create' + ' --project %(project_id)s' + ' --service %(service_id)s' + ' --resource-limit %(resource_limit)s' + ' %(resource_name)s' % params + ) + items = self.parse_show(raw_output) + limit_id = self._extract_value_from_items('id', items) + + if add_clean_up: + self.addCleanup(self.openstack, 'limit delete %s' % limit_id) + + self.assert_show_fields(items, self.LIMIT_FIELDS) + return limit_id diff --git a/openstackclient/tests/functional/identity/v3/test_limit.py b/openstackclient/tests/functional/identity/v3/test_limit.py new file mode 100644 index 000000000..03bcb06e4 --- /dev/null +++ b/openstackclient/tests/functional/identity/v3/test_limit.py @@ -0,0 +1,192 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.common.utils import data_utils + +from openstackclient.tests.functional.identity.v3 import common + + +class LimitTestCase(common.IdentityTests): + + def test_limit_create_with_service_name(self): + registered_limit_id = self._create_dummy_registered_limit() + raw_output = self.openstack( + 'registered limit show %s' % registered_limit_id + ) + items = self.parse_show(raw_output) + service_id = self._extract_value_from_items('service_id', items) + resource_name = self._extract_value_from_items('resource_name', items) + + raw_output = self.openstack('service show %s' % service_id) + items = self.parse_show(raw_output) + service_name = self._extract_value_from_items('name', items) + + project_name = self._create_dummy_project() + raw_output = self.openstack('project show %s' % project_name) + items = self.parse_show(raw_output) + project_id = self._extract_value_from_items('id', items) + + params = { + 'project_id': project_id, + 'service_name': service_name, + 'resource_name': resource_name, + 'resource_limit': 15 + } + raw_output = self.openstack( + 'limit create' + ' --project %(project_id)s' + ' --service %(service_name)s' + ' --resource-limit %(resource_limit)s' + ' %(resource_name)s' % params + ) + items = self.parse_show(raw_output) + limit_id = self._extract_value_from_items('id', items) + self.addCleanup(self.openstack, 'limit delete %s' % limit_id) + + self.assert_show_fields(items, self.LIMIT_FIELDS) + + def test_limit_create_with_project_name(self): + registered_limit_id = self._create_dummy_registered_limit() + raw_output = self.openstack( + 'registered limit show %s' % registered_limit_id + ) + items = self.parse_show(raw_output) + service_id = self._extract_value_from_items('service_id', items) + resource_name = self._extract_value_from_items('resource_name', items) + + raw_output = self.openstack('service show %s' % service_id) + items = self.parse_show(raw_output) + service_name = self._extract_value_from_items('name', items) + + project_name = self._create_dummy_project() + + params = { + 'project_name': project_name, + 'service_name': service_name, + 'resource_name': resource_name, + 'resource_limit': 15 + } + raw_output = self.openstack( + 'limit create' + ' --project %(project_name)s' + ' --service %(service_name)s' + ' --resource-limit %(resource_limit)s' + ' %(resource_name)s' % params + ) + items = self.parse_show(raw_output) + limit_id = self._extract_value_from_items('id', items) + self.addCleanup(self.openstack, 'limit delete %s' % limit_id) + + self.assert_show_fields(items, self.LIMIT_FIELDS) + registered_limit_id = self._create_dummy_registered_limit() + + def test_limit_create_with_service_id(self): + self._create_dummy_limit() + + def test_limit_create_with_project_id(self): + self._create_dummy_limit() + + def test_limit_create_with_options(self): + registered_limit_id = self._create_dummy_registered_limit() + region_id = self._create_dummy_region() + + params = { + 'region_id': region_id, + 'registered_limit_id': registered_limit_id + } + + raw_output = self.openstack( + 'registered limit set' + ' %(registered_limit_id)s' + ' --region %(region_id)s' % params + ) + items = self.parse_show(raw_output) + service_id = self._extract_value_from_items('service_id', items) + resource_name = self._extract_value_from_items('resource_name', items) + + project_name = self._create_dummy_project() + raw_output = self.openstack('project show %s' % project_name) + items = self.parse_show(raw_output) + project_id = self._extract_value_from_items('id', items) + description = data_utils.arbitrary_string() + + params = { + 'project_id': project_id, + 'service_id': service_id, + 'resource_name': resource_name, + 'resource_limit': 15, + 'region_id': region_id, + 'description': description + } + raw_output = self.openstack( + 'limit create' + ' --project %(project_id)s' + ' --service %(service_id)s' + ' --resource-limit %(resource_limit)s' + ' --region %(region_id)s' + ' --description %(description)s' + ' %(resource_name)s' % params + ) + items = self.parse_show(raw_output) + limit_id = self._extract_value_from_items('id', items) + self.addCleanup(self.openstack, 'limit delete %s' % limit_id) + + self.assert_show_fields(items, self.LIMIT_FIELDS) + + def test_limit_show(self): + limit_id = self._create_dummy_limit() + raw_output = self.openstack('limit show %s' % limit_id) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.LIMIT_FIELDS) + + def test_limit_set_description(self): + limit_id = self._create_dummy_limit() + + params = { + 'description': data_utils.arbitrary_string(), + 'limit_id': limit_id + } + + raw_output = self.openstack( + 'limit set' + ' --description %(description)s' + ' %(limit_id)s' % params + ) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.LIMIT_FIELDS) + + def test_limit_set_resource_limit(self): + limit_id = self._create_dummy_limit() + + params = { + 'resource_limit': 5, + 'limit_id': limit_id + } + + raw_output = self.openstack( + 'limit set' + ' --resource-limit %(resource_limit)s' + ' %(limit_id)s' % params + ) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.LIMIT_FIELDS) + + def test_limit_list(self): + self._create_dummy_limit() + raw_output = self.openstack('limit list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.LIMIT_LIST_HEADERS) + + def test_limit_delete(self): + limit_id = self._create_dummy_limit(add_clean_up=False) + raw_output = self.openstack('limit delete %s' % limit_id) + self.assertEqual(0, len(raw_output)) diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index 3cae45157..27ee9fd02 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -507,6 +507,29 @@ REGISTERED_LIMIT_OPTIONS = { 'region_id': region_id } +limit_id = 'limit-id' +limit_resource_limit = 15 +limit_description = 'limit of foobars' +limit_resource_name = 'foobars' +LIMIT = { + 'id': limit_id, + 'project_id': project_id, + 'resource_limit': limit_resource_limit, + 'resource_name': limit_resource_name, + 'service_id': service_id, + 'description': None, + 'region_id': None +} +LIMIT_OPTIONS = { + 'id': limit_id, + 'project_id': project_id, + 'resource_limit': limit_resource_limit, + 'resource_name': limit_resource_name, + 'service_id': service_id, + 'description': limit_description, + 'region_id': region_id +} + def fake_auth_ref(fake_token, fake_service=None): """Create an auth_ref using keystoneauth's fixtures""" @@ -601,6 +624,8 @@ class FakeIdentityv3Client(object): self.inference_rules.resource_class = fakes.FakeResource(None, {}) self.registered_limits = mock.Mock() self.registered_limits.resource_class = fakes.FakeResource(None, {}) + self.limits = mock.Mock() + self.limits.resource_class = fakes.FakeResource(None, {}) class FakeFederationManager(object): diff --git a/openstackclient/tests/unit/identity/v3/test_limit.py b/openstackclient/tests/unit/identity/v3/test_limit.py new file mode 100644 index 000000000..44c0358d8 --- /dev/null +++ b/openstackclient/tests/unit/identity/v3/test_limit.py @@ -0,0 +1,382 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from keystoneauth1.exceptions import http as ksa_exceptions +from osc_lib import exceptions + +from openstackclient.identity.v3 import limit +from openstackclient.tests.unit import fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes + + +class TestLimit(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestLimit, self).setUp() + + identity_manager = self.app.client_manager.identity + + self.limit_mock = identity_manager.limits + + self.services_mock = identity_manager.services + self.services_mock.reset_mock() + + self.projects_mock = identity_manager.projects + self.projects_mock.reset_mock() + + self.regions_mock = identity_manager.regions + self.regions_mock.reset_mock() + + +class TestLimitCreate(TestLimit): + + def setUp(self): + super(TestLimitCreate, self).setUp() + + self.service = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True + ) + self.services_mock.get.return_value = self.service + + self.project = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True + ) + self.projects_mock.get.return_value = self.project + + self.region = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.REGION), + loaded=True + ) + self.regions_mock.get.return_value = self.region + + self.cmd = limit.CreateLimit(self.app, None) + + def test_limit_create_without_options(self): + self.limit_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.LIMIT), + loaded=True + ) + + resource_limit = 15 + arglist = [ + '--project', identity_fakes.project_id, + '--service', identity_fakes.service_id, + '--resource-limit', str(resource_limit), + identity_fakes.limit_resource_name + ] + verifylist = [ + ('project', identity_fakes.project_id), + ('service', identity_fakes.service_id), + ('resource_name', identity_fakes.limit_resource_name), + ('resource_limit', resource_limit) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + kwargs = {'description': None, 'region': None} + self.limit_mock.create.assert_called_with( + self.project, + self.service, + identity_fakes.limit_resource_name, + resource_limit, + **kwargs + ) + + collist = ('description', 'id', 'project_id', 'region_id', + 'resource_limit', 'resource_name', 'service_id') + self.assertEqual(collist, columns) + datalist = ( + None, + identity_fakes.limit_id, + identity_fakes.project_id, + None, + resource_limit, + identity_fakes.limit_resource_name, + identity_fakes.service_id + ) + self.assertEqual(datalist, data) + + def test_limit_create_with_options(self): + self.limit_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.LIMIT_OPTIONS), + loaded=True + ) + + resource_limit = 15 + arglist = [ + '--project', identity_fakes.project_id, + '--service', identity_fakes.service_id, + '--resource-limit', str(resource_limit), + '--region', identity_fakes.region_id, + '--description', identity_fakes.limit_description, + identity_fakes.limit_resource_name + ] + verifylist = [ + ('project', identity_fakes.project_id), + ('service', identity_fakes.service_id), + ('resource_name', identity_fakes.limit_resource_name), + ('resource_limit', resource_limit), + ('region', identity_fakes.region_id), + ('description', identity_fakes.limit_description) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'description': identity_fakes.limit_description, + 'region': self.region + } + self.limit_mock.create.assert_called_with( + self.project, + self.service, + identity_fakes.limit_resource_name, + resource_limit, + **kwargs + ) + + collist = ('description', 'id', 'project_id', 'region_id', + 'resource_limit', 'resource_name', 'service_id') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.limit_description, + identity_fakes.limit_id, + identity_fakes.project_id, + identity_fakes.region_id, + resource_limit, + identity_fakes.limit_resource_name, + identity_fakes.service_id + ) + self.assertEqual(datalist, data) + + +class TestLimitDelete(TestLimit): + + def setUp(self): + super(TestLimitDelete, self).setUp() + self.cmd = limit.DeleteLimit(self.app, None) + + def test_limit_delete(self): + self.limit_mock.delete.return_value = None + + arglist = [identity_fakes.limit_id] + verifylist = [ + ('limit_id', [identity_fakes.limit_id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.limit_mock.delete.assert_called_with( + identity_fakes.limit_id + ) + self.assertIsNone(result) + + def test_limit_delete_with_exception(self): + return_value = ksa_exceptions.NotFound() + self.limit_mock.delete.side_effect = return_value + + arglist = ['fake-limit-id'] + verifylist = [ + ('limit_id', ['fake-limit-id']) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 1 limits failed to delete.', str(e) + ) + + +class TestLimitShow(TestLimit): + + def setUp(self): + super(TestLimitShow, self).setUp() + + self.limit_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.LIMIT), + loaded=True + ) + + self.cmd = limit.ShowLimit(self.app, None) + + def test_limit_show(self): + arglist = [identity_fakes.limit_id] + verifylist = [('limit_id', identity_fakes.limit_id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.limit_mock.get.assert_called_with(identity_fakes.limit_id) + + collist = ( + 'description', 'id', 'project_id', 'region_id', 'resource_limit', + 'resource_name', 'service_id' + ) + self.assertEqual(collist, columns) + datalist = ( + None, + identity_fakes.limit_id, + identity_fakes.project_id, + None, + identity_fakes.limit_resource_limit, + identity_fakes.limit_resource_name, + identity_fakes.service_id + ) + self.assertEqual(datalist, data) + + +class TestLimitSet(TestLimit): + + def setUp(self): + super(TestLimitSet, self).setUp() + self.cmd = limit.SetLimit(self.app, None) + + def test_limit_set_description(self): + limit = copy.deepcopy(identity_fakes.LIMIT) + limit['description'] = identity_fakes.limit_description + self.limit_mock.update.return_value = fakes.FakeResource( + None, limit, loaded=True + ) + + arglist = [ + '--description', identity_fakes.limit_description, + identity_fakes.limit_id + ] + verifylist = [ + ('description', identity_fakes.limit_description), + ('limit_id', identity_fakes.limit_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.limit_mock.update.assert_called_with( + identity_fakes.limit_id, + description=identity_fakes.limit_description, + resource_limit=None + ) + + collist = ( + 'description', 'id', 'project_id', 'region_id', 'resource_limit', + 'resource_name', 'service_id' + ) + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.limit_description, + identity_fakes.limit_id, + identity_fakes.project_id, + None, + identity_fakes.limit_resource_limit, + identity_fakes.limit_resource_name, + identity_fakes.service_id + ) + self.assertEqual(datalist, data) + + def test_limit_set_resource_limit(self): + resource_limit = 20 + limit = copy.deepcopy(identity_fakes.LIMIT) + limit['resource_limit'] = resource_limit + self.limit_mock.update.return_value = fakes.FakeResource( + None, limit, loaded=True + ) + + arglist = [ + '--resource-limit', str(resource_limit), + identity_fakes.limit_id + ] + verifylist = [ + ('resource_limit', resource_limit), + ('limit_id', identity_fakes.limit_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.limit_mock.update.assert_called_with( + identity_fakes.limit_id, + description=None, + resource_limit=resource_limit + ) + + collist = ( + 'description', 'id', 'project_id', 'region_id', 'resource_limit', + 'resource_name', 'service_id' + ) + self.assertEqual(collist, columns) + datalist = ( + None, + identity_fakes.limit_id, + identity_fakes.project_id, + None, + resource_limit, + identity_fakes.limit_resource_name, + identity_fakes.service_id + ) + self.assertEqual(datalist, data) + + +class TestLimitList(TestLimit): + + def setUp(self): + super(TestLimitList, self).setUp() + + self.limit_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.LIMIT), + loaded=True + ) + ] + + self.cmd = limit.ListLimit(self.app, None) + + def test_limit_list(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.limit_mock.list.assert_called_with( + service=None, resource_name=None, region=None + ) + + collist = ( + 'ID', 'Project ID', 'Service ID', 'Resource Name', + 'Resource Limit', 'Description', 'Region ID' + ) + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.limit_id, + identity_fakes.project_id, + identity_fakes.service_id, + identity_fakes.limit_resource_name, + identity_fakes.limit_resource_limit, + None, + None + ), ) + self.assertEqual(datalist, tuple(data)) diff --git a/releasenotes/notes/bp-unified-limits-6c5fdb1c26805d86.yaml b/releasenotes/notes/bp-unified-limits-6c5fdb1c26805d86.yaml new file mode 100644 index 000000000..b00de40c6 --- /dev/null +++ b/releasenotes/notes/bp-unified-limits-6c5fdb1c26805d86.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + [`bp unified-limits `_] + Support has been added for managing project-specific limits in keystone via + the ``limit`` command. Limits define limits of resources for projects to + consume once a limit has been registered. diff --git a/setup.cfg b/setup.cfg index af7bbbf20..64a5533ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -268,6 +268,12 @@ openstack.identity.v3 = implied_role_delete = openstackclient.identity.v3.implied_role:DeleteImpliedRole implied_role_list = openstackclient.identity.v3.implied_role:ListImpliedRole + limit_create = openstackclient.identity.v3.limit:CreateLimit + limit_delete = openstackclient.identity.v3.limit:DeleteLimit + limit_list = openstackclient.identity.v3.limit:ListLimit + limit_set = openstackclient.identity.v3.limit:SetLimit + limit_show = openstackclient.identity.v3.limit:ShowLimit + mapping_create = openstackclient.identity.v3.mapping:CreateMapping mapping_delete = openstackclient.identity.v3.mapping:DeleteMapping mapping_list = openstackclient.identity.v3.mapping:ListMapping