· 8 years ago · Dec 19, 2017, 09:28 PM
1diff --git a/nova/api/openstack/compute/quota_sets.py b/nova/api/openstack/compute/quota_sets.py
2index ff0b6c160f..2a533c6957 100644
3--- a/nova/api/openstack/compute/quota_sets.py
4+++ b/nova/api/openstack/compute/quota_sets.py
5@@ -33,6 +33,7 @@ from nova.i18n import _
6 from nova import objects
7 from nova.policies import quota_sets as qs_policies
8 from nova import quota
9+from nova import utils
10
11
12 CONF = nova.conf.CONF
13@@ -52,6 +53,12 @@ class QuotaSetsController(wsgi.Controller):
14 'project_parent_id': project['parent_id'],
15 }
16
17+ def _normalize_parent(self, parent_id):
18+ # we ignore quota check for default domain
19+ if parent_id == 'default':
20+ return None
21+ return parent_id
22+
23 def _format_quota_set(self, project_id, quota_set, filtered_quotas):
24 """Convert the quota object to a result dict."""
25 if project_id:
26@@ -65,6 +72,48 @@ class QuotaSetsController(wsgi.Controller):
27 result[resource] = quota_set[resource]
28 return dict(quota_set=result)
29
30+ def _validate_quota_hierarchy(self, quota, key, project_quotas=None,
31+ parent_project_quotas=None):
32+ value = utils.validate_integer(
33+ quota[key], key, min_value=-1, max_value=0x7FFFFFFF)
34+ # NOTE: -1 is a flag value for unlimited
35+ if value < -1:
36+ msg = (_("Quota limit %(limit)s for %(key)s "
37+ "must be -1 or greater.") %
38+ {'limit': value, 'key': key})
39+ raise webob.exc.HTTPBadRequest(explanation=msg)
40+ # does quota value may be none ?
41+ current = 0
42+ if key in project_quotas:
43+ current = project_quotas[key]['limit']
44+ # decrement current value
45+ if value < current:
46+ return value - current
47+
48+ if parent_project_quotas:
49+ parent_limit = parent_project_quotas[key]['limit']
50+ if parent_limit == -1:
51+ # parent is unlimited
52+ return value
53+
54+ free_quota = (parent_limit -
55+ parent_project_quotas[key]['in_use'] -
56+ parent_project_quotas[key]['reserved'] -
57+ parent_project_quotas[key]['allocated'])
58+
59+ current = 0
60+ if project_quotas.get(key):
61+ current = project_quotas[key]['limit']
62+ if value == -1 and current != -1:
63+ msg = (_("Quota limit for %s "
64+ "cannot be unlimited since parent has limit.") % key)
65+ raise webob.exc.HTTPBadRequest(explanation=msg)
66+
67+ if value - current > free_quota:
68+ msg = _("Free quota available is %s.") % free_quota
69+ raise webob.exc.HTTPBadRequest(explanation=msg)
70+ return value
71+
72 def _validate_quota_limit(self, resource, limit, minimum, maximum):
73 def conv_inf(value):
74 return float("inf") if value == -1 else value
75@@ -81,18 +130,34 @@ class QuotaSetsController(wsgi.Controller):
76 {'limit': limit, 'resource': resource, 'maximum': maximum})
77 raise webob.exc.HTTPBadRequest(explanation=msg)
78
79- def _get_quotas(self, context, id, user_id=None, usages=False):
80+ def _get_quotas(self, context, id, user_id=None, usages=False,
81+ parent_project_id=None):
82 if user_id:
83 values = QUOTAS.get_user_quotas(context, id, user_id,
84 usages=usages)
85 else:
86- values = QUOTAS.get_project_quotas(context, id, usages=usages)
87+ values = QUOTAS.get_project_quotas(
88+ context, id, usages=usages,
89+ parent_project_id=parent_project_id)
90
91 if usages:
92 return values
93 else:
94 return {k: v['limit'] for k, v in values.items()}
95
96+ def _update_allocated(self, context, project_id, key, allocated, quotas):
97+ try:
98+ objects.Quotas.update_allocated(
99+ context, project_id, key, allocated
100+ )
101+ except exception.ProjectQuotaNotFound:
102+ limit = quotas[key]['limit']
103+ objects.Quotas.create_limit(context, project_id, key, limit)
104+
105+ objects.Quotas.update_allocated(
106+ context, project_id, key, allocated
107+ )
108+
109 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
110 @extensions.expected_errors(400)
111 def show(self, req, id):
112@@ -104,12 +169,15 @@ class QuotaSetsController(wsgi.Controller):
113
114 def _show(self, req, id, filtered_quotas):
115 context = req.environ['nova.context']
116- context.can(qs_policies.POLICY_ROOT % 'show',
117- self._get_target(context, id))
118+ project_id = id
119+ target = self._get_target(context, project_id)
120+ context.can(qs_policies.POLICY_ROOT % 'show', target)
121+ parent_id = self._normalize_parent(target['project_parent_id'])
122 params = urlparse.parse_qs(req.environ.get('QUERY_STRING', ''))
123 user_id = params.get('user_id', [None])[0]
124 return self._format_quota_set(
125- id, self._get_quotas(context, id, user_id=user_id),
126+ project_id, self._get_quotas(context, id, user_id=user_id,
127+ parent_project_id=parent_id),
128 filtered_quotas=filtered_quotas)
129
130 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
131@@ -124,12 +192,15 @@ class QuotaSetsController(wsgi.Controller):
132
133 def _detail(self, req, id, filtered_quotas):
134 context = req.environ['nova.context']
135- context.can(qs_policies.POLICY_ROOT % 'detail',
136- self._get_target(context, id))
137+ project_id = id
138+ target = self._get_target(context, project_id)
139+ context.can(qs_policies.POLICY_ROOT % 'detail', target)
140 user_id = req.GET.get('user_id', None)
141+ parent_id = self._normalize_parent(target['project_parent_id'])
142 return self._format_quota_set(
143- id,
144- self._get_quotas(context, id, user_id=user_id, usages=True),
145+ project_id,
146+ self._get_quotas(context, id, user_id=user_id, usages=True,
147+ parent_project_id=parent_id),
148 filtered_quotas=filtered_quotas)
149
150 @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
151@@ -146,9 +217,10 @@ class QuotaSetsController(wsgi.Controller):
152
153 def _update(self, req, id, body, filtered_quotas):
154 context = req.environ['nova.context']
155- context.can(qs_policies.POLICY_ROOT % 'update',
156- self._get_target(context, id))
157 project_id = id
158+ target = self._get_target(context, project_id)
159+ context.can(qs_policies.POLICY_ROOT % 'update', target)
160+ parent_id = self._normalize_parent(target['project_parent_id'])
161 params = urlparse.parse_qs(req.environ.get('QUERY_STRING', ''))
162 user_id = params.get('user_id', [None])[0]
163
164@@ -164,12 +236,23 @@ class QuotaSetsController(wsgi.Controller):
165
166 force_update = strutils.bool_from_string(quota_set.get('force',
167 'False'))
168- settable_quotas = QUOTAS.get_settable_quotas(context, project_id,
169- user_id=user_id)
170-
171+ if parent_id:
172+ # Get the children of the project which the token is scoped to in
173+ # order to know if the target_project is in its hierarchy.
174+ parent_quotas = QUOTAS.get_project_quotas(
175+ context, parent_id, parent_project_id=parent_id)
176+
177+ settable_quotas = QUOTAS.get_settable_quotas(
178+ context, project_id, user_id=user_id,
179+ parent_project_id=parent_id)
180 # NOTE(dims): Pass #1 - In this loop for quota_set.items(), we validate
181 # min/max values and bail out if any of the items in the set is bad.
182 valid_quotas = {}
183+ allocated_quotas = {}
184+ quota_values = QUOTAS.get_project_quotas(
185+ context, project_id, defaults=False,
186+ parent_project_id=parent_id)
187+
188 for key, value in six.iteritems(body['quota_set']):
189 if key == 'force' or (not value and value != 0):
190 continue
191@@ -181,6 +264,13 @@ class QuotaSetsController(wsgi.Controller):
192 minimum = settable_quotas[key]['minimum']
193 maximum = settable_quotas[key]['maximum']
194 self._validate_quota_limit(key, value, minimum, maximum)
195+
196+ if parent_id:
197+ value = self._validate_quota_hierarchy(
198+ body['quota_set'], key, quota_values, parent_quotas)
199+ allocated_quotas[key] = \
200+ parent_quotas[key].get('allocated', 0) + value
201+
202 valid_quotas[key] = value
203
204 # NOTE(dims): Pass #2 - At this point we know that all the
205@@ -194,11 +284,22 @@ class QuotaSetsController(wsgi.Controller):
206 except exception.QuotaExists:
207 objects.Quotas.update_limit(context, project_id,
208 key, value, user_id=user_id)
209+
210+ # If hierarchical projects, update child's quota first
211+ # and then parents quota. In future this needs to be an
212+ # atomic operation.
213+ if parent_id and key in allocated_quotas:
214+ self._update_allocated(
215+ context, parent_id, key, allocated_quotas[key],
216+ parent_quotas
217+ )
218+
219 # Note(gmann): Removed 'id' from update's response to make it same
220 # as V2. If needed it can be added with microversion.
221 return self._format_quota_set(
222 None,
223- self._get_quotas(context, id, user_id=user_id),
224+ self._get_quotas(context, project_id, user_id=user_id,
225+ parent_project_id=parent_id),
226 filtered_quotas=filtered_quotas)
227
228 @wsgi.Controller.api_version("2.0", MAX_PROXY_API_SUPPORT_VERSION)
229@@ -213,11 +314,13 @@ class QuotaSetsController(wsgi.Controller):
230
231 def _defaults(self, req, id, filtered_quotas):
232 context = req.environ['nova.context']
233- context.can(qs_policies.POLICY_ROOT % 'defaults',
234- self._get_target(context, id))
235- values = QUOTAS.get_defaults(context)
236- return self._format_quota_set(id, values,
237- filtered_quotas=filtered_quotas)
238+ project_id = id
239+ target = self._get_target(context, project_id)
240+ context.can(qs_policies.POLICY_ROOT % 'defaults', target)
241+ parent_id = self._normalize_parent(target['project_parent_id'])
242+ values = QUOTAS.get_defaults(context, parent_project_id=parent_id)
243+ return self._format_quota_set(
244+ project_id, values, filtered_quotas=filtered_quotas)
245
246 # TODO(oomichi): Here should be 204(No Content) instead of 202 by v2.1
247 # +microversions because the resource quota-set has been deleted completely
248@@ -226,15 +329,49 @@ class QuotaSetsController(wsgi.Controller):
249 @wsgi.response(202)
250 def delete(self, req, id):
251 context = req.environ['nova.context']
252- context.can(qs_policies.POLICY_ROOT % 'delete',
253- self._get_target(context, id))
254+ project_id = id
255+ target = self._get_target(context, project_id)
256+ context.can(qs_policies.POLICY_ROOT % 'delete', target)
257+ parent_id = self._normalize_parent(target['project_parent_id'])
258 params = urlparse.parse_qs(req.environ.get('QUERY_STRING', ''))
259 user_id = params.get('user_id', [None])[0]
260- if user_id:
261- QUOTAS.destroy_all_by_project_and_user(context,
262- id, user_id)
263+ try:
264+ project_quotas = QUOTAS.get_project_quotas(
265+ context, project_id, usages=False,
266+ parent_project_id=parent_id, defaults=False)
267+ except exception.Unauthorized:
268+ raise webob.exc.HTTPForbidden()
269+
270+ # If the project which is being deleted has allocated part of its quota
271+ # to its subprojects, then subprojects' quotas should be deleted first.
272+ for key in project_quotas:
273+ if project_quotas[key].get('allocated'):
274+ msg = _("About to delete child projects having "
275+ "non-zero quota. This should not be performed")
276+ raise webob.exc.HTTPBadRequest(explanation=msg)
277+
278+ if parent_id:
279+ parent_quotas = QUOTAS.get_project_quotas(
280+ context, parent_id, parent_project_id=parent_id)
281+
282+ # Delete child quota first and later update parent's quota.
283+ try:
284+ QUOTAS.destroy_all_by_project(context, project_id)
285+ except exception.AdminRequired:
286+ raise webob.exc.HTTPForbidden()
287+
288+ # Update the allocated of the parent
289+ for key, value in project_quotas.items():
290+ project_hard_limit = project_quotas[key]['limit']
291+ parent_allocated = parent_quotas[key]['allocated']
292+ parent_allocated -= project_hard_limit
293+ self._update_allocated(
294+ context, parent_id, key, parent_allocated, parent_quotas)
295+ elif user_id:
296+ QUOTAS.destroy_all_by_project_and_user(
297+ context, project_id, user_id)
298 else:
299- QUOTAS.destroy_all_by_project(context, id)
300+ QUOTAS.destroy_all_by_project(context, project_id)
301
302
303 class QuotaSets(extensions.V21APIExtensionBase):
304diff --git a/nova/db/api.py b/nova/db/api.py
305index 1102268210..5c6f57fcdf 100644
306--- a/nova/db/api.py
307+++ b/nova/db/api.py
308@@ -1124,6 +1124,17 @@ def quota_get_all(context, project_id):
309 return IMPL.quota_get_all(context, project_id)
310
311
312+def quota_allocated_get_all_by_project(context, project_id):
313+ """Retrieve all allocated quotas associated with a given project."""
314+ return IMPL.quota_allocated_get_all_by_project(context, project_id)
315+
316+
317+def quota_allocated_update(context, project_id, resource, allocated):
318+ """Update allocated quota to subprojects or raise if it does not exist."""
319+ return IMPL.quota_allocated_update(
320+ context, project_id, resource, allocated)
321+
322+
323 def quota_update(context, project_id, resource, limit, user_id=None):
324 """Update a quota or raise if it does not exist."""
325 return IMPL.quota_update(context, project_id, resource, limit,
326diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
327index 01f3d8685a..c37a92f85d 100644
328--- a/nova/db/sqlalchemy/api.py
329+++ b/nova/db/sqlalchemy/api.py
330@@ -3347,6 +3347,17 @@ def quota_get_all_by_project(context, project_id):
331 return result
332
333
334+@require_context
335+@main_context_manager.reader
336+def quota_allocated_get_all_by_project(context, project_id):
337+ rows = model_query(context, models.Quota, read_deleted='no').\
338+ filter_by(project_id=project_id).all()
339+ result = {'project_id': project_id}
340+ for row in rows:
341+ result[row.resource] = row.allocated
342+ return result
343+
344+
345 @require_context
346 @main_context_manager.reader
347 def quota_get_all(context, project_id):
348@@ -3396,6 +3407,19 @@ def quota_update(context, project_id, resource, limit, user_id=None):
349 raise exception.ProjectQuotaNotFound(project_id=project_id)
350
351
352+@main_context_manager.writer
353+def quota_allocated_update(context, project_id, resource, allocated):
354+ model = models.Quota
355+ query = model_query(context, model).\
356+ filter_by(project_id=project_id).\
357+ filter_by(resource=resource)
358+
359+ result = query.update({'allocated': allocated})
360+ if not result:
361+ raise exception.ProjectQuotaNotFound(project_id=project_id)
362+ return result
363+
364+
365 ###################
366
367
368diff --git a/nova/db/sqlalchemy/migrate_repo/versions/335_add_allocated_in_quotas.py b/nova/db/sqlalchemy/migrate_repo/versions/335_add_allocated_in_quotas.py
369index e69de29bb2..32b6cb1620 100644
370--- a/nova/db/sqlalchemy/migrate_repo/versions/335_add_allocated_in_quotas.py
371+++ b/nova/db/sqlalchemy/migrate_repo/versions/335_add_allocated_in_quotas.py
372@@ -0,0 +1,29 @@
373+# Copyright 2015 OpenStack Foundation
374+# All Rights Reserved
375+#
376+# Licensed under the Apache License, Version 2.0 (the "License"); you may
377+# not use this file except in compliance with the License. You may obtain
378+# a copy of the License at
379+#
380+# http://www.apache.org/licenses/LICENSE-2.0
381+#
382+# Unless required by applicable law or agreed to in writing, software
383+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
384+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
385+# License for the specific language governing permissions and limitations
386+# under the License.
387+
388+from sqlalchemy import Column, Integer, MetaData, Table
389+
390+
391+def upgrade(migrate_engine):
392+ meta = MetaData(bind=migrate_engine)
393+
394+ quotas = Table('quotas', meta, autoload=True)
395+ shadow_quotas = Table('shadow_quotas', meta, autoload=True)
396+
397+ allocated = Column('allocated', Integer, default=0)
398+ if not hasattr(quotas.c, 'allocated'):
399+ quotas.create_column(allocated)
400+ if not hasattr(shadow_quotas.c, 'allocated'):
401+ shadow_quotas.create_column(allocated.copy())
402diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
403index 173c50e99a..50c0a36d1d 100644
404--- a/nova/db/sqlalchemy/models.py
405+++ b/nova/db/sqlalchemy/models.py
406@@ -441,6 +441,11 @@ class Quota(BASE, NovaBase, models.SoftDeleteMixin):
407 resource = Column(String(255), nullable=False)
408 hard_limit = Column(Integer)
409
410+ # allocated is the sum of the quota hard_limit values of immediate child
411+ # projects. It defaults to 0, since it will be zero till the parent project
412+ # allocated a finite quota to its immediate child projects
413+ allocated = Column(Integer, default=0)
414+
415
416 class ProjectUserQuota(BASE, NovaBase, models.SoftDeleteMixin):
417 """Represents a single quota override for a user with in a project."""
418diff --git a/nova/objects/quotas.py b/nova/objects/quotas.py
419index 6cdb07917d..e4a70de436 100644
420--- a/nova/objects/quotas.py
421+++ b/nova/objects/quotas.py
422@@ -51,7 +51,8 @@ class Quotas(base.NovaObject):
423 # Version 1.0: initial version
424 # Version 1.1: Added create_limit() and update_limit()
425 # Version 1.2: Added limit_check() and count()
426- VERSION = '1.2'
427+ # Version 1.2: Added update_allocated()
428+ VERSION = '1.3'
429
430 fields = {
431 'reservations': fields.ListOfStringsField(nullable=True),
432@@ -142,6 +143,13 @@ class Quotas(base.NovaObject):
433 # logic in the db api layer for this, just pass this through for now.
434 db.quota_update(context, project_id, resource, limit, user_id=user_id)
435
436+ @base.remotable_classmethod
437+ def update_allocated(cls, context, project_id, resource, allocated):
438+ # NOTE(danms,comstud): Quotas likely needs an overhaul and currently
439+ # doesn't map very well to objects. Since there is quite a bit of
440+ # logic in the db api layer for this, just pass this through for now.
441+ db.quota_allocated_update(context, project_id, resource, allocated)
442+
443
444 @base.NovaObjectRegistry.register
445 class QuotasNoOp(Quotas):
446diff --git a/nova/quota.py b/nova/quota.py
447index 6313acf9fd..aa42b58192 100644
448--- a/nova/quota.py
449+++ b/nova/quota.py
450@@ -57,20 +57,25 @@ class DbQuotaDriver(object):
451
452 return db.quota_class_get(context, quota_class, resource)
453
454- def get_defaults(self, context, resources):
455+ def get_defaults(self, context, resources, parent_project_id=None):
456 """Given a list of resources, retrieve the default quotas.
457 Use the class quotas named `_DEFAULT_QUOTA_NAME` as default quotas,
458 if it exists.
459
460 :param context: The request context, for access checks.
461 :param resources: A dictionary of the registered resources.
462+ :param parent_project_id: The id of the current project's parent,
463+ if any.
464 """
465
466 quotas = {}
467- default_quotas = db.quota_class_get_default(context)
468+ default_quotas = {}
469+ if not parent_project_id:
470+ default_quotas = db.quota_class_get_default(context)
471 for resource in resources.values():
472 quotas[resource.name] = default_quotas.get(resource.name,
473- resource.default)
474+ (0 if parent_project_id
475+ else resource.default))
476
477 return quotas
478
479@@ -99,7 +104,7 @@ class DbQuotaDriver(object):
480
481 def _process_quotas(self, context, resources, project_id, quotas,
482 quota_class=None, defaults=True, usages=None,
483- remains=False):
484+ remains=False, parent_project_id=None):
485 modified_quotas = {}
486 # Get the quotas for the appropriate class. If the project ID
487 # matches the one in the context, we use the quota_class from
488@@ -112,8 +117,12 @@ class DbQuotaDriver(object):
489 else:
490 class_quotas = {}
491
492- default_quotas = self.get_defaults(context, resources)
493+ default_quotas = self.get_defaults(context, resources,
494+ parent_project_id=parent_project_id)
495
496+ allocated_quotas = db.quota_allocated_get_all_by_project(
497+ context, project_id)
498+ allocated_quotas.pop('project_id')
499 for resource in resources.values():
500 # Omit default/quota class values
501 if not defaults and resource.name not in quotas:
502@@ -132,6 +141,10 @@ class DbQuotaDriver(object):
503 in_use=usage.get('in_use', 0),
504 reserved=usage.get('reserved', 0),
505 )
506+ if parent_project_id or allocated_quotas:
507+ modified_quotas[resource.name].update(
508+ allocated=allocated_quotas.get(resource.name, 0)
509+ )
510 # Initialize remains quotas.
511 if remains:
512 modified_quotas[resource.name].update(remains=limit)
513@@ -194,7 +207,8 @@ class DbQuotaDriver(object):
514
515 def get_project_quotas(self, context, resources, project_id,
516 quota_class=None, defaults=True,
517- usages=True, remains=False, project_quotas=None):
518+ usages=True, remains=False, project_quotas=None,
519+ parent_project_id=None):
520 """Given a list of resources, retrieve the quotas for the given
521 project.
522
523@@ -215,6 +229,8 @@ class DbQuotaDriver(object):
524 :param remains: If True, the current remains of the project will
525 will be returned.
526 :param project_quotas: Quotas dictionary for the specified project.
527+ :param parent_project_id: The id of the current project's parent,
528+ if any.
529 """
530 project_quotas = project_quotas or db.quota_get_all_by_project(
531 context, project_id)
532@@ -226,7 +242,8 @@ class DbQuotaDriver(object):
533 return self._process_quotas(context, resources, project_id,
534 project_quotas, quota_class,
535 defaults=defaults, usages=project_usages,
536- remains=remains)
537+ remains=remains,
538+ parent_project_id=parent_project_id)
539
540 def _is_unlimited_value(self, v):
541 """A helper method to check for unlimited value.
542@@ -253,7 +270,7 @@ class DbQuotaDriver(object):
543 return v1 - v2
544
545 def get_settable_quotas(self, context, resources, project_id,
546- user_id=None):
547+ user_id=None, parent_project_id=None):
548 """Given a list of resources, retrieve the range of settable quotas for
549 the given user or project.
550
551@@ -261,13 +278,15 @@ class DbQuotaDriver(object):
552 :param resources: A dictionary of the registered resources.
553 :param project_id: The ID of the project to return quotas for.
554 :param user_id: The ID of the user to return quotas for.
555+ :param parent_project_id: The id of the current project's parent,
556+ if any.
557 """
558
559 settable_quotas = {}
560 db_proj_quotas = db.quota_get_all_by_project(context, project_id)
561 project_quotas = self.get_project_quotas(context, resources,
562- project_id, remains=True,
563- project_quotas=db_proj_quotas)
564+ project_id, remains=True, project_quotas=db_proj_quotas,
565+ parent_project_id=parent_project_id)
566 if user_id:
567 setted_quotas = db.quota_get_all_by_project_and_user(context,
568 project_id,
569@@ -319,7 +338,7 @@ class DbQuotaDriver(object):
570 return syncable_resources
571
572 def _get_quotas(self, context, resources, keys, has_sync, project_id=None,
573- user_id=None, project_quotas=None):
574+ user_id=None, project_quotas=None, parent_project_id=None):
575 """A helper method which retrieves the quotas for the specific
576 resources identified by keys, and which apply to the current
577 context.
578@@ -338,6 +357,8 @@ class DbQuotaDriver(object):
579 is admin and admin wants to impact on
580 common user.
581 :param project_quotas: Quotas dictionary for the specified project.
582+ :param parent_project_id: The id of the current project's parent,
583+ if any.
584 """
585
586 # Filter resources
587@@ -368,11 +389,10 @@ class DbQuotaDriver(object):
588 LOG.debug('Getting quotas for project %(project_id)s. Resources: '
589 '%(keys)s', {'project_id': project_id, 'keys': keys})
590 # Grab and return the quotas (without usages)
591- quotas = self.get_project_quotas(context, sub_resources,
592- project_id,
593- context.quota_class,
594- usages=False,
595- project_quotas=project_quotas)
596+ quotas = self.get_project_quotas(
597+ context, sub_resources, project_id, context.quota_class,
598+ usages=False, project_quotas=project_quotas,
599+ parent_project_id=parent_project_id)
600
601 return {k: v['limit'] for k, v in quotas.items()}
602
603@@ -719,7 +739,7 @@ class NoopQuotaDriver(object):
604 # Unlimited
605 return -1
606
607- def get_defaults(self, context, resources):
608+ def get_defaults(self, context, resources, parent_project_id=None):
609 """Given a list of resources, retrieve the default quotas.
610
611 :param context: The request context, for access checks.
612@@ -786,7 +806,8 @@ class NoopQuotaDriver(object):
613
614 def get_project_quotas(self, context, resources, project_id,
615 quota_class=None, defaults=True,
616- usages=True, remains=False):
617+ usages=True, remains=False,
618+ get_project_quotas=None):
619 """Given a list of resources, retrieve the quotas for the given
620 project.
621
622@@ -806,11 +827,13 @@ class NoopQuotaDriver(object):
623 will also be returned.
624 :param remains: If True, the current remains of the project will
625 will be returned.
626+ :param parent_project_id: The id of the current project's parent,
627+ if any.
628 """
629 return self._get_noop_quotas(resources, usages=usages, remains=remains)
630
631 def get_settable_quotas(self, context, resources, project_id,
632- user_id=None):
633+ user_id=None, parent_project_id=None):
634 """Given a list of resources, retrieve the range of settable quotas for
635 the given user or project.
636
637@@ -818,6 +841,8 @@ class NoopQuotaDriver(object):
638 :param resources: A dictionary of the registered resources.
639 :param project_id: The ID of the project to return quotas for.
640 :param user_id: The ID of the user to return quotas for.
641+ :param parent_project_id: The id of the current project's parent,
642+ if any.
643 """
644 quotas = {}
645 for resource in resources.values():
646@@ -998,17 +1023,20 @@ class NoopQuotaDriver(object):
647 class BaseResource(object):
648 """Describe a single resource for quota checking."""
649
650- def __init__(self, name, flag=None):
651+ def __init__(self, name, flag=None, parent_project_id=None):
652 """Initializes a Resource.
653
654 :param name: The name of the resource, i.e., "instances".
655 :param flag: The name of the flag or configuration option
656 which specifies the default value of the quota
657 for this resource.
658+ :param parent_project_id: The id of the current project's parent,
659+ if any.
660 """
661
662 self.name = name
663 self.flag = flag
664+ self.parent_project_id = parent_project_id
665
666 def quota(self, driver, context, **kwargs):
667 """Given a driver and context, obtain the quota for this
668@@ -1060,6 +1088,9 @@ class BaseResource(object):
669 def default(self):
670 """Return the default value of the quota."""
671
672+ if self.parent_project_id:
673+ return 0
674+
675 return CONF[self.flag] if self.flag else -1
676
677
678@@ -1198,13 +1229,16 @@ class QuotaEngine(object):
679
680 return self._driver.get_by_class(context, quota_class, resource)
681
682- def get_defaults(self, context):
683+ def get_defaults(self, context, parent_project_id=None):
684 """Retrieve the default quotas.
685
686 :param context: The request context, for access checks.
687+ :param parent_project_id: The id of the current project's parent,
688+ if any.
689 """
690
691- return self._driver.get_defaults(context, self._resources)
692+ return self._driver.get_defaults(context, self._resources,
693+ parent_project_id=parent_project_id)
694
695 def get_class_quotas(self, context, quota_class, defaults=True):
696 """Retrieve the quotas for the given quota class.
697@@ -1245,7 +1279,8 @@ class QuotaEngine(object):
698 usages=usages)
699
700 def get_project_quotas(self, context, project_id, quota_class=None,
701- defaults=True, usages=True, remains=False):
702+ defaults=True, usages=True, remains=False,
703+ parent_project_id=None):
704 """Retrieve the quotas for the given project.
705
706 :param context: The request context, for access checks.
707@@ -1261,27 +1296,30 @@ class QuotaEngine(object):
708 will also be returned.
709 :param remains: If True, the current remains of the project will
710 will be returned.
711+ :param parent_project_id: The id of the current project's parent,
712+ if any.
713 """
714
715- return self._driver.get_project_quotas(context, self._resources,
716- project_id,
717- quota_class=quota_class,
718- defaults=defaults,
719- usages=usages,
720- remains=remains)
721+ return self._driver.get_project_quotas(
722+ context, self._resources, project_id, quota_class=quota_class,
723+ defaults=defaults, usages=usages, remains=remains,
724+ parent_project_id=parent_project_id)
725
726- def get_settable_quotas(self, context, project_id, user_id=None):
727+ def get_settable_quotas(self, context, project_id, user_id=None,
728+ parent_project_id=None):
729 """Given a list of resources, retrieve the range of settable quotas for
730 the given user or project.
731
732 :param context: The request context, for access checks.
733 :param project_id: The ID of the project to return quotas for.
734 :param user_id: The ID of the user to return quotas for.
735+ :param parent_project_id: The id of the current project's parent,
736+ if any.
737 """
738
739- return self._driver.get_settable_quotas(context, self._resources,
740- project_id,
741- user_id=user_id)
742+ return self._driver.get_settable_quotas(
743+ context, self._resources, project_id, user_id=user_id,
744+ parent_project_id=parent_project_id)
745
746 def count(self, context, resource, *args, **kwargs):
747 """Count a resource.
748diff --git a/nova/tests/functional/api_sample_tests/test_quota_sets.py b/nova/tests/functional/api_sample_tests/test_quota_sets.py
749index 84d79ad976..eace771648 100644
750--- a/nova/tests/functional/api_sample_tests/test_quota_sets.py
751+++ b/nova/tests/functional/api_sample_tests/test_quota_sets.py
752@@ -13,6 +13,8 @@
753 # License for the specific language governing permissions and limitations
754 # under the License.
755
756+import mock
757+
758 from nova.tests.functional.api_sample_tests import api_sample_base
759
760
761@@ -20,6 +22,14 @@ class QuotaSetsSampleJsonTests(api_sample_base.ApiSampleTestBaseV21):
762 ADMIN_API = True
763 sample_dir = "os-quota-sets"
764
765+ def setUp(self):
766+ super(QuotaSetsSampleJsonTests, self).setUp()
767+ keystone = mock.patch(
768+ 'nova.api.openstack.identity.get_project',
769+ return_value={'id': 'fake_project', 'parent_id': None}
770+ ).start()
771+ self.addCleanup(keystone.stop)
772+
773 def test_show_quotas(self):
774 # Get api sample to show quotas.
775 response = self._do_get('os-quota-sets/fake_tenant')
776diff --git a/nova/tests/unit/api/openstack/compute/test_quotas.py b/nova/tests/unit/api/openstack/compute/test_quotas.py
777index dd3442ecaf..869ef099d9 100644
778--- a/nova/tests/unit/api/openstack/compute/test_quotas.py
779+++ b/nova/tests/unit/api/openstack/compute/test_quotas.py
780@@ -46,10 +46,13 @@ class BaseQuotaSetsTest(test.TestCase):
781 self.project_id = 'fake'
782 keystone = mock.patch(
783 'nova.api.openstack.identity.get_project',
784- return_value={'id': self.project_id, 'parent_id': 'default'}
785+ side_effect=self.get_project_by_id
786 ).start()
787 self.addCleanup(keystone.stop)
788
789+ def get_project_by_id(self, context, project_id):
790+ return {'id': project_id, 'parent_id': None}
791+
792 def get_delete_status_int(self, res):
793 # NOTE: on v2.1, http status code is set as wsgi_code of API
794 # method instead of status_int in a response object.
795@@ -323,13 +326,15 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest):
796 self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
797 self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
798
799- def fake_get_quotas(self, context, id, user_id=None, usages=False):
800+ def fake_get_quotas(self, context, id, user_id=None, usages=False,
801+ parent_project_id=None):
802 if usages:
803 return self.fake_quotas
804 else:
805 return {k: v['limit'] for k, v in self.fake_quotas.items()}
806
807- def fake_get_settable_quotas(self, context, project_id, user_id=None):
808+ def fake_get_settable_quotas(self, context, project_id, user_id=None,
809+ parent_project_id=None):
810 return {
811 'ram': {'minimum': self.fake_quotas['ram']['in_use'] +
812 self.fake_quotas['ram']['reserved'],
813@@ -400,6 +405,138 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest):
814 len(mock_createlimit.mock_calls))
815
816
817+class HierarchicalQuotasTestV21(QuotaSetsTestV21):
818+ def setUp(self):
819+ super(HierarchicalQuotasTestV21, self).setUp()
820+ self.default_subproject_quotas = (
821+ {k: 0 for k, v in self.default_quotas.items()})
822+ self._create_project_hierarchy()
823+
824+ def _create_project_hierarchy(self):
825+ """Sets an environment used for nested quotas tests.
826+ Create a project hierarchy such as follows:
827+ +-----------+
828+ | |
829+ | A |
830+ | / \ |
831+ | B C |
832+ | / |
833+ | D |
834+ +-----------+
835+ """
836+ self.project_a = {'id': 'project_a', 'parent_id': None}
837+ self.project_b = {'id': 'project_b', 'parent_id': self.project_a['id']}
838+ self.project_c = {'id': 'project_c', 'parent_id': self.project_a['id']}
839+ self.project_d = {'id': 'project_d', 'parent_id': self.project_b['id']}
840+ # project_by_id attribute is used to recover a project based on its id.
841+ self.project_by_id = {
842+ self.project_a['id']: self.project_a,
843+ self.project_b['id']: self.project_b,
844+ self.project_c['id']: self.project_c,
845+ self.project_d['id']: self.project_d,
846+ }
847+
848+ def get_project_by_id(self, context, project_id):
849+ if project_id in self.project_by_id:
850+ return self.project_by_id[project_id]
851+ return super(HierarchicalQuotasTestV21, self).get_project_by_id(
852+ context, project_id)
853+
854+ def test_quotas_subproject_defaults(self):
855+ uri = '/v2/%s/os-quota-sets/%s/defaults' % \
856+ (self.project_a['id'], self.project_b['id'])
857+ req = fakes.HTTPRequest.blank(uri)
858+ res_dict = self.controller.defaults(req, self.project_b['id'])
859+ self.default_subproject_quotas.update({'id': self.project_b['id']})
860+ expected = {'quota_set': self.default_subproject_quotas}
861+ self.assertEqual(res_dict, expected)
862+
863+ def test_quotas_subproject_show(self):
864+ req = self._get_http_request()
865+ req.environ['nova.context'].project_id = self.project_a['id']
866+ res_dict = self.controller.show(req, self.project_b['id'])
867+
868+ self.default_subproject_quotas.update({'id': self.project_b['id']})
869+ expected = {'quota_set': self.default_subproject_quotas}
870+ self.assertEqual(res_dict, expected)
871+
872+ def test_quotas_subproject_update(self):
873+ req = self._get_http_request()
874+ req.environ['nova.context'].project_id = self.project_a['id']
875+ self.default_subproject_quotas.update({
876+ 'instances': 50,
877+ 'cores': 50
878+ })
879+ body = {'quota_set': self.default_subproject_quotas}
880+ self.controller.update(req, self.project_a['id'], body=body)
881+ res_dict = self.controller.update(req, self.project_b['id'], body=body)
882+ self.assertEqual(body, res_dict)
883+
884+ def test_quotas_subproject_decrement(self):
885+ req = self._get_http_request()
886+ req.environ['nova.context'].project_id = self.project_a['id']
887+ self.default_subproject_quotas.update(instances=10)
888+ body = {'quota_set': self.default_subproject_quotas}
889+ self.controller.update(req, self.project_a['id'], body=body)
890+ self.controller.update(req, self.project_b['id'], body=body)
891+ b = quota.QUOTAS.get_by_project(
892+ req.environ['nova.context'], self.project_a['id'], 'instances')
893+ self.assertEqual(10, b.allocated)
894+ self.default_subproject_quotas.update(instances=5)
895+ self.controller.update(req, self.project_b['id'], body=body)
896+ a = quota.QUOTAS.get_by_project(
897+ req.environ['nova.context'], self.project_a['id'], 'instances')
898+ self.assertEqual(5, a.allocated)
899+
900+ def test_subproject_overquota_update(self):
901+ req = self._get_http_request()
902+ req.environ['nova.context'].project_id = self.project_b['id']
903+ self.default_subproject_quotas.update({
904+ 'instances': 50,
905+ 'cores': 50
906+ })
907+ body = {'quota_set': self.default_subproject_quotas}
908+ self.controller.update(req, self.project_a['id'], body=body)
909+ self.default_subproject_quotas.update({
910+ 'instances': 60,
911+ 'cores': 60
912+ })
913+ body = {'quota_set': self.default_subproject_quotas}
914+ self.assertRaises(webob.exc.HTTPBadRequest,
915+ self.controller.update,
916+ req, self.project_b['id'], body=body)
917+
918+ def test_subproject_underquota_update(self):
919+ req = self._get_http_request()
920+ req.environ['nova.context'].project_id = self.project_a['id']
921+ self.default_subproject_quotas.update({
922+ 'instances': 50,
923+ 'cores': 50
924+ })
925+ body = {'quota_set': self.default_subproject_quotas}
926+ self.controller.update(req, self.project_a['id'], body=body)
927+ self.default_subproject_quotas.update({
928+ 'instances': -10,
929+ 'cores': -20
930+ })
931+ body = {'quota_set': self.default_subproject_quotas}
932+ self.assertRaises(exception.ValidationError,
933+ self.controller.update,
934+ req, self.project_b['id'], body=body)
935+
936+ def test_quotas_subproject_delete(self):
937+ req = self._get_http_request()
938+ req.environ['nova.context'].project_id = self.project_a['id']
939+ self.mox.StubOutWithMock(quota.QUOTAS,
940+ "destroy_all_by_project")
941+ quota.QUOTAS.destroy_all_by_project(req.environ['nova.context'],
942+ self.project_b['id'])
943+ self.mox.ReplayAll()
944+ res = self.controller.delete(req, self.project_b['id'])
945+ self.mox.VerifyAll()
946+ self.assertEqual(202, self.get_delete_status_int(res))
947+
948+
949 class UserQuotasTestV21(BaseQuotaSetsTest):
950 plugin = quotas_v21
951 include_server_group_quotas = True
952@@ -494,7 +631,7 @@ class QuotaSetsPolicyEnforcementV21(test.NoDBTestCase):
953 self.req = fakes.HTTPRequest.blank('', project_id=project_id)
954 keystone = mock.patch(
955 'nova.api.openstack.identity.get_project',
956- return_value={'id': project_id, 'parent_id': 'default'}
957+ return_value={'id': project_id, 'parent_id': None}
958 ).start()
959 self.addCleanup(keystone.stop)
960
961@@ -567,7 +704,7 @@ class QuotaSetsTestV236(test.NoDBTestCase):
962 self.addCleanup(self._remove_network_quota)
963 keystone = mock.patch(
964 'nova.api.openstack.identity.get_project',
965- return_value={'id': project_id, 'parent_id': 'default'}
966+ return_value={'id': project_id, 'parent_id': None}
967 ).start()
968 self.addCleanup(keystone.stop)
969
970diff --git a/nova/tests/unit/db/test_migrations.py b/nova/tests/unit/db/test_migrations.py
971index 7c47f4cedd..b77def7f20 100644
972--- a/nova/tests/unit/db/test_migrations.py
973+++ b/nova/tests/unit/db/test_migrations.py
974@@ -916,6 +916,28 @@ class NovaMigrationsCheckers(test_migrations.ModelsMigrationsSync,
975 self.assertColumnExists(engine, 'shadow_instance_extra',
976 'device_metadata')
977
978+ def _pre_upgrade_335(self, engine):
979+ # create a fake quota for checking whether allocated quota will
980+ # take default value 0
981+ quotas = oslodbutils.get_table(engine, 'quotas')
982+ fake_quota = {'id': 1, 'resource': 'fake'}
983+ quotas.insert().execute(fake_quota)
984+
985+ def _check_335(self, engine, data):
986+ self.assertColumnExists(engine, 'quotas', 'allocated')
987+ self.assertColumnExists(engine, 'shadow_quotas', 'allocated')
988+ shadow_quotas = oslodbutils.get_table(engine, 'shadow_quotas')
989+
990+ quotas = oslodbutils.get_table(engine, 'quotas')
991+ quota = quotas.select(
992+ quotas.c.id == 1).execute().first()
993+ self.assertEqual(0, quota.allocated)
994+
995+ self.assertIsInstance(quotas.c.allocated.type,
996+ sqlalchemy.types.Integer)
997+ self.assertIsInstance(shadow_quotas.c.allocated.type,
998+ sqlalchemy.types.Integer)
999+
1000
1001 class TestNovaMigrationsSQLite(NovaMigrationsCheckers,
1002 test_base.DbTestCase,
1003diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py
1004index 6bc72d6d1f..1f998e329a 100644
1005--- a/nova/tests/unit/objects/test_objects.py
1006+++ b/nova/tests/unit/objects/test_objects.py
1007@@ -1177,8 +1177,8 @@ object_data = {
1008 'PciDeviceList': '1.3-52ff14355491c8c580bdc0ba34c26210',
1009 'PciDevicePool': '1.1-3f5ddc3ff7bfa14da7f6c7e9904cc000',
1010 'PciDevicePoolList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
1011- 'Quotas': '1.2-1fe4cd50593aaf5d36a6dc5ab3f98fb3',
1012- 'QuotasNoOp': '1.2-e041ddeb7dc8188ca71706f78aad41c1',
1013+ 'Quotas': '1.3-088f4e2d6fb1b298c60de569ece5c462',
1014+ 'QuotasNoOp': '1.3-cb12c37903aaa617e1626e1a4c5dcdf8',
1015 'RequestSpec': '1.6-c1cb516acdf120d367a42d343ed695b5',
1016 'ResourceProvider': '1.1-7bbcd5ea1c51782692f55489ab08dea6',
1017 'ResourceProviderList': '1.0-82bd48d8d0f7913bbe7266f3835c81bf',
1018diff --git a/nova/tests/unit/test_quota.py b/nova/tests/unit/test_quota.py
1019index 57b8efa359..65df7daa54 100644
1020--- a/nova/tests/unit/test_quota.py
1021+++ b/nova/tests/unit/test_quota.py
1022@@ -285,8 +285,9 @@ class FakeDriver(object):
1023 except KeyError:
1024 raise exception.QuotaClassNotFound(class_name=quota_class)
1025
1026- def get_defaults(self, context, resources):
1027- self.called.append(('get_defaults', context, resources))
1028+ def get_defaults(self, context, resources, parent_project_id=None):
1029+ self.called.append(('get_defaults', context, resources,
1030+ parent_project_id))
1031 return resources
1032
1033 def get_class_quotas(self, context, resources, quota_class,
1034@@ -304,10 +305,10 @@ class FakeDriver(object):
1035
1036 def get_project_quotas(self, context, resources, project_id,
1037 quota_class=None, defaults=True, usages=True,
1038- remains=False):
1039+ remains=False, parent_project_id=None):
1040 self.called.append(('get_project_quotas', context, resources,
1041 project_id, quota_class, defaults, usages,
1042- remains))
1043+ remains, parent_project_id))
1044 return resources
1045
1046 def limit_check(self, context, resources, values, project_id=None,
1047@@ -413,6 +414,16 @@ class BaseResourceTestCase(test.TestCase):
1048
1049 self.assertEqual(quota_value, 15)
1050
1051+ def test_quota_override_subproject_no_class(self):
1052+ self.flags(quota_instances=10)
1053+ resource = quota.BaseResource('test_resource', 'quota_instances',
1054+ parent_project_id='test_parent_project')
1055+ driver = FakeDriver()
1056+ context = FakeContext('test_project', None)
1057+ quota_value = resource.quota(driver, context)
1058+
1059+ self.assertEqual(quota_value, 0)
1060+
1061 def test_quota_override_project_with_class(self):
1062 self.flags(quota_instances=10)
1063 resource = quota.BaseResource('test_resource', 'quota_instances')
1064@@ -577,11 +588,13 @@ class QuotaEngineTestCase(test.TestCase):
1065 def test_get_defaults(self):
1066 context = FakeContext(None, None)
1067 driver = FakeDriver()
1068+ parent_project_id = None
1069 quota_obj = self._make_quota_obj(driver)
1070 result = quota_obj.get_defaults(context)
1071
1072 self.assertEqual(driver.called, [
1073- ('get_defaults', context, quota_obj._resources),
1074+ ('get_defaults', context, quota_obj._resources,
1075+ parent_project_id),
1076 ])
1077 self.assertEqual(result, quota_obj._resources)
1078
1079@@ -625,6 +638,7 @@ class QuotaEngineTestCase(test.TestCase):
1080 def test_get_project_quotas(self):
1081 context = FakeContext(None, None)
1082 driver = FakeDriver()
1083+ parent_project_id = None
1084 quota_obj = self._make_quota_obj(driver)
1085 result1 = quota_obj.get_project_quotas(context, 'test_project')
1086 result2 = quota_obj.get_project_quotas(context, 'test_project',
1087@@ -634,9 +648,37 @@ class QuotaEngineTestCase(test.TestCase):
1088
1089 self.assertEqual(driver.called, [
1090 ('get_project_quotas', context, quota_obj._resources,
1091- 'test_project', None, True, True, False),
1092+ 'test_project', None, True, True, False,
1093+ parent_project_id),
1094+ ('get_project_quotas', context, quota_obj._resources,
1095+ 'test_project', 'test_class', False, False, False,
1096+ parent_project_id),
1097+ ])
1098+ self.assertEqual(result1, quota_obj._resources)
1099+ self.assertEqual(result2, quota_obj._resources)
1100+
1101+ def test_get_subproject_quotas(self):
1102+ context = FakeContext(None, None)
1103+ driver = FakeDriver()
1104+ parent_project_id = 'test_parent_project_id'
1105+ quota_obj = self._make_quota_obj(driver)
1106+ result1 = quota_obj.get_project_quotas(context, 'test_project',
1107+ parent_project_id=
1108+ parent_project_id)
1109+ result2 = quota_obj.get_project_quotas(context, 'test_project',
1110+ quota_class='test_class',
1111+ defaults=False,
1112+ usages=False,
1113+ parent_project_id=
1114+ parent_project_id)
1115+
1116+ self.assertEqual(driver.called, [
1117+ ('get_project_quotas', context, quota_obj._resources,
1118+ 'test_project', None, True, True, False,
1119+ parent_project_id),
1120 ('get_project_quotas', context, quota_obj._resources,
1121- 'test_project', 'test_class', False, False, False),
1122+ 'test_project', 'test_class', False, False, False,
1123+ parent_project_id),
1124 ])
1125 self.assertEqual(result1, quota_obj._resources)
1126 self.assertEqual(result2, quota_obj._resources)
1127@@ -856,6 +898,29 @@ class DbQuotaDriverTestCase(test.TestCase):
1128 server_group_members=10,
1129 ))
1130
1131+ def test_get_subproject_defaults(self):
1132+ # Test subproject default values
1133+ parent_project_id = 'test_parent_project_id'
1134+ result = self.driver.get_defaults(None, quota.QUOTAS._resources,
1135+ parent_project_id)
1136+
1137+ self.assertEqual(result, dict(
1138+ instances=0,
1139+ cores=0,
1140+ ram=0,
1141+ floating_ips=0,
1142+ fixed_ips=0,
1143+ metadata_items=0,
1144+ injected_files=0,
1145+ injected_file_content_bytes=0,
1146+ injected_file_path_bytes=0,
1147+ security_groups=0,
1148+ security_group_rules=0,
1149+ key_pairs=0,
1150+ server_groups=0,
1151+ server_group_members=0,
1152+ ))
1153+
1154 def _stub_quota_class_get_default(self):
1155 # Stub out quota_class_get_default
1156 def fake_qcgd(context):
1157@@ -1107,6 +1172,30 @@ class DbQuotaDriverTestCase(test.TestCase):
1158 self._stub_quota_class_get_all_by_name()
1159 self._stub_quota_class_get_default()
1160
1161+ def _stub_get_by_subproject(self):
1162+ def fake_qgabp(context, project_id):
1163+ self.calls.append('quota_get_all_by_project')
1164+ self.assertEqual(project_id, 'test_project')
1165+ return dict(
1166+ cores=10,
1167+ injected_files=2,
1168+ injected_file_path_bytes=127,
1169+ )
1170+
1171+ def fake_qugabp(context, project_id):
1172+ self.calls.append('quota_usage_get_all_by_project')
1173+ self.assertEqual(project_id, 'test_project')
1174+ return dict(cores=dict(in_use=4, reserved=4),)
1175+
1176+ def fake_quota_get_all(context, project_id):
1177+ self.calls.append('quota_get_all')
1178+ self.assertEqual(project_id, 'test_project')
1179+ return []
1180+
1181+ self.stubs.Set(db, 'quota_get_all_by_project', fake_qgabp)
1182+ self.stubs.Set(db, 'quota_usage_get_all_by_project', fake_qugabp)
1183+ self.stubs.Set(db, 'quota_get_all', fake_quota_get_all)
1184+
1185 def test_get_project_quotas(self):
1186 self.maxDiff = None
1187 self._stub_get_by_project()
1188@@ -1193,6 +1282,104 @@ class DbQuotaDriverTestCase(test.TestCase):
1189 ),
1190 ))
1191
1192+ def test_get_subproject_quotas(self):
1193+ self.maxDiff = None
1194+ self._stub_get_by_subproject()
1195+ parent_project_id = 'test_parent_project_id'
1196+ result = self.driver.get_project_quotas(
1197+ FakeContext('test_project', None),
1198+ quota.QUOTAS._resources, 'test_project',
1199+ parent_project_id=parent_project_id)
1200+
1201+ self.assertIn('quota_get_all_by_project', self.calls)
1202+ self.assertIn('quota_usage_get_all_by_project', self.calls)
1203+ self.assertEqual(result, dict(
1204+ instances=dict(
1205+ allocated=0,
1206+ limit=0,
1207+ in_use=0,
1208+ reserved=0,
1209+ ),
1210+ cores=dict(
1211+ allocated=0,
1212+ limit=10,
1213+ in_use=4,
1214+ reserved=4,
1215+ ),
1216+ ram=dict(
1217+ allocated=0,
1218+ limit=0,
1219+ in_use=0,
1220+ reserved=0,
1221+ ),
1222+ floating_ips=dict(
1223+ allocated=0,
1224+ limit=0,
1225+ in_use=0,
1226+ reserved=0,
1227+ ),
1228+ fixed_ips=dict(
1229+ allocated=0,
1230+ limit=0,
1231+ in_use=0,
1232+ reserved=0,
1233+ ),
1234+ metadata_items=dict(
1235+ allocated=0,
1236+ limit=0,
1237+ in_use=0,
1238+ reserved=0,
1239+ ),
1240+ injected_files=dict(
1241+ allocated=0,
1242+ limit=2,
1243+ in_use=0,
1244+ reserved=0,
1245+ ),
1246+ injected_file_content_bytes=dict(
1247+ allocated=0,
1248+ limit=0,
1249+ in_use=0,
1250+ reserved=0,
1251+ ),
1252+ injected_file_path_bytes=dict(
1253+ allocated=0,
1254+ limit=127,
1255+ in_use=0,
1256+ reserved=0,
1257+ ),
1258+ security_groups=dict(
1259+ allocated=0,
1260+ limit=0,
1261+ in_use=0,
1262+ reserved=0,
1263+ ),
1264+ security_group_rules=dict(
1265+ allocated=0,
1266+ limit=0,
1267+ in_use=0,
1268+ reserved=0,
1269+ ),
1270+ key_pairs=dict(
1271+ allocated=0,
1272+ limit=0,
1273+ in_use=0,
1274+ reserved=0,
1275+ ),
1276+ server_groups=dict(
1277+ allocated=0,
1278+ limit=0,
1279+ in_use=0,
1280+ reserved=0,
1281+ ),
1282+ server_group_members=dict(
1283+ allocated=0,
1284+ limit=0,
1285+ in_use=0,
1286+ reserved=0,
1287+ ),
1288+ ))
1289+
1290 def test_get_project_quotas_with_remains(self):
1291 self.maxDiff = None
1292 self._stub_get_by_project()
1293@@ -1200,13 +1387,11 @@ class DbQuotaDriverTestCase(test.TestCase):
1294 FakeContext('test_project', 'test_class'),
1295 quota.QUOTAS._resources, 'test_project', remains=True)
1296
1297- self.assertEqual(self.calls, [
1298- 'quota_get_all_by_project',
1299- 'quota_usage_get_all_by_project',
1300- 'quota_class_get_all_by_name',
1301- 'quota_class_get_default',
1302- 'quota_get_all',
1303- ])
1304+ self.assertIn('quota_get_all_by_project', self.calls)
1305+ self.assertIn('quota_usage_get_all_by_project', self.calls)
1306+ self.assertIn('quota_class_get_all_by_name', self.calls)
1307+ self.assertIn('quota_class_get_default', self.calls)
1308+ self.assertIn('quota_get_all', self.calls)
1309 self.assertEqual(result, dict(
1310 instances=dict(
1311 limit=5,
1312@@ -1817,9 +2002,12 @@ class DbQuotaDriverTestCase(test.TestCase):
1313 return {'floating_ips': 20}
1314
1315 def fake_get_project_quotas(dbdrv, context, resources, project_id,
1316- quota_class=None, defaults=True,
1317- usages=True, remains=False,
1318- project_quotas=None):
1319+ quota_class=None,
1320+ defaults=True,
1321+ usages=True,
1322+ remains=False,
1323+ project_quotas=None,
1324+ parent_project_id=None):
1325 self.calls.append('get_project_quotas')
1326 result = {}
1327 for k, v in resources.items():
1328@@ -1876,11 +2064,12 @@ class DbQuotaDriverTestCase(test.TestCase):
1329 self.stub_out('nova.db.quota_get_all_by_project_and_user',
1330 fake_qgabpau)
1331
1332- def test_get_settable_quotas_with_user(self):
1333+ def test_get_settable_quotas_with_user(self, parent_project_id=None):
1334 self._stub_get_settable_quotas()
1335 result = self.driver.get_settable_quotas(
1336 FakeContext('test_project', 'test_class'),
1337- quota.QUOTAS._resources, 'test_project', user_id='test_user')
1338+ quota.QUOTAS._resources, 'test_project', user_id='test_user',
1339+ parent_project_id=parent_project_id)
1340
1341 self.assertEqual(self.calls, [
1342 'quota_get_all_by_project',
1343@@ -2091,7 +2280,8 @@ class DbQuotaDriverTestCase(test.TestCase):
1344 def fake_get_project_quotas(dbdrv, context, resources, project_id,
1345 quota_class=None, defaults=True,
1346 usages=True, remains=False,
1347- project_quotas=None):
1348+ project_quotas=None,
1349+ parent_project_id=None):
1350 self.calls.append('get_project_quotas')
1351 return {k: dict(limit=v.default) for k, v in resources.items()}