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.
16
17"""Quotas for volumes."""
18
19from collections import deque
20import datetime
21
22from oslo_config import cfg
23from oslo_log import log as logging
24from oslo_log import versionutils
25from oslo_utils import importutils
26from oslo_utils import timeutils
27import six
28
29from cinder import context
30from cinder import db
31from cinder import exception
32from cinder.i18n import _
33from cinder import quota_utils
34
35
36LOG = logging.getLogger(__name__)
37
38quota_opts = [
39    cfg.IntOpt('quota_volumes',
40               default=10,
41               help='Number of volumes allowed per project'),
42    cfg.IntOpt('quota_snapshots',
43               default=10,
44               help='Number of volume snapshots allowed per project'),
45    cfg.IntOpt('quota_consistencygroups',
46               default=10,
47               help='Number of consistencygroups allowed per project'),
48    cfg.IntOpt('quota_groups',
49               default=10,
50               help='Number of groups allowed per project'),
51    cfg.IntOpt('quota_gigabytes',
52               default=1000,
53               help='Total amount of storage, in gigabytes, allowed '
54                    'for volumes and snapshots per project'),
55    cfg.IntOpt('quota_backups',
56               default=10,
57               help='Number of volume backups allowed per project'),
58    cfg.IntOpt('quota_backup_gigabytes',
59               default=1000,
60               help='Total amount of storage, in gigabytes, allowed '
61                    'for backups per project'),
62    cfg.IntOpt('reservation_expire',
63               default=86400,
64               help='Number of seconds until a reservation expires'),
65    cfg.IntOpt('reservation_clean_interval',
66               default='$reservation_expire',
67               help='Interval between periodic task runs to clean expired '
68                    'reservations in seconds.'),
69    cfg.IntOpt('until_refresh',
70               default=0,
71               help='Count of reservations until usage is refreshed'),
72    cfg.IntOpt('max_age',
73               default=0,
74               help='Number of seconds between subsequent usage refreshes'),
75    cfg.StrOpt('quota_driver',
76               default="cinder.quota.DbQuotaDriver",
77               help='Default driver to use for quota checks'),
78    cfg.BoolOpt('use_default_quota_class',
79                default=True,
80                help='Enables or disables use of default quota class '
81                     'with default quota.'),
82    cfg.IntOpt('per_volume_size_limit',
83               default=-1,
84               help='Max size allowed per volume, in gigabytes'), ]
85
86CONF = cfg.CONF
87CONF.register_opts(quota_opts)
88
89
90class DbQuotaDriver(object):
91
92    """Driver to perform check to enforcement of quotas.
93
94    Also allows to obtain quota information.
95    The default driver utilizes the local database.
96    """
97
98    def get_by_project(self, context, project_id, resource_name):
99        """Get a specific quota by project."""
100
101        return db.quota_get(context, project_id, resource_name)
102
103    def get_by_class(self, context, quota_class, resource_name):
104        """Get a specific quota by quota class."""
105
106        return db.quota_class_get(context, quota_class, resource_name)
107
108    def get_default(self, context, resource, project_id):
109        """Get a specific default quota for a resource."""
110        default_quotas = db.quota_class_get_defaults(context)
111        return default_quotas.get(resource.name, resource.default)
112
113    def get_defaults(self, context, resources, project_id=None):
114        """Given a list of resources, retrieve the default quotas.
115
116        Use the class quotas named `_DEFAULT_QUOTA_NAME` as default quotas,
117        if it exists.
118
119        :param context: The request context, for access checks.
120        :param resources: A dictionary of the registered resources.
121        :param project_id: The id of the current project
122        """
123
124        quotas = {}
125        default_quotas = {}
126        if CONF.use_default_quota_class:
127            default_quotas = db.quota_class_get_defaults(context)
128
129        for resource in resources.values():
130            if default_quotas:
131                if resource.name not in default_quotas:
132                    versionutils.report_deprecated_feature(LOG, _(
133                        "Default quota for resource: %(res)s is set "
134                        "by the default quota flag: quota_%(res)s, "
135                        "it is now deprecated. Please use the "
136                        "default quota class for default "
137                        "quota.") % {'res': resource.name})
138            quotas[resource.name] = default_quotas.get(resource.name,
139                                                       resource.default)
140        return quotas
141
142    def get_class_quotas(self, context, resources, quota_class,
143                         defaults=True):
144        """Given list of resources, retrieve the quotas for given quota class.
145
146        :param context: The request context, for access checks.
147        :param resources: A dictionary of the registered resources.
148        :param quota_class: The name of the quota class to return
149                            quotas for.
150        :param defaults: If True, the default value will be reported
151                         if there is no specific value for the
152                         resource.
153        """
154
155        quotas = {}
156        default_quotas = {}
157        class_quotas = db.quota_class_get_all_by_name(context, quota_class)
158        if defaults:
159            default_quotas = db.quota_class_get_defaults(context)
160        for resource in resources.values():
161            if resource.name in class_quotas:
162                quotas[resource.name] = class_quotas[resource.name]
163                continue
164
165            if defaults:
166                quotas[resource.name] = default_quotas.get(resource.name,
167                                                           resource.default)
168
169        return quotas
170
171    def get_project_quotas(self, context, resources, project_id,
172                           quota_class=None, defaults=True,
173                           usages=True):
174        """Retrieve quotas for a project.
175
176        Given a list of resources, retrieve the quotas for the given
177        project.
178
179        :param context: The request context, for access checks.
180        :param resources: A dictionary of the registered resources.
181        :param project_id: The ID of the project to return quotas for.
182        :param quota_class: If project_id != context.project_id, the
183                            quota class cannot be determined.  This
184                            parameter allows it to be specified.  It
185                            will be ignored if project_id ==
186                            context.project_id.
187        :param defaults: If True, the quota class value (or the
188                         default value, if there is no value from the
189                         quota class) will be reported if there is no
190                         specific value for the resource.
191        :param usages: If True, the current in_use, reserved and allocated
192                       counts will also be returned.
193        """
194
195        quotas = {}
196        project_quotas = db.quota_get_all_by_project(context, project_id)
197        allocated_quotas = None
198        default_quotas = None
199        if usages:
200            project_usages = db.quota_usage_get_all_by_project(context,
201                                                               project_id)
202            allocated_quotas = db.quota_allocated_get_all_by_project(
203                context, project_id)
204            allocated_quotas.pop('project_id')
205
206        # Get the quotas for the appropriate class.  If the project ID
207        # matches the one in the context, we use the quota_class from
208        # the context, otherwise, we use the provided quota_class (if
209        # any)
210        if project_id == context.project_id:
211            quota_class = context.quota_class
212        if quota_class:
213            class_quotas = db.quota_class_get_all_by_name(context, quota_class)
214        else:
215            class_quotas = {}
216
217        for resource in resources.values():
218            # Omit default/quota class values
219            if not defaults and resource.name not in project_quotas:
220                continue
221
222            quota_val = project_quotas.get(resource.name)
223            if quota_val is None:
224                quota_val = class_quotas.get(resource.name)
225                if quota_val is None:
226                    # Lazy load the default quotas
227                    if default_quotas is None:
228                        default_quotas = self.get_defaults(
229                            context, resources, project_id)
230                    quota_val = default_quotas[resource.name]
231
232            quotas[resource.name] = {'limit': quota_val}
233
234            # Include usages if desired.  This is optional because one
235            # internal consumer of this interface wants to access the
236            # usages directly from inside a transaction.
237            if usages:
238                usage = project_usages.get(resource.name, {})
239                quotas[resource.name].update(
240                    in_use=usage.get('in_use', 0),
241                    reserved=usage.get('reserved', 0), )
242            if allocated_quotas:
243                quotas[resource.name].update(
244                    allocated=allocated_quotas.get(resource.name, 0), )
245        return quotas
246
247    def _get_quotas(self, context, resources, keys, has_sync, project_id=None):
248        """A helper method which retrieves the quotas for specific resources.
249
250        This specific resource is identified by keys, and which apply to the
251        current context.
252
253        :param context: The request context, for access checks.
254        :param resources: A dictionary of the registered resources.
255        :param keys: A list of the desired quotas to retrieve.
256        :param has_sync: If True, indicates that the resource must
257                         have a sync attribute; if False, indicates
258                         that the resource must NOT have a sync
259                         attribute.
260        :param project_id: Specify the project_id if current context
261                           is admin and admin wants to impact on
262                           common user's tenant.
263        """
264
265        # Filter resources
266        if has_sync:
267            sync_filt = lambda x: hasattr(x, 'sync')
268        else:
269            sync_filt = lambda x: not hasattr(x, 'sync')
270        desired = set(keys)
271        sub_resources = {k: v for k, v in resources.items()
272                         if k in desired and sync_filt(v)}
273
274        # Make sure we accounted for all of them...
275        if len(keys) != len(sub_resources):
276            unknown = desired - set(sub_resources.keys())
277            raise exception.QuotaResourceUnknown(unknown=sorted(unknown))
278
279        # Grab and return the quotas (without usages)
280        quotas = self.get_project_quotas(context, sub_resources,
281                                         project_id,
282                                         context.quota_class, usages=False)
283
284        return {k: v['limit'] for k, v in quotas.items()}
285
286    def limit_check(self, context, resources, values, project_id=None):
287        """Check simple quota limits.
288
289        For limits--those quotas for which there is no usage
290        synchronization function--this method checks that a set of
291        proposed values are permitted by the limit restriction.
292
293        This method will raise a QuotaResourceUnknown exception if a
294        given resource is unknown or if it is not a simple limit
295        resource.
296
297        If any of the proposed values is over the defined quota, an
298        OverQuota exception will be raised with the sorted list of the
299        resources which are too high.  Otherwise, the method returns
300        nothing.
301
302        :param context: The request context, for access checks.
303        :param resources: A dictionary of the registered resources.
304        :param values: A dictionary of the values to check against the
305                       quota.
306        :param project_id: Specify the project_id if current context
307                           is admin and admin wants to impact on
308                           common user's tenant.
309        """
310
311        # Ensure no value is less than zero
312        unders = [key for key, val in values.items() if val < 0]
313        if unders:
314            raise exception.InvalidQuotaValue(unders=sorted(unders))
315
316        # If project_id is None, then we use the project_id in context
317        if project_id is None:
318            project_id = context.project_id
319
320        # Get the applicable quotas
321        quotas = self._get_quotas(context, resources, values.keys(),
322                                  has_sync=False, project_id=project_id)
323        # Check the quotas and construct a list of the resources that
324        # would be put over limit by the desired values
325        overs = [key for key, val in values.items()
326                 if quotas[key] >= 0 and quotas[key] < val]
327        if overs:
328            raise exception.OverQuota(overs=sorted(overs), quotas=quotas,
329                                      usages={})
330
331    def reserve(self, context, resources, deltas, expire=None,
332                project_id=None):
333        """Check quotas and reserve resources.
334
335        For counting quotas--those quotas for which there is a usage
336        synchronization function--this method checks quotas against
337        current usage and the desired deltas.
338
339        This method will raise a QuotaResourceUnknown exception if a
340        given resource is unknown or if it does not have a usage
341        synchronization function.
342
343        If any of the proposed values is over the defined quota, an
344        OverQuota exception will be raised with the sorted list of the
345        resources which are too high.  Otherwise, the method returns a
346        list of reservation UUIDs which were created.
347
348        :param context: The request context, for access checks.
349        :param resources: A dictionary of the registered resources.
350        :param deltas: A dictionary of the proposed delta changes.
351        :param expire: An optional parameter specifying an expiration
352                       time for the reservations.  If it is a simple
353                       number, it is interpreted as a number of
354                       seconds and added to the current time; if it is
355                       a datetime.timedelta object, it will also be
356                       added to the current time.  A datetime.datetime
357                       object will be interpreted as the absolute
358                       expiration time.  If None is specified, the
359                       default expiration time set by
360                       --default-reservation-expire will be used (this
361                       value will be treated as a number of seconds).
362        :param project_id: Specify the project_id if current context
363                           is admin and admin wants to impact on
364                           common user's tenant.
365        """
366
367        # Set up the reservation expiration
368        if expire is None:
369            expire = CONF.reservation_expire
370        if isinstance(expire, six.integer_types):
371            expire = datetime.timedelta(seconds=expire)
372        if isinstance(expire, datetime.timedelta):
373            expire = timeutils.utcnow() + expire
374        if not isinstance(expire, datetime.datetime):
375            raise exception.InvalidReservationExpiration(expire=expire)
376
377        # If project_id is None, then we use the project_id in context
378        if project_id is None:
379            project_id = context.project_id
380
381        # Get the applicable quotas.
382        # NOTE(Vek): We're not worried about races at this point.
383        #            Yes, the admin may be in the process of reducing
384        #            quotas, but that's a pretty rare thing.
385        quotas = self._get_quotas(context, resources, deltas.keys(),
386                                  has_sync=True, project_id=project_id)
387        return self._reserve(context, resources, quotas, deltas, expire,
388                             project_id)
389
390    def _reserve(self, context, resources, quotas, deltas, expire, project_id):
391        # NOTE(Vek): Most of the work here has to be done in the DB
392        #            API, because we have to do it in a transaction,
393        #            which means access to the session.  Since the
394        #            session isn't available outside the DBAPI, we
395        #            have to do the work there.
396        return db.quota_reserve(context, resources, quotas, deltas, expire,
397                                CONF.until_refresh, CONF.max_age,
398                                project_id=project_id)
399
400    def commit(self, context, reservations, project_id=None):
401        """Commit reservations.
402
403        :param context: The request context, for access checks.
404        :param reservations: A list of the reservation UUIDs, as
405                             returned by the reserve() method.
406        :param project_id: Specify the project_id if current context
407                           is admin and admin wants to impact on
408                           common user's tenant.
409        """
410        # If project_id is None, then we use the project_id in context
411        if project_id is None:
412            project_id = context.project_id
413
414        db.reservation_commit(context, reservations, project_id=project_id)
415
416    def rollback(self, context, reservations, project_id=None):
417        """Roll back reservations.
418
419        :param context: The request context, for access checks.
420        :param reservations: A list of the reservation UUIDs, as
421                             returned by the reserve() method.
422        :param project_id: Specify the project_id if current context
423                           is admin and admin wants to impact on
424                           common user's tenant.
425        """
426        # If project_id is None, then we use the project_id in context
427        if project_id is None:
428            project_id = context.project_id
429
430        db.reservation_rollback(context, reservations, project_id=project_id)
431
432    def destroy_by_project(self, context, project_id):
433        """Destroy all limit quotas associated with a project.
434
435        Leave usage and reservation quotas intact.
436
437        :param context: The request context, for access checks.
438        :param project_id: The ID of the project being deleted.
439        """
440        db.quota_destroy_by_project(context, project_id)
441
442    def expire(self, context):
443        """Expire reservations.
444
445        Explores all currently existing reservations and rolls back
446        any that have expired.
447
448        :param context: The request context, for access checks.
449        """
450
451        db.reservation_expire(context)
452
453
454class NestedDbQuotaDriver(DbQuotaDriver):
455    def validate_nested_setup(self, ctxt, resources, project_tree,
456                              fix_allocated_quotas=False):
457        """Ensures project_tree has quotas that make sense as nested quotas.
458
459        Validates the following:
460          * No parent project has child_projects who have more combined quota
461            than the parent's quota limit
462          * No child quota has a larger in-use value than it's current limit
463            (could happen before because child default values weren't enforced)
464          * All parent projects' "allocated" quotas match the sum of the limits
465            of its children projects
466
467        TODO(mc_nair): need a better way to "flip the switch" to use nested
468                       quotas to make this less race-ee
469        """
470        self._allocated = {}
471        project_queue = deque(project_tree.items())
472        borked_allocated_quotas = {}
473
474        while project_queue:
475            # Tuple of (current root node, subtree)
476            cur_proj_id, project_subtree = project_queue.popleft()
477
478            # If we're on a leaf node, no need to do validation on it, and in
479            # order to avoid complication trying to get its children, skip it.
480            if not project_subtree:
481                continue
482
483            cur_project_quotas = self.get_project_quotas(
484                ctxt, resources, cur_proj_id)
485
486            # Validate each resource when compared to it's child quotas
487            for resource in cur_project_quotas.keys():
488                parent_quota = cur_project_quotas[resource]
489                parent_limit = parent_quota['limit']
490                parent_usage = (parent_quota['in_use'] +
491                                parent_quota['reserved'])
492
493                cur_parent_allocated = parent_quota.get('allocated', 0)
494                calc_parent_allocated = self._get_cur_project_allocated(
495                    ctxt, resources[resource], {cur_proj_id: project_subtree})
496
497                if parent_limit > 0:
498                    parent_free_quota = parent_limit - parent_usage
499                    if parent_free_quota < calc_parent_allocated:
500                        msg = _("Sum of child usage '%(sum)s' is greater "
501                                "than free quota of '%(free)s' for project "
502                                "'%(proj)s' for resource '%(res)s'. Please "
503                                "lower the limit or usage for one or more of "
504                                "the following projects: '%(child_ids)s'") % {
505                            'sum': calc_parent_allocated,
506                            'free': parent_free_quota,
507                            'proj': cur_proj_id, 'res': resource,
508                            'child_ids': ', '.join(project_subtree.keys())
509                        }
510                        raise exception.InvalidNestedQuotaSetup(reason=msg)
511
512                # If "allocated" value wasn't right either err or fix DB
513                if calc_parent_allocated != cur_parent_allocated:
514                    if fix_allocated_quotas:
515                        try:
516                            db.quota_allocated_update(ctxt, cur_proj_id,
517                                                      resource,
518                                                      calc_parent_allocated)
519                        except exception.ProjectQuotaNotFound:
520                            # If it was default quota create DB entry for it
521                            db.quota_create(
522                                ctxt, cur_proj_id, resource,
523                                parent_limit, allocated=calc_parent_allocated)
524                    else:
525                        if cur_proj_id not in borked_allocated_quotas:
526                            borked_allocated_quotas[cur_proj_id] = {}
527
528                        borked_allocated_quotas[cur_proj_id][resource] = {
529                            'db_allocated_quota': cur_parent_allocated,
530                            'expected_allocated_quota': calc_parent_allocated}
531
532            project_queue.extend(project_subtree.items())
533
534        if borked_allocated_quotas:
535            msg = _("Invalid allocated quotas defined for the following "
536                    "project quotas: %s") % borked_allocated_quotas
537            raise exception.InvalidNestedQuotaSetup(message=msg)
538
539    def _get_cur_project_allocated(self, ctxt, resource, project_tree):
540        """Recursively calculates the allocated value of a project
541
542        :param ctxt: context used to retrieve DB values
543        :param resource: the resource to calculate allocated value for
544        :param project_tree: the project tree used to calculate allocated
545                e.g. {'A': {'B': {'D': None}, 'C': None}}
546
547        A project's "allocated" value depends on:
548            1) the quota limits which have been "given" to it's children, in
549               the case those limits are not unlimited (-1)
550            2) the current quota being used by a child plus whatever the child
551               has given to it's children, in the case of unlimited (-1) limits
552
553        Scenario #2 requires recursively calculating allocated, and in order
554        to efficiently calculate things we will save off any previously
555        calculated allocated values.
556
557        NOTE: this currently leaves a race condition when a project's allocated
558        value has been calculated (with a -1 limit), but then a child project
559        gets a volume created, thus changing the in-use value and messing up
560        the child's allocated value. We should look into updating the allocated
561        values as we're going along and switching to NestedQuotaDriver with
562        flip of a switch.
563        """
564        # Grab the current node
565        cur_project_id = list(project_tree)[0]
566        project_subtree = project_tree[cur_project_id]
567        res_name = resource.name
568
569        if cur_project_id not in self._allocated:
570            self._allocated[cur_project_id] = {}
571
572        if res_name not in self._allocated[cur_project_id]:
573            # Calculate the allocated value for this resource since haven't yet
574            cur_project_allocated = 0
575            child_proj_ids = project_subtree.keys() if project_subtree else {}
576            res_dict = {res_name: resource}
577            child_project_quotas = {child_id: self.get_project_quotas(
578                ctxt, res_dict, child_id) for child_id in child_proj_ids}
579
580            for child_id, child_quota in child_project_quotas.items():
581                child_limit = child_quota[res_name]['limit']
582                # Non-unlimited quota is easy, anything explicitly given to a
583                # child project gets added into allocated value
584                if child_limit != -1:
585                    if child_quota[res_name].get('in_use', 0) > child_limit:
586                        msg = _("Quota limit invalid for project '%(proj)s' "
587                                "for resource '%(res)s': limit of %(limit)d "
588                                "is less than in-use value of %(used)d") % {
589                            'proj': child_id, 'res': res_name,
590                            'limit': child_limit,
591                            'used': child_quota[res_name]['in_use']
592                        }
593                        raise exception.InvalidNestedQuotaSetup(reason=msg)
594
595                    cur_project_allocated += child_limit
596                # For -1, take any quota being eaten up by child, as well as
597                # what the child itself has given up to its children
598                else:
599                    child_in_use = child_quota[res_name].get('in_use', 0)
600                    # Recursively calculate child's allocated
601                    child_alloc = self._get_cur_project_allocated(
602                        ctxt, resource, {child_id: project_subtree[child_id]})
603                    cur_project_allocated += child_in_use + child_alloc
604
605            self._allocated[cur_project_id][res_name] = cur_project_allocated
606
607        return self._allocated[cur_project_id][res_name]
608
609    def get_default(self, context, resource, project_id):
610        """Get a specific default quota for a resource."""
611        resource = super(NestedDbQuotaDriver, self).get_default(
612            context, resource, project_id)
613
614        return 0 if quota_utils.get_parent_project_id(
615            context, project_id) else resource.default
616
617    def get_defaults(self, context, resources, project_id=None):
618        defaults = super(NestedDbQuotaDriver, self).get_defaults(
619            context, resources, project_id)
620        # All defaults are 0 for child project
621        if quota_utils.get_parent_project_id(context, project_id):
622            for key in defaults.keys():
623                defaults[key] = 0
624        return defaults
625
626    def _reserve(self, context, resources, quotas, deltas, expire, project_id):
627        reserved = []
628        # As to not change the exception behavior, flag every res that would
629        # be over instead of failing on first OverQuota
630        resources_failed_to_update = []
631        failed_usages = {}
632        for res in deltas.keys():
633            try:
634                reserved += db.quota_reserve(
635                    context, resources, quotas, {res: deltas[res]},
636                    expire, CONF.until_refresh, CONF.max_age, project_id)
637                if quotas[res] == -1:
638                    reserved += quota_utils.update_alloc_to_next_hard_limit(
639                        context, resources, deltas, res, expire, project_id)
640            except exception.OverQuota as e:
641                resources_failed_to_update.append(res)
642                failed_usages.update(e.kwargs['usages'])
643
644        if resources_failed_to_update:
645            db.reservation_rollback(context, reserved, project_id)
646            # We change OverQuota to OverVolumeLimit in other places and expect
647            # to find all of the OverQuota kwargs
648            raise exception.OverQuota(overs=sorted(resources_failed_to_update),
649                                      quotas=quotas, usages=failed_usages)
650
651        return reserved
652
653
654class BaseResource(object):
655    """Describe a single resource for quota checking."""
656
657    def __init__(self, name, flag=None, parent_project_id=None):
658        """Initializes a Resource.
659
660        :param name: The name of the resource, i.e., "volumes".
661        :param flag: The name of the flag or configuration option
662                     which specifies the default value of the quota
663                     for this resource.
664        :param parent_project_id: The id of the current project's parent,
665                                  if any.
666        """
667
668        self.name = name
669        self.flag = flag
670        self.parent_project_id = parent_project_id
671
672    def quota(self, driver, context, **kwargs):
673        """Given a driver and context, obtain the quota for this resource.
674
675        :param driver: A quota driver.
676        :param context: The request context.
677        :param project_id: The project to obtain the quota value for.
678                           If not provided, it is taken from the
679                           context.  If it is given as None, no
680                           project-specific quota will be searched
681                           for.
682        :param quota_class: The quota class corresponding to the
683                            project, or for which the quota is to be
684                            looked up.  If not provided, it is taken
685                            from the context.  If it is given as None,
686                            no quota class-specific quota will be
687                            searched for.  Note that the quota class
688                            defaults to the value in the context,
689                            which may not correspond to the project if
690                            project_id is not the same as the one in
691                            the context.
692        """
693
694        # Get the project ID
695        project_id = kwargs.get('project_id', context.project_id)
696
697        # Ditto for the quota class
698        quota_class = kwargs.get('quota_class', context.quota_class)
699
700        # Look up the quota for the project
701        if project_id:
702            try:
703                return driver.get_by_project(context, project_id, self.name)
704            except exception.ProjectQuotaNotFound:
705                pass
706
707        # Try for the quota class
708        if quota_class:
709            try:
710                return driver.get_by_class(context, quota_class, self.name)
711            except exception.QuotaClassNotFound:
712                pass
713
714        # OK, return the default
715        return driver.get_default(context, self,
716                                  parent_project_id=self.parent_project_id)
717
718    @property
719    def default(self):
720        """Return the default value of the quota."""
721
722        if self.parent_project_id:
723            return 0
724
725        return CONF[self.flag] if self.flag else -1
726
727
728class ReservableResource(BaseResource):
729    """Describe a reservable resource."""
730
731    def __init__(self, name, sync, flag=None):
732        """Initializes a ReservableResource.
733
734        Reservable resources are those resources which directly
735        correspond to objects in the database, i.e., volumes, gigabytes,
736        etc.  A ReservableResource must be constructed with a usage
737        synchronization function, which will be called to determine the
738        current counts of one or more resources.
739
740        The usage synchronization function will be passed three
741        arguments: an admin context, the project ID, and an opaque
742        session object, which should in turn be passed to the
743        underlying database function.  Synchronization functions
744        should return a dictionary mapping resource names to the
745        current in_use count for those resources; more than one
746        resource and resource count may be returned.  Note that
747        synchronization functions may be associated with more than one
748        ReservableResource.
749
750        :param name: The name of the resource, i.e., "volumes".
751        :param sync: A dbapi methods name which returns a dictionary
752                     to resynchronize the in_use count for one or more
753                     resources, as described above.
754        :param flag: The name of the flag or configuration option
755                     which specifies the default value of the quota
756                     for this resource.
757        """
758
759        super(ReservableResource, self).__init__(name, flag=flag)
760        if sync:
761            self.sync = sync
762
763
764class AbsoluteResource(BaseResource):
765    """Describe a non-reservable resource."""
766
767    pass
768
769
770class CountableResource(AbsoluteResource):
771    """Describe a resource where counts aren't based only on the project ID."""
772
773    def __init__(self, name, count, flag=None):
774        """Initializes a CountableResource.
775
776        Countable resources are those resources which directly
777        correspond to objects in the database, i.e., volumes, gigabytes,
778        etc., but for which a count by project ID is inappropriate.  A
779        CountableResource must be constructed with a counting
780        function, which will be called to determine the current counts
781        of the resource.
782
783        The counting function will be passed the context, along with
784        the extra positional and keyword arguments that are passed to
785        Quota.count().  It should return an integer specifying the
786        count.
787
788        Note that this counting is not performed in a transaction-safe
789        manner.  This resource class is a temporary measure to provide
790        required functionality, until a better approach to solving
791        this problem can be evolved.
792
793        :param name: The name of the resource, i.e., "volumes".
794        :param count: A callable which returns the count of the
795                      resource.  The arguments passed are as described
796                      above.
797        :param flag: The name of the flag or configuration option
798                     which specifies the default value of the quota
799                     for this resource.
800        """
801
802        super(CountableResource, self).__init__(name, flag=flag)
803        self.count = count
804
805
806class VolumeTypeResource(ReservableResource):
807    """ReservableResource for a specific volume type."""
808
809    def __init__(self, part_name, volume_type):
810        """Initializes a VolumeTypeResource.
811
812        :param part_name: The kind of resource, i.e., "volumes".
813        :param volume_type: The volume type for this resource.
814        """
815
816        self.volume_type_name = volume_type['name']
817        self.volume_type_id = volume_type['id']
818        name = "%s_%s" % (part_name, self.volume_type_name)
819        super(VolumeTypeResource, self).__init__(name, "_sync_%s" % part_name)
820
821
822class QuotaEngine(object):
823    """Represent the set of recognized quotas."""
824
825    def __init__(self, quota_driver_class=None):
826        """Initialize a Quota object."""
827
828        self._resources = {}
829        self._quota_driver_class = quota_driver_class
830        self._driver_class = None
831
832    @property
833    def _driver(self):
834        # Lazy load the driver so we give a chance for the config file to
835        # be read before grabbing the config for which QuotaDriver to use
836        if self._driver_class:
837            return self._driver_class
838
839        if not self._quota_driver_class:
840            # Grab the current driver class from CONF
841            self._quota_driver_class = CONF.quota_driver
842
843        if isinstance(self._quota_driver_class, six.string_types):
844            self._quota_driver_class = importutils.import_object(
845                self._quota_driver_class)
846
847        self._driver_class = self._quota_driver_class
848        return self._driver_class
849
850    def using_nested_quotas(self):
851        """Returns true if nested quotas are being used"""
852        return isinstance(self._driver, NestedDbQuotaDriver)
853
854    def __contains__(self, resource):
855        return resource in self.resources
856
857    def register_resource(self, resource):
858        """Register a resource."""
859
860        self._resources[resource.name] = resource
861
862    def register_resources(self, resources):
863        """Register a list of resources."""
864
865        for resource in resources:
866            self.register_resource(resource)
867
868    def get_by_project(self, context, project_id, resource_name):
869        """Get a specific quota by project."""
870        return self._driver.get_by_project(context, project_id, resource_name)
871
872    def get_by_project_or_default(self, context, project_id, resource_name):
873        """Get specific quota by project or default quota if doesn't exists."""
874        try:
875            val = self.get_by_project(
876                context, project_id, resource_name).hard_limit
877        except exception.ProjectQuotaNotFound:
878            val = self.get_defaults(context, project_id)[resource_name]
879
880        return val
881
882    def get_by_class(self, context, quota_class, resource_name):
883        """Get a specific quota by quota class."""
884
885        return self._driver.get_by_class(context, quota_class, resource_name)
886
887    def get_default(self, context, resource, parent_project_id=None):
888        """Get a specific default quota for a resource.
889
890        :param parent_project_id: The id of the current project's parent,
891                                  if any.
892        """
893
894        return self._driver.get_default(context, resource,
895                                        parent_project_id=parent_project_id)
896
897    def get_defaults(self, context, project_id=None):
898        """Retrieve the default quotas.
899
900        :param context: The request context, for access checks.
901        :param project_id: The id of the current project
902        """
903
904        return self._driver.get_defaults(context, self.resources,
905                                         project_id)
906
907    def get_class_quotas(self, context, quota_class, defaults=True):
908        """Retrieve the quotas for the given quota class.
909
910        :param context: The request context, for access checks.
911        :param quota_class: The name of the quota class to return
912                            quotas for.
913        :param defaults: If True, the default value will be reported
914                         if there is no specific value for the
915                         resource.
916        """
917
918        return self._driver.get_class_quotas(context, self.resources,
919                                             quota_class, defaults=defaults)
920
921    def get_project_quotas(self, context, project_id, quota_class=None,
922                           defaults=True, usages=True):
923        """Retrieve the quotas for the given project.
924
925        :param context: The request context, for access checks.
926        :param project_id: The ID of the project to return quotas for.
927        :param quota_class: If project_id != context.project_id, the
928                            quota class cannot be determined.  This
929                            parameter allows it to be specified.
930        :param defaults: If True, the quota class value (or the
931                         default value, if there is no value from the
932                         quota class) will be reported if there is no
933                         specific value for the resource.
934        :param usages: If True, the current in_use, reserved and
935                       allocated counts will also be returned.
936        """
937        return self._driver.get_project_quotas(context, self.resources,
938                                               project_id,
939                                               quota_class=quota_class,
940                                               defaults=defaults,
941                                               usages=usages)
942
943    def count(self, context, resource, *args, **kwargs):
944        """Count a resource.
945
946        For countable resources, invokes the count() function and
947        returns its result.  Arguments following the context and
948        resource are passed directly to the count function declared by
949        the resource.
950
951        :param context: The request context, for access checks.
952        :param resource: The name of the resource, as a string.
953        """
954
955        # Get the resource
956        res = self.resources.get(resource)
957        if not res or not hasattr(res, 'count'):
958            raise exception.QuotaResourceUnknown(unknown=[resource])
959
960        return res.count(context, *args, **kwargs)
961
962    def limit_check(self, context, project_id=None, **values):
963        """Check simple quota limits.
964
965        For limits--those quotas for which there is no usage
966        synchronization function--this method checks that a set of
967        proposed values are permitted by the limit restriction.  The
968        values to check are given as keyword arguments, where the key
969        identifies the specific quota limit to check, and the value is
970        the proposed value.
971
972        This method will raise a QuotaResourceUnknown exception if a
973        given resource is unknown or if it is not a simple limit
974        resource.
975
976        If any of the proposed values is over the defined quota, an
977        OverQuota exception will be raised with the sorted list of the
978        resources which are too high.  Otherwise, the method returns
979        nothing.
980
981        :param context: The request context, for access checks.
982        :param project_id: Specify the project_id if current context
983                           is admin and admin wants to impact on
984                           common user's tenant.
985        """
986
987        return self._driver.limit_check(context, self.resources, values,
988                                        project_id=project_id)
989
990    def reserve(self, context, expire=None, project_id=None, **deltas):
991        """Check quotas and reserve resources.
992
993        For counting quotas--those quotas for which there is a usage
994        synchronization function--this method checks quotas against
995        current usage and the desired deltas.  The deltas are given as
996        keyword arguments, and current usage and other reservations
997        are factored into the quota check.
998
999        This method will raise a QuotaResourceUnknown exception if a
1000        given resource is unknown or if it does not have a usage
1001        synchronization function.
1002
1003        If any of the proposed values is over the defined quota, an
1004        OverQuota exception will be raised with the sorted list of the
1005        resources which are too high.  Otherwise, the method returns a
1006        list of reservation UUIDs which were created.
1007
1008        :param context: The request context, for access checks.
1009        :param expire: An optional parameter specifying an expiration
1010                       time for the reservations.  If it is a simple
1011                       number, it is interpreted as a number of
1012                       seconds and added to the current time; if it is
1013                       a datetime.timedelta object, it will also be
1014                       added to the current time.  A datetime.datetime
1015                       object will be interpreted as the absolute
1016                       expiration time.  If None is specified, the
1017                       default expiration time set by
1018                       --default-reservation-expire will be used (this
1019                       value will be treated as a number of seconds).
1020        :param project_id: Specify the project_id if current context
1021                           is admin and admin wants to impact on
1022                           common user's tenant.
1023        """
1024
1025        reservations = self._driver.reserve(context, self.resources, deltas,
1026                                            expire=expire,
1027                                            project_id=project_id)
1028
1029        LOG.debug("Created reservations %s", reservations)
1030
1031        return reservations
1032
1033    def commit(self, context, reservations, project_id=None):
1034        """Commit reservations.
1035
1036        :param context: The request context, for access checks.
1037        :param reservations: A list of the reservation UUIDs, as
1038                             returned by the reserve() method.
1039        :param project_id: Specify the project_id if current context
1040                           is admin and admin wants to impact on
1041                           common user's tenant.
1042        """
1043
1044        try:
1045            self._driver.commit(context, reservations, project_id=project_id)
1046        except Exception:
1047            # NOTE(Vek): Ignoring exceptions here is safe, because the
1048            # usage resynchronization and the reservation expiration
1049            # mechanisms will resolve the issue.  The exception is
1050            # logged, however, because this is less than optimal.
1051            LOG.exception("Failed to commit reservations %s", reservations)
1052
1053    def rollback(self, context, reservations, project_id=None):
1054        """Roll back reservations.
1055
1056        :param context: The request context, for access checks.
1057        :param reservations: A list of the reservation UUIDs, as
1058                             returned by the reserve() method.
1059        :param project_id: Specify the project_id if current context
1060                           is admin and admin wants to impact on
1061                           common user's tenant.
1062        """
1063
1064        try:
1065            self._driver.rollback(context, reservations, project_id=project_id)
1066        except Exception:
1067            # NOTE(Vek): Ignoring exceptions here is safe, because the
1068            # usage resynchronization and the reservation expiration
1069            # mechanisms will resolve the issue.  The exception is
1070            # logged, however, because this is less than optimal.
1071            LOG.exception("Failed to roll back reservations %s", reservations)
1072
1073    def destroy_by_project(self, context, project_id):
1074        """Destroy all quota limits associated with a project.
1075
1076        :param context: The request context, for access checks.
1077        :param project_id: The ID of the project being deleted.
1078        """
1079
1080        self._driver.destroy_by_project(context, project_id)
1081
1082    def expire(self, context):
1083        """Expire reservations.
1084
1085        Explores all currently existing reservations and rolls back
1086        any that have expired.
1087
1088        :param context: The request context, for access checks.
1089        """
1090
1091        self._driver.expire(context)
1092
1093    def add_volume_type_opts(self, context, opts, volume_type_id):
1094        """Add volume type resource options.
1095
1096        Adds elements to the opts hash for volume type quotas.
1097        If a resource is being reserved ('gigabytes', etc) and the volume
1098        type is set up for its own quotas, these reservations are copied
1099        into keys for 'gigabytes_<volume type name>', etc.
1100
1101        :param context: The request context, for access checks.
1102        :param opts: The reservations options hash.
1103        :param volume_type_id: The volume type id for this reservation.
1104        """
1105        if not volume_type_id:
1106            return
1107
1108        # NOTE(jdg): set inactive to True in volume_type_get, as we
1109        # may be operating on a volume that was created with a type
1110        # that has since been deleted.
1111        volume_type = db.volume_type_get(context, volume_type_id, True)
1112
1113        for quota in ('volumes', 'gigabytes', 'snapshots'):
1114            if quota in opts:
1115                vtype_quota = "%s_%s" % (quota, volume_type['name'])
1116                opts[vtype_quota] = opts[quota]
1117
1118    @property
1119    def resource_names(self):
1120        return sorted(self.resources.keys())
1121
1122    @property
1123    def resources(self):
1124        return self._resources
1125
1126
1127class VolumeTypeQuotaEngine(QuotaEngine):
1128    """Represent the set of all quotas."""
1129
1130    @property
1131    def resources(self):
1132        """Fetches all possible quota resources."""
1133
1134        result = {}
1135        # Global quotas.
1136        argses = [('volumes', '_sync_volumes', 'quota_volumes'),
1137                  ('per_volume_gigabytes', None, 'per_volume_size_limit'),
1138                  ('snapshots', '_sync_snapshots', 'quota_snapshots'),
1139                  ('gigabytes', '_sync_gigabytes', 'quota_gigabytes'),
1140                  ('backups', '_sync_backups', 'quota_backups'),
1141                  ('backup_gigabytes', '_sync_backup_gigabytes',
1142                   'quota_backup_gigabytes')]
1143        for args in argses:
1144            resource = ReservableResource(*args)
1145            result[resource.name] = resource
1146
1147        # Volume type quotas.
1148        volume_types = db.volume_type_get_all(context.get_admin_context(),
1149                                              False)
1150        for volume_type in volume_types.values():
1151            for part_name in ('volumes', 'gigabytes', 'snapshots'):
1152                resource = VolumeTypeResource(part_name, volume_type)
1153                result[resource.name] = resource
1154        return result
1155
1156    def register_resource(self, resource):
1157        raise NotImplementedError(_("Cannot register resource"))
1158
1159    def register_resources(self, resources):
1160        raise NotImplementedError(_("Cannot register resources"))
1161
1162    def update_quota_resource(self, context, old_type_name, new_type_name):
1163        """Update resource in quota.
1164
1165        This is to update resource in quotas, quota_classes, and
1166        quota_usages once the name of a volume type is changed.
1167
1168        :param context: The request context, for access checks.
1169        :param old_type_name: old name of volume type.
1170        :param new_type_name: new name of volume type.
1171        """
1172
1173        for quota in ('volumes', 'gigabytes', 'snapshots'):
1174            old_res = "%s_%s" % (quota, old_type_name)
1175            new_res = "%s_%s" % (quota, new_type_name)
1176            db.quota_usage_update_resource(context,
1177                                           old_res,
1178                                           new_res)
1179            db.quota_class_update_resource(context,
1180                                           old_res,
1181                                           new_res)
1182            db.quota_update_resource(context,
1183                                     old_res,
1184                                     new_res)
1185
1186
1187class CGQuotaEngine(QuotaEngine):
1188    """Represent the consistencygroup quotas."""
1189
1190    @property
1191    def resources(self):
1192        """Fetches all possible quota resources."""
1193
1194        result = {}
1195        # Global quotas.
1196        argses = [('consistencygroups', '_sync_consistencygroups',
1197                   'quota_consistencygroups'), ]
1198        for args in argses:
1199            resource = ReservableResource(*args)
1200            result[resource.name] = resource
1201
1202        return result
1203
1204    def register_resource(self, resource):
1205        raise NotImplementedError(_("Cannot register resource"))
1206
1207    def register_resources(self, resources):
1208        raise NotImplementedError(_("Cannot register resources"))
1209
1210
1211class GroupQuotaEngine(QuotaEngine):
1212    """Represent the group quotas."""
1213
1214    @property
1215    def resources(self):
1216        """Fetches all possible quota resources."""
1217
1218        result = {}
1219        # Global quotas.
1220        argses = [('groups', '_sync_groups',
1221                   'quota_groups'), ]
1222        for args in argses:
1223            resource = ReservableResource(*args)
1224            result[resource.name] = resource
1225
1226        return result
1227
1228    def register_resource(self, resource):
1229        raise NotImplementedError(_("Cannot register resource"))
1230
1231    def register_resources(self, resources):
1232        raise NotImplementedError(_("Cannot register resources"))
1233
1234QUOTAS = VolumeTypeQuotaEngine()
1235CGQUOTAS = CGQuotaEngine()
1236GROUP_QUOTAS = GroupQuotaEngine()
1237