Coverage for manila/quota.py: 97%
257 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright 2010 United States Government as represented by the
2# Administrator of the National Aeronautics and Space Administration.
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.
17"""Quotas for shares."""
19import datetime
21from oslo_config import cfg
22from oslo_log import log
23from oslo_utils import importutils
24from oslo_utils import timeutils
26from manila import db
27from manila import exception
29LOG = log.getLogger(__name__)
31QUOTA_GROUP = 'quota'
33quota_opts = [
34 cfg.IntOpt('shares',
35 default=50,
36 help='Number of shares allowed per project.',
37 deprecated_group='DEFAULT',
38 deprecated_name='quota_shares'),
39 cfg.IntOpt('snapshots',
40 default=50,
41 help='Number of share snapshots allowed per project.',
42 deprecated_group='DEFAULT',
43 deprecated_name='quota_snapshots'),
44 cfg.IntOpt('gigabytes',
45 default=1000,
46 help='Number of share gigabytes allowed per project.',
47 deprecated_group='DEFAULT',
48 deprecated_name='quota_gigabytes'),
49 cfg.IntOpt('per_share_gigabytes',
50 default=-1,
51 help='Max size allowed per share, in gigabytes.',
52 deprecated_group='DEFAULT',
53 deprecated_name='quota_per_share_gigabytes'),
54 cfg.IntOpt('snapshot_gigabytes',
55 default=1000,
56 help='Number of snapshot gigabytes allowed per project.',
57 deprecated_group='DEFAULT',
58 deprecated_name='quota_snapshot_gigabytes'),
59 cfg.IntOpt('share_networks',
60 default=10,
61 help='Number of share-networks allowed per project.',
62 deprecated_group='DEFAULT',
63 deprecated_name='quota_share_networks'),
64 cfg.IntOpt('share_replicas',
65 default=100,
66 help='Number of share-replicas allowed per project.',
67 deprecated_group='DEFAULT',
68 deprecated_name='quota_share_replicas'),
69 cfg.IntOpt('replica_gigabytes',
70 default=1000,
71 help='Number of replica gigabytes allowed per project.',
72 deprecated_group='DEFAULT',
73 deprecated_name='quota_replica_gigabytes'),
74 cfg.IntOpt('share_groups',
75 default=50,
76 help='Number of share groups allowed.',
77 deprecated_group='DEFAULT',
78 deprecated_name='quota_share_groups'),
79 cfg.IntOpt('share_group_snapshots',
80 default=50,
81 help='Number of share group snapshots allowed.',
82 deprecated_group='DEFAULT',
83 deprecated_name='quota_share_group_snapshots'),
84 cfg.IntOpt('reservation_expire',
85 default=86400,
86 help='Number of seconds until a reservation expires.',
87 deprecated_group='DEFAULT'),
88 cfg.IntOpt('until_refresh',
89 default=0,
90 help='Count of reservations until usage is refreshed.',
91 deprecated_group='DEFAULT'),
92 cfg.IntOpt('max_age',
93 default=0,
94 help='Number of seconds between subsequent usage refreshes.',
95 deprecated_group='DEFAULT'),
96 cfg.StrOpt('driver',
97 default='manila.quota.DbQuotaDriver',
98 help='Default driver to use for quota checks.',
99 deprecated_group='DEFAULT',
100 deprecated_name='quota_driver'),
101 cfg.IntOpt('backups',
102 default=10,
103 help='Number of share backups allowed per project.',
104 deprecated_group='DEFAULT',
105 deprecated_name='quota_backups'),
106 cfg.IntOpt('backup_gigabytes',
107 default=1000,
108 help='Total amount of storage, in gigabytes, allowed '
109 'for backups per project.',
110 deprecated_group='DEFAULT',
111 deprecated_name='quota_backup_gigabytes'),
112 cfg.IntOpt('encryption_keys',
113 default=100,
114 help='Number of encryption keys allowed per project.'), ]
116CONF = cfg.CONF
117CONF.register_opts(quota_opts, QUOTA_GROUP)
120class DbQuotaDriver(object):
121 """Database Quota driver.
123 Driver to perform necessary checks to enforce quotas and obtain
124 quota information. The default driver utilizes the local
125 database.
126 """
128 def get_by_class(self, context, quota_class, resource):
129 """Get a specific quota by quota class."""
131 return db.quota_class_get(context, quota_class, resource)
133 def get_defaults(self, context, resources):
134 """Given a list of resources, retrieve the default quotas.
136 :param context: The request context, for access checks.
137 :param resources: A dictionary of the registered resources.
138 """
140 quotas = {}
141 default_quotas = db.quota_class_get_default(context)
142 for resource in resources.values():
143 quotas[resource.name] = default_quotas.get(resource.name,
144 resource.default)
145 return quotas
147 def get_class_quotas(self, context, resources, quota_class,
148 defaults=True):
149 """Retrieve quotas for a quota class.
151 Given a list of resources, retrieve the quotas for the given
152 quota class.
154 :param context: The request context, for access checks.
155 :param resources: A dictionary of the registered resources.
156 :param quota_class: The name of the quota class to return
157 quotas for.
158 :param defaults: If True, the default value will be reported
159 if there is no specific value for the
160 resource.
161 """
163 quotas = {}
164 class_quotas = db.quota_class_get_all_by_name(context, quota_class)
165 for resource in resources.values():
166 if defaults or resource.name in class_quotas:
167 quotas[resource.name] = class_quotas.get(resource.name,
168 resource.default)
170 return quotas
172 def _process_quotas(self, context, resources, project_id, quotas,
173 quota_class=None, defaults=True, usages=None,
174 remains=False):
175 modified_quotas = {}
176 # Get the quotas for the appropriate class. If the project ID
177 # matches the one in the context, we use the quota_class from
178 # the context, otherwise, we use the provided quota_class (if
179 # any)
180 if project_id == context.project_id:
181 quota_class = context.quota_class
182 if quota_class:
183 class_quotas = db.quota_class_get_all_by_name(context, quota_class)
184 else:
185 class_quotas = {}
187 default_quotas = self.get_defaults(context, resources)
189 for resource in resources.values():
190 # Omit default/quota class values
191 if not defaults and resource.name not in quotas: 191 ↛ 192line 191 didn't jump to line 192 because the condition on line 191 was never true
192 continue
194 limit = quotas.get(
195 resource.name,
196 class_quotas.get(resource.name, default_quotas[resource.name]))
197 modified_quotas[resource.name] = dict(limit=limit)
199 # Include usages if desired. This is optional because one
200 # internal consumer of this interface wants to access the
201 # usages directly from inside a transaction.
202 if usages:
203 usage = usages.get(resource.name, {})
204 modified_quotas[resource.name].update(
205 in_use=usage.get('in_use', 0),
206 reserved=usage.get('reserved', 0),
207 )
208 # Initialize remains quotas.
209 if remains:
210 modified_quotas[resource.name].update(remains=limit)
212 if remains:
213 all_quotas = db.quota_get_all(context, project_id)
214 for quota in all_quotas: 214 ↛ 215line 214 didn't jump to line 215 because the loop on line 214 never started
215 if quota.resource in modified_quotas:
216 modified_quotas[quota.resource]['remains'] -= (
217 quota.hard_limit)
219 return modified_quotas
221 def get_project_quotas(self, context, resources, project_id,
222 quota_class=None, defaults=True,
223 usages=True, remains=False):
224 """Retrieve quotas for project.
226 Given a list of resources, retrieve the quotas for the given
227 project.
229 :param context: The request context, for access checks.
230 :param resources: A dictionary of the registered resources.
231 :param project_id: The ID of the project to return quotas for.
232 :param quota_class: If project_id != context.project_id, the
233 quota class cannot be determined. This
234 parameter allows it to be specified. It
235 will be ignored if project_id ==
236 context.project_id.
237 :param defaults: If True, the quota class value (or the
238 default value, if there is no value from the
239 quota class) will be reported if there is no
240 specific value for the resource.
241 :param usages: If True, the current in_use and reserved counts
242 will also be returned.
243 :param remains: If True, the current remains of the project will
244 will be returned.
245 """
246 project_quotas = db.quota_get_all_by_project(context, project_id)
247 project_usages = None
248 if usages:
249 project_usages = db.quota_usage_get_all_by_project(context,
250 project_id)
251 return self._process_quotas(context, resources, project_id,
252 project_quotas, quota_class,
253 defaults=defaults, usages=project_usages,
254 remains=remains)
256 def get_user_quotas(self, context, resources, project_id, user_id,
257 quota_class=None, defaults=True, usages=True):
258 """Retrieve quotas for user and project.
260 Given a list of resources, retrieve the quotas for the given
261 user and project.
263 :param context: The request context, for access checks.
264 :param resources: A dictionary of the registered resources.
265 :param project_id: The ID of the project to return quotas for.
266 :param user_id: The ID of the user to return quotas for.
267 :param quota_class: If project_id != context.project_id, the
268 quota class cannot be determined. This
269 parameter allows it to be specified. It
270 will be ignored if project_id ==
271 context.project_id.
272 :param defaults: If True, the quota class value (or the
273 default value, if there is no value from the
274 quota class) will be reported if there is no
275 specific value for the resource.
276 :param usages: If True, the current in_use and reserved counts
277 will also be returned.
278 """
279 user_quotas = db.quota_get_all_by_project_and_user(
280 context, project_id, user_id)
281 # Use the project quota for default user quota.
282 proj_quotas = db.quota_get_all_by_project(context, project_id)
283 for key, value in proj_quotas.items():
284 if key not in user_quotas.keys():
285 user_quotas[key] = value
286 user_usages = None
287 if usages:
288 user_usages = db.quota_usage_get_all_by_project_and_user(
289 context, project_id, user_id)
290 return self._process_quotas(context, resources, project_id,
291 user_quotas, quota_class,
292 defaults=defaults, usages=user_usages)
294 def get_share_type_quotas(self, context, resources, project_id,
295 share_type_id, quota_class=None, defaults=True,
296 usages=True):
297 """Retrieve quotas for share_type and project.
299 Given a list of resources, retrieve the quotas for the given
300 share_type and project.
302 :param context: The request context, for access checks.
303 :param resources: A dictionary of the registered resources.
304 :param project_id: The UUID of the project to return quotas for.
305 :param share_type: UUID/name of a share type to return quotas for.
306 :param quota_class: If project_id != context.project_id, the
307 quota class cannot be determined. This
308 parameter allows it to be specified. It
309 will be ignored if project_id ==
310 context.project_id.
311 :param defaults: If True, the quota class value (or the
312 default value, if there is no value from the
313 quota class) will be reported if there is no
314 specific value for the resource.
315 :param usages: If True, the current in_use and reserved counts
316 will also be returned.
317 """
318 st_quotas = db.quota_get_all_by_project_and_share_type(
319 context, project_id, share_type_id)
320 # Use the project quota for default share_type quota.
321 project_quotas = db.quota_get_all_by_project(context, project_id)
322 for key, value in project_quotas.items():
323 if key not in st_quotas.keys():
324 st_quotas[key] = value
325 st_usages = None
326 if usages:
327 st_usages = db.quota_usage_get_all_by_project_and_share_type(
328 context, project_id, share_type_id)
329 return self._process_quotas(
330 context, resources, project_id, st_quotas, quota_class,
331 defaults=defaults, usages=st_usages)
333 def get_settable_quotas(self, context, resources, project_id,
334 user_id=None, share_type_id=None):
335 """Retrieve range of settable quotas.
337 Given a list of resources, retrieve the range of settable quotas for
338 the given user or project.
340 :param context: The request context, for access checks.
341 :param resources: A dictionary of the registered resources.
342 :param project_id: The ID of the project to return quotas for.
343 :param user_id: The ID of the user to return quotas for.
344 :param share_type_id: The UUID of the share_type to return quotas for.
345 """
346 settable_quotas = {}
347 project_quotas = self.get_project_quotas(
348 context, resources, project_id, remains=True)
349 if user_id or share_type_id:
350 if user_id:
351 subquotas = self.get_user_quotas(
352 context, resources, project_id, user_id)
353 else:
354 subquotas = self.get_share_type_quotas(
355 context, resources, project_id, share_type_id)
356 for key, value in subquotas.items():
357 settable_quotas[key] = {
358 "minimum": value['in_use'] + value['reserved'],
359 "maximum": project_quotas[key]["limit"],
360 }
361 else:
362 for key, value in project_quotas.items():
363 minimum = max(
364 int(value['limit'] - value['remains']),
365 int(value['in_use'] + value['reserved'])
366 )
367 settable_quotas[key] = {"minimum": minimum, "maximum": -1}
368 return settable_quotas
370 def _get_quotas(self, context, resources, keys, has_sync, project_id=None,
371 user_id=None, share_type_id=None):
372 """Retrieve quotas for a resource.
374 A helper method which retrieves the quotas for the specific
375 resources identified by keys, and which apply to the current
376 context.
378 :param context: The request context, for access checks.
379 :param resources: A dictionary of the registered resources.
380 :param keys: A list of the desired quotas to retrieve.
381 :param has_sync: If True, indicates that the resource must
382 have a sync attribute; if False, indicates
383 that the resource must NOT have a sync
384 attribute.
385 :param project_id: Specify the project_id if current context
386 is admin and admin wants to impact on
387 common user's tenant.
388 :param user_id: Specify the user_id if current context
389 is admin and admin wants to impact on
390 common user. (Special case: user operates on
391 resource, owned/created by different user)
392 """
394 # Filter resources
395 if has_sync:
396 sync_filt = lambda x: hasattr(x, 'sync') # noqa: E731
397 else:
398 sync_filt = lambda x: not hasattr(x, 'sync') # noqa: E731
399 desired = set(keys)
400 sub_resources = {k: v for k, v in resources.items()
401 if k in desired and sync_filt(v)}
403 # Make sure we accounted for all of them...
404 if len(keys) != len(sub_resources):
405 unknown = desired - set(sub_resources.keys())
406 raise exception.QuotaResourceUnknown(unknown=sorted(unknown))
408 if user_id:
409 # Grab and return the quotas (without usages)
410 quotas = self.get_user_quotas(context, sub_resources,
411 project_id, user_id,
412 context.quota_class, usages=False)
413 elif share_type_id:
414 # Grab and return the quotas (without usages)
415 quotas = self.get_share_type_quotas(
416 context, sub_resources, project_id, share_type_id,
417 context.quota_class, usages=False)
418 else:
419 # Grab and return the quotas (without usages)
420 quotas = self.get_project_quotas(context, sub_resources,
421 project_id,
422 context.quota_class,
423 usages=False)
425 return {k: v['limit'] for k, v in quotas.items()}
427 def limit_check(self, context, resources, values, project_id=None):
428 """Check simple quota limits.
430 For limits--those quotas for which there is no usage
431 synchronization function--this method checks that a set of
432 proposed values are permitted by the limit restriction.
434 This method will raise a QuotaResourceUnknown exception if a
435 given resource is unknown or if it is not a simple limit
436 resource.
438 If any of the proposed values is over the defined quota, an
439 OverQuota exception will be raised with the sorted list of the
440 resources which are too high. Otherwise, the method returns
441 nothing.
443 :param context: The request context, for access checks.
444 :param resources: A dictionary of the registered resources.
445 :param values: A dictionary of the values to check against the
446 quota.
447 :param project_id: Specify the project_id if current context
448 is admin and admin wants to impact on
449 common user's tenant.
450 """
452 # Ensure no value is less than zero
453 unders = [key for key, val in values.items() if val < 0]
454 if unders: 454 ↛ 455line 454 didn't jump to line 455 because the condition on line 454 was never true
455 raise exception.InvalidQuotaValue(unders=sorted(unders))
457 # If project_id is None, then we use the project_id in context
458 if project_id is None:
459 project_id = context.project_id
461 quotas = self._get_quotas(context, resources, values.keys(),
462 has_sync=False, project_id=project_id)
463 # Check the quotas and construct a list of the resources that
464 # would be put over limit by the desired values
465 overs = [key for key, val in values.items()
466 if quotas[key] >= 0 and quotas[key] < val]
467 if overs:
468 raise exception.OverQuota(overs=sorted(overs), quotas=quotas,
469 usages={})
471 def reserve(self, context, resources, deltas, expire=None,
472 project_id=None, user_id=None, share_type_id=None,
473 overquota_allowed=False):
474 """Check quotas and reserve resources.
476 For counting quotas--those quotas for which there is a usage
477 synchronization function--this method checks quotas against
478 current usage and the desired deltas.
480 This method will raise a QuotaResourceUnknown exception if a
481 given resource is unknown or if it does not have a usage
482 synchronization function.
484 If any of the proposed values is over the defined quota, an
485 OverQuota exception will be raised with the sorted list of the
486 resources which are too high. Otherwise, the method returns a
487 list of reservation UUIDs which were created.
489 :param context: The request context, for access checks.
490 :param resources: A dictionary of the registered resources.
491 :param deltas: A dictionary of the proposed delta changes.
492 :param expire: An optional parameter specifying an expiration
493 time for the reservations. If it is a simple
494 number, it is interpreted as a number of
495 seconds and added to the current time; if it is
496 a datetime.timedelta object, it will also be
497 added to the current time. A datetime.datetime
498 object will be interpreted as the absolute
499 expiration time. If None is specified, the
500 default expiration time set by
501 --default-reservation-expire will be used (this
502 value will be treated as a number of seconds).
503 :param project_id: Specify the project_id if current context
504 is admin and admin wants to impact on
505 common user's tenant.
506 :param user_id: Specify the user_id if current context
507 is admin and admin wants to impact on
508 common user. (Special case: user operates on
509 resource, owned/created by different user)
510 """
512 # Set up the reservation expiration
513 if expire is None:
514 expire = CONF.quota.reservation_expire
515 if isinstance(expire, int):
516 expire = datetime.timedelta(seconds=expire)
517 if isinstance(expire, datetime.timedelta):
518 expire = timeutils.utcnow() + expire
519 if not isinstance(expire, datetime.datetime):
520 raise exception.InvalidReservationExpiration(expire=expire)
522 # If project_id is None, then we use the project_id in context
523 if project_id is None:
524 project_id = context.project_id
525 # If user_id is None, then we use the user_id in context
526 if user_id is None:
527 user_id = context.user_id
529 # Get the applicable quotas.
530 # NOTE(Vek): We're not worried about races at this point.
531 # Yes, the admin may be in the process of reducing
532 # quotas, but that's a pretty rare thing.
533 quotas = self._get_quotas(
534 context, resources, deltas, has_sync=True, project_id=project_id)
535 user_quotas = self._get_quotas(
536 context, resources, deltas, has_sync=True,
537 project_id=project_id, user_id=user_id)
538 if share_type_id:
539 share_type_quotas = self._get_quotas(
540 context, resources, deltas, has_sync=True,
541 project_id=project_id, share_type_id=share_type_id)
542 else:
543 share_type_quotas = {}
545 # NOTE(Vek): Most of the work here has to be done in the DB
546 # API, because we have to do it in a transaction,
547 # which means access to the session. Since the
548 # session isn't available outside the DBAPI, we
549 # have to do the work there.
550 return db.quota_reserve(
551 context, resources, quotas, user_quotas, share_type_quotas,
552 deltas, expire, CONF.quota.until_refresh, CONF.quota.max_age,
553 project_id=project_id, user_id=user_id,
554 share_type_id=share_type_id, overquota_allowed=overquota_allowed)
556 def commit(self, context, reservations, project_id=None, user_id=None,
557 share_type_id=None):
558 """Commit reservations.
560 :param context: The request context, for access checks.
561 :param reservations: A list of the reservation UUIDs, as
562 returned by the reserve() method.
563 :param project_id: Specify the project_id if current context
564 is admin and admin wants to impact on
565 common user's tenant.
566 :param user_id: Specify the user_id if current context
567 is admin and admin wants to impact on
568 common user. (Special case: user operates on
569 resource, owned/created by different user)
570 """
571 # If project_id is None, then we use the project_id in context
572 if project_id is None:
573 project_id = context.project_id
574 # If user_id is None, then we use the user_id in context
575 if user_id is None:
576 user_id = context.user_id
578 db.reservation_commit(
579 context, reservations, project_id=project_id, user_id=user_id,
580 share_type_id=share_type_id)
582 def rollback(self, context, reservations, project_id=None, user_id=None,
583 share_type_id=None):
584 """Roll back reservations.
586 :param context: The request context, for access checks.
587 :param reservations: A list of the reservation UUIDs, as
588 returned by the reserve() method.
589 :param project_id: Specify the project_id if current context
590 is admin and admin wants to impact on
591 common user's tenant.
592 :param user_id: Specify the user_id if current context
593 is admin and admin wants to impact on
594 common user. (Special case: user operates on
595 resource, owned/created by different user)
596 """
597 # If project_id is None, then we use the project_id in context
598 if project_id is None:
599 project_id = context.project_id
600 # If user_id is None, then we use the user_id in context
601 if user_id is None:
602 user_id = context.user_id
604 db.reservation_rollback(
605 context, reservations, project_id=project_id, user_id=user_id,
606 share_type_id=share_type_id)
608 def usage_reset(self, context, resources):
609 """Reset usage records.
611 Reset the usage records for a particular user on a list of
612 resources. This will force that user's usage records to be
613 refreshed the next time a reservation is made.
615 Note: this does not affect the currently outstanding
616 reservations the user has; those reservations must be
617 committed or rolled back (or expired).
619 :param context: The request context, for access checks.
620 :param resources: A list of the resource names for which the
621 usage must be reset.
622 """
624 # We need an elevated context for the calls to
625 # quota_usage_update()
626 elevated = context.elevated()
628 for resource in resources:
629 try:
630 # Reset the usage to -1, which will force it to be
631 # refreshed
632 db.quota_usage_update(elevated, context.project_id,
633 context.user_id,
634 resource, in_use=-1)
635 except exception.QuotaUsageNotFound:
636 # That means it'll be refreshed anyway
637 pass
639 def destroy_all_by_project(self, context, project_id):
640 """Destroy metadata associated with a project.
642 Destroy all quotas, usages, and reservations associated with a
643 project.
645 :param context: The request context, for access checks.
646 :param project_id: The ID of the project being deleted.
647 """
649 db.quota_destroy_all_by_project(context, project_id)
651 def destroy_all_by_project_and_user(self, context, project_id, user_id):
652 """Destroy metadata associated with a project and user.
654 Destroy all quotas, usages, and reservations associated with a
655 project and user.
657 :param context: The request context, for access checks.
658 :param project_id: The ID of the project being deleted.
659 :param user_id: The ID of the user being deleted.
660 """
662 db.quota_destroy_all_by_project_and_user(context, project_id, user_id)
664 def destroy_all_by_project_and_share_type(self, context, project_id,
665 share_type_id):
666 """Destroy metadata associated with a project and share_type.
668 Destroy all quotas, usages, and reservations associated with a
669 project and share_type.
671 :param context: The request context, for access checks.
672 :param project_id: The ID of the project.
673 :param share_type_id: The UUID of the share type.
674 """
676 db.quota_destroy_all_by_share_type(context, share_type_id,
677 project_id=project_id)
679 def expire(self, context):
680 """Expire reservations.
682 Explores all currently existing reservations and rolls back
683 any that have expired.
685 :param context: The request context, for access checks.
686 """
688 db.reservation_expire(context)
691class BaseResource(object):
692 """Describe a single resource for quota checking."""
694 def __init__(self, name, flag=None):
695 """Initializes a Resource.
697 :param name: The name of the resource, i.e., "shares".
698 :param flag: The name of the flag or configuration option
699 which specifies the default value of the quota
700 for this resource.
701 """
703 self.name = name
704 self.flag = flag
706 @property
707 def default(self):
708 """Return the default value of the quota."""
710 return CONF.quota[self.flag] if self.flag else -1
713class ReservableResource(BaseResource):
714 """Describe a reservable resource."""
716 def __init__(self, name, sync, flag=None):
717 """Initializes a ReservableResource.
719 Reservable resources are those resources which directly
720 correspond to objects in the database, i.e., shares, gigabytes,
721 etc. A ReservableResource must be constructed with a usage
722 synchronization function, which will be called to determine the
723 current counts of one or more resources.
725 The usage synchronization function will be passed three
726 arguments: an admin context, the project ID, and an opaque
727 session object, which should in turn be passed to the
728 underlying database function. Synchronization functions
729 should return a dictionary mapping resource names to the
730 current in_use count for those resources; more than one
731 resource and resource count may be returned. Note that
732 synchronization functions may be associated with more than one
733 ReservableResource.
735 :param name: The name of the resource, i.e., "shares".
736 :param sync: A callable which returns a dictionary to
737 resynchronize the in_use count for one or more
738 resources, as described above.
739 :param flag: The name of the flag or configuration option
740 which specifies the default value of the quota
741 for this resource.
742 """
744 super(ReservableResource, self).__init__(name, flag=flag)
745 if sync:
746 self.sync = sync
749class AbsoluteResource(BaseResource):
750 """Describe a non-reservable resource."""
752 pass
755class CountableResource(AbsoluteResource):
756 """Describe a countable resource.
758 Describe a resource where the counts aren't based solely on the
759 project ID.
760 """
762 def __init__(self, name, count, flag=None):
763 """Initializes a CountableResource.
765 Countable resources are those resources which directly
766 correspond to objects in the database, i.e., shares, gigabytes,
767 etc., but for which a count by project ID is inappropriate. A
768 CountableResource must be constructed with a counting
769 function, which will be called to determine the current counts
770 of the resource.
772 The counting function will be passed the context, along with
773 the extra positional and keyword arguments that are passed to
774 Quota.count(). It should return an integer specifying the
775 count.
777 Note that this counting is not performed in a transaction-safe
778 manner. This resource class is a temporary measure to provide
779 required functionality, until a better approach to solving
780 this problem can be evolved.
782 :param name: The name of the resource, i.e., "shares".
783 :param count: A callable which returns the count of the
784 resource. The arguments passed are as described
785 above.
786 :param flag: The name of the flag or configuration option
787 which specifies the default value of the quota
788 for this resource.
789 """
791 super(CountableResource, self).__init__(name, flag=flag)
792 self.count = count
795class QuotaEngine(object):
796 """Represent the set of recognized quotas."""
798 def __init__(self, quota_driver_class=None):
799 """Initialize a Quota object."""
800 self._resources = {}
801 self._driver_cls = quota_driver_class
802 self.__driver = None
804 @property
805 def _driver(self):
806 if self.__driver:
807 return self.__driver
808 if not self._driver_cls: 808 ↛ 810line 808 didn't jump to line 810 because the condition on line 808 was always true
809 self._driver_cls = CONF.quota.driver
810 if isinstance(self._driver_cls, str): 810 ↛ 812line 810 didn't jump to line 812 because the condition on line 810 was always true
811 self._driver_cls = importutils.import_object(self._driver_cls)
812 self.__driver = self._driver_cls
813 return self.__driver
815 def __contains__(self, resource):
816 return resource in self._resources
818 def register_resource(self, resource):
819 """Register a resource."""
821 self._resources[resource.name] = resource
823 def register_resources(self, resources):
824 """Register a list of resources."""
826 for resource in resources:
827 self.register_resource(resource)
829 def get_by_class(self, context, quota_class, resource):
830 """Get a specific quota by quota class."""
832 return self._driver.get_by_class(context, quota_class, resource)
834 def get_defaults(self, context):
835 """Retrieve the default quotas.
837 :param context: The request context, for access checks.
838 """
840 return self._driver.get_defaults(context, self._resources)
842 def get_class_quotas(self, context, quota_class, defaults=True):
843 """Retrieve the quotas for the given quota class.
845 :param context: The request context, for access checks.
846 :param quota_class: The name of the quota class to return
847 quotas for.
848 :param defaults: If True, the default value will be reported
849 if there is no specific value for the
850 resource.
851 """
853 return self._driver.get_class_quotas(context, self._resources,
854 quota_class, defaults=defaults)
856 def get_user_quotas(self, context, project_id, user_id, quota_class=None,
857 defaults=True, usages=True):
858 """Retrieve the quotas for the given user and project.
860 :param context: The request context, for access checks.
861 :param project_id: The ID of the project to return quotas for.
862 :param user_id: The ID of the user to return quotas for.
863 :param quota_class: If project_id != context.project_id, the
864 quota class cannot be determined. This
865 parameter allows it to be specified.
866 :param defaults: If True, the quota class value (or the
867 default value, if there is no value from the
868 quota class) will be reported if there is no
869 specific value for the resource.
870 :param usages: If True, the current in_use and reserved counts
871 will also be returned.
872 """
874 return self._driver.get_user_quotas(context, self._resources,
875 project_id, user_id,
876 quota_class=quota_class,
877 defaults=defaults,
878 usages=usages)
880 def get_share_type_quotas(self, context, project_id, share_type_id,
881 quota_class=None, defaults=True, usages=True):
882 """Retrieve the quotas for the given user and project.
884 :param context: The request context, for access checks.
885 :param project_id: The ID of the project to return quotas for.
886 :param share_type_id: The UUID of the user to return quotas for.
887 :param quota_class: If project_id != context.project_id, the
888 quota class cannot be determined. This
889 parameter allows it to be specified.
890 :param defaults: If True, the quota class value (or the
891 default value, if there is no value from the
892 quota class) will be reported if there is no
893 specific value for the resource.
894 :param usages: If True, the current in_use and reserved counts
895 will also be returned.
896 """
898 return self._driver.get_share_type_quotas(
899 context, self._resources, project_id, share_type_id,
900 quota_class=quota_class, defaults=defaults, usages=usages)
902 def get_project_quotas(self, context, project_id, quota_class=None,
903 defaults=True, usages=True, remains=False):
904 """Retrieve the quotas for the given project.
906 :param context: The request context, for access checks.
907 :param project_id: The ID of the project to return quotas for.
908 :param quota_class: If project_id != context.project_id, the
909 quota class cannot be determined. This
910 parameter allows it to be specified.
911 :param defaults: If True, the quota class value (or the
912 default value, if there is no value from the
913 quota class) will be reported if there is no
914 specific value for the resource.
915 :param usages: If True, the current in_use and reserved counts
916 will also be returned.
917 :param remains: If True, the current remains of the project will
918 will be returned.
919 """
921 return self._driver.get_project_quotas(context, self._resources,
922 project_id,
923 quota_class=quota_class,
924 defaults=defaults,
925 usages=usages,
926 remains=remains)
928 def get_settable_quotas(self, context, project_id, user_id=None,
929 share_type_id=None):
930 """Get settable quotas.
932 Given a list of resources, retrieve the range of settable quotas for
933 the given user or project.
935 :param context: The request context, for access checks.
936 :param resources: A dictionary of the registered resources.
937 :param project_id: The ID of the project to return quotas for.
938 :param user_id: The ID of the user to return quotas for.
939 :param share_type_id: The UUID of the share_type to return quotas for.
940 """
942 return self._driver.get_settable_quotas(
943 context, self._resources, project_id, user_id=user_id,
944 share_type_id=share_type_id)
946 def count(self, context, resource, *args, **kwargs):
947 """Count a resource.
949 For countable resources, invokes the count() function and
950 returns its result. Arguments following the context and
951 resource are passed directly to the count function declared by
952 the resource.
954 :param context: The request context, for access checks.
955 :param resource: The name of the resource, as a string.
956 """
958 # Get the resource
959 res = self._resources.get(resource)
960 if not res or not hasattr(res, 'count'):
961 raise exception.QuotaResourceUnknown(unknown=[resource])
963 return res.count(context, *args, **kwargs)
965 def limit_check(self, context, project_id=None, **values):
966 """Check simple quota limits.
968 For limits--those quotas for which there is no usage
969 synchronization function--this method checks that a set of
970 proposed values are permitted by the limit restriction. The
971 values to check are given as keyword arguments, where the key
972 identifies the specific quota limit to check, and the value is
973 the proposed value.
975 This method will raise a QuotaResourceUnknown exception if a
976 given resource is unknown or if it is not a simple limit
977 resource.
979 If any of the proposed values is over the defined quota, an
980 OverQuota exception will be raised with the sorted list of the
981 resources which are too high. Otherwise, the method returns
982 nothing.
984 :param context: The request context, for access checks.
985 :param project_id: Specify the project_id if current context
986 is admin and admin wants to impact on
987 common user's tenant.
988 """
990 return self._driver.limit_check(context, self._resources, values,
991 project_id=project_id)
993 def reserve(self, context, expire=None, project_id=None, user_id=None,
994 share_type_id=None, overquota_allowed=False, **deltas):
995 """Check quotas and reserve resources.
997 For counting quotas--those quotas for which there is a usage
998 synchronization function--this method checks quotas against
999 current usage and the desired deltas. The deltas are given as
1000 keyword arguments, and current usage and other reservations
1001 are factored into the quota check.
1003 This method will raise a QuotaResourceUnknown exception if a
1004 given resource is unknown or if it does not have a usage
1005 synchronization function.
1007 If any of the proposed values is over the defined quota, an
1008 OverQuota exception will be raised with the sorted list of the
1009 resources which are too high. Otherwise, the method returns a
1010 list of reservation UUIDs which were created.
1012 :param context: The request context, for access checks.
1013 :param expire: An optional parameter specifying an expiration
1014 time for the reservations. If it is a simple
1015 number, it is interpreted as a number of
1016 seconds and added to the current time; if it is
1017 a datetime.timedelta object, it will also be
1018 added to the current time. A datetime.datetime
1019 object will be interpreted as the absolute
1020 expiration time. If None is specified, the
1021 default expiration time set by
1022 --default-reservation-expire will be used (this
1023 value will be treated as a number of seconds).
1024 :param project_id: Specify the project_id if current context
1025 is admin and admin wants to impact on
1026 common user's tenant.
1027 """
1029 reservations = self._driver.reserve(
1030 context, self._resources, deltas,
1031 expire=expire,
1032 project_id=project_id,
1033 user_id=user_id,
1034 share_type_id=share_type_id,
1035 overquota_allowed=overquota_allowed
1036 )
1038 LOG.debug("Created reservations %s", reservations)
1040 return reservations
1042 def commit(self, context, reservations, project_id=None, user_id=None,
1043 share_type_id=None):
1044 """Commit reservations.
1046 :param context: The request context, for access checks.
1047 :param reservations: A list of the reservation UUIDs, as
1048 returned by the reserve() method.
1049 :param project_id: Specify the project_id if current context
1050 is admin and admin wants to impact on
1051 common user's tenant.
1052 """
1054 try:
1055 self._driver.commit(
1056 context, reservations, project_id=project_id,
1057 user_id=user_id, share_type_id=share_type_id)
1058 except Exception:
1059 # NOTE(Vek): Ignoring exceptions here is safe, because the
1060 # usage resynchronization and the reservation expiration
1061 # mechanisms will resolve the issue. The exception is
1062 # logged, however, because this is less than optimal.
1063 LOG.exception("Failed to commit reservations %s",
1064 reservations)
1065 return
1066 LOG.debug("Committed reservations %s", reservations)
1068 def rollback(self, context, reservations, project_id=None, user_id=None,
1069 share_type_id=None):
1070 """Roll back reservations.
1072 :param context: The request context, for access checks.
1073 :param reservations: A list of the reservation UUIDs, as
1074 returned by the reserve() method.
1075 :param project_id: Specify the project_id if current context
1076 is admin and admin wants to impact on
1077 common user's tenant.
1078 """
1080 try:
1081 self._driver.rollback(
1082 context, reservations, project_id=project_id,
1083 user_id=user_id, share_type_id=share_type_id)
1084 except Exception:
1085 # NOTE(Vek): Ignoring exceptions here is safe, because the
1086 # usage resynchronization and the reservation expiration
1087 # mechanisms will resolve the issue. The exception is
1088 # logged, however, because this is less than optimal.
1089 LOG.exception("Failed to roll back reservations %s",
1090 reservations)
1091 return
1092 LOG.debug("Rolled back reservations %s", reservations)
1094 def usage_reset(self, context, resources):
1095 """Reset usage records.
1097 Reset the usage records for a particular user on a list of
1098 resources. This will force that user's usage records to be
1099 refreshed the next time a reservation is made.
1101 Note: this does not affect the currently outstanding
1102 reservations the user has; those reservations must be
1103 committed or rolled back (or expired).
1105 :param context: The request context, for access checks.
1106 :param resources: A list of the resource names for which the
1107 usage must be reset.
1108 """
1110 self._driver.usage_reset(context, resources)
1112 def destroy_all_by_project_and_user(self, context, project_id, user_id):
1113 """Destroy metadata associated with a project and user.
1115 Destroy all quotas, usages, and reservations associated with a
1116 project and user.
1118 :param context: The request context, for access checks.
1119 :param project_id: The ID of the project being deleted.
1120 :param user_id: The ID of the user being deleted.
1121 """
1123 self._driver.destroy_all_by_project_and_user(context,
1124 project_id, user_id)
1126 def destroy_all_by_project_and_share_type(self, context, project_id,
1127 share_type_id):
1128 """Destroy metadata associated with a project and share_type.
1130 Destroy all quotas, usages, and reservations associated with a
1131 project and share_type.
1133 :param context: The request context, for access checks.
1134 :param project_id: The ID of the project.
1135 :param share_type_id: The UUID of the share_type.
1136 """
1138 self._driver.destroy_all_by_project_and_share_type(
1139 context, project_id, share_type_id)
1141 def destroy_all_by_project(self, context, project_id):
1142 """Destroy metadata associated with a project.
1144 Destroy all quotas, usages, and reservations associated with a
1145 project.
1147 :param context: The request context, for access checks.
1148 :param project_id: The ID of the project being deleted.
1149 """
1151 self._driver.destroy_all_by_project(context, project_id)
1153 def expire(self, context):
1154 """Expire reservations.
1156 Explores all currently existing reservations and rolls back
1157 any that have expired.
1159 :param context: The request context, for access checks.
1160 """
1162 self._driver.expire(context)
1164 @property
1165 def resources(self):
1166 return sorted(self._resources.keys())
1169QUOTAS = QuotaEngine()
1172resources = [
1173 ReservableResource('shares', '_sync_shares', 'shares'),
1174 ReservableResource('snapshots', '_sync_snapshots', 'snapshots'),
1175 ReservableResource('gigabytes', '_sync_gigabytes', 'gigabytes'),
1176 ReservableResource('per_share_gigabytes', None,
1177 'per_share_gigabytes'),
1178 ReservableResource('snapshot_gigabytes', '_sync_snapshot_gigabytes',
1179 'snapshot_gigabytes'),
1180 ReservableResource('share_networks', '_sync_share_networks',
1181 'share_networks'),
1182 ReservableResource('share_groups', '_sync_share_groups',
1183 'share_groups'),
1184 ReservableResource('share_group_snapshots', '_sync_share_group_snapshots',
1185 'share_group_snapshots'),
1186 ReservableResource('share_replicas', '_sync_share_replicas',
1187 'share_replicas'),
1188 ReservableResource('replica_gigabytes', '_sync_replica_gigabytes',
1189 'replica_gigabytes'),
1190 ReservableResource('backups', '_sync_backups', 'backups'),
1191 ReservableResource('backup_gigabytes', '_sync_backup_gigabytes',
1192 'backup_gigabytes'),
1193 ReservableResource('encryption_keys',
1194 '_sync_encryption_keys',
1195 'encryption_keys'),
1196]
1199QUOTAS.register_resources(resources)