diff options
| author | Jenkins <jenkins@review.openstack.org> | 2013-07-30 01:32:17 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2013-07-30 01:32:17 +0000 |
| commit | e94b2024dfc5f4dd581ba318ef13d1fdb231003d (patch) | |
| tree | 0acd311f9d46a06fcf2378ebf58f991af3c4ddeb /openstack | |
| parent | 53ab0d34bb4df109c8e53bb058c26e4c95aa74c3 (diff) | |
| parent | 19dfee6febc666e398909a38482b20b5672d8b9c (diff) | |
| download | oslo-e94b2024dfc5f4dd581ba318ef13d1fdb231003d.tar.gz oslo-e94b2024dfc5f4dd581ba318ef13d1fdb231003d.tar.xz oslo-e94b2024dfc5f4dd581ba318ef13d1fdb231003d.zip | |
Merge "Import common quota code from nova and cinder"
Diffstat (limited to 'openstack')
| -rw-r--r-- | openstack/common/quota.py | 1173 |
1 files changed, 1173 insertions, 0 deletions
diff --git a/openstack/common/quota.py b/openstack/common/quota.py new file mode 100644 index 0000000..e4b67de --- /dev/null +++ b/openstack/common/quota.py @@ -0,0 +1,1173 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Common quotas""" + +import datetime + +from oslo.config import cfg + +from openstack.common.gettextutils import _ # noqa +from openstack.common import importutils +from openstack.common import log as logging +from openstack.common import timeutils + +LOG = logging.getLogger(__name__) + +common_quota_opts = [ + cfg.BoolOpt('use_default_quota_class', + default=True, + help='whether to use default quota class for default quota'), + cfg.StrOpt('quota_driver', + default='openstack.common.quota.DbQuotaDriver', + help='default driver to use for quota checks'), + cfg.IntOpt('until_refresh', + default=0, + help='count of reservations until usage is refreshed'), + cfg.IntOpt('max_age', + default=0, + help='number of seconds between subsequent usage refreshes'), + cfg.IntOpt('reservation_expire', + default=86400, + help='number of seconds until a reservation expires'), +] + +CONF = cfg.CONF +CONF.register_opts(common_quota_opts) + + +class QuotaException(Exception): + """Base exception for quota.""" + + message = _("Quota exception occurred.") + code = 500 + headers = {} + safe = False + + def __init__(self, message=None, **kwargs): + self.kwargs = kwargs + + if 'code' not in self.kwargs: + try: + self.kwargs['code'] = self.code + except AttributeError: + pass + + if not message: + try: + message = self.message % kwargs + + except Exception: + # kwargs doesn't match a variable in the message + # log the issue and the kwargs + LOG.exception(_('Exception in string format operation')) + for name, value in kwargs.iteritems(): + LOG.error("%s: %s" % (name, value)) + else: + # at least get the core message out if something happened + message = self.message + + super(QuotaException, self).__init__(message) + + def format_message(self): + if self.__class__.__name__.endswith('_Remote'): + return self.args[0] + else: + return unicode(self) + + +class QuotaError(QuotaException): + message = _("Quota exceeded") + ": code=%(code)s" + code = 413 + headers = {'Retry-After': 0} + safe = True + + +class InvalidQuotaValue(QuotaException): + message = _("Change would make usage less than 0 for the following " + "resources: %(unders)s") + + +class OverQuota(QuotaException): + message = _("Quota exceeded for resources: %(overs)s") + + +class QuotaResourceUnknown(QuotaException): + message = _("Unknown quota resources %(unknown)s.") + + +class QuotaNotFound(QuotaException): + code = 404 + message = _("Quota could not be found") + + +class QuotaUsageNotFound(QuotaNotFound): + message = _("Quota usage for project %(project_id)s could not be found.") + + +class ProjectQuotaNotFound(QuotaNotFound): + message = _("Quota for project %(project_id)s could not be found.") + + +class QuotaClassNotFound(QuotaNotFound): + message = _("Quota class %(class_name)s could not be found.") + + +class ReservationNotFound(QuotaNotFound): + message = _("Quota reservation %(uuid)s could not be found.") + + +class InvalidReservationExpiration(QuotaException): + code = 400 + message = _("Invalid reservation expiration %(expire)s.") + + +class DbQuotaDriver(object): + """Database quota driver. + + Driver to perform necessary checks to enforce quotas and obtain + quota information. The default driver utilizes the local + database. + """ + + def __init__(self, db): + self.db = db + + def get_by_project_and_user(self, context, project_id, user_id, resource): + """Get a specific quota by project and user.""" + + return self.db.quota_get(context, project_id, user_id, resource) + + def get_by_project(self, context, project_id, resource_name): + """Get a specific quota by project.""" + + return self.db.quota_get(context, project_id, resource_name) + + def get_by_class(self, context, quota_class, resource_name): + """Get a specific quota by quota class.""" + + return self.db.quota_class_get(context, quota_class, resource_name) + + def get_default(self, context, resource): + """Get a specific default quota for a resource.""" + + default_quotas = self.db.quota_class_get_default(context) + return default_quotas.get(resource.name, resource.default) + + def get_defaults(self, context, resources): + """Given a list of resources, retrieve the default quotas. + + Use the class quotas named `_DEFAULT_QUOTA_NAME` as default quotas, + if it exists. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + """ + + quotas = {} + default_quotas = {} + if CONF.use_default_quota_class: + default_quotas = self.db.quota_class_get_default(context) + for resource in resources.values(): + if resource.name not in default_quotas: + LOG.deprecated(_("Default quota for resource: %(res)s is set " + "by the default quota flag: quota_%(res)s, " + "it is now deprecated. Please use the " + "the default quota class for default " + "quota.") % {'res': resource.name}) + quotas[resource.name] = default_quotas.get(resource.name, + resource.default) + + return quotas + + def get_class_quotas(self, context, resources, quota_class, + defaults=True): + """Given a list of resources, get quotas for the given quota class. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + :param quota_class: The name of the quota class to return + quotas for. + :param defaults: If True, the default value will be reported + if there is no specific value for the + resource. + """ + + quotas = {} + default_quotas = {} + class_quotas = self.db.quota_class_get_all_by_name(context, + quota_class) + if defaults: + default_quotas = self.db.quota_class_get_default(context) + for resource in resources.values(): + if resource.name in class_quotas: + quotas[resource.name] = class_quotas[resource.name] + continue + + if defaults: + quotas[resource.name] = default_quotas.get(resource.name, + resource.default) + + return quotas + + def _process_quotas(self, context, resources, project_id, quotas, + quota_class=None, defaults=True, usages=None, + remains=False): + """Get the quotas for the appropriate class. + + If the project ID matches the one in the context, we use the + quota_class from the context, otherwise, we use the provided + quota_class (if any) + """ + + modified_quotas = {} + if project_id == context.project_id: + quota_class = context.quota_class + if quota_class: + class_quotas = self.db.quota_class_get_all_by_name(context, + quota_class) + else: + class_quotas = {} + + default_quotas = self.get_defaults(context, resources) + + for resource in resources.values(): + # Omit default/quota class values + if not defaults and resource.name not in quotas: + continue + class_quota = class_quotas.get(resource.name, + default_quotas[resource.name]) + limit = quotas.get(resource.name, class_quota) + modified_quotas[resource.name] = dict(limit=limit) + + # Include usages if desired. This is optional because one + # internal consumer of this interface wants to access the + # usages directly from inside a transaction. + if usages: + usage = usages.get(resource.name, {}) + modified_quotas[resource.name].update( + in_use=usage.get('in_use', 0), + reserved=usage.get('reserved', 0), + ) + # Initialize remains quotas. + if remains: + modified_quotas[resource.name].update(remains=limit) + + if remains: + all_quotas = self.db.quota_get_all(context, project_id) + for quota in all_quotas: + if quota['resource'] in modified_quotas: + modified_quotas[quota['resource']]['remains'] -= \ + quota['hard_limit'] + + return modified_quotas + + def get_user_quotas(self, context, resources, project_id, user_id, + quota_class=None, defaults=True, + usages=True): + """Get user quotas for given user and project. + + Given a list of resources, retrieve the quotas for the given + user and project. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + :param project_id: The ID of the project to return quotas for. + :param user_id: The ID of the user to return quotas for. + :param quota_class: If project_id != context.project_id, the + quota class cannot be determined. This + parameter allows it to be specified. It + will be ignored if project_id == + context.project_id. + :param defaults: If True, the quota class value (or the + default value, if there is no value from the + quota class) will be reported if there is no + specific value for the resource. + :param usages: If True, the current in_use and reserved counts + will also be returned. + """ + user_quotas = self.db.quota_get_all_by_project_and_user( + context, project_id, user_id) + user_usages = None + if usages: + user_usages = self.db.quota_usage_get_all_by_project_and_user( + context, project_id, user_id) + return self._process_quotas(context, resources, project_id, + user_quotas, quota_class, + defaults=defaults, usages=user_usages) + + def get_project_quotas(self, context, resources, project_id, + quota_class=None, defaults=True, + usages=True, remains=False): + """Given a list of resources, get the quotas for the given project. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + :param project_id: The ID of the project to return quotas for. + :param quota_class: If project_id != context.project_id, the + quota class cannot be determined. This + parameter allows it to be specified. It + will be ignored if project_id == + context.project_id. + :param defaults: If True, the quota class value (or the + default value, if there is no value from the + quota class) will be reported if there is no + specific value for the resource. + :param usages: If True, the current in_use and reserved counts + will also be returned. + :param remains: If True, the current remains of the project will + will be returned. + """ + project_quotas = self.db.quota_get_all_by_project( + context, project_id) + project_usages = None + if usages: + project_usages = self.db.quota_usage_get_all_by_project( + context, project_id) + return self._process_quotas(context, resources, project_id, + project_quotas, quota_class, + defaults=defaults, usages=project_usages, + remains=remains) + + def get_settable_quotas(self, context, resources, project_id, + user_id=None): + """Get settable quotas for given user and project. + + Given a list of resources, retrieve the range of settable quotas for + the given user or project. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + :param project_id: The ID of the project to return quotas for. + :param user_id: The ID of the user to return quotas for. + """ + settable_quotas = {} + project_quotas = self.get_project_quotas(context, resources, + project_id, remains=True) + if user_id: + user_quotas = self.get_user_quotas(context, resources, + project_id, user_id) + setted_quotas = self.db.quota_get_all_by_project_and_user( + context, project_id, user_id) + for key, value in user_quotas.items(): + maximum = project_quotas[key]['remains'] +\ + setted_quotas.get(key, 0) + settable_quotas[key] = dict( + minimum=value['in_use'] + value['reserved'], + maximum=maximum, + ) + else: + for key, value in project_quotas.items(): + minimum = max(int(value['limit'] - value['remains']), + int(value['in_use'] + value['reserved'])) + settable_quotas[key] = dict(minimum=minimum, maximum=-1) + return settable_quotas + + def _get_quotas(self, context, resources, keys, has_sync, project_id=None, + user_id=None): + """Get guotas for resources identified by keys. + + A helper method which retrieves the quotas for the specific + resources identified by keys, and which apply to the current + context. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + :param keys: A list of the desired quotas to retrieve. + :param has_sync: If True, indicates that the resource must + have a sync attribute; if False, indicates + that the resource must NOT have a sync + attribute. + :param project_id: Specify the project_id if current context + is admin and admin wants to impact on + common user's tenant. + :param user_id: Specify the user_id if current context + is admin and admin wants to impact on + common user. + """ + + # Filter resources + if has_sync: + sync_filt = lambda x: hasattr(x, 'sync') + else: + sync_filt = lambda x: not hasattr(x, 'sync') + desired = set(keys) + sub_resources = dict((k, v) for k, v in resources.items() + if k in desired and sync_filt(v)) + + # Make sure we accounted for all of them... + if len(keys) != len(sub_resources): + unknown = desired - set(sub_resources.keys()) + raise QuotaResourceUnknown(unknown=sorted(unknown)) + + if user_id: + # Grab and return the quotas (without usages) + quotas = self.get_user_quotas(context, sub_resources, + project_id, user_id, + context.quota_class, usages=False) + else: + # Grab and return the quotas (without usages) + quotas = self.get_project_quotas(context, sub_resources, + project_id, + context.quota_class, + usages=False) + + return dict((k, v['limit']) for k, v in quotas.items()) + + def limit_check(self, context, resources, values, project_id=None, + user_id=None): + """Check simple quota limits. + + For limits--those quotas for which there is no usage + synchronization function--this method checks that a set of + proposed values are permitted by the limit restriction. + + This method will raise a QuotaResourceUnknown exception if a + given resource is unknown or if it is not a simple limit + resource. + + If any of the proposed values is over the defined quota, an + OverQuota exception will be raised with the sorted list of the + resources which are too high. Otherwise, the method returns + nothing. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + :param values: A dictionary of the values to check against the + quota. + :param project_id: Specify the project_id if current context + is admin and admin wants to impact on + common user's tenant. + :param user_id: Specify the user_id if current context + is admin and admin wants to impact on + common user. + """ + + # Ensure no value is less than zero + unders = [key for key, val in values.items() if val < 0] + if unders: + raise InvalidQuotaValue(unders=sorted(unders)) + + # If project_id is None, then we use the project_id in context + if project_id is None: + project_id = context.project_id + # If user id is None, then we use the user_id in context + if user_id is None: + user_id = context.user_id + + # Get the applicable quotas + quotas = self._get_quotas(context, resources, values.keys(), + has_sync=False, project_id=project_id) + user_quotas = self._get_quotas(context, resources, values.keys(), + has_sync=False, project_id=project_id, + user_id=user_id) + + # Check the quotas and construct a list of the resources that + # would be put over limit by the desired values + overs = [key for key, val in values.items() + if (quotas[key] >= 0 and quotas[key] < val) or + (user_quotas[key] >= 0 and user_quotas[key] < val)] + if overs: + raise OverQuota(overs=sorted(overs), quotas=quotas, + usages={}) + + def reserve(self, context, resources, deltas, expire=None, + project_id=None, user_id=None): + """Check quotas and reserve resources. + + For counting quotas--those quotas for which there is a usage + synchronization function--this method checks quotas against + current usage and the desired deltas. + + This method will raise a QuotaResourceUnknown exception if a + given resource is unknown or if it does not have a usage + synchronization function. + + If any of the proposed values is over the defined quota, an + OverQuota exception will be raised with the sorted list of the + resources which are too high. Otherwise, the method returns a + list of reservation UUIDs which were created. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + :param deltas: A dictionary of the proposed delta changes. + :param expire: An optional parameter specifying an expiration + time for the reservations. If it is a simple + number, it is interpreted as a number of + seconds and added to the current time; if it is + a datetime.timedelta object, it will also be + added to the current time. A datetime.datetime + object will be interpreted as the absolute + expiration time. If None is specified, the + default expiration time set by + --default-reservation-expire will be used (this + value will be treated as a number of seconds). + :param project_id: Specify the project_id if current context + is admin and admin wants to impact on + common user's tenant. + :param user_id: Specify the user_id if current context + is admin and admin wants to impact on + common user. + """ + + # Set up the reservation expiration + if expire is None: + expire = CONF.reservation_expire + if isinstance(expire, (int, long)): + expire = datetime.timedelta(seconds=expire) + if isinstance(expire, datetime.timedelta): + expire = timeutils.utcnow() + expire + if not isinstance(expire, datetime.datetime): + raise InvalidReservationExpiration(expire=expire) + + # If project_id is None, then we use the project_id in context + if project_id is None: + project_id = context.project_id + # If user_id is None, then we use the project_id in context + if user_id is None: + user_id = context.user_id + + # Get the applicable quotas. + # NOTE(Vek): We're not worried about races at this point. + # Yes, the admin may be in the process of reducing + # quotas, but that's a pretty rare thing. + quotas = self._get_quotas(context, resources, deltas.keys(), + has_sync=True, project_id=project_id) + user_quotas = self._get_quotas(context, resources, deltas.keys(), + has_sync=True, project_id=project_id, + user_id=user_id) + + # NOTE(Vek): Most of the work here has to be done in the DB + # API, because we have to do it in a transaction, + # which means access to the session. Since the + # session isn't available outside the DBAPI, we + # have to do the work there. + return self.db.quota_reserve(context, resources, quotas, user_quotas, + deltas, expire, + CONF.until_refresh, CONF.max_age, + project_id=project_id, user_id=user_id) + + def commit(self, context, reservations, project_id=None, user_id=None): + """Commit reservations. + + :param context: The request context, for access checks. + :param reservations: A list of the reservation UUIDs, as + returned by the reserve() method. + :param project_id: Specify the project_id if current context + is admin and admin wants to impact on + common user's tenant. + :param user_id: Specify the user_id if current context + is admin and admin wants to impact on + common user. + """ + # If project_id is None, then we use the project_id in context + if project_id is None: + project_id = context.project_id + # If user_id is None, then we use the user_id in context + if user_id is None: + user_id = context.user_id + + self.db.reservation_commit(context, reservations, + project_id=project_id, user_id=user_id) + + def rollback(self, context, reservations, project_id=None, user_id=None): + """Roll back reservations. + + :param context: The request context, for access checks. + :param reservations: A list of the reservation UUIDs, as + returned by the reserve() method. + :param project_id: Specify the project_id if current context + is admin and admin wants to impact on + common user's tenant. + :param user_id: Specify the user_id if current context + is admin and admin wants to impact on + common user. + """ + # If project_id is None, then we use the project_id in context + if project_id is None: + project_id = context.project_id + # If user_id is None, then we use the user_id in context + if user_id is None: + user_id = context.user_id + + self.db.reservation_rollback(context, reservations, + project_id=project_id, user_id=user_id) + + def usage_reset(self, context, resources): + """Reset the usage records. + + Reset usages for a particular user on a list of resources. + This will force that user's usage records to be refreshed + the next time a reservation is made. + + Note: this does not affect the currently outstanding + reservations the user has; those reservations must be + committed or rolled back (or expired). + + :param context: The request context, for access checks. + :param resources: A list of the resource names for which the + usage must be reset. + """ + + # We need an elevated context for the calls to + # quota_usage_update() + elevated = context.elevated() + + for resource in resources: + try: + # Reset the usage to -1, which will force it to be + # refreshed + self.db.quota_usage_update(elevated, context.project_id, + context.user_id, + resource, in_use=-1) + except QuotaUsageNotFound: + # That means it'll be refreshed anyway + pass + + def destroy_all_by_project_and_user(self, context, project_id, user_id): + """Destroy objects by project and user. + + Destroy all quotas, usages, and reservations associated with a + project and user. + + :param context: The request context, for access checks. + :param project_id: The ID of the project being deleted. + :param user_id: The ID of the user being deleted. + """ + + self.db.quota_destroy_all_by_project_and_user(context, project_id, + user_id) + + def destroy_all_by_project(self, context, project_id): + """Destroy quotas, usages, and reservations for given project. + + :param context: The request context, for access checks. + :param project_id: The ID of the project being deleted. + """ + + self.db.quota_destroy_all_by_project(context, project_id) + + def expire(self, context): + """Expire reservations. + + Explores all currently existing reservations and rolls back + any that have expired. + + :param context: The request context, for access checks. + """ + + self.db.reservation_expire(context) + + +class BaseResource(object): + """Describe a single resource for quota checking.""" + + def __init__(self, name, flag=None): + """Initializes a Resource. + + :param name: The name of the resource, i.e., "volumes". + :param flag: The name of the flag or configuration option + which specifies the default value of the quota + for this resource. + """ + + self.name = name + self.flag = flag + + def quota(self, driver, context, **kwargs): + """Given a driver and context, obtain the quota for this resource. + + :param driver: A quota driver. + :param context: The request context. + :param project_id: The project to obtain the quota value for. + If not provided, it is taken from the + context. If it is given as None, no + project-specific quota will be searched + for. + :param quota_class: The quota class corresponding to the + project, or for which the quota is to be + looked up. If not provided, it is taken + from the context. If it is given as None, + no quota class-specific quota will be + searched for. Note that the quota class + defaults to the value in the context, + which may not correspond to the project if + project_id is not the same as the one in + the context. + """ + + # Get the project ID + project_id = kwargs.get('project_id', context.project_id) + + # Ditto for the quota class + quota_class = kwargs.get('quota_class', context.quota_class) + + # Look up the quota for the project + if project_id: + try: + return driver.get_by_project(context, project_id, self.name) + except ProjectQuotaNotFound: + pass + + # Try for the quota class + if quota_class: + try: + return driver.get_by_class(context, quota_class, self.name) + except QuotaClassNotFound: + pass + + # OK, return the default + return driver.get_default(context, self) + + @property + def default(self): + """Return the default value of the quota.""" + + return CONF[self.flag] if self.flag else -1 + + +class ReservableResource(BaseResource): + """Describe a reservable resource.""" + + def __init__(self, name, sync, flag=None): + """Initializes a ReservableResource. + + Reservable resources are those resources which directly + correspond to objects in the database, i.e., instances, + cores, etc. + + Usage synchronization function must be associated with each + object. This function will be called to determine the current + counts of one or more resources. This association is done in + database backend. See QUOTA_SYNC_FUNCTIONS in db/sqlalchemy/api.py. + + The usage synchronization function will be passed three + arguments: an admin context, the project ID, and an opaque + session object, which should in turn be passed to the + underlying database function. Synchronization functions + should return a dictionary mapping resource names to the + current in_use count for those resources; more than one + resource and resource count may be returned. Note that + synchronization functions may be associated with more than one + ReservableResource. + + :param name: The name of the resource, i.e., "volumes". + :param sync: A dbapi methods name which returns a dictionary + to resynchronize the in_use count for one or more + resources, as described above. + :param flag: The name of the flag or configuration option + which specifies the default value of the quota + for this resource. + """ + + super(ReservableResource, self).__init__(name, flag=flag) + self.sync = sync + + +class AbsoluteResource(BaseResource): + """Describe a non-reservable resource.""" + + pass + + +class CountableResource(AbsoluteResource): + """Countable resource. + + Describe a resource where the counts aren't based solely on the + project ID. + """ + + def __init__(self, name, count, flag=None): + """Initializes a CountableResource. + + Countable resources are those resources which directly + correspond to objects in the database, i.e., volumes, gigabytes, + etc., but for which a count by project ID is inappropriate. A + CountableResource must be constructed with a counting + function, which will be called to determine the current counts + of the resource. + + The counting function will be passed the context, along with + the extra positional and keyword arguments that are passed to + Quota.count(). It should return an integer specifying the + count. + + Note that this counting is not performed in a transaction-safe + manner. This resource class is a temporary measure to provide + required functionality, until a better approach to solving + this problem can be evolved. + + :param name: The name of the resource, i.e., "volumes". + :param count: A callable which returns the count of the + resource. The arguments passed are as described + above. + :param flag: The name of the flag or configuration option + which specifies the default value of the quota + for this resource. + """ + + super(CountableResource, self).__init__(name, flag=flag) + self.count = count + + +class QuotaEngine(object): + """Represent the set of recognized quotas.""" + + def __init__(self, db, quota_driver_class=None): + """Initialize a Quota object.""" + self.db = db + self._resources = {} + self._driver_cls = quota_driver_class + self.__driver = None + + @property + def _driver(self): + if self.__driver: + return self.__driver + if not self._driver_cls: + self._driver_cls = CONF.quota_driver + if isinstance(self._driver_cls, basestring): + self._driver_cls = importutils.import_object(self._driver_cls, + self.db) + self.__driver = self._driver_cls + return self.__driver + + def __contains__(self, resource): + return resource in self._resources + + def register_resource(self, resource): + """Register a resource.""" + + self._resources[resource.name] = resource + + def register_resources(self, resources): + """Register a list of resources.""" + + for resource in resources: + self.register_resource(resource) + + def get_by_project_and_user(self, context, project_id, user_id, resource): + """Get a specific quota by project and user.""" + + return self._driver.get_by_project_and_user(context, project_id, + user_id, resource) + + def get_by_project(self, context, project_id, resource_name): + """Get a specific quota by project.""" + + return self._driver.get_by_project(context, project_id, resource_name) + + def get_by_class(self, context, quota_class, resource_name): + """Get a specific quota by quota class.""" + + return self._driver.get_by_class(context, quota_class, resource_name) + + def get_default(self, context, resource): + """Get a specific default quota for a resource.""" + + return self._driver.get_default(context, resource) + + def get_defaults(self, context): + """Retrieve the default quotas. + + :param context: The request context, for access checks. + """ + + return self._driver.get_defaults(context, self.resources) + + def get_class_quotas(self, context, quota_class, defaults=True): + """Retrieve the quotas for the given quota class. + + :param context: The request context, for access checks. + :param quota_class: The name of the quota class to return + quotas for. + :param defaults: If True, the default value will be reported + if there is no specific value for the + resource. + """ + + return self._driver.get_class_quotas(context, self.resources, + quota_class, defaults=defaults) + + def get_user_quotas(self, context, project_id, user_id, quota_class=None, + defaults=True, usages=True): + """Retrieve the quotas for the given user and project. + + :param context: The request context, for access checks. + :param project_id: The ID of the project to return quotas for. + :param user_id: The ID of the user to return quotas for. + :param quota_class: If project_id != context.project_id, the + quota class cannot be determined. This + parameter allows it to be specified. + :param defaults: If True, the quota class value (or the + default value, if there is no value from the + quota class) will be reported if there is no + specific value for the resource. + :param usages: If True, the current in_use and reserved counts + will also be returned. + """ + + return self._driver.get_user_quotas(context, self._resources, + project_id, user_id, + quota_class=quota_class, + defaults=defaults, + usages=usages) + + def get_project_quotas(self, context, project_id, quota_class=None, + defaults=True, usages=True, remains=False): + """Retrieve the quotas for the given project. + + :param context: The request context, for access checks. + :param project_id: The ID of the project to return quotas for. + :param quota_class: If project_id != context.project_id, the + quota class cannot be determined. This + parameter allows it to be specified. + :param defaults: If True, the quota class value (or the + default value, if there is no value from the + quota class) will be reported if there is no + specific value for the resource. + :param usages: If True, the current in_use and reserved counts + will also be returned. + :param remains: If True, the current remains of the project will + will be returned. + """ + + return self._driver.get_project_quotas(context, self._resources, + project_id, + quota_class=quota_class, + defaults=defaults, + usages=usages, + remains=remains) + + def get_settable_quotas(self, context, project_id, user_id=None): + """Get settable quotas for given user and project. + + Given a list of resources, retrieve the range of settable quotas for + the given user or project. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + :param project_id: The ID of the project to return quotas for. + :param user_id: The ID of the user to return quotas for. + """ + + return self._driver.get_settable_quotas(context, self._resources, + project_id, + user_id=user_id) + + def count(self, context, resource, *args, **kwargs): + """Count a resource. + + For countable resources, invokes the count() function and + returns its result. Arguments following the context and + resource are passed directly to the count function declared by + the resource. + + :param context: The request context, for access checks. + :param resource: The name of the resource, as a string. + """ + + # Get the resource + res = self.resources.get(resource) + if not res or not hasattr(res, 'count'): + raise QuotaResourceUnknown(unknown=[resource]) + + return res.count(context, *args, **kwargs) + + def limit_check(self, context, project_id=None, user_id=None, **values): + """Check simple quota limits. + + For limits--those quotas for which there is no usage + synchronization function--this method checks that a set of + proposed values are permitted by the limit restriction. The + values to check are given as keyword arguments, where the key + identifies the specific quota limit to check, and the value is + the proposed value. + + This method will raise a QuotaResourceUnknown exception if a + given resource is unknown or if it is not a simple limit + resource. + + If any of the proposed values is over the defined quota, an + OverQuota exception will be raised with the sorted list of the + resources which are too high. Otherwise, the method returns + nothing. + + :param context: The request context, for access checks. + :param project_id: Specify the project_id if current context + is admin and admin wants to impact on + common user's tenant. + :param user_id: Specify the user_id if current context + is admin and admin wants to impact on + common user. + """ + + return self._driver.limit_check(context, self._resources, values, + project_id=project_id, user_id=user_id) + + def reserve(self, context, expire=None, project_id=None, user_id=None, + **deltas): + """Check quotas and reserve resources. + + For counting quotas--those quotas for which there is a usage + synchronization function--this method checks quotas against + current usage and the desired deltas. The deltas are given as + keyword arguments, and current usage and other reservations + are factored into the quota check. + + This method will raise a QuotaResourceUnknown exception if a + given resource is unknown or if it does not have a usage + synchronization function. + + If any of the proposed values is over the defined quota, an + OverQuota exception will be raised with the sorted list of the + resources which are too high. Otherwise, the method returns a + list of reservation UUIDs which were created. + + :param context: The request context, for access checks. + :param expire: An optional parameter specifying an expiration + time for the reservations. If it is a simple + number, it is interpreted as a number of + seconds and added to the current time; if it is + a datetime.timedelta object, it will also be + added to the current time. A datetime.datetime + object will be interpreted as the absolute + expiration time. If None is specified, the + default expiration time set by + --default-reservation-expire will be used (this + value will be treated as a number of seconds). + :param project_id: Specify the project_id if current context + is admin and admin wants to impact on + common user's tenant. + """ + + reservations = self._driver.reserve(context, self._resources, deltas, + expire=expire, + project_id=project_id, + user_id=user_id) + + LOG.debug(_("Created reservations %s"), reservations) + + return reservations + + def commit(self, context, reservations, project_id=None, user_id=None): + """Commit reservations. + + :param context: The request context, for access checks. + :param reservations: A list of the reservation UUIDs, as + returned by the reserve() method. + :param project_id: Specify the project_id if current context + is admin and admin wants to impact on + common user's tenant. + """ + + try: + self._driver.commit(context, reservations, project_id=project_id, + user_id=user_id) + except Exception: + # NOTE(Vek): Ignoring exceptions here is safe, because the + # usage resynchronization and the reservation expiration + # mechanisms will resolve the issue. The exception is + # logged, however, because this is less than optimal. + LOG.exception(_("Failed to commit reservations %s"), reservations) + return + LOG.debug(_("Committed reservations %s"), reservations) + + def rollback(self, context, reservations, project_id=None, user_id=None): + """Roll back reservations. + + :param context: The request context, for access checks. + :param reservations: A list of the reservation UUIDs, as + returned by the reserve() method. + :param project_id: Specify the project_id if current context + is admin and admin wants to impact on + common user's tenant. + """ + + try: + self._driver.rollback(context, reservations, project_id=project_id, + user_id=user_id) + except Exception: + # NOTE(Vek): Ignoring exceptions here is safe, because the + # usage resynchronization and the reservation expiration + # mechanisms will resolve the issue. The exception is + # logged, however, because this is less than optimal. + LOG.exception(_("Failed to roll back reservations %s"), + reservations) + return + LOG.debug(_("Rolled back reservations %s"), reservations) + + def usage_reset(self, context, resources): + """Reset the usage records. + + Reset usages for a particular user on a list of resources. + This will force that user's usage records to be refreshed + the next time a reservation is made. + + Note: this does not affect the currently outstanding + reservations the user has; those reservations must be + committed or rolled back (or expired). + + :param context: The request context, for access checks. + :param resources: A list of the resource names for which the + usage must be reset. + """ + + self._driver.usage_reset(context, resources) + + def destroy_all_by_project_and_user(self, context, project_id, user_id): + """Destroy all objects for given user and project. + + Destroy all quotas, usages, and reservations associated with a + project and user. + + :param context: The request context, for access checks. + :param project_id: The ID of the project being deleted. + :param user_id: The ID of the user being deleted. + """ + + self._driver.destroy_all_by_project_and_user(context, + project_id, user_id) + + def destroy_all_by_project(self, context, project_id): + """Destroy all quotas, usages, and reservations for given project. + + :param context: The request context, for access checks. + :param project_id: The ID of the project being deleted. + """ + + self._driver.destroy_all_by_project(context, project_id) + + def expire(self, context): + """Expire reservations. + + Explores all currently existing reservations and rolls back + any that have expired. + + :param context: The request context, for access checks. + """ + + self._driver.expire(context) + + @property + def resource_names(self): + return sorted(self.resources.keys()) + + @property + def resources(self): + return self._resources |
