Browse Source

Merge "Implement support for project limits"

tags/3.16.0
Zuul 10 months ago
parent
commit
6469d86522

+ 128
- 0
doc/source/cli/command-objects/limit.rst View File

@@ -0,0 +1,128 @@
1
+=====
2
+limit
3
+=====
4
+
5
+Identity v3
6
+
7
+Limits are used to specify project-specific limits thresholds of resources.
8
+
9
+limit create
10
+------------
11
+
12
+Create a new limit
13
+
14
+.. program:: limit create
15
+.. code:: bash
16
+
17
+    openstack limit create
18
+        [--description <description>]
19
+        [--region <region>]
20
+        --project <project>
21
+        --service <service>
22
+        --resource-limit <resource-limit>
23
+        <resource-name>
24
+
25
+.. option:: --description <description>
26
+
27
+   Useful description of the limit or its purpose
28
+
29
+.. option:: --region <region>
30
+
31
+   Region that the limit should be applied to
32
+
33
+.. describe:: --project <project>
34
+
35
+   The project that the limit applies to (required)
36
+
37
+.. describe:: --service <service>
38
+
39
+   The service that is responsible for the resource being limited (required)
40
+
41
+.. describe:: --resource-limit <resource-limit>
42
+
43
+   The limit to apply to the project (required)
44
+
45
+.. describe:: <resource-name>
46
+
47
+   The name of the resource to limit (e.g. cores or volumes)
48
+
49
+limit delete
50
+------------
51
+
52
+Delete project-specific limit(s)
53
+
54
+.. program:: limit delete
55
+.. code:: bash
56
+
57
+    openstack limit delete
58
+        <limit-id> [<limit-id> ...]
59
+
60
+.. describe:: <limit-id>
61
+
62
+    Limit(s) to delete (ID)
63
+
64
+limit list
65
+----------
66
+
67
+List project-specific limits
68
+
69
+.. program:: limit list
70
+.. code:: bash
71
+
72
+    openstack limit list
73
+        [--service <service>]
74
+        [--resource-name <resource-name>]
75
+        [--region <region>]
76
+
77
+.. option:: --service <service>
78
+
79
+    The service to filter the response by (name or ID)
80
+
81
+.. option:: --resource-name <resource-name>
82
+
83
+    The name of the resource to filter the response by
84
+
85
+.. option:: --region <region>
86
+
87
+   The region name to filter the response by
88
+
89
+limit show
90
+----------
91
+
92
+Display details about a limit
93
+
94
+.. program:: limit show
95
+.. code:: bash
96
+
97
+    openstack limit show
98
+        <limit-id>
99
+
100
+.. describe:: <limit-id>
101
+
102
+   Limit to display (ID)
103
+
104
+limit set
105
+---------
106
+
107
+Update a limit
108
+
109
+.. program:: limit show
110
+.. code:: bash
111
+
112
+    openstack limit set
113
+        [--description <description>]
114
+        [--resource-limit <resource-limit>]
115
+        <limit-id>
116
+
117
+
118
+.. option:: --description <description>
119
+
120
+   Useful description of the limit or its purpose
121
+
122
+.. option:: --resource-limit <resource-limit>
123
+
124
+   The limit to apply to the project
125
+
126
+.. describe:: <limit-id>
127
+
128
+   Limit to update (ID)

+ 238
- 0
openstackclient/identity/v3/limit.py View File

@@ -0,0 +1,238 @@
1
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#   not use this file except in compliance with the License. You may obtain
3
+#   a copy of the License at
4
+#
5
+#        http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#   Unless required by applicable law or agreed to in writing, software
8
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#   License for the specific language governing permissions and limitations
11
+#   under the License.
12
+#
13
+
14
+"""Limits action implementations."""
15
+
16
+import logging
17
+
18
+from osc_lib.command import command
19
+from osc_lib import exceptions
20
+from osc_lib import utils
21
+import six
22
+
23
+from openstackclient.i18n import _
24
+from openstackclient.identity import common as common_utils
25
+
26
+LOG = logging.getLogger(__name__)
27
+
28
+
29
+class CreateLimit(command.ShowOne):
30
+    _description = _("Create a limit")
31
+
32
+    def get_parser(self, prog_name):
33
+        parser = super(CreateLimit, self).get_parser(prog_name)
34
+        parser.add_argument(
35
+            '--description',
36
+            metavar='<description>',
37
+            help=_('Description of the limit'),
38
+        )
39
+        parser.add_argument(
40
+            '--region',
41
+            metavar='<region>',
42
+            help=_('Region for the limit to affect.'),
43
+        )
44
+        parser.add_argument(
45
+            '--project',
46
+            metavar='<project>',
47
+            required=True,
48
+            help=_('Project to associate the resource limit to'),
49
+        )
50
+        parser.add_argument(
51
+            '--service',
52
+            metavar='<service>',
53
+            required=True,
54
+            help=_('Service responsible for the resource to limit'),
55
+        )
56
+        parser.add_argument(
57
+            '--resource-limit',
58
+            metavar='<resource-limit>',
59
+            required=True,
60
+            type=int,
61
+            help=_('The resource limit for the project to assume'),
62
+        )
63
+        parser.add_argument(
64
+            'resource_name',
65
+            metavar='<resource-name>',
66
+            help=_('The name of the resource to limit'),
67
+        )
68
+        return parser
69
+
70
+    def take_action(self, parsed_args):
71
+        identity_client = self.app.client_manager.identity
72
+
73
+        project = common_utils.find_project(
74
+            identity_client, parsed_args.project
75
+        )
76
+        service = common_utils.find_service(
77
+            identity_client, parsed_args.service
78
+        )
79
+        region = None
80
+        if parsed_args.region:
81
+            region = utils.find_resource(
82
+                identity_client.regions, parsed_args.region
83
+            )
84
+
85
+        limit = identity_client.limits.create(
86
+            project,
87
+            service,
88
+            parsed_args.resource_name,
89
+            parsed_args.resource_limit,
90
+            description=parsed_args.description,
91
+            region=region
92
+        )
93
+
94
+        limit._info.pop('links', None)
95
+        return zip(*sorted(six.iteritems(limit._info)))
96
+
97
+
98
+class ListLimit(command.Lister):
99
+    _description = _("List limits")
100
+
101
+    def get_parser(self, prog_name):
102
+        parser = super(ListLimit, self).get_parser(prog_name)
103
+        parser.add_argument(
104
+            '--service',
105
+            metavar='<service>',
106
+            help=_('Service responsible for the resource to limit'),
107
+        )
108
+        parser.add_argument(
109
+            '--resource-name',
110
+            metavar='<resource-name>',
111
+            dest='resource_name',
112
+            help=_('The name of the resource to limit'),
113
+        )
114
+        parser.add_argument(
115
+            '--region',
116
+            metavar='<region>',
117
+            help=_('Region for the registered limit to affect.'),
118
+        )
119
+        return parser
120
+
121
+    def take_action(self, parsed_args):
122
+        identity_client = self.app.client_manager.identity
123
+
124
+        service = None
125
+        if parsed_args.service:
126
+            service = common_utils.find_service(
127
+                identity_client, parsed_args.service
128
+            )
129
+        region = None
130
+        if parsed_args.region:
131
+            region = utils.find_resource(
132
+                identity_client.regions, parsed_args.region
133
+            )
134
+
135
+        limits = identity_client.limits.list(
136
+            service=service,
137
+            resource_name=parsed_args.resource_name,
138
+            region=region
139
+        )
140
+
141
+        columns = (
142
+            'ID', 'Project ID', 'Service ID', 'Resource Name',
143
+            'Resource Limit', 'Description', 'Region ID'
144
+        )
145
+        return (
146
+            columns,
147
+            (utils.get_item_properties(s, columns) for s in limits),
148
+        )
149
+
150
+
151
+class ShowLimit(command.ShowOne):
152
+    _description = _("Display limit details")
153
+
154
+    def get_parser(self, prog_name):
155
+        parser = super(ShowLimit, self).get_parser(prog_name)
156
+        parser.add_argument(
157
+            'limit_id',
158
+            metavar='<limit-id>',
159
+            help=_('Limit to display (ID)'),
160
+        )
161
+        return parser
162
+
163
+    def take_action(self, parsed_args):
164
+        identity_client = self.app.client_manager.identity
165
+        limit = identity_client.limits.get(parsed_args.limit_id)
166
+        limit._info.pop('links', None)
167
+        return zip(*sorted(six.iteritems(limit._info)))
168
+
169
+
170
+class SetLimit(command.ShowOne):
171
+    _description = _("Update information about a limit")
172
+
173
+    def get_parser(self, prog_name):
174
+        parser = super(SetLimit, self).get_parser(prog_name)
175
+        parser.add_argument(
176
+            'limit_id',
177
+            metavar='<limit-id>',
178
+            help=_('Limit to update (ID)'),
179
+        )
180
+        parser.add_argument(
181
+            '--description',
182
+            metavar='<description>',
183
+            help=_('Description of the limit'),
184
+        )
185
+        parser.add_argument(
186
+            '--resource-limit',
187
+            metavar='<resource-limit>',
188
+            dest='resource_limit',
189
+            type=int,
190
+            help=_('The resource limit for the project to assume'),
191
+        )
192
+        return parser
193
+
194
+    def take_action(self, parsed_args):
195
+        identity_client = self.app.client_manager.identity
196
+
197
+        limit = identity_client.limits.update(
198
+            parsed_args.limit_id,
199
+            description=parsed_args.description,
200
+            resource_limit=parsed_args.resource_limit
201
+        )
202
+
203
+        limit._info.pop('links', None)
204
+
205
+        return zip(*sorted(six.iteritems(limit._info)))
206
+
207
+
208
+class DeleteLimit(command.Command):
209
+    _description = _("Delete a limit")
210
+
211
+    def get_parser(self, prog_name):
212
+        parser = super(DeleteLimit, self).get_parser(prog_name)
213
+        parser.add_argument(
214
+            'limit_id',
215
+            metavar='<limit-id>',
216
+            nargs="+",
217
+            help=_('Limit to delete (ID)'),
218
+        )
219
+        return parser
220
+
221
+    def take_action(self, parsed_args):
222
+        identity_client = self.app.client_manager.identity
223
+
224
+        errors = 0
225
+        for limit_id in parsed_args.limit_id:
226
+            try:
227
+                identity_client.limits.delete(limit_id)
228
+            except Exception as e:
229
+                errors += 1
230
+                LOG.error(_("Failed to delete limit with ID "
231
+                            "'%(id)s': %(e)s"),
232
+                          {'id': limit_id, 'e': e})
233
+
234
+        if errors > 0:
235
+            total = len(parsed_args.limit_id)
236
+            msg = (_("%(errors)s of %(total)s limits failed to "
237
+                   "delete.") % {'errors': errors, 'total': total})
238
+            raise exceptions.CommandError(msg)

+ 43
- 0
openstackclient/tests/functional/identity/v3/common.py View File

@@ -59,6 +59,10 @@ class IdentityTests(base.TestCase):
59 59
     REGISTERED_LIMIT_LIST_HEADERS = ['ID', 'Service ID', 'Resource Name',
60 60
                                      'Default Limit', 'Description',
61 61
                                      'Region ID']
62
+    LIMIT_FIELDS = ['id', 'project_id', 'service_id', 'resource_name',
63
+                    'resource_limit', 'description', 'region_id']
64
+    LIMIT_LIST_HEADERS = ['ID', 'Project ID', 'Service ID', 'Resource Name',
65
+                          'Resource Limit', 'Description', 'Region ID']
62 66
 
63 67
     @classmethod
64 68
     def setUpClass(cls):
@@ -356,3 +360,42 @@ class IdentityTests(base.TestCase):
356 360
             for k, v in d.iteritems():
357 361
                 if k == key:
358 362
                     return v
363
+
364
+    def _create_dummy_limit(self, add_clean_up=True):
365
+        registered_limit_id = self._create_dummy_registered_limit()
366
+
367
+        raw_output = self.openstack(
368
+            'registered limit show %s' % registered_limit_id
369
+        )
370
+        items = self.parse_show(raw_output)
371
+        resource_name = self._extract_value_from_items('resource_name', items)
372
+        service_id = self._extract_value_from_items('service_id', items)
373
+        resource_limit = 15
374
+
375
+        project_name = self._create_dummy_project()
376
+        raw_output = self.openstack('project show %s' % project_name)
377
+        items = self.parse_show(raw_output)
378
+        project_id = self._extract_value_from_items('id', items)
379
+
380
+        params = {
381
+            'project_id': project_id,
382
+            'service_id': service_id,
383
+            'resource_name': resource_name,
384
+            'resource_limit': resource_limit
385
+        }
386
+
387
+        raw_output = self.openstack(
388
+            'limit create'
389
+            ' --project %(project_id)s'
390
+            ' --service %(service_id)s'
391
+            ' --resource-limit %(resource_limit)s'
392
+            ' %(resource_name)s' % params
393
+        )
394
+        items = self.parse_show(raw_output)
395
+        limit_id = self._extract_value_from_items('id', items)
396
+
397
+        if add_clean_up:
398
+            self.addCleanup(self.openstack, 'limit delete %s' % limit_id)
399
+
400
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
401
+        return limit_id

+ 192
- 0
openstackclient/tests/functional/identity/v3/test_limit.py View File

@@ -0,0 +1,192 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from tempest.lib.common.utils import data_utils
14
+
15
+from openstackclient.tests.functional.identity.v3 import common
16
+
17
+
18
+class LimitTestCase(common.IdentityTests):
19
+
20
+    def test_limit_create_with_service_name(self):
21
+        registered_limit_id = self._create_dummy_registered_limit()
22
+        raw_output = self.openstack(
23
+            'registered limit show %s' % registered_limit_id
24
+        )
25
+        items = self.parse_show(raw_output)
26
+        service_id = self._extract_value_from_items('service_id', items)
27
+        resource_name = self._extract_value_from_items('resource_name', items)
28
+
29
+        raw_output = self.openstack('service show %s' % service_id)
30
+        items = self.parse_show(raw_output)
31
+        service_name = self._extract_value_from_items('name', items)
32
+
33
+        project_name = self._create_dummy_project()
34
+        raw_output = self.openstack('project show %s' % project_name)
35
+        items = self.parse_show(raw_output)
36
+        project_id = self._extract_value_from_items('id', items)
37
+
38
+        params = {
39
+            'project_id': project_id,
40
+            'service_name': service_name,
41
+            'resource_name': resource_name,
42
+            'resource_limit': 15
43
+        }
44
+        raw_output = self.openstack(
45
+            'limit create'
46
+            ' --project %(project_id)s'
47
+            ' --service %(service_name)s'
48
+            ' --resource-limit %(resource_limit)s'
49
+            ' %(resource_name)s' % params
50
+        )
51
+        items = self.parse_show(raw_output)
52
+        limit_id = self._extract_value_from_items('id', items)
53
+        self.addCleanup(self.openstack, 'limit delete %s' % limit_id)
54
+
55
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
56
+
57
+    def test_limit_create_with_project_name(self):
58
+        registered_limit_id = self._create_dummy_registered_limit()
59
+        raw_output = self.openstack(
60
+            'registered limit show %s' % registered_limit_id
61
+        )
62
+        items = self.parse_show(raw_output)
63
+        service_id = self._extract_value_from_items('service_id', items)
64
+        resource_name = self._extract_value_from_items('resource_name', items)
65
+
66
+        raw_output = self.openstack('service show %s' % service_id)
67
+        items = self.parse_show(raw_output)
68
+        service_name = self._extract_value_from_items('name', items)
69
+
70
+        project_name = self._create_dummy_project()
71
+
72
+        params = {
73
+            'project_name': project_name,
74
+            'service_name': service_name,
75
+            'resource_name': resource_name,
76
+            'resource_limit': 15
77
+        }
78
+        raw_output = self.openstack(
79
+            'limit create'
80
+            ' --project %(project_name)s'
81
+            ' --service %(service_name)s'
82
+            ' --resource-limit %(resource_limit)s'
83
+            ' %(resource_name)s' % params
84
+        )
85
+        items = self.parse_show(raw_output)
86
+        limit_id = self._extract_value_from_items('id', items)
87
+        self.addCleanup(self.openstack, 'limit delete %s' % limit_id)
88
+
89
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
90
+        registered_limit_id = self._create_dummy_registered_limit()
91
+
92
+    def test_limit_create_with_service_id(self):
93
+        self._create_dummy_limit()
94
+
95
+    def test_limit_create_with_project_id(self):
96
+        self._create_dummy_limit()
97
+
98
+    def test_limit_create_with_options(self):
99
+        registered_limit_id = self._create_dummy_registered_limit()
100
+        region_id = self._create_dummy_region()
101
+
102
+        params = {
103
+            'region_id': region_id,
104
+            'registered_limit_id': registered_limit_id
105
+        }
106
+
107
+        raw_output = self.openstack(
108
+            'registered limit set'
109
+            ' %(registered_limit_id)s'
110
+            ' --region %(region_id)s' % params
111
+        )
112
+        items = self.parse_show(raw_output)
113
+        service_id = self._extract_value_from_items('service_id', items)
114
+        resource_name = self._extract_value_from_items('resource_name', items)
115
+
116
+        project_name = self._create_dummy_project()
117
+        raw_output = self.openstack('project show %s' % project_name)
118
+        items = self.parse_show(raw_output)
119
+        project_id = self._extract_value_from_items('id', items)
120
+        description = data_utils.arbitrary_string()
121
+
122
+        params = {
123
+            'project_id': project_id,
124
+            'service_id': service_id,
125
+            'resource_name': resource_name,
126
+            'resource_limit': 15,
127
+            'region_id': region_id,
128
+            'description': description
129
+        }
130
+        raw_output = self.openstack(
131
+            'limit create'
132
+            ' --project %(project_id)s'
133
+            ' --service %(service_id)s'
134
+            ' --resource-limit %(resource_limit)s'
135
+            ' --region %(region_id)s'
136
+            ' --description %(description)s'
137
+            ' %(resource_name)s' % params
138
+        )
139
+        items = self.parse_show(raw_output)
140
+        limit_id = self._extract_value_from_items('id', items)
141
+        self.addCleanup(self.openstack, 'limit delete %s' % limit_id)
142
+
143
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
144
+
145
+    def test_limit_show(self):
146
+        limit_id = self._create_dummy_limit()
147
+        raw_output = self.openstack('limit show %s' % limit_id)
148
+        items = self.parse_show(raw_output)
149
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
150
+
151
+    def test_limit_set_description(self):
152
+        limit_id = self._create_dummy_limit()
153
+
154
+        params = {
155
+            'description': data_utils.arbitrary_string(),
156
+            'limit_id': limit_id
157
+        }
158
+
159
+        raw_output = self.openstack(
160
+            'limit set'
161
+            ' --description %(description)s'
162
+            ' %(limit_id)s' % params
163
+        )
164
+        items = self.parse_show(raw_output)
165
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
166
+
167
+    def test_limit_set_resource_limit(self):
168
+        limit_id = self._create_dummy_limit()
169
+
170
+        params = {
171
+            'resource_limit': 5,
172
+            'limit_id': limit_id
173
+        }
174
+
175
+        raw_output = self.openstack(
176
+            'limit set'
177
+            ' --resource-limit %(resource_limit)s'
178
+            ' %(limit_id)s' % params
179
+        )
180
+        items = self.parse_show(raw_output)
181
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
182
+
183
+    def test_limit_list(self):
184
+        self._create_dummy_limit()
185
+        raw_output = self.openstack('limit list')
186
+        items = self.parse_listing(raw_output)
187
+        self.assert_table_structure(items, self.LIMIT_LIST_HEADERS)
188
+
189
+    def test_limit_delete(self):
190
+        limit_id = self._create_dummy_limit(add_clean_up=False)
191
+        raw_output = self.openstack('limit delete %s' % limit_id)
192
+        self.assertEqual(0, len(raw_output))

+ 25
- 0
openstackclient/tests/unit/identity/v3/fakes.py View File

@@ -507,6 +507,29 @@ REGISTERED_LIMIT_OPTIONS = {
507 507
     'region_id': region_id
508 508
 }
509 509
 
510
+limit_id = 'limit-id'
511
+limit_resource_limit = 15
512
+limit_description = 'limit of foobars'
513
+limit_resource_name = 'foobars'
514
+LIMIT = {
515
+    'id': limit_id,
516
+    'project_id': project_id,
517
+    'resource_limit': limit_resource_limit,
518
+    'resource_name': limit_resource_name,
519
+    'service_id': service_id,
520
+    'description': None,
521
+    'region_id': None
522
+}
523
+LIMIT_OPTIONS = {
524
+    'id': limit_id,
525
+    'project_id': project_id,
526
+    'resource_limit': limit_resource_limit,
527
+    'resource_name': limit_resource_name,
528
+    'service_id': service_id,
529
+    'description': limit_description,
530
+    'region_id': region_id
531
+}
532
+
510 533
 
511 534
 def fake_auth_ref(fake_token, fake_service=None):
512 535
     """Create an auth_ref using keystoneauth's fixtures"""
@@ -601,6 +624,8 @@ class FakeIdentityv3Client(object):
601 624
         self.inference_rules.resource_class = fakes.FakeResource(None, {})
602 625
         self.registered_limits = mock.Mock()
603 626
         self.registered_limits.resource_class = fakes.FakeResource(None, {})
627
+        self.limits = mock.Mock()
628
+        self.limits.resource_class = fakes.FakeResource(None, {})
604 629
 
605 630
 
606 631
 class FakeFederationManager(object):

+ 382
- 0
openstackclient/tests/unit/identity/v3/test_limit.py View File

@@ -0,0 +1,382 @@
1
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#   not use this file except in compliance with the License. You may obtain
3
+#   a copy of the License at
4
+#
5
+#        http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#   Unless required by applicable law or agreed to in writing, software
8
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#   License for the specific language governing permissions and limitations
11
+#   under the License.
12
+
13
+import copy
14
+
15
+from keystoneauth1.exceptions import http as ksa_exceptions
16
+from osc_lib import exceptions
17
+
18
+from openstackclient.identity.v3 import limit
19
+from openstackclient.tests.unit import fakes
20
+from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
21
+
22
+
23
+class TestLimit(identity_fakes.TestIdentityv3):
24
+
25
+    def setUp(self):
26
+        super(TestLimit, self).setUp()
27
+
28
+        identity_manager = self.app.client_manager.identity
29
+
30
+        self.limit_mock = identity_manager.limits
31
+
32
+        self.services_mock = identity_manager.services
33
+        self.services_mock.reset_mock()
34
+
35
+        self.projects_mock = identity_manager.projects
36
+        self.projects_mock.reset_mock()
37
+
38
+        self.regions_mock = identity_manager.regions
39
+        self.regions_mock.reset_mock()
40
+
41
+
42
+class TestLimitCreate(TestLimit):
43
+
44
+    def setUp(self):
45
+        super(TestLimitCreate, self).setUp()
46
+
47
+        self.service = fakes.FakeResource(
48
+            None,
49
+            copy.deepcopy(identity_fakes.SERVICE),
50
+            loaded=True
51
+        )
52
+        self.services_mock.get.return_value = self.service
53
+
54
+        self.project = fakes.FakeResource(
55
+            None,
56
+            copy.deepcopy(identity_fakes.PROJECT),
57
+            loaded=True
58
+        )
59
+        self.projects_mock.get.return_value = self.project
60
+
61
+        self.region = fakes.FakeResource(
62
+            None,
63
+            copy.deepcopy(identity_fakes.REGION),
64
+            loaded=True
65
+        )
66
+        self.regions_mock.get.return_value = self.region
67
+
68
+        self.cmd = limit.CreateLimit(self.app, None)
69
+
70
+    def test_limit_create_without_options(self):
71
+        self.limit_mock.create.return_value = fakes.FakeResource(
72
+            None,
73
+            copy.deepcopy(identity_fakes.LIMIT),
74
+            loaded=True
75
+        )
76
+
77
+        resource_limit = 15
78
+        arglist = [
79
+            '--project', identity_fakes.project_id,
80
+            '--service', identity_fakes.service_id,
81
+            '--resource-limit', str(resource_limit),
82
+            identity_fakes.limit_resource_name
83
+        ]
84
+        verifylist = [
85
+            ('project', identity_fakes.project_id),
86
+            ('service', identity_fakes.service_id),
87
+            ('resource_name', identity_fakes.limit_resource_name),
88
+            ('resource_limit', resource_limit)
89
+        ]
90
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
91
+
92
+        columns, data = self.cmd.take_action(parsed_args)
93
+
94
+        kwargs = {'description': None, 'region': None}
95
+        self.limit_mock.create.assert_called_with(
96
+            self.project,
97
+            self.service,
98
+            identity_fakes.limit_resource_name,
99
+            resource_limit,
100
+            **kwargs
101
+        )
102
+
103
+        collist = ('description', 'id', 'project_id', 'region_id',
104
+                   'resource_limit', 'resource_name', 'service_id')
105
+        self.assertEqual(collist, columns)
106
+        datalist = (
107
+            None,
108
+            identity_fakes.limit_id,
109
+            identity_fakes.project_id,
110
+            None,
111
+            resource_limit,
112
+            identity_fakes.limit_resource_name,
113
+            identity_fakes.service_id
114
+        )
115
+        self.assertEqual(datalist, data)
116
+
117
+    def test_limit_create_with_options(self):
118
+        self.limit_mock.create.return_value = fakes.FakeResource(
119
+            None,
120
+            copy.deepcopy(identity_fakes.LIMIT_OPTIONS),
121
+            loaded=True
122
+        )
123
+
124
+        resource_limit = 15
125
+        arglist = [
126
+            '--project', identity_fakes.project_id,
127
+            '--service', identity_fakes.service_id,
128
+            '--resource-limit', str(resource_limit),
129
+            '--region', identity_fakes.region_id,
130
+            '--description', identity_fakes.limit_description,
131
+            identity_fakes.limit_resource_name
132
+        ]
133
+        verifylist = [
134
+            ('project', identity_fakes.project_id),
135
+            ('service', identity_fakes.service_id),
136
+            ('resource_name', identity_fakes.limit_resource_name),
137
+            ('resource_limit', resource_limit),
138
+            ('region', identity_fakes.region_id),
139
+            ('description', identity_fakes.limit_description)
140
+        ]
141
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
142
+
143
+        columns, data = self.cmd.take_action(parsed_args)
144
+
145
+        kwargs = {
146
+            'description': identity_fakes.limit_description,
147
+            'region': self.region
148
+        }
149
+        self.limit_mock.create.assert_called_with(
150
+            self.project,
151
+            self.service,
152
+            identity_fakes.limit_resource_name,
153
+            resource_limit,
154
+            **kwargs
155
+        )
156
+
157
+        collist = ('description', 'id', 'project_id', 'region_id',
158
+                   'resource_limit', 'resource_name', 'service_id')
159
+        self.assertEqual(collist, columns)
160
+        datalist = (
161
+            identity_fakes.limit_description,
162
+            identity_fakes.limit_id,
163
+            identity_fakes.project_id,
164
+            identity_fakes.region_id,
165
+            resource_limit,
166
+            identity_fakes.limit_resource_name,
167
+            identity_fakes.service_id
168
+        )
169
+        self.assertEqual(datalist, data)
170
+
171
+
172
+class TestLimitDelete(TestLimit):
173
+
174
+    def setUp(self):
175
+        super(TestLimitDelete, self).setUp()
176
+        self.cmd = limit.DeleteLimit(self.app, None)
177
+
178
+    def test_limit_delete(self):
179
+        self.limit_mock.delete.return_value = None
180
+
181
+        arglist = [identity_fakes.limit_id]
182
+        verifylist = [
183
+            ('limit_id', [identity_fakes.limit_id])
184
+        ]
185
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
186
+
187
+        result = self.cmd.take_action(parsed_args)
188
+
189
+        self.limit_mock.delete.assert_called_with(
190
+            identity_fakes.limit_id
191
+        )
192
+        self.assertIsNone(result)
193
+
194
+    def test_limit_delete_with_exception(self):
195
+        return_value = ksa_exceptions.NotFound()
196
+        self.limit_mock.delete.side_effect = return_value
197
+
198
+        arglist = ['fake-limit-id']
199
+        verifylist = [
200
+            ('limit_id', ['fake-limit-id'])
201
+        ]
202
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
203
+
204
+        try:
205
+            self.cmd.take_action(parsed_args)
206
+            self.fail('CommandError should be raised.')
207
+        except exceptions.CommandError as e:
208
+            self.assertEqual(
209
+                '1 of 1 limits failed to delete.', str(e)
210
+            )
211
+
212
+
213
+class TestLimitShow(TestLimit):
214
+
215
+    def setUp(self):
216
+        super(TestLimitShow, self).setUp()
217
+
218
+        self.limit_mock.get.return_value = fakes.FakeResource(
219
+            None,
220
+            copy.deepcopy(identity_fakes.LIMIT),
221
+            loaded=True
222
+        )
223
+
224
+        self.cmd = limit.ShowLimit(self.app, None)
225
+
226
+    def test_limit_show(self):
227
+        arglist = [identity_fakes.limit_id]
228
+        verifylist = [('limit_id', identity_fakes.limit_id)]
229
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
230
+
231
+        columns, data = self.cmd.take_action(parsed_args)
232
+
233
+        self.limit_mock.get.assert_called_with(identity_fakes.limit_id)
234
+
235
+        collist = (
236
+            'description', 'id', 'project_id', 'region_id', 'resource_limit',
237
+            'resource_name', 'service_id'
238
+        )
239
+        self.assertEqual(collist, columns)
240
+        datalist = (
241
+            None,
242
+            identity_fakes.limit_id,
243
+            identity_fakes.project_id,
244
+            None,
245
+            identity_fakes.limit_resource_limit,
246
+            identity_fakes.limit_resource_name,
247
+            identity_fakes.service_id
248
+        )
249
+        self.assertEqual(datalist, data)
250
+
251
+
252
+class TestLimitSet(TestLimit):
253
+
254
+    def setUp(self):
255
+        super(TestLimitSet, self).setUp()
256
+        self.cmd = limit.SetLimit(self.app, None)
257
+
258
+    def test_limit_set_description(self):
259
+        limit = copy.deepcopy(identity_fakes.LIMIT)
260
+        limit['description'] = identity_fakes.limit_description
261
+        self.limit_mock.update.return_value = fakes.FakeResource(
262
+            None, limit, loaded=True
263
+        )
264
+
265
+        arglist = [
266
+            '--description', identity_fakes.limit_description,
267
+            identity_fakes.limit_id
268
+        ]
269
+        verifylist = [
270
+            ('description', identity_fakes.limit_description),
271
+            ('limit_id', identity_fakes.limit_id)
272
+        ]
273
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
274
+
275
+        columns, data = self.cmd.take_action(parsed_args)
276
+
277
+        self.limit_mock.update.assert_called_with(
278
+            identity_fakes.limit_id,
279
+            description=identity_fakes.limit_description,
280
+            resource_limit=None
281
+        )
282
+
283
+        collist = (
284
+            'description', 'id', 'project_id', 'region_id', 'resource_limit',
285
+            'resource_name', 'service_id'
286
+        )
287
+        self.assertEqual(collist, columns)
288
+        datalist = (
289
+            identity_fakes.limit_description,
290
+            identity_fakes.limit_id,
291
+            identity_fakes.project_id,
292
+            None,
293
+            identity_fakes.limit_resource_limit,
294
+            identity_fakes.limit_resource_name,
295
+            identity_fakes.service_id
296
+        )
297
+        self.assertEqual(datalist, data)
298
+
299
+    def test_limit_set_resource_limit(self):
300
+        resource_limit = 20
301
+        limit = copy.deepcopy(identity_fakes.LIMIT)
302
+        limit['resource_limit'] = resource_limit
303
+        self.limit_mock.update.return_value = fakes.FakeResource(
304
+            None, limit, loaded=True
305
+        )
306
+
307
+        arglist = [
308
+            '--resource-limit', str(resource_limit),
309
+            identity_fakes.limit_id
310
+        ]
311
+        verifylist = [
312
+            ('resource_limit', resource_limit),
313
+            ('limit_id', identity_fakes.limit_id)
314
+        ]
315
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
316
+
317
+        columns, data = self.cmd.take_action(parsed_args)
318
+
319
+        self.limit_mock.update.assert_called_with(
320
+            identity_fakes.limit_id,
321
+            description=None,
322
+            resource_limit=resource_limit
323
+        )
324
+
325
+        collist = (
326
+            'description', 'id', 'project_id', 'region_id', 'resource_limit',
327
+            'resource_name', 'service_id'
328
+        )
329
+        self.assertEqual(collist, columns)
330
+        datalist = (
331
+            None,
332
+            identity_fakes.limit_id,
333
+            identity_fakes.project_id,
334
+            None,
335
+            resource_limit,
336
+            identity_fakes.limit_resource_name,
337
+            identity_fakes.service_id
338
+        )
339
+        self.assertEqual(datalist, data)
340
+
341
+
342
+class TestLimitList(TestLimit):
343
+
344
+    def setUp(self):
345
+        super(TestLimitList, self).setUp()
346
+
347
+        self.limit_mock.list.return_value = [
348
+            fakes.FakeResource(
349
+                None,
350
+                copy.deepcopy(identity_fakes.LIMIT),
351
+                loaded=True
352
+            )
353
+        ]
354
+
355
+        self.cmd = limit.ListLimit(self.app, None)
356
+
357
+    def test_limit_list(self):
358
+        arglist = []
359
+        verifylist = []
360
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
361
+
362
+        columns, data = self.cmd.take_action(parsed_args)
363
+
364
+        self.limit_mock.list.assert_called_with(
365
+            service=None, resource_name=None, region=None
366
+        )
367
+
368
+        collist = (
369
+            'ID', 'Project ID', 'Service ID', 'Resource Name',
370
+            'Resource Limit', 'Description', 'Region ID'
371
+        )
372
+        self.assertEqual(collist, columns)
373
+        datalist = ((
374
+            identity_fakes.limit_id,
375
+            identity_fakes.project_id,
376
+            identity_fakes.service_id,
377
+            identity_fakes.limit_resource_name,
378
+            identity_fakes.limit_resource_limit,
379
+            None,
380
+            None
381
+        ), )
382
+        self.assertEqual(datalist, tuple(data))

+ 7
- 0
releasenotes/notes/bp-unified-limits-6c5fdb1c26805d86.yaml View File

@@ -0,0 +1,7 @@
1
+---
2
+features:
3
+  - |
4
+    [`bp unified-limits <https://blueprints.launchpad.net/keystone/+spec/unified-limit>`_]
5
+    Support has been added for managing project-specific limits in keystone via
6
+    the ``limit`` command. Limits define limits of resources for projects to
7
+    consume once a limit has been registered.

+ 6
- 0
setup.cfg View File

@@ -268,6 +268,12 @@ openstack.identity.v3 =
268 268
     implied_role_delete = openstackclient.identity.v3.implied_role:DeleteImpliedRole
269 269
     implied_role_list = openstackclient.identity.v3.implied_role:ListImpliedRole
270 270
 
271
+    limit_create = openstackclient.identity.v3.limit:CreateLimit
272
+    limit_delete = openstackclient.identity.v3.limit:DeleteLimit
273
+    limit_list = openstackclient.identity.v3.limit:ListLimit
274
+    limit_set = openstackclient.identity.v3.limit:SetLimit
275
+    limit_show = openstackclient.identity.v3.limit:ShowLimit
276
+
271 277
     mapping_create = openstackclient.identity.v3.mapping:CreateMapping
272 278
     mapping_delete = openstackclient.identity.v3.mapping:DeleteMapping
273 279
     mapping_list = openstackclient.identity.v3.mapping:ListMapping

Loading…
Cancel
Save