Coverage for nova/objects/quotas.py: 74%
353 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Copyright 2013 Rackspace Hosting.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
15import collections
17from oslo_db import exception as db_exc
19from nova.db.api import api as api_db_api
20from nova.db.api import models as api_models
21from nova.db.main import api as main_db_api
22from nova.db.main import models as main_models
23from nova.db import utils as db_utils
24from nova import exception
25from nova.objects import base
26from nova.objects import fields
27from nova import quota
30def ids_from_instance(context, instance):
31 if (context.is_admin and
32 context.project_id != instance['project_id']):
33 project_id = instance['project_id']
34 else:
35 project_id = context.project_id
36 if context.user_id != instance['user_id']:
37 user_id = instance['user_id']
38 else:
39 user_id = context.user_id
40 return project_id, user_id
43# TODO(lyj): This method needs to be cleaned up once the
44# ids_from_instance helper method is renamed or some common
45# method is added for objects.quotas.
46def ids_from_security_group(context, security_group):
47 return ids_from_instance(context, security_group)
50# TODO(PhilD): This method needs to be cleaned up once the
51# ids_from_instance helper method is renamed or some common
52# method is added for objects.quotas.
53def ids_from_server_group(context, server_group):
54 return ids_from_instance(context, server_group)
57@base.NovaObjectRegistry.register
58class Quotas(base.NovaObject):
59 # Version 1.0: initial version
60 # Version 1.1: Added create_limit() and update_limit()
61 # Version 1.2: Added limit_check() and count()
62 # Version 1.3: Added check_deltas(), limit_check_project_and_user(),
63 # and count_as_dict()
64 VERSION = '1.3'
66 fields = {
67 # TODO(melwitt): Remove this field in version 2.0 of the object.
68 'reservations': fields.ListOfStringsField(nullable=True,
69 default=[]),
70 'project_id': fields.StringField(nullable=True,
71 default=None),
72 'user_id': fields.StringField(nullable=True,
73 default=None),
74 }
76 def obj_load_attr(self, attr):
77 self.obj_set_defaults(attr)
78 # NOTE(danms): This is strange because resetting these would cause
79 # them not to be saved to the database. I would imagine this is
80 # from overzealous defaulting and that all three fields ultimately
81 # get set all the time. However, quotas are weird, so replicate the
82 # longstanding behavior of setting defaults and clearing their
83 # dirty bit.
84 self.obj_reset_changes(fields=[attr])
86 @staticmethod
87 @api_db_api.context_manager.reader
88 def _get_from_db(context, project_id, resource, user_id=None):
89 model = api_models.ProjectUserQuota if user_id else api_models.Quota
90 query = context.session.query(model).\
91 filter_by(project_id=project_id).\
92 filter_by(resource=resource)
93 if user_id:
94 query = query.filter_by(user_id=user_id)
95 result = query.first()
96 if not result:
97 if user_id:
98 raise exception.ProjectUserQuotaNotFound(project_id=project_id,
99 user_id=user_id)
100 else:
101 raise exception.ProjectQuotaNotFound(project_id=project_id)
102 return result
104 @staticmethod
105 @api_db_api.context_manager.reader
106 def _get_all_from_db(context, project_id):
107 return context.session.query(api_models.ProjectUserQuota).\
108 filter_by(project_id=project_id).\
109 all()
111 @staticmethod
112 @api_db_api.context_manager.reader
113 def _get_all_from_db_by_project(context, project_id):
114 # by_project refers to the returned dict that has a 'project_id' key
115 rows = context.session.query(api_models.Quota).\
116 filter_by(project_id=project_id).\
117 all()
118 result = {'project_id': project_id}
119 for row in rows:
120 result[row.resource] = row.hard_limit
121 return result
123 @staticmethod
124 @api_db_api.context_manager.reader
125 def _get_all_from_db_by_project_and_user(context, project_id, user_id):
126 # by_project_and_user refers to the returned dict that has
127 # 'project_id' and 'user_id' keys
128 columns = (api_models.ProjectUserQuota.resource,
129 api_models.ProjectUserQuota.hard_limit)
130 user_quotas = context.session.query(*columns).\
131 filter_by(project_id=project_id).\
132 filter_by(user_id=user_id).\
133 all()
134 result = {'project_id': project_id, 'user_id': user_id}
135 for user_quota in user_quotas:
136 result[user_quota.resource] = user_quota.hard_limit
137 return result
139 @staticmethod
140 @api_db_api.context_manager.writer
141 def _destroy_all_in_db_by_project(context, project_id):
142 per_project = context.session.query(api_models.Quota).\
143 filter_by(project_id=project_id).\
144 delete(synchronize_session=False)
145 per_user = context.session.query(api_models.ProjectUserQuota).\
146 filter_by(project_id=project_id).\
147 delete(synchronize_session=False)
148 if not per_project and not per_user: 148 ↛ exitline 148 didn't return from function '_destroy_all_in_db_by_project' because the condition on line 148 was always true
149 raise exception.ProjectQuotaNotFound(project_id=project_id)
151 @staticmethod
152 @api_db_api.context_manager.writer
153 def _destroy_all_in_db_by_project_and_user(context, project_id, user_id):
154 result = context.session.query(api_models.ProjectUserQuota).\
155 filter_by(project_id=project_id).\
156 filter_by(user_id=user_id).\
157 delete(synchronize_session=False)
158 if not result: 158 ↛ exitline 158 didn't return from function '_destroy_all_in_db_by_project_and_user' because the condition on line 158 was always true
159 raise exception.ProjectUserQuotaNotFound(project_id=project_id,
160 user_id=user_id)
162 @staticmethod
163 @api_db_api.context_manager.reader
164 def _get_class_from_db(context, class_name, resource):
165 result = context.session.query(api_models.QuotaClass).\
166 filter_by(class_name=class_name).\
167 filter_by(resource=resource).\
168 first()
169 if not result: 169 ↛ 171line 169 didn't jump to line 171 because the condition on line 169 was always true
170 raise exception.QuotaClassNotFound(class_name=class_name)
171 return result
173 @staticmethod
174 @api_db_api.context_manager.reader
175 def _get_all_class_from_db_by_name(context, class_name):
176 # by_name refers to the returned dict that has a 'class_name' key
177 rows = context.session.query(api_models.QuotaClass).\
178 filter_by(class_name=class_name).\
179 all()
180 result = {'class_name': class_name}
181 for row in rows:
182 result[row.resource] = row.hard_limit
183 return result
185 @staticmethod
186 @api_db_api.context_manager.writer
187 def _create_limit_in_db(context, project_id, resource, limit,
188 user_id=None):
189 # TODO(melwitt): We won't have per project resources after nova-network
190 # is removed.
191 # TODO(stephenfin): We need to do something here now...but what?
192 per_user = (
193 user_id and
194 resource not in main_db_api.quota_get_per_project_resources()
195 )
196 quota_ref = (api_models.ProjectUserQuota() if per_user
197 else api_models.Quota())
198 if per_user:
199 quota_ref.user_id = user_id
200 quota_ref.project_id = project_id
201 quota_ref.resource = resource
202 quota_ref.hard_limit = limit
203 try:
204 quota_ref.save(context.session)
205 except db_exc.DBDuplicateEntry:
206 raise exception.QuotaExists(project_id=project_id,
207 resource=resource)
208 return quota_ref
210 @staticmethod
211 @api_db_api.context_manager.writer
212 def _update_limit_in_db(context, project_id, resource, limit,
213 user_id=None):
214 # TODO(melwitt): We won't have per project resources after nova-network
215 # is removed.
216 # TODO(stephenfin): We need to do something here now...but what?
217 per_user = (
218 user_id and
219 resource not in main_db_api.quota_get_per_project_resources()
220 )
221 model = api_models.ProjectUserQuota if per_user else api_models.Quota
222 query = context.session.query(model).\
223 filter_by(project_id=project_id).\
224 filter_by(resource=resource)
225 if per_user:
226 query = query.filter_by(user_id=user_id)
228 result = query.update({'hard_limit': limit})
229 if not result:
230 if per_user:
231 raise exception.ProjectUserQuotaNotFound(project_id=project_id,
232 user_id=user_id)
233 else:
234 raise exception.ProjectQuotaNotFound(project_id=project_id)
236 @staticmethod
237 @api_db_api.context_manager.writer
238 def _create_class_in_db(context, class_name, resource, limit):
239 # NOTE(melwitt): There's no unique constraint on the QuotaClass model,
240 # so check for duplicate manually.
241 try:
242 Quotas._get_class_from_db(context, class_name, resource)
243 except exception.QuotaClassNotFound:
244 pass
245 else:
246 raise exception.QuotaClassExists(class_name=class_name,
247 resource=resource)
248 quota_class_ref = api_models.QuotaClass()
249 quota_class_ref.class_name = class_name
250 quota_class_ref.resource = resource
251 quota_class_ref.hard_limit = limit
252 quota_class_ref.save(context.session)
253 return quota_class_ref
255 @staticmethod
256 @api_db_api.context_manager.writer
257 def _update_class_in_db(context, class_name, resource, limit):
258 result = context.session.query(api_models.QuotaClass).\
259 filter_by(class_name=class_name).\
260 filter_by(resource=resource).\
261 update({'hard_limit': limit})
262 if not result: 262 ↛ exitline 262 didn't return from function '_update_class_in_db' because the condition on line 262 was always true
263 raise exception.QuotaClassNotFound(class_name=class_name)
265 # TODO(melwitt): Remove this method in version 2.0 of the object.
266 @base.remotable
267 def reserve(self, expire=None, project_id=None, user_id=None,
268 **deltas):
269 # Honor the expected attributes even though we're not reserving
270 # anything anymore. This will protect against things exploding if
271 # someone has an Ocata compute host running by accident, for example.
272 self.reservations = None
273 self.project_id = project_id
274 self.user_id = user_id
275 self.obj_reset_changes()
277 # TODO(melwitt): Remove this method in version 2.0 of the object.
278 @base.remotable
279 def commit(self):
280 pass
282 # TODO(melwitt): Remove this method in version 2.0 of the object.
283 @base.remotable
284 def rollback(self):
285 pass
287 @base.remotable_classmethod
288 def limit_check(cls, context, project_id=None, user_id=None, **values):
289 """Check quota limits."""
290 return quota.QUOTAS.limit_check(
291 context, project_id=project_id, user_id=user_id, **values)
293 @base.remotable_classmethod
294 def limit_check_project_and_user(cls, context, project_values=None,
295 user_values=None, project_id=None,
296 user_id=None):
297 """Check values against quota limits."""
298 return quota.QUOTAS.limit_check_project_and_user(context,
299 project_values=project_values, user_values=user_values,
300 project_id=project_id, user_id=user_id)
302 # NOTE(melwitt): This can be removed once no old code can call count().
303 @base.remotable_classmethod
304 def count(cls, context, resource, *args, **kwargs):
305 """Count a resource."""
306 count = quota.QUOTAS.count_as_dict(context, resource, *args, **kwargs)
307 key = 'user' if 'user' in count else 'project'
308 return count[key][resource]
310 @base.remotable_classmethod
311 def count_as_dict(cls, context, resource, *args, **kwargs):
312 """Count a resource and return a dict."""
313 return quota.QUOTAS.count_as_dict(
314 context, resource, *args, **kwargs)
316 @base.remotable_classmethod
317 def check_deltas(cls, context, deltas, *count_args, **count_kwargs):
318 """Check usage delta against quota limits.
320 This does a Quotas.count_as_dict() followed by a
321 Quotas.limit_check_project_and_user() using the provided deltas.
323 :param context: The request context, for access checks
324 :param deltas: A dict of {resource_name: delta, ...} to check against
325 the quota limits
326 :param count_args: Optional positional arguments to pass to
327 count_as_dict()
328 :param count_kwargs: Optional keyword arguments to pass to
329 count_as_dict()
330 :param check_project_id: Optional project_id for scoping the limit
331 check to a different project than in the
332 context
333 :param check_user_id: Optional user_id for scoping the limit check to a
334 different user than in the context
335 :raises: exception.OverQuota if the limit check exceeds the quota
336 limits
337 """
338 # We can't do f(*args, kw=None, **kwargs) in python 2.x
339 check_project_id = count_kwargs.pop('check_project_id', None)
340 check_user_id = count_kwargs.pop('check_user_id', None)
342 check_kwargs = collections.defaultdict(dict)
343 for resource in deltas:
344 # If we already counted a resource in a batch count, avoid
345 # unnecessary re-counting and avoid creating empty dicts in
346 # the defaultdict.
347 if (resource in check_kwargs.get('project_values', {}) or
348 resource in check_kwargs.get('user_values', {})):
349 continue
350 count = cls.count_as_dict(context, resource, *count_args,
351 **count_kwargs)
352 for res in count.get('project', {}):
353 if res in deltas:
354 total = count['project'][res] + deltas[res]
355 check_kwargs['project_values'][res] = total
356 for res in count.get('user', {}):
357 if res in deltas:
358 total = count['user'][res] + deltas[res]
359 check_kwargs['user_values'][res] = total
360 if check_project_id is not None:
361 check_kwargs['project_id'] = check_project_id
362 if check_user_id is not None:
363 check_kwargs['user_id'] = check_user_id
364 try:
365 cls.limit_check_project_and_user(context, **check_kwargs)
366 except exception.OverQuota as exc:
367 # Report usage in the exception when going over quota
368 key = 'user' if 'user' in count else 'project'
369 exc.kwargs['usages'] = count[key]
370 raise exc
372 @base.remotable_classmethod
373 def create_limit(cls, context, project_id, resource, limit, user_id=None):
374 try:
375 main_db_api.quota_get(
376 context, project_id, resource, user_id=user_id)
377 except exception.QuotaNotFound:
378 cls._create_limit_in_db(context, project_id, resource, limit,
379 user_id=user_id)
380 else:
381 raise exception.QuotaExists(project_id=project_id,
382 resource=resource)
384 @base.remotable_classmethod
385 def update_limit(cls, context, project_id, resource, limit, user_id=None):
386 try:
387 cls._update_limit_in_db(context, project_id, resource, limit,
388 user_id=user_id)
389 except exception.QuotaNotFound:
390 main_db_api.quota_update(context, project_id, resource, limit,
391 user_id=user_id)
393 @classmethod
394 def create_class(cls, context, class_name, resource, limit):
395 try:
396 main_db_api.quota_class_get(context, class_name, resource)
397 except exception.QuotaClassNotFound:
398 cls._create_class_in_db(context, class_name, resource, limit)
399 else:
400 raise exception.QuotaClassExists(class_name=class_name,
401 resource=resource)
403 @classmethod
404 def update_class(cls, context, class_name, resource, limit):
405 try:
406 cls._update_class_in_db(context, class_name, resource, limit)
407 except exception.QuotaClassNotFound:
408 main_db_api.quota_class_update(
409 context, class_name, resource, limit)
411 # NOTE(melwitt): The following methods are not remotable and return
412 # dict-like database model objects. We are using classmethods to provide
413 # a common interface for accessing the api/main databases.
414 @classmethod
415 def get(cls, context, project_id, resource, user_id=None):
416 try:
417 quota = cls._get_from_db(context, project_id, resource,
418 user_id=user_id)
419 except exception.QuotaNotFound:
420 quota = main_db_api.quota_get(context, project_id, resource,
421 user_id=user_id)
422 return quota
424 @classmethod
425 def get_all(cls, context, project_id):
426 api_db_quotas = cls._get_all_from_db(context, project_id)
427 main_db_quotas = main_db_api.quota_get_all(context, project_id)
428 return api_db_quotas + main_db_quotas
430 @classmethod
431 def get_all_by_project(cls, context, project_id):
432 api_db_quotas_dict = cls._get_all_from_db_by_project(context,
433 project_id)
434 main_db_quotas_dict = main_db_api.quota_get_all_by_project(
435 context, project_id)
436 for k, v in api_db_quotas_dict.items():
437 main_db_quotas_dict[k] = v
438 return main_db_quotas_dict
440 @classmethod
441 def get_all_by_project_and_user(cls, context, project_id, user_id):
442 api_db_quotas_dict = cls._get_all_from_db_by_project_and_user(
443 context, project_id, user_id)
444 main_db_quotas_dict = main_db_api.quota_get_all_by_project_and_user(
445 context, project_id, user_id)
446 for k, v in api_db_quotas_dict.items():
447 main_db_quotas_dict[k] = v
448 return main_db_quotas_dict
450 @classmethod
451 def destroy_all_by_project(cls, context, project_id):
452 try:
453 cls._destroy_all_in_db_by_project(context, project_id)
454 except exception.ProjectQuotaNotFound:
455 main_db_api.quota_destroy_all_by_project(context, project_id)
457 @classmethod
458 def destroy_all_by_project_and_user(cls, context, project_id, user_id):
459 try:
460 cls._destroy_all_in_db_by_project_and_user(context, project_id,
461 user_id)
462 except exception.ProjectUserQuotaNotFound:
463 main_db_api.quota_destroy_all_by_project_and_user(
464 context, project_id, user_id)
466 @classmethod
467 def get_class(cls, context, class_name, resource):
468 try:
469 qclass = cls._get_class_from_db(context, class_name, resource)
470 except exception.QuotaClassNotFound:
471 qclass = main_db_api.quota_class_get(context, class_name, resource)
472 return qclass
474 @classmethod
475 def get_default_class(cls, context):
476 try:
477 qclass = cls._get_all_class_from_db_by_name(
478 context, main_db_api._DEFAULT_QUOTA_NAME)
479 except exception.QuotaClassNotFound:
480 qclass = main_db_api.quota_class_get_default(context)
481 return qclass
483 @classmethod
484 def get_all_class_by_name(cls, context, class_name):
485 api_db_quotas_dict = cls._get_all_class_from_db_by_name(context,
486 class_name)
487 main_db_quotas_dict = main_db_api.quota_class_get_all_by_name(context,
488 class_name)
489 for k, v in api_db_quotas_dict.items():
490 main_db_quotas_dict[k] = v
491 return main_db_quotas_dict
494@base.NovaObjectRegistry.register
495class QuotasNoOp(Quotas):
496 # TODO(melwitt): Remove this method in version 2.0 of the object.
497 def reserve(context, expire=None, project_id=None, user_id=None,
498 **deltas):
499 pass
501 # TODO(melwitt): Remove this method in version 2.0 of the object.
502 def commit(self, context=None):
503 pass
505 # TODO(melwitt): Remove this method in version 2.0 of the object.
506 def rollback(self, context=None):
507 pass
509 def check_deltas(cls, context, deltas, *count_args, **count_kwargs):
510 pass
513@db_utils.require_context
514@main_db_api.pick_context_manager_reader
515def _get_main_per_project_limits(context, limit):
516 return context.session.query(main_models.Quota).\
517 filter_by(deleted=0).\
518 limit(limit).\
519 all()
522@db_utils.require_context
523@main_db_api.pick_context_manager_reader
524def _get_main_per_user_limits(context, limit):
525 return context.session.query(main_models.ProjectUserQuota).\
526 filter_by(deleted=0).\
527 limit(limit).\
528 all()
531@db_utils.require_context
532@main_db_api.pick_context_manager_writer
533def _destroy_main_per_project_limits(context, project_id, resource):
534 context.session.query(main_models.Quota).\
535 filter_by(deleted=0).\
536 filter_by(project_id=project_id).\
537 filter_by(resource=resource).\
538 soft_delete(synchronize_session=False)
541@db_utils.require_context
542@main_db_api.pick_context_manager_writer
543def _destroy_main_per_user_limits(context, project_id, resource, user_id):
544 context.session.query(main_models.ProjectUserQuota).\
545 filter_by(deleted=0).\
546 filter_by(project_id=project_id).\
547 filter_by(user_id=user_id).\
548 filter_by(resource=resource).\
549 soft_delete(synchronize_session=False)
552@api_db_api.context_manager.writer
553def _create_limits_in_api_db(context, db_limits, per_user=False):
554 for db_limit in db_limits:
555 user_id = db_limit.user_id if per_user else None
556 Quotas._create_limit_in_db(context, db_limit.project_id,
557 db_limit.resource, db_limit.hard_limit,
558 user_id=user_id)
561def migrate_quota_limits_to_api_db(context, max_count):
562 # Migrate per project limits
563 main_per_project_limits = _get_main_per_project_limits(context, max_count)
564 done = 0
565 try:
566 # Create all the limits in a single transaction.
567 _create_limits_in_api_db(context, main_per_project_limits)
568 except exception.QuotaExists:
569 # NOTE(melwitt): This can happen if the migration is interrupted after
570 # limits were created in the api db but before they were deleted from
571 # the main db, and the migration is re-run.
572 pass
573 # Delete the limits separately.
574 for db_limit in main_per_project_limits:
575 _destroy_main_per_project_limits(context, db_limit.project_id,
576 db_limit.resource)
577 done += 1
578 if done == max_count:
579 return len(main_per_project_limits), done
580 # Migrate per user limits
581 max_count -= done
582 main_per_user_limits = _get_main_per_user_limits(context, max_count)
583 try:
584 # Create all the limits in a single transaction.
585 _create_limits_in_api_db(context, main_per_user_limits, per_user=True)
586 except exception.QuotaExists:
587 # NOTE(melwitt): This can happen if the migration is interrupted after
588 # limits were created in the api db but before they were deleted from
589 # the main db, and the migration is re-run.
590 pass
591 # Delete the limits separately.
592 for db_limit in main_per_user_limits:
593 _destroy_main_per_user_limits(context, db_limit.project_id,
594 db_limit.resource, db_limit.user_id)
595 done += 1
596 return len(main_per_project_limits) + len(main_per_user_limits), done
599@db_utils.require_context
600@main_db_api.pick_context_manager_reader
601def _get_main_quota_classes(context, limit):
602 return context.session.query(main_models.QuotaClass).\
603 filter_by(deleted=0).\
604 limit(limit).\
605 all()
608@main_db_api.pick_context_manager_writer
609def _destroy_main_quota_classes(context, db_classes):
610 for db_class in db_classes:
611 context.session.query(main_models.QuotaClass).\
612 filter_by(deleted=0).\
613 filter_by(id=db_class.id).\
614 soft_delete(synchronize_session=False)
617@api_db_api.context_manager.writer
618def _create_classes_in_api_db(context, db_classes):
619 for db_class in db_classes:
620 Quotas._create_class_in_db(context, db_class.class_name,
621 db_class.resource, db_class.hard_limit)
624def migrate_quota_classes_to_api_db(context, max_count):
625 main_quota_classes = _get_main_quota_classes(context, max_count)
626 done = 0
627 try:
628 # Create all the classes in a single transaction.
629 _create_classes_in_api_db(context, main_quota_classes)
630 except exception.QuotaClassExists:
631 # NOTE(melwitt): This can happen if the migration is interrupted after
632 # classes were created in the api db but before they were deleted from
633 # the main db, and the migration is re-run.
634 pass
635 # Delete the classes in a single transaction.
636 _destroy_main_quota_classes(context, main_quota_classes)
637 found = done = len(main_quota_classes)
638 return found, done