"Fossies" - the Fresh Open Source Software Archive 
Member "cinder-13.0.7/cinder/tests/unit/api/contrib/test_quotas.py" (4 Oct 2019, 49365 Bytes) of package /linux/misc/openstack/cinder-13.0.7.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style:
standard) with prefixed line numbers.
Alternatively you can here
view or
download the uninterpreted source code file.
See also the last
Fossies "Diffs" side-by-side code changes report for "test_quotas.py":
14.0.2_vs_15.0.0.
1 #
2 # Copyright 2013 OpenStack Foundation
3 # All Rights Reserved.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License"); you may
6 # not use this file except in compliance with the License. You may obtain
7 # a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 # License for the specific language governing permissions and limitations
15 # under the License.
16
17 """
18 Tests for cinder.api.contrib.quotas.py
19 """
20
21
22 import ddt
23 import mock
24 import uuid
25 import webob.exc
26
27 from cinder.api.contrib import quotas
28 from cinder import context
29 from cinder import db
30 from cinder import exception
31 from cinder import quota
32 from cinder import test
33 from cinder.tests.unit import fake_constants as fake
34 from cinder.tests.unit import test_db_api
35
36
37 from oslo_config import cfg
38 from oslo_config import fixture as config_fixture
39
40
41 CONF = cfg.CONF
42
43
44 def make_body(root=True, gigabytes=1000, snapshots=10,
45 volumes=10, backups=10, backup_gigabytes=1000,
46 tenant_id=fake.PROJECT_ID, per_volume_gigabytes=-1, groups=10):
47 resources = {'gigabytes': gigabytes,
48 'snapshots': snapshots,
49 'volumes': volumes,
50 'backups': backups,
51 'backup_gigabytes': backup_gigabytes,
52 'per_volume_gigabytes': per_volume_gigabytes,
53 'groups': groups}
54 # need to consider preexisting volume types as well
55 volume_types = db.volume_type_get_all(context.get_admin_context())
56
57 for volume_type in volume_types:
58 resources['gigabytes_' + volume_type] = -1
59 resources['snapshots_' + volume_type] = -1
60 resources['volumes_' + volume_type] = -1
61
62 if tenant_id:
63 resources['id'] = tenant_id
64 if root:
65 result = {'quota_set': resources}
66 else:
67 result = resources
68 return result
69
70
71 def make_subproject_body(root=True, gigabytes=0, snapshots=0,
72 volumes=0, backups=0, backup_gigabytes=0,
73 tenant_id=fake.PROJECT_ID, per_volume_gigabytes=0):
74 return make_body(root=root, gigabytes=gigabytes, snapshots=snapshots,
75 volumes=volumes, backups=backups,
76 backup_gigabytes=backup_gigabytes, tenant_id=tenant_id,
77 per_volume_gigabytes=per_volume_gigabytes)
78
79
80 class QuotaSetsControllerTestBase(test.TestCase):
81
82 class FakeProject(object):
83
84 def __init__(self, id=fake.PROJECT_ID, parent_id=None,
85 is_admin_project=False):
86 self.id = id
87 self.parent_id = parent_id
88 self.subtree = None
89 self.parents = None
90 self.is_admin_project = is_admin_project
91
92 def setUp(self):
93 super(QuotaSetsControllerTestBase, self).setUp()
94
95 self.controller = quotas.QuotaSetsController()
96
97 self.req = mock.Mock()
98 self.req.environ = {'cinder.context': context.get_admin_context()}
99 self.req.environ['cinder.context'].is_admin = True
100 self.req.params = {}
101
102 self._create_project_hierarchy()
103 self.req.environ['cinder.context'].project_id = self.A.id
104
105 get_patcher = mock.patch('cinder.quota_utils.get_project_hierarchy',
106 self._get_project)
107 get_patcher.start()
108 self.addCleanup(get_patcher.stop)
109
110 def _list_projects(context):
111 return self.project_by_id.values()
112
113 list_patcher = mock.patch('cinder.quota_utils.get_all_projects',
114 _list_projects)
115 list_patcher.start()
116 self.addCleanup(list_patcher.stop)
117
118 self.auth_url = 'http://localhost:5000'
119 self.fixture = self.useFixture(config_fixture.Config(CONF))
120 self.fixture.config(auth_uri=self.auth_url, group='keystone_authtoken')
121
122 def _create_project_hierarchy(self):
123 r"""Sets an environment used for nested quotas tests.
124
125 Create a project hierarchy such as follows:
126 +-----------+
127 | |
128 | A |
129 | / \ |
130 | B C |
131 | / |
132 | D |
133 +-----------+
134 """
135 self.A = self.FakeProject(id=uuid.uuid4().hex, parent_id=None)
136 self.B = self.FakeProject(id=uuid.uuid4().hex, parent_id=self.A.id)
137 self.C = self.FakeProject(id=uuid.uuid4().hex, parent_id=self.A.id)
138 self.D = self.FakeProject(id=uuid.uuid4().hex, parent_id=self.B.id)
139
140 # update projects subtrees
141 self.B.subtree = {self.D.id: self.D.subtree}
142 self.A.subtree = {self.B.id: self.B.subtree, self.C.id: self.C.subtree}
143
144 self.A.parents = None
145 self.B.parents = {self.A.id: None}
146 self.C.parents = {self.A.id: None}
147 self.D.parents = {self.B.id: self.B.parents}
148
149 # project_by_id attribute is used to recover a project based on its id.
150 self.project_by_id = {self.A.id: self.A, self.B.id: self.B,
151 self.C.id: self.C, self.D.id: self.D}
152
153 def _get_project(self, context, id, subtree_as_ids=False,
154 parents_as_ids=False, is_admin_project=False):
155 return self.project_by_id.get(id, self.FakeProject())
156
157 def _create_fake_quota_usages(self, usage_map):
158 self._fake_quota_usages = {}
159 for key, val in usage_map.items():
160 self._fake_quota_usages[key] = {'in_use': val}
161
162 def _fake_quota_usage_get_all_by_project(self, context, project_id):
163 return {'volumes': self._fake_quota_usages[project_id]}
164
165
166 class QuotaSetsControllerTest(QuotaSetsControllerTestBase):
167 def test_defaults(self):
168 result = self.controller.defaults(self.req, fake.PROJECT_ID)
169 self.assertDictEqual(make_body(), result)
170
171 def test_show(self):
172 result = self.controller.show(self.req, fake.PROJECT_ID)
173 self.assertDictEqual(make_body(), result)
174
175 def test_show_not_authorized(self):
176 self.req.environ['cinder.context'].is_admin = False
177 self.req.environ['cinder.context'].user_id = fake.USER_ID
178 self.req.environ['cinder.context'].project_id = fake.PROJECT_ID
179 self.assertRaises(exception.PolicyNotAuthorized, self.controller.show,
180 self.req, fake.PROJECT2_ID)
181
182 def test_show_non_admin_user(self):
183 self.controller._get_quotas = mock.Mock(side_effect=
184 self.controller._get_quotas)
185 result = self.controller.show(self.req, fake.PROJECT_ID)
186 self.assertDictEqual(make_body(), result)
187 self.controller._get_quotas.assert_called_with(
188 self.req.environ['cinder.context'], fake.PROJECT_ID, False)
189
190 def test_show_with_invalid_usage_param(self):
191 self.req.params = {'usage': 'InvalidBool'}
192 self.assertRaises(exception.InvalidParameterValue,
193 self.controller.show,
194 self.req, fake.PROJECT2_ID)
195
196 def test_show_with_valid_usage_param(self):
197 self.req.params = {'usage': 'false'}
198 result = self.controller.show(self.req, fake.PROJECT_ID)
199 self.assertDictEqual(make_body(), result)
200
201 def test_update(self):
202 body = make_body(gigabytes=2000, snapshots=15,
203 volumes=5, backups=5, tenant_id=None)
204 result = self.controller.update(self.req, fake.PROJECT_ID, body=body)
205 self.assertDictEqual(body, result)
206
207 body = make_body(gigabytes=db.MAX_INT, tenant_id=None)
208 result = self.controller.update(self.req, fake.PROJECT_ID, body=body)
209 self.assertDictEqual(body, result)
210
211 def test_update_subproject_not_in_hierarchy_non_nested(self):
212 # When not using nested quotas, the hierarchy should not be considered
213 # for an update
214 E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None)
215 F = self.FakeProject(id=uuid.uuid4().hex, parent_id=E.id)
216 E.subtree = {F.id: F.subtree}
217 self.project_by_id[E.id] = E
218 self.project_by_id[F.id] = F
219
220 # Update the project A quota.
221 self.req.environ['cinder.context'].project_id = self.A.id
222 body = make_body(gigabytes=2000, snapshots=15,
223 volumes=5, backups=5, tenant_id=None)
224 result = self.controller.update(self.req, self.A.id, body=body)
225 self.assertDictEqual(body, result)
226 # Try to update the quota of F, it will be allowed even though
227 # project E doesn't belong to the project hierarchy of A, because
228 # we are NOT using the nested quota driver
229 self.req.environ['cinder.context'].project_id = self.A.id
230 body = make_body(gigabytes=2000, snapshots=15,
231 volumes=5, backups=5, tenant_id=None)
232 self.controller.update(self.req, F.id, body=body)
233
234 @mock.patch(
235 'cinder.api.openstack.wsgi.Controller.validate_string_length')
236 def test_update_limit(self, mock_validate):
237 body = {'quota_set': {'volumes': 10}}
238 result = self.controller.update(self.req, fake.PROJECT_ID, body=body)
239
240 self.assertEqual(10, result['quota_set']['volumes'])
241 self.assertTrue(mock_validate.called)
242
243 def test_update_wrong_key(self):
244 body = {'quota_set': {'bad': 'bad'}}
245 self.assertRaises(exception.InvalidInput, self.controller.update,
246 self.req, fake.PROJECT_ID, body=body)
247
248 def test_update_invalid_value_key_value(self):
249 body = {'quota_set': {'gigabytes': "should_be_int"}}
250 self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
251 self.req, fake.PROJECT_ID, body=body)
252
253 def test_update_invalid_type_key_value(self):
254 body = {'quota_set': {'gigabytes': None}}
255 self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
256 self.req, fake.PROJECT_ID, body=body)
257
258 def test_update_with_no_body(self):
259 body = {}
260 self.assertRaises(exception.ValidationError, self.controller.update,
261 self.req, fake.PROJECT_ID, body=body)
262
263 def test_update_with_wrong_body(self):
264 body = {'test': {}}
265 self.assertRaises(exception.ValidationError, self.controller.update,
266 self.req, fake.PROJECT_ID, body=body)
267
268 def test_update_multi_value_with_bad_data(self):
269 orig_quota = self.controller.show(self.req, fake.PROJECT_ID)
270 body = make_body(gigabytes=2000, snapshots=15, volumes="should_be_int",
271 backups=5, tenant_id=None)
272 self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
273 self.req, fake.PROJECT_ID, body=body)
274 # Verify that quota values are not updated in db
275 new_quota = self.controller.show(self.req, fake.PROJECT_ID)
276 self.assertDictEqual(orig_quota, new_quota)
277
278 def test_update_bad_quota_limit(self):
279 body = {'quota_set': {'gigabytes': -1000}}
280 self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
281 self.req, fake.PROJECT_ID, body=body)
282 body = {'quota_set': {'gigabytes': db.MAX_INT + 1}}
283 self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
284 self.req, fake.PROJECT_ID, body=body)
285
286 def test_update_no_admin(self):
287 self.req.environ['cinder.context'].is_admin = False
288 self.req.environ['cinder.context'].project_id = fake.PROJECT_ID
289 self.req.environ['cinder.context'].user_id = 'foo_user'
290 self.assertRaises(exception.PolicyNotAuthorized,
291 self.controller.update, self.req, fake.PROJECT_ID,
292 body=make_body(tenant_id=None))
293
294 def test_update_without_quota_set_field(self):
295 body = {'fake_quota_set': {'gigabytes': 100}}
296 self.assertRaises(exception.ValidationError, self.controller.update,
297 self.req, fake.PROJECT_ID, body=body)
298
299 def test_update_empty_body(self):
300 body = {}
301 self.assertRaises(exception.ValidationError, self.controller.update,
302 self.req, fake.PROJECT_ID, body=body)
303
304 def _commit_quota_reservation(self):
305 # Create simple quota and quota usage.
306 ctxt = context.get_admin_context()
307 res = test_db_api._quota_reserve(ctxt, fake.PROJECT_ID)
308 db.reservation_commit(ctxt, res, fake.PROJECT_ID)
309 expected = {'project_id': fake.PROJECT_ID,
310 'volumes': {'reserved': 0, 'in_use': 1},
311 'gigabytes': {'reserved': 0, 'in_use': 2},
312 }
313 self.assertEqual(expected,
314 db.quota_usage_get_all_by_project(ctxt,
315 fake.PROJECT_ID))
316
317 def test_update_lower_than_existing_resources(self):
318 self._commit_quota_reservation()
319 body = {'quota_set': {'volumes': 0}}
320 self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
321 self.req, fake.PROJECT_ID, body=body)
322 # Ensure that validation works even if some resources are valid
323 body = {'quota_set': {'gigabytes': 1, 'volumes': 10}}
324 self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
325 self.req, fake.PROJECT_ID, body=body)
326
327 def test_delete(self):
328 result_show = self.controller.show(self.req, fake.PROJECT_ID)
329 self.assertDictEqual(make_body(), result_show)
330
331 body = make_body(gigabytes=2000, snapshots=15,
332 volumes=5, backups=5,
333 backup_gigabytes=1000, tenant_id=None)
334 result_update = self.controller.update(self.req, fake.PROJECT_ID,
335 body=body)
336 self.assertDictEqual(body, result_update)
337
338 self.controller.delete(self.req, fake.PROJECT_ID)
339
340 result_show_after = self.controller.show(self.req, fake.PROJECT_ID)
341 self.assertDictEqual(result_show, result_show_after)
342
343 def test_delete_with_allocated_quota_different_from_zero(self):
344 self.req.environ['cinder.context'].project_id = self.A.id
345
346 body = make_body(gigabytes=2000, snapshots=15,
347 volumes=5, backups=5,
348 backup_gigabytes=1000, tenant_id=None)
349 result_update = self.controller.update(self.req, self.A.id, body=body)
350 self.assertDictEqual(body, result_update)
351
352 # Set usage param to True in order to see get allocated values.
353 self.req.params = {'usage': 'True'}
354 result_show = self.controller.show(self.req, self.A.id)
355
356 result_update = self.controller.update(self.req, self.B.id, body=body)
357 self.assertDictEqual(body, result_update)
358
359 self.controller.delete(self.req, self.B.id)
360
361 result_show_after = self.controller.show(self.req, self.A.id)
362 self.assertDictEqual(result_show, result_show_after)
363
364 def test_delete_no_admin(self):
365 self.req.environ['cinder.context'].is_admin = False
366 self.assertRaises(exception.PolicyNotAuthorized,
367 self.controller.delete, self.req, fake.PROJECT_ID)
368
369 def test_subproject_show_not_using_nested_quotas(self):
370 # Current roles say for non-nested quotas, an admin should be able to
371 # see anyones quota
372 self.req.environ['cinder.context'].project_id = self.B.id
373 self.controller.show(self.req, self.C.id)
374 self.controller.show(self.req, self.A.id)
375
376
377 @ddt.ddt
378 class QuotaSetControllerValidateNestedQuotaSetup(QuotaSetsControllerTestBase):
379 """Validates the setup before using NestedQuota driver.
380
381 Test case validates flipping on NestedQuota driver after using the
382 non-nested quota driver for some time.
383 """
384
385 def _create_project_hierarchy(self):
386 r"""Sets an environment used for nested quotas tests.
387
388 Create a project hierarchy such as follows:
389 +-----------------+
390 | |
391 | A G E |
392 | / \ \ |
393 | B C F |
394 | / |
395 | D |
396 +-----------------+
397 """
398 super(QuotaSetControllerValidateNestedQuotaSetup,
399 self)._create_project_hierarchy()
400 # Project A, B, C, D are already defined by parent test class
401 self.E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None)
402 self.F = self.FakeProject(id=uuid.uuid4().hex, parent_id=self.E.id)
403 self.G = self.FakeProject(id=uuid.uuid4().hex, parent_id=None)
404
405 self.E.subtree = {self.F.id: self.F.subtree}
406
407 self.project_by_id.update({self.E.id: self.E, self.F.id: self.F,
408 self.G.id: self.G})
409
410 @ddt.data({'param': None, 'result': False},
411 {'param': 'true', 'result': True},
412 {'param': 'false', 'result': False})
413 @ddt.unpack
414 def test_validate_setup_for_nested_quota_use_with_param(self, param,
415 result):
416 with mock.patch(
417 'cinder.quota_utils.validate_setup_for_nested_quota_use') as \
418 mock_quota_utils:
419 if param:
420 self.req.params['fix_allocated_quotas'] = param
421 self.controller.validate_setup_for_nested_quota_use(self.req)
422 mock_quota_utils.assert_called_once_with(
423 self.req.environ['cinder.context'],
424 mock.ANY, mock.ANY,
425 fix_allocated_quotas=result)
426
427 def test_validate_setup_for_nested_quota_use_with_invalid_param(self):
428 self.req.params['fix_allocated_quotas'] = 'non_boolean'
429 self.assertRaises(
430 webob.exc.HTTPBadRequest,
431 self.controller.validate_setup_for_nested_quota_use,
432 self.req)
433
434 def test_validate_nested_quotas_no_in_use_vols(self):
435 # Update the project A quota.
436 self.req.environ['cinder.context'].project_id = self.A.id
437 quota = {'volumes': 5}
438 body = {'quota_set': quota}
439 self.controller.update(self.req, self.A.id, body=body)
440
441 quota['volumes'] = 3
442 self.controller.update(self.req, self.B.id, body=body)
443 # Allocated value for quota A is borked, because update was done
444 # without nested quota driver
445 self.assertRaises(webob.exc.HTTPBadRequest,
446 self.controller.validate_setup_for_nested_quota_use,
447 self.req)
448
449 # Fix the allocated values in DB
450 self.req.params['fix_allocated_quotas'] = True
451 self.controller.validate_setup_for_nested_quota_use(
452 self.req)
453
454 self.req.params['fix_allocated_quotas'] = False
455 # Ensure that we've properly fixed the allocated quotas
456 self.controller.validate_setup_for_nested_quota_use(self.req)
457
458 # Over-allocate the quotas between children
459 self.controller.update(self.req, self.C.id, body=body)
460
461 # This is we should fail because the child limits are too big
462 self.assertRaises(webob.exc.HTTPBadRequest,
463 self.controller.validate_setup_for_nested_quota_use,
464 self.req)
465
466 quota['volumes'] = 1
467 self.controller.update(self.req, self.C.id, body=body)
468
469 # Make sure we're validating all hierarchy trees
470 self.req.environ['cinder.context'].project_id = self.E.id
471 quota['volumes'] = 1
472 self.controller.update(self.req, self.E.id, body=body)
473 quota['volumes'] = 3
474 self.controller.update(self.req, self.F.id, body=body)
475
476 self.assertRaises(
477 webob.exc.HTTPBadRequest,
478 self.controller.validate_setup_for_nested_quota_use,
479 self.req)
480
481 # Put quotas in a good state
482 quota['volumes'] = 1
483 self.controller.update(self.req, self.F.id, body=body)
484 self.req.params['fix_allocated_quotas'] = True
485 self.controller.validate_setup_for_nested_quota_use(self.req)
486
487 @mock.patch('cinder.db.quota_usage_get_all_by_project')
488 def test_validate_nested_quotas_in_use_vols(self, mock_usage):
489 self._create_fake_quota_usages(
490 {self.A.id: 1, self.B.id: 1, self.D.id: 0, self.C.id: 3,
491 self.E.id: 0, self.F.id: 0, self.G.id: 0})
492 mock_usage.side_effect = self._fake_quota_usage_get_all_by_project
493
494 # Update the project A quota.
495 self.req.environ['cinder.context'].project_id = self.A.id
496 quota_limit = {'volumes': 7}
497 body = {'quota_set': quota_limit}
498 self.controller.update(self.req, self.A.id, body=body)
499
500 quota_limit['volumes'] = 3
501 self.controller.update(self.req, self.B.id, body=body)
502
503 quota_limit['volumes'] = 3
504 self.controller.update(self.req, self.C.id, body=body)
505
506 self.req.params['fix_allocated_quotas'] = True
507 self.controller.validate_setup_for_nested_quota_use(self.req)
508
509 quota_limit['volumes'] = 6
510 self.controller.update(self.req, self.A.id, body=body)
511
512 # Should fail because the one in_use volume of 'A'
513 self.assertRaises(
514 webob.exc.HTTPBadRequest,
515 self.controller.validate_setup_for_nested_quota_use,
516 self.req)
517
518 @mock.patch('cinder.db.quota_usage_get_all_by_project')
519 def test_validate_nested_quotas_quota_borked(self, mock_usage):
520 self._create_fake_quota_usages(
521 {self.A.id: 1, self.B.id: 1, self.D.id: 0, self.C.id: 3,
522 self.E.id: 0, self.F.id: 0, self.G.id: 0})
523 mock_usage.side_effect = self._fake_quota_usage_get_all_by_project
524
525 # Update the project A quota.
526 self.req.environ['cinder.context'].project_id = self.A.id
527 quota_limit = {'volumes': 7}
528 body = {'quota_set': quota_limit}
529 self.controller.update(self.req, self.A.id, body=body)
530
531 # Other quotas would default to 0 but already have some limit being
532 # used
533 self.assertRaises(
534 webob.exc.HTTPBadRequest,
535 self.controller.validate_setup_for_nested_quota_use,
536 self.req)
537
538 @mock.patch('cinder.db.quota_usage_get_all_by_project')
539 def test_validate_nested_quota_negative_limits(self, mock_usage):
540 # TODO(mc_nair): this test case can be moved to Tempest once nested
541 # quota coverage added
542 self._create_fake_quota_usages(
543 {self.A.id: 1, self.B.id: 3, self.C.id: 0, self.D.id: 2,
544 self.E.id: 2, self.F.id: 0, self.G.id: 0})
545 mock_usage.side_effect = self._fake_quota_usage_get_all_by_project
546
547 # Setting E-F as children of D for this test case to flex the muscles
548 # of more complex nesting
549 self.D.subtree = {self.E.id: self.E.subtree}
550 self.E.parent_id = self.D.id
551 # Get B's subtree up to date with this change
552 self.B.subtree[self.D.id] = self.D.subtree
553
554 # Quota hierarchy now is
555 # / B - D - E - F
556 # A
557 # \ C
558 #
559 # G
560
561 self.req.environ['cinder.context'].project_id = self.A.id
562 quota_limit = {'volumes': 10}
563 body = {'quota_set': quota_limit}
564 self.controller.update(self.req, self.A.id, body=body)
565
566 quota_limit['volumes'] = 1
567 self.controller.update(self.req, self.C.id, body=body)
568
569 quota_limit['volumes'] = -1
570 self.controller.update(self.req, self.B.id, body=body)
571 self.controller.update(self.req, self.D.id, body=body)
572 self.controller.update(self.req, self.F.id, body=body)
573 quota_limit['volumes'] = 5
574 self.controller.update(self.req, self.E.id, body=body)
575
576 # Should fail because too much is allocated to children for A
577 self.assertRaises(webob.exc.HTTPBadRequest,
578 self.controller.validate_setup_for_nested_quota_use,
579 self.req)
580
581 # When root has -1 limit, children can allocate as much as they want
582 quota_limit['volumes'] = -1
583 self.controller.update(self.req, self.A.id, body=body)
584 self.req.params['fix_allocated_quotas'] = True
585 self.controller.validate_setup_for_nested_quota_use(self.req)
586
587 # Not unlimited, but make children's allocated within bounds
588 quota_limit['volumes'] = 10
589 self.controller.update(self.req, self.A.id, body=body)
590 quota_limit['volumes'] = 3
591 self.controller.update(self.req, self.E.id, body=body)
592 self.req.params['fix_allocated_quotas'] = True
593 self.controller.validate_setup_for_nested_quota_use(self.req)
594 self.req.params['fix_allocated_quotas'] = False
595 self.controller.validate_setup_for_nested_quota_use(self.req)
596
597
598 class QuotaSetsControllerNestedQuotasTest(QuotaSetsControllerTestBase):
599 def setUp(self):
600 super(QuotaSetsControllerNestedQuotasTest, self).setUp()
601 driver = quota.NestedDbQuotaDriver()
602 patcher = mock.patch('cinder.quota.VolumeTypeQuotaEngine._driver',
603 driver)
604 patcher.start()
605 self.addCleanup(patcher.stop)
606
607 def test_subproject_defaults(self):
608 context = self.req.environ['cinder.context']
609 context.project_id = self.B.id
610 result = self.controller.defaults(self.req, self.B.id)
611 expected = make_subproject_body(tenant_id=self.B.id)
612 self.assertDictEqual(expected, result)
613
614 def test_subproject_show(self):
615 self.req.environ['cinder.context'].project_id = self.A.id
616 result = self.controller.show(self.req, self.B.id)
617 expected = make_subproject_body(tenant_id=self.B.id)
618 self.assertDictEqual(expected, result)
619
620 def test_subproject_show_in_hierarchy(self):
621 # A user scoped to a root project in a hierarchy can see its children
622 # quotas.
623 self.req.environ['cinder.context'].project_id = self.A.id
624 result = self.controller.show(self.req, self.D.id)
625 expected = make_subproject_body(tenant_id=self.D.id)
626 self.assertDictEqual(expected, result)
627 # A user scoped to a parent project can see its immediate children
628 # quotas.
629 self.req.environ['cinder.context'].project_id = self.B.id
630 result = self.controller.show(self.req, self.D.id)
631 expected = make_subproject_body(tenant_id=self.D.id)
632 self.assertDictEqual(expected, result)
633
634 def test_subproject_show_not_in_hierarchy_admin_context(self):
635 E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None,
636 is_admin_project=True)
637 self.project_by_id[E.id] = E
638 self.req.environ['cinder.context'].project_id = E.id
639 result = self.controller.show(self.req, self.B.id)
640 expected = make_subproject_body(tenant_id=self.B.id)
641 self.assertDictEqual(expected, result)
642
643 def test_subproject_show_target_project_equals_to_context_project(
644 self):
645 self.req.environ['cinder.context'].project_id = self.B.id
646 result = self.controller.show(self.req, self.B.id)
647 expected = make_subproject_body(tenant_id=self.B.id)
648 self.assertDictEqual(expected, result)
649
650 def test_subproject_show_not_authorized(self):
651 self.req.environ['cinder.context'].project_id = self.B.id
652 self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
653 self.req, self.C.id)
654 self.req.environ['cinder.context'].project_id = self.B.id
655 self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
656 self.req, self.A.id)
657
658 def test_update_subproject_not_in_hierarchy(self):
659 # Create another project hierarchy
660 E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None)
661 F = self.FakeProject(id=uuid.uuid4().hex, parent_id=E.id)
662 E.subtree = {F.id: F.subtree}
663 self.project_by_id[E.id] = E
664 self.project_by_id[F.id] = F
665
666 # Update the project A quota.
667 self.req.environ['cinder.context'].project_id = self.A.id
668 body = make_body(gigabytes=2000, snapshots=15,
669 volumes=5, backups=5, tenant_id=None)
670 result = self.controller.update(self.req, self.A.id, body=body)
671 self.assertDictEqual(body, result)
672 # Try to update the quota of F, it will not be allowed, since the
673 # project E doesn't belongs to the project hierarchy of A.
674 self.req.environ['cinder.context'].project_id = self.A.id
675 body = make_body(gigabytes=2000, snapshots=15,
676 volumes=5, backups=5, tenant_id=None)
677 self.assertRaises(webob.exc.HTTPForbidden,
678 self.controller.update, self.req, F.id, body=body)
679
680 def test_update_subproject_not_in_hierarchy_admin_context(self):
681 E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None,
682 is_admin_project=True)
683 self.project_by_id[E.id] = E
684 self.req.environ['cinder.context'].project_id = E.id
685 body = make_body(gigabytes=2000, snapshots=15,
686 volumes=5, backups=5, tenant_id=None)
687 # Update the project A quota, not in the project hierarchy
688 # of E but it will be allowed because E is the cloud admin.
689 result = self.controller.update(self.req, self.A.id, body=body)
690 self.assertDictEqual(body, result)
691 # Update the quota of B to be equal to its parent A.
692 result = self.controller.update(self.req, self.B.id, body=body)
693 self.assertDictEqual(body, result)
694 # Remove the admin role from project E
695 E.is_admin_project = False
696 # Now updating the quota of B will fail, because it is not
697 # a member of E's hierarchy and E is no longer a cloud admin.
698 self.assertRaises(webob.exc.HTTPForbidden,
699 self.controller.update, self.req, self.B.id,
700 body=body)
701
702 def test_update_subproject(self):
703 # Update the project A quota.
704 self.req.environ['cinder.context'].project_id = self.A.id
705 body = make_body(gigabytes=2000, snapshots=15,
706 volumes=5, backups=5, tenant_id=None)
707 result = self.controller.update(self.req, self.A.id, body=body)
708 self.assertDictEqual(body, result)
709 # Update the quota of B to be equal to its parent quota
710 self.req.environ['cinder.context'].project_id = self.A.id
711 body = make_body(gigabytes=2000, snapshots=15,
712 volumes=5, backups=5, tenant_id=None)
713 result = self.controller.update(self.req, self.B.id, body=body)
714 self.assertDictEqual(body, result)
715 # Try to update the quota of C, it will not be allowed, since the
716 # project A doesn't have free quota available.
717 self.req.environ['cinder.context'].project_id = self.A.id
718 body = make_body(gigabytes=2000, snapshots=15,
719 volumes=5, backups=5, tenant_id=None)
720 self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
721 self.req, self.C.id, body=body)
722 # Successfully update the quota of D.
723 self.req.environ['cinder.context'].project_id = self.A.id
724 body = make_body(gigabytes=1000, snapshots=7,
725 volumes=3, backups=3, tenant_id=None)
726 result = self.controller.update(self.req, self.D.id, body=body)
727 self.assertDictEqual(body, result)
728 # An admin of B can also update the quota of D, since D is its
729 # immediate child.
730 self.req.environ['cinder.context'].project_id = self.B.id
731 body = make_body(gigabytes=1500, snapshots=10,
732 volumes=4, backups=4, tenant_id=None)
733 self.controller.update(self.req, self.D.id, body=body)
734
735 def test_update_subproject_repetitive(self):
736 # Update the project A volumes quota.
737 self.req.environ['cinder.context'].project_id = self.A.id
738 body = make_body(gigabytes=2000, snapshots=15,
739 volumes=10, backups=5, tenant_id=None)
740 result = self.controller.update(self.req, self.A.id, body=body)
741 self.assertDictEqual(body, result)
742 # Update the quota of B to be equal to its parent quota
743 # three times should be successful, the quota will not be
744 # allocated to 'allocated' value of parent project
745 for i in range(0, 3):
746 self.req.environ['cinder.context'].project_id = self.A.id
747 body = make_body(gigabytes=2000, snapshots=15,
748 volumes=10, backups=5, tenant_id=None)
749 result = self.controller.update(self.req, self.B.id, body=body)
750 self.assertDictEqual(body, result)
751
752 def test_update_subproject_with_not_root_context_project(self):
753 # Update the project A quota.
754 self.req.environ['cinder.context'].project_id = self.A.id
755 body = make_body(gigabytes=2000, snapshots=15,
756 volumes=5, backups=5, tenant_id=None)
757 result = self.controller.update(self.req, self.A.id, body=body)
758 self.assertDictEqual(body, result)
759 # Try to update the quota of B, it will not be allowed, since the
760 # project in the context (B) is not a root project.
761 self.req.environ['cinder.context'].project_id = self.B.id
762 body = make_body(gigabytes=2000, snapshots=15,
763 volumes=5, backups=5, tenant_id=None)
764 self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
765 self.req, self.B.id, body=body)
766
767 def test_update_subproject_quota_when_parent_has_default_quotas(self):
768 # Since the quotas of the project A were not updated, it will have
769 # default quotas.
770 self.req.environ['cinder.context'].project_id = self.A.id
771 # Update the project B quota.
772 expected = make_body(gigabytes=1000, snapshots=10,
773 volumes=5, backups=5, tenant_id=None)
774 result = self.controller.update(self.req, self.B.id, body=expected)
775 self.assertDictEqual(expected, result)
776
777 def _assert_quota_show(self, proj_id, resource, in_use=0, reserved=0,
778 allocated=0, limit=0):
779 self.req.params = {'usage': 'True'}
780 show_res = self.controller.show(self.req, proj_id)
781 expected = {'in_use': in_use, 'reserved': reserved,
782 'allocated': allocated, 'limit': limit}
783 self.assertEqual(expected, show_res['quota_set'][resource])
784
785 def test_project_allocated_considered_on_reserve(self):
786 def _reserve(project_id):
787 quotas.QUOTAS._driver.reserve(
788 self.req.environ['cinder.context'], quotas.QUOTAS.resources,
789 {'volumes': 1}, project_id=project_id)
790
791 # A's quota will default to 10 for volumes
792 quota = {'volumes': 5}
793 body = {'quota_set': quota}
794 self.controller.update(self.req, self.B.id, body=body)
795 self._assert_quota_show(self.A.id, 'volumes', allocated=5, limit=10)
796 quota['volumes'] = 3
797 self.controller.update(self.req, self.C.id, body=body)
798 self._assert_quota_show(self.A.id, 'volumes', allocated=8, limit=10)
799 _reserve(self.A.id)
800 _reserve(self.A.id)
801 self.assertRaises(exception.OverQuota, _reserve, self.A.id)
802
803 def test_update_parent_project_lower_than_child(self):
804 # A's quota will be default of 10
805 quota = {'volumes': 10}
806 body = {'quota_set': quota}
807 self.controller.update(self.req, self.B.id, body=body)
808 quota['volumes'] = 9
809 self.assertRaises(webob.exc.HTTPBadRequest,
810 self.controller.update, self.req, self.A.id,
811 body=body)
812
813 def test_project_delete_with_default_quota_less_than_in_use(self):
814 quota = {'volumes': 11}
815 body = {'quota_set': quota}
816 self.controller.update(self.req, self.A.id, body=body)
817 quotas.QUOTAS._driver.reserve(
818 self.req.environ['cinder.context'], quotas.QUOTAS.resources,
819 quota, project_id=self.A.id)
820 # Should not be able to delete if it will cause the used values to go
821 # over quota when nested quotas are used
822 self.assertRaises(webob.exc.HTTPBadRequest,
823 self.controller.delete,
824 self.req,
825 self.A.id)
826
827 def test_subproject_delete_with_default_quota_less_than_in_use(self):
828 quota = {'volumes': 1}
829 body = {'quota_set': quota}
830 self.controller.update(self.req, self.B.id, body=body)
831 quotas.QUOTAS._driver.reserve(
832 self.req.environ['cinder.context'], quotas.QUOTAS.resources,
833 quota, project_id=self.B.id)
834
835 # Should not be able to delete if it will cause the used values to go
836 # over quota when nested quotas are used
837 self.assertRaises(webob.exc.HTTPBadRequest,
838 self.controller.delete,
839 self.req,
840 self.B.id)
841
842 def test_subproject_delete(self):
843 self.req.environ['cinder.context'].project_id = self.A.id
844
845 body = make_body(gigabytes=2000, snapshots=15, volumes=5, backups=5,
846 backup_gigabytes=1000, tenant_id=None)
847 result_update = self.controller.update(self.req, self.A.id, body=body)
848 self.assertDictEqual(body, result_update)
849
850 # Set usage param to True in order to see get allocated values.
851 self.req.params = {'usage': 'True'}
852 result_show = self.controller.show(self.req, self.A.id)
853
854 result_update = self.controller.update(self.req, self.B.id, body=body)
855 self.assertDictEqual(body, result_update)
856
857 self.controller.delete(self.req, self.B.id)
858
859 result_show_after = self.controller.show(self.req, self.A.id)
860 self.assertDictEqual(result_show, result_show_after)
861
862 def test_subproject_delete_not_considering_default_quotas(self):
863 """Test delete subprojects' quotas won't consider default quotas.
864
865 Test plan:
866 - Update the volume quotas of project A
867 - Update the volume quotas of project B
868 - Delete the quotas of project B
869
870 Resources with default quotas aren't expected to be considered when
871 updating the allocated values of the parent project. Thus, the delete
872 operation should succeed.
873 """
874 self.req.environ['cinder.context'].project_id = self.A.id
875
876 body = {'quota_set': {'volumes': 5}}
877 result = self.controller.update(self.req, self.A.id, body=body)
878 self.assertEqual(body['quota_set']['volumes'],
879 result['quota_set']['volumes'])
880
881 body = {'quota_set': {'volumes': 2}}
882 result = self.controller.update(self.req, self.B.id, body=body)
883 self.assertEqual(body['quota_set']['volumes'],
884 result['quota_set']['volumes'])
885
886 self.controller.delete(self.req, self.B.id)
887
888 def test_subproject_delete_with_child_present(self):
889 # Update the project A quota.
890 self.req.environ['cinder.context'].project_id = self.A.id
891 body = make_body(volumes=5)
892 self.controller.update(self.req, self.A.id, body=body)
893
894 # Allocate some of that quota to a child project
895 body = make_body(volumes=3)
896 self.controller.update(self.req, self.B.id, body=body)
897
898 # Deleting 'A' should be disallowed since 'B' is using some of that
899 # quota
900 self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
901 self.req, self.A.id)
902
903 def test_subproject_delete_with_child_updates_parent_allocated(self):
904 quota = {'volumes': 5}
905 body = {'quota_set': quota}
906 self.controller.update(self.req, self.A.id, body=body)
907
908 # Allocate some of that quota to a child project using hard limit
909 quota['volumes'] = -1
910 self.controller.update(self.req, self.B.id, body=body)
911 quota['volumes'] = 2
912 self.controller.update(self.req, self.D.id, body=body)
913
914 res = 'volumes'
915 self._assert_quota_show(self.A.id, res, allocated=2, limit=5)
916 self._assert_quota_show(self.B.id, res, allocated=2, limit=-1)
917 self.controller.delete(self.req, self.D.id)
918 self._assert_quota_show(self.A.id, res, allocated=0, limit=5)
919 self._assert_quota_show(self.B.id, res, allocated=0, limit=-1)
920
921 def test_negative_child_limit_not_affecting_parents_free_quota(self):
922 quota = {'volumes': -1}
923 body = {'quota_set': quota}
924 self.controller.update(self.req, self.C.id, body=body)
925 self.controller.update(self.req, self.B.id, body=body)
926
927 # Shouldn't be able to set greater than parent
928 quota['volumes'] = 11
929 self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
930 self.req, self.B.id, body=body)
931
932 def test_child_neg_limit_set_grandkid_zero_limit(self):
933 cur_quota_a = self.controller.show(self.req, self.A.id)
934 self.assertEqual(10, cur_quota_a['quota_set']['volumes'])
935
936 quota = {'volumes': -1}
937 body = {'quota_set': quota}
938 self.controller.update(self.req, self.B.id, body=body)
939
940 cur_quota_d = self.controller.show(self.req, self.D.id)
941 # Default child value is 0
942 self.assertEqual(0, cur_quota_d['quota_set']['volumes'])
943 # Should be able to set D explicitly to 0 since that's already the val
944 quota['volumes'] = 0
945 self.controller.update(self.req, self.D.id, body=body)
946
947 def test_grandkid_negative_one_limit_enforced(self):
948 quota = {'volumes': 2, 'gigabytes': 2}
949 body = {'quota_set': quota}
950 self.controller.update(self.req, self.A.id, body=body)
951
952 quota['volumes'] = -1
953 quota['gigabytes'] = -1
954 self.controller.update(self.req, self.B.id, body=body)
955 self.controller.update(self.req, self.C.id, body=body)
956 self.controller.update(self.req, self.D.id, body=body)
957
958 def _reserve(project_id):
959 quotas.QUOTAS._driver.reserve(
960 self.req.environ['cinder.context'], quotas.QUOTAS.resources,
961 {'volumes': 1, 'gigabytes': 1}, project_id=project_id)
962
963 _reserve(self.C.id)
964 _reserve(self.D.id)
965 self.assertRaises(exception.OverQuota, _reserve, self.B.id)
966 self.assertRaises(exception.OverQuota, _reserve, self.C.id)
967 self.assertRaises(exception.OverQuota, _reserve, self.D.id)
968
969 # Make sure the rollbacks went successfully for allocated for all res
970 for res in quota.keys():
971 self._assert_quota_show(self.A.id, res, allocated=2, limit=2)
972 self._assert_quota_show(self.B.id, res, allocated=1, limit=-1)
973 self._assert_quota_show(self.C.id, res, reserved=1, limit=-1)
974 self._assert_quota_show(self.D.id, res, reserved=1, limit=-1)
975
976 def test_child_update_affects_allocated_and_rolls_back(self):
977 quota = {'gigabytes': -1, 'volumes': 3}
978 body = {'quota_set': quota}
979 self.controller.update(self.req, self.A.id, body=body)
980 quota['volumes'] = -1
981 self.controller.update(self.req, self.B.id, body=body)
982 quota['volumes'] = 1
983 self.controller.update(self.req, self.C.id, body=body)
984
985 # Shouldn't be able to update to greater than the grandparent
986 quota['volumes'] = 3
987 quota['gigabytes'] = 1
988 self.assertRaises(webob.exc.HTTPBadRequest,
989 self.controller.update, self.req, self.D.id,
990 body=body)
991 # Validate we haven't updated either parents' allocated value for
992 # any of the keys (even if some keys were valid)
993 self._assert_quota_show(self.A.id, 'volumes', allocated=1, limit=3)
994 self._assert_quota_show(self.A.id, 'gigabytes', limit=-1)
995 self._assert_quota_show(self.B.id, 'volumes', limit=-1)
996 self._assert_quota_show(self.B.id, 'gigabytes', limit=-1)
997
998 quota['volumes'] = 2
999 self.controller.update(self.req, self.D.id, body=body)
1000 # Validate we have now updated the parent and grandparents'
1001 self.req.params = {'usage': 'True'}
1002 self._assert_quota_show(self.A.id, 'volumes', allocated=3, limit=3)
1003 self._assert_quota_show(self.A.id, 'gigabytes', allocated=1, limit=-1)
1004 self._assert_quota_show(self.B.id, 'volumes', allocated=2, limit=-1)
1005 self._assert_quota_show(self.B.id, 'gigabytes', allocated=1, limit=-1)
1006
1007 def test_negative_child_limit_reserve_and_rollback(self):
1008 quota = {'volumes': 2, 'gigabytes': 2}
1009 body = {'quota_set': quota}
1010 self.controller.update(self.req, self.A.id, body=body)
1011
1012 quota['volumes'] = -1
1013 quota['gigabytes'] = -1
1014 self.controller.update(self.req, self.B.id, body=body)
1015 self.controller.update(self.req, self.C.id, body=body)
1016 self.controller.update(self.req, self.D.id, body=body)
1017
1018 res = quotas.QUOTAS._driver.reserve(
1019 self.req.environ['cinder.context'], quotas.QUOTAS.resources,
1020 {'volumes': 2, 'gigabytes': 2}, project_id=self.D.id)
1021
1022 self.req.params = {'usage': 'True'}
1023 quota_b = self.controller.show(self.req, self.B.id)
1024 self.assertEqual(2, quota_b['quota_set']['volumes']['allocated'])
1025 # A will be the next hard limit to set
1026 quota_a = self.controller.show(self.req, self.A.id)
1027 self.assertEqual(2, quota_a['quota_set']['volumes']['allocated'])
1028 quota_d = self.controller.show(self.req, self.D.id)
1029 self.assertEqual(2, quota_d['quota_set']['volumes']['reserved'])
1030
1031 quotas.QUOTAS.rollback(self.req.environ['cinder.context'], res,
1032 self.D.id)
1033 # After the rollback, A's limit should be properly set again
1034 quota_a = self.controller.show(self.req, self.A.id)
1035 self.assertEqual(0, quota_a['quota_set']['volumes']['allocated'])
1036 quota_d = self.controller.show(self.req, self.D.id)
1037 self.assertEqual(0, quota_d['quota_set']['volumes']['in_use'])
1038
1039 @mock.patch('cinder.db.sqlalchemy.api._get_quota_usages')
1040 @mock.patch('cinder.db.quota_usage_get_all_by_project')
1041 def test_nested_quota_set_negative_limit(self, mock_usage, mock_get_usage):
1042 # TODO(mc_nair): this test should be moved to Tempest once nested quota
1043 # coverage is added
1044 fake_usages = {self.A.id: 1, self.B.id: 1, self.D.id: 2, self.C.id: 0}
1045 self._create_fake_quota_usages(fake_usages)
1046 mock_usage.side_effect = self._fake_quota_usage_get_all_by_project
1047
1048 class FakeUsage(object):
1049 def __init__(self, in_use, reserved):
1050 self.in_use = in_use
1051 self.reserved = reserved
1052 self.until_refresh = None
1053 self.total = self.reserved + self.in_use
1054
1055 def _fake__get_quota_usages(context, session, project_id,
1056 resources=None):
1057 if not project_id:
1058 return {}
1059 return {'volumes': FakeUsage(fake_usages[project_id], 0)}
1060 mock_get_usage.side_effect = _fake__get_quota_usages
1061
1062 # Update the project A quota.
1063 quota_limit = {'volumes': 7}
1064 body = {'quota_set': quota_limit}
1065 self.controller.update(self.req, self.A.id, body=body)
1066
1067 quota_limit['volumes'] = 4
1068 self.controller.update(self.req, self.B.id, body=body)
1069 quota_limit['volumes'] = -1
1070 self.controller.update(self.req, self.D.id, body=body)
1071
1072 quota_limit['volumes'] = 1
1073 self.controller.update(self.req, self.C.id, body=body)
1074
1075 self.req.params['fix_allocated_quotas'] = True
1076 self.controller.validate_setup_for_nested_quota_use(self.req)
1077
1078 # Validate that the allocated values look right for each project
1079 self.req.params = {'usage': 'True'}
1080
1081 res = 'volumes'
1082 # A has given 4 vols to B and 1 vol to C (from limits)
1083 self._assert_quota_show(self.A.id, res, allocated=5, in_use=1, limit=7)
1084 self._assert_quota_show(self.B.id, res, allocated=2, in_use=1, limit=4)
1085 self._assert_quota_show(self.D.id, res, in_use=2, limit=-1)
1086 self._assert_quota_show(self.C.id, res, limit=1)
1087
1088 # Update B to -1 limit, and make sure that A's allocated gets updated
1089 # with B + D's in_use values (one less than current limit
1090 quota_limit['volumes'] = -1
1091 self.controller.update(self.req, self.B.id, body=body)
1092 self._assert_quota_show(self.A.id, res, allocated=4, in_use=1, limit=7)
1093
1094 quota_limit['volumes'] = 6
1095 self.assertRaises(
1096 webob.exc.HTTPBadRequest,
1097 self.controller.update, self.req, self.B.id, body=body)
1098
1099 quota_limit['volumes'] = 5
1100 self.controller.update(self.req, self.B.id, body=body)
1101 self._assert_quota_show(self.A.id, res, allocated=6, in_use=1, limit=7)