Add the concept of resource reservation in neutron.
Usage tracking logic is also updated to support reservations.
Reservations are not however available with the now deprecated
configuration-based quota driver.
The base API controller will now use reservations to perform
quota checks rather than counting resource usage and then
invoking the limit_check routine.
The limit_check routine however has not been removed and
depreacated as a part of this patch. In order to ensure all
quota drivers expose a consistent interface, a
make_reservation method has been added to the configuration
based driver as well. This method simply performs "old-style"
limit checks by counting resource usage and then invoking
limit_check.
DocImpact
Implements blueprint better-quotas.
Change-Id: Ifea07f461def564884af5b291c8a56655a4d818b
if self._collection in body:
# Have to account for bulk create
items = body[self._collection]
- deltas = {}
- bulk = True
else:
items = [body]
- bulk = False
# Ensure policy engine is initialized
policy.init()
+ # Store requested resource amounts grouping them by tenant
+ # This won't work with multiple resources. However because of the
+ # current structure of this controller there will hardly be more than
+ # one resource for which reservations are being made
+ request_deltas = {}
for item in items:
self._validate_network_tenant_ownership(request,
item[self._resource])
if 'tenant_id' not in item[self._resource]:
# no tenant_id - no quota check
continue
- try:
- tenant_id = item[self._resource]['tenant_id']
- count = quota.QUOTAS.count(request.context, self._resource,
- self._plugin, tenant_id)
- if bulk:
- delta = deltas.get(tenant_id, 0) + 1
- deltas[tenant_id] = delta
- else:
- delta = 1
- kwargs = {self._resource: count + delta}
- except exceptions.QuotaResourceUnknown as e:
+ tenant_id = item[self._resource]['tenant_id']
+ delta = request_deltas.get(tenant_id, 0)
+ delta = delta + 1
+ request_deltas[tenant_id] = delta
+ # Quota enforcement
+ reservations = []
+ try:
+ for tenant in request_deltas:
+ reservation = quota.QUOTAS.make_reservation(
+ request.context,
+ tenant,
+ {self._resource:
+ request_deltas[tenant]},
+ self._plugin)
+ reservations.append(reservation)
+ except exceptions.QuotaResourceUnknown as e:
# We don't want to quota this resource
LOG.debug(e)
- else:
- quota.QUOTAS.limit_check(request.context,
- item[self._resource]['tenant_id'],
- **kwargs)
def notify(create_result):
# Ensure usage trackers for all resources affected by this API
# operation are marked as dirty
- # TODO(salv-orlando): This operation will happen in a single
- # transaction with reservation commit once that is implemented
- resource_registry.set_resources_dirty(request.context)
+ with request.context.session.begin():
+ # Commit the reservation(s)
+ for reservation in reservations:
+ quota.QUOTAS.commit_reservation(
+ request.context, reservation.reservation_id)
+ resource_registry.set_resources_dirty(request.context)
notifier_method = self._resource + '.create.end'
self._notifier.info(request.context,
notifier_method)
return create_result
- kwargs = {self._parent_id_name: parent_id} if parent_id else {}
+ def do_create(body, bulk=False, emulated=False):
+ kwargs = {self._parent_id_name: parent_id} if parent_id else {}
+ if bulk and not emulated:
+ obj_creator = getattr(self._plugin, "%s_bulk" % action)
+ else:
+ obj_creator = getattr(self._plugin, action)
+ try:
+ if emulated:
+ return self._emulate_bulk_create(obj_creator, request,
+ body, parent_id)
+ else:
+ if self._collection in body:
+ # This is weird but fixing it requires changes to the
+ # plugin interface
+ kwargs.update({self._collection: body})
+ else:
+ kwargs.update({self._resource: body})
+ return obj_creator(request.context, **kwargs)
+ except Exception:
+ # In case of failure the plugin will always raise an
+ # exception. Cancel the reservation
+ with excutils.save_and_reraise_exception():
+ for reservation in reservations:
+ quota.QUOTAS.cancel_reservation(
+ request.context, reservation.reservation_id)
+
if self._collection in body and self._native_bulk:
# plugin does atomic bulk create operations
- obj_creator = getattr(self._plugin, "%s_bulk" % action)
- objs = obj_creator(request.context, body, **kwargs)
+ objs = do_create(body, bulk=True)
# Use first element of list to discriminate attributes which
# should be removed because of authZ policies
fields_to_strip = self._exclude_attributes_by_policy(
request.context, obj, fields_to_strip=fields_to_strip)
for obj in objs]})
else:
- obj_creator = getattr(self._plugin, action)
if self._collection in body:
# Emulate atomic bulk behavior
- objs = self._emulate_bulk_create(obj_creator, request,
- body, parent_id)
+ objs = do_create(body, bulk=True, emulated=True)
return notify({self._collection: objs})
else:
- kwargs.update({self._resource: body})
- obj = obj_creator(request.context, **kwargs)
+ obj = do_create(body)
self._send_nova_notification(action, {},
{self._resource: obj})
return notify({self._resource: self._view(request.context,
2a16083502f3
-48153cb5f051
+9859ac9c136
kilo
--- /dev/null
+# Copyright 2015 OpenStack Foundation
+#
+# 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.
+#
+
+"""quota_reservations
+
+Revision ID: 9859ac9c136
+Revises: 48153cb5f051
+Create Date: 2015-03-11 06:40:56.775075
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '9859ac9c136'
+down_revision = '48153cb5f051'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.create_table(
+ 'reservations',
+ sa.Column('id', sa.String(length=36), nullable=False),
+ sa.Column('tenant_id', sa.String(length=255), nullable=True),
+ sa.Column('expiration', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('id'))
+
+ op.create_table(
+ 'resourcedeltas',
+ sa.Column('resource', sa.String(length=255), nullable=False),
+ sa.Column('reservation_id', sa.String(length=36), nullable=False),
+ sa.Column('amount', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['reservation_id'], ['reservations.id'],
+ ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('resource', 'reservation_id'))
# under the License.
import collections
+import datetime
+
+import sqlalchemy as sa
+from sqlalchemy.orm import exc as orm_exc
+from sqlalchemy import sql
from neutron.db import common_db_mixin as common_db_api
from neutron.db.quota import models as quota_models
+# Wrapper for utcnow - needed for mocking it in unit tests
+def utcnow():
+ return datetime.datetime.utcnow()
+
+
class QuotaUsageInfo(collections.namedtuple(
'QuotaUsageInfo', ['resource', 'tenant_id', 'used', 'reserved', 'dirty'])):
return self.reserved + self.used
+class ReservationInfo(object):
+ """Information about a resource reservation."""
+
+ def __init__(self, reservation_id, tenant_id, expiration, deltas):
+ self._reservation_id = reservation_id
+ self._tenant_id = tenant_id
+ self._expiration = expiration
+ self._deltas = deltas
+
+ @property
+ def reservation_id(self):
+ return self._reservation_id
+
+ @property
+ def tenant_id(self):
+ return self._tenant_id
+
+ @property
+ def expiration(self):
+ return self._expiration
+
+ @property
+ def deltas(self):
+ return self._deltas
+
+
def get_quota_usage_by_resource_and_tenant(context, resource, tenant_id,
lock_for_update=False):
"""Return usage info for a given resource and tenant.
query = common_db_api.model_query(context, quota_models.QuotaUsage)
query = query.filter_by(resource=resource)
return query.update({'dirty': dirty})
+
+
+def create_reservation(context, tenant_id, deltas, expiration=None):
+ # This method is usually called from within another transaction.
+ # Consider using begin_nested
+ with context.session.begin(subtransactions=True):
+ expiration = expiration or (utcnow() + datetime.timedelta(0, 120))
+ resv = quota_models.Reservation(tenant_id=tenant_id,
+ expiration=expiration)
+ context.session.add(resv)
+ for (resource, delta) in deltas.items():
+ context.session.add(
+ quota_models.ResourceDelta(resource=resource,
+ amount=delta,
+ reservation=resv))
+ # quota_usage for all resources involved in this reservation must
+ # be marked as dirty
+ set_resources_quota_usage_dirty(
+ context, deltas.keys(), tenant_id)
+ return ReservationInfo(resv['id'],
+ resv['tenant_id'],
+ resv['expiration'],
+ dict((delta.resource, delta.amount)
+ for delta in resv.resource_deltas))
+
+
+def get_reservation(context, reservation_id):
+ query = context.session.query(quota_models.Reservation).filter_by(
+ id=reservation_id)
+ resv = query.first()
+ if not resv:
+ return
+ return ReservationInfo(resv['id'],
+ resv['tenant_id'],
+ resv['expiration'],
+ dict((delta.resource, delta.amount)
+ for delta in resv.resource_deltas))
+
+
+def remove_reservation(context, reservation_id, set_dirty=False):
+ delete_query = context.session.query(quota_models.Reservation).filter_by(
+ id=reservation_id)
+ # Not handling MultipleResultsFound as the query is filtering by primary
+ # key
+ try:
+ reservation = delete_query.one()
+ except orm_exc.NoResultFound:
+ # TODO(salv-orlando): Raise here and then handle the exception?
+ return
+ tenant_id = reservation.tenant_id
+ resources = [delta.resource for delta in reservation.resource_deltas]
+ num_deleted = delete_query.delete()
+ if set_dirty:
+ # quota_usage for all resource involved in this reservation must
+ # be marked as dirty
+ set_resources_quota_usage_dirty(context, resources, tenant_id)
+ return num_deleted
+
+
+def get_reservations_for_resources(context, tenant_id, resources,
+ expired=False):
+ """Retrieve total amount of reservations for specified resources.
+
+ :param context: Neutron context with db session
+ :param tenant_id: Tenant identifier
+ :param resources: Resources for which reserved amounts should be fetched
+ :param expired: False to fetch active reservations, True to fetch expired
+ reservations (defaults to False)
+ :returns: a dictionary mapping resources with corresponding deltas
+ """
+ if not resources:
+ # Do not waste time
+ return
+ now = utcnow()
+ resv_query = context.session.query(
+ quota_models.ResourceDelta.resource,
+ quota_models.Reservation.expiration,
+ sql.func.sum(quota_models.ResourceDelta.amount)).join(
+ quota_models.Reservation)
+ if expired:
+ exp_expr = (quota_models.Reservation.expiration < now)
+ else:
+ exp_expr = (quota_models.Reservation.expiration >= now)
+ resv_query = resv_query.filter(sa.and_(
+ quota_models.Reservation.tenant_id == tenant_id,
+ quota_models.ResourceDelta.resource.in_(resources),
+ exp_expr)).group_by(
+ quota_models.ResourceDelta.resource)
+ return dict((resource, total_reserved)
+ for (resource, exp, total_reserved) in resv_query)
+
+
+def remove_expired_reservations(context, tenant_id=None):
+ now = utcnow()
+ resv_query = context.session.query(quota_models.Reservation)
+ if tenant_id:
+ tenant_expr = (quota_models.Reservation.tenant_id == tenant_id)
+ else:
+ tenant_expr = sql.true()
+ resv_query = resv_query.filter(sa.and_(
+ tenant_expr, quota_models.Reservation.expiration < now))
+ return resv_query.delete()
# License for the specific language governing permissions and limitations
# under the License.
+from oslo_db import api as oslo_db_api
+from oslo_log import log
+
from neutron.common import exceptions
+from neutron.db import api as db_api
+from neutron.db.quota import api as quota_api
from neutron.db.quota import models as quota_models
+LOG = log.getLogger(__name__)
+
class DbQuotaDriver(object):
"""Driver to perform necessary checks to enforce quotas and obtain quota
# update with tenant specific limits
q_qry = context.session.query(quota_models.Quota).filter_by(
tenant_id=tenant_id)
- tenant_quota.update((q['resource'], q['limit']) for q in q_qry)
+ for item in q_qry:
+ tenant_quota[item['resource']] = item['limit']
return tenant_quota
return dict((k, v) for k, v in quotas.items())
+ def _handle_expired_reservations(self, context, tenant_id,
+ resource, expired_amount):
+ LOG.debug(("Adjusting usage for resource %(resource)s: "
+ "removing %(expired)d reserved items"),
+ {'resource': resource,
+ 'expired': expired_amount})
+ # TODO(salv-orlando): It should be possible to do this
+ # operation for all resources with a single query.
+ # Update reservation usage
+ quota_api.set_quota_usage(
+ context,
+ resource,
+ tenant_id,
+ reserved=-expired_amount,
+ delta=True)
+ # Delete expired reservations (we don't want them to accrue
+ # in the database)
+ quota_api.remove_expired_reservations(
+ context, tenant_id=tenant_id)
+
+ @oslo_db_api.wrap_db_retry(max_retries=db_api.MAX_RETRIES,
+ retry_on_request=True,
+ retry_on_deadlock=True)
+ def make_reservation(self, context, tenant_id, resources, deltas, plugin):
+ # Lock current reservation table
+ # NOTE(salv-orlando): This routine uses DB write locks.
+ # These locks are acquired by the count() method invoked on resources.
+ # Please put your shotguns aside.
+ # A non locking algorithm for handling reservation is feasible, however
+ # it will require two database writes even in cases when there are not
+ # concurrent reservations.
+ # For this reason it might be advisable to handle contention using
+ # this kind of locks and paying the cost of a write set certification
+ # failure when a mysql galera cluster is employed. Also, this class of
+ # locks should be ok to use when support for sending "hotspot" writes
+ # to a single node will be avaialable.
+ requested_resources = deltas.keys()
+ with context.session.begin():
+ # Gather current usage information
+ # TODO(salv-orlando): calling count() for every resource triggers
+ # multiple queries on quota usage. This should be improved, however
+ # this is not an urgent matter as the REST API currently only
+ # allows allocation of a resource at a time
+ # NOTE: pass plugin too for compatibility with CountableResource
+ # instances
+ current_usages = dict(
+ (resource, resources[resource].count(
+ context, plugin, tenant_id)) for
+ resource in requested_resources)
+ # get_tenant_quotes needs in inout a dictionary mapping resource
+ # name to BaseResosurce instances so that the default quota can be
+ # retrieved
+ current_limits = self.get_tenant_quotas(
+ context, resources, tenant_id)
+ # Adjust for expired reservations. Apparently it is cheaper than
+ # querying everytime for active reservations and counting overall
+ # quantity of resources reserved
+ expired_deltas = quota_api.get_reservations_for_resources(
+ context, tenant_id, requested_resources, expired=True)
+ # Verify that the request can be accepted with current limits
+ resources_over_limit = []
+ for resource in requested_resources:
+ expired_reservations = expired_deltas.get(resource, 0)
+ total_usage = current_usages[resource] - expired_reservations
+ # A negative quota limit means infinite
+ if current_limits[resource] < 0:
+ LOG.debug(("Resource %(resource)s has unlimited quota "
+ "limit. It is possible to allocate %(delta)s "
+ "items."), {'resource': resource,
+ 'delta': deltas[resource]})
+ continue
+ res_headroom = current_limits[resource] - total_usage
+ LOG.debug(("Attempting to reserve %(delta)d items for "
+ "resource %(resource)s. Total usage: %(total)d; "
+ "quota limit: %(limit)d; headroom:%(headroom)d"),
+ {'resource': resource,
+ 'delta': deltas[resource],
+ 'total': total_usage,
+ 'limit': current_limits[resource],
+ 'headroom': res_headroom})
+ if res_headroom < deltas[resource]:
+ resources_over_limit.append(resource)
+ if expired_reservations:
+ self._handle_expired_reservations(
+ context, tenant_id, resource, expired_reservations)
+
+ if resources_over_limit:
+ raise exceptions.OverQuota(overs=sorted(resources_over_limit))
+ # Success, store the reservation
+ # TODO(salv-orlando): Make expiration time configurable
+ return quota_api.create_reservation(
+ context, tenant_id, deltas)
+
+ def commit_reservation(self, context, reservation_id):
+ # Do not mark resource usage as dirty. If a reservation is committed,
+ # then the releveant resources have been created. Usage data for these
+ # resources has therefore already been marked dirty.
+ quota_api.remove_reservation(context, reservation_id,
+ set_dirty=False)
+
+ def cancel_reservation(self, context, reservation_id):
+ # Mark resource usage as dirty so the next time both actual resources
+ # used and reserved will be recalculated
+ quota_api.remove_reservation(context, reservation_id,
+ set_dirty=True)
+
def limit_check(self, context, tenant_id, resources, values):
"""Check simple quota limits.
# under the License.
import sqlalchemy as sa
+from sqlalchemy import orm
from sqlalchemy import sql
from neutron.db import model_base
from neutron.db import models_v2
+class ResourceDelta(model_base.BASEV2):
+ resource = sa.Column(sa.String(255), primary_key=True)
+ reservation_id = sa.Column(sa.String(36),
+ sa.ForeignKey('reservations.id',
+ ondelete='CASCADE'),
+ primary_key=True,
+ nullable=False)
+ # Requested amount of resource
+ amount = sa.Column(sa.Integer)
+
+
+class Reservation(model_base.BASEV2, models_v2.HasId):
+ tenant_id = sa.Column(sa.String(255))
+ expiration = sa.Column(sa.DateTime())
+ resource_deltas = orm.relationship(ResourceDelta,
+ backref='reservation',
+ lazy="joined",
+ cascade='all, delete-orphan')
+
+
class Quota(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant):
"""Represent a single quota override for a tenant.
import webob
from neutron.common import exceptions
+from neutron.db.quota import api as quota_api
from neutron.i18n import _LI, _LW
from neutron.quota import resource_registry
msg = _('Access to this resource was denied.')
raise webob.exc.HTTPForbidden(msg)
+ def make_reservation(self, context, tenant_id, resources, deltas, plugin):
+ """This driver does not support reservations.
+
+ This routine is provided for backward compatibility purposes with
+ the API controllers which have now been adapted to make reservations
+ rather than counting resources and checking limits - as this
+ routine ultimately does.
+ """
+ for resource in deltas.keys():
+ count = QUOTAS.count(context, resource, plugin, tenant_id)
+ total_use = deltas.get(resource, 0) + count
+ deltas[resource] = total_use
+
+ self.limit_check(
+ context,
+ tenant_id,
+ resource_registry.get_all_resources(),
+ deltas)
+ # return a fake reservation - the REST controller expects it
+ return quota_api.ReservationInfo('fake', None, None, None)
+
+ def commit_reservation(self, context, reservation_id):
+ """Tnis is a noop as this driver does not support reservations."""
+
+ def cancel_reservation(self, context, reservation_id):
+ """Tnis is a noop as this driver does not support reservations."""
+
class QuotaEngine(object):
"""Represent the set of recognized quotas."""
return res.count(context, *args, **kwargs)
+ def make_reservation(self, context, tenant_id, deltas, plugin):
+ # Verify that resources are managed by the quota engine
+ # Ensure no value is less than zero
+ unders = [key for key, val in deltas.items() if val < 0]
+ if unders:
+ raise exceptions.InvalidQuotaValue(unders=sorted(unders))
+
+ requested_resources = set(deltas.keys())
+ all_resources = resource_registry.get_all_resources()
+ managed_resources = set([res for res in all_resources.keys()
+ if res in requested_resources])
+ # Make sure we accounted for all of them...
+ unknown_resources = requested_resources - managed_resources
+
+ if unknown_resources:
+ raise exceptions.QuotaResourceUnknown(
+ unknown=sorted(unknown_resources))
+ # FIXME(salv-orlando): There should be no reason for sending all the
+ # resource in the registry to the quota driver, but as other driver
+ # APIs request them, this will be sorted out with a different patch.
+ return self.get_driver().make_reservation(
+ context,
+ tenant_id,
+ all_resources,
+ deltas,
+ plugin)
+
+ def commit_reservation(self, context, reservation_id):
+ self.get_driver().commit_reservation(context, reservation_id)
+
+ def cancel_reservation(self, context, reservation_id):
+ self.get_driver().cancel_reservation(context, reservation_id)
+
def limit_check(self, context, tenant_id, **values):
"""Check simple quota limits.
:param tenant_id: Tenant for which the quota limit is being checked
:param values: Dict specifying requested deltas for each resource
"""
+ # TODO(salv-orlando): Deprecate calls to this API
# Verify that resources are managed by the quota engine
requested_resources = set(values.keys())
managed_resources = set([res for res in
max_retries=db_api.MAX_RETRIES,
exception_checker=lambda exc:
isinstance(exc, oslo_db_exception.DBDuplicateEntry))
- def _set_quota_usage(self, context, tenant_id, in_use):
- return quota_api.set_quota_usage(context, self.name,
- tenant_id, in_use=in_use)
+ def _set_quota_usage(self, context, tenant_id, in_use, reserved):
+ return quota_api.set_quota_usage(context, self.name, tenant_id,
+ in_use=in_use, reserved=reserved)
- def _resync(self, context, tenant_id, in_use):
+ def _resync(self, context, tenant_id, in_use, reserved):
# Update quota usage
usage_info = self._set_quota_usage(
- context, tenant_id, in_use=in_use)
+ context, tenant_id, in_use, reserved)
+
self._dirty_tenants.discard(tenant_id)
self._out_of_sync_tenants.discard(tenant_id)
LOG.debug(("Unset dirty status for tenant:%(tenant_id)s on "
{'tenant_id': tenant_id, 'resource': self.name})
in_use = context.session.query(self._model_class).filter_by(
tenant_id=tenant_id).count()
+ reservations = quota_api.get_reservations_for_resources(
+ context, tenant_id, [self.name])
+ reserved = reservations.get(self.name, 0)
# Update quota usage
- return self._resync(context, tenant_id, in_use)
+ return self._resync(context, tenant_id, in_use, reserved)
def count(self, context, _plugin, tenant_id, resync_usage=False):
- """Return the current usage count for the resource."""
- # Load current usage data
+ """Return the current usage count for the resource.
+
+ This method will fetch the information from resource usage data,
+ unless usage data are marked as "dirty", in which case both used and
+ reserved resource are explicitly counted.
+
+ The _plugin and _resource parameters are unused but kept for
+ compatibility with the signature of the count method for
+ CountableResource instances.
+ """
+ # Load current usage data, setting a row-level lock on the DB
usage_info = quota_api.get_quota_usage_by_resource_and_tenant(
- context, self.name, tenant_id)
+ context, self.name, tenant_id, lock_for_update=True)
# If dirty or missing, calculate actual resource usage querying
# the database and set/create usage info data
# NOTE: this routine "trusts" usage counters at service startup. This
# assumption is generally valid, but if the database is tampered with,
# or if data migrations do not take care of usage counters, the
# assumption will not hold anymore
- if (tenant_id in self._dirty_tenants or not usage_info
- or usage_info.dirty):
+ if (tenant_id in self._dirty_tenants or
+ not usage_info or usage_info.dirty):
LOG.debug(("Usage tracker for resource:%(resource)s and tenant:"
"%(tenant_id)s is out of sync, need to count used "
"quota"), {'resource': self.name,
'tenant_id': tenant_id})
in_use = context.session.query(self._model_class).filter_by(
tenant_id=tenant_id).count()
+ reservations = quota_api.get_reservations_for_resources(
+ context, tenant_id, [self.name])
+ reserved = reservations.get(self.name, 0)
+
# Update quota usage, if requested (by default do not do that, as
# typically one counts before adding a record, and that would mark
# the usage counter as dirty again)
if resync_usage or not usage_info:
- usage_info = self._resync(context, tenant_id, in_use)
+ usage_info = self._resync(context, tenant_id,
+ in_use, reserved)
else:
usage_info = quota_api.QuotaUsageInfo(usage_info.resource,
usage_info.tenant_id,
in_use,
- usage_info.reserved,
+ reserved,
usage_info.dirty)
+ LOG.debug(("Quota usage for %(resource)s was recalculated. "
+ "Used quota:%(used)d; Reserved quota:%(reserved)d"),
+ {'resource': self.name,
+ 'used': usage_info.used,
+ 'reserved': usage_info.reserved})
return usage_info.total
def register_events(self):
return
for res in get_all_resources().values():
- with context.session.begin():
+ with context.session.begin(subtransactions=True):
if is_tracked(res.name) and res.dirty:
res.mark_dirty(context, nested=True)
# License for the specific language governing permissions and limitations
# under the License.
+import datetime
+
+import mock
+
from neutron import context
from neutron.db.quota import api as quota_api
from neutron.tests.unit import testlib_api
self.context = context.Context('Gonzalo', self.tenant_id,
is_admin=False, is_advsvc=False)
+ def _create_reservation(self, resource_deltas,
+ tenant_id=None, expiration=None):
+ tenant_id = tenant_id or self.tenant_id
+ return quota_api.create_reservation(
+ self.context, tenant_id, resource_deltas, expiration)
+
def _create_quota_usage(self, resource, used, reserved, tenant_id=None):
tenant_id = tenant_id or self.tenant_id
return quota_api.set_quota_usage(
self.assertIsNone(quota_api.get_quota_usage_by_resource_and_tenant(
self.context, 'goals', self.tenant_id))
+ def _verify_reserved_resources(self, expected, actual):
+ for (resource, delta) in actual.items():
+ self.assertIn(resource, expected)
+ self.assertEqual(delta, expected[resource])
+ del expected[resource]
+ self.assertFalse(expected)
+
+ def test_create_reservation(self):
+ resources = {'goals': 2, 'assists': 1}
+ resv = self._create_reservation(resources)
+ self.assertEqual(self.tenant_id, resv.tenant_id)
+ self._verify_reserved_resources(resources, resv.deltas)
+
+ def test_create_reservation_with_expirtion(self):
+ resources = {'goals': 2, 'assists': 1}
+ exp_date = datetime.datetime(2016, 3, 31, 14, 30)
+ resv = self._create_reservation(resources, expiration=exp_date)
+ self.assertEqual(self.tenant_id, resv.tenant_id)
+ self.assertEqual(exp_date, resv.expiration)
+ self._verify_reserved_resources(resources, resv.deltas)
+
+ def _test_remove_reservation(self, set_dirty):
+ resources = {'goals': 2, 'assists': 1}
+ resv = self._create_reservation(resources)
+ self.assertEqual(1, quota_api.remove_reservation(
+ self.context, resv.reservation_id, set_dirty=set_dirty))
+
+ def test_remove_reservation(self):
+ self._test_remove_reservation(False)
+
+ def test_remove_reservation_and_set_dirty(self):
+ routine = 'neutron.db.quota.api.set_resources_quota_usage_dirty'
+ with mock.patch(routine) as mock_routine:
+ self._test_remove_reservation(False)
+ mock_routine.assert_called_once_with(
+ self.context, mock.ANY, self.tenant_id)
+
+ def test_remove_non_existent_reservation(self):
+ self.assertIsNone(quota_api.remove_reservation(self.context, 'meh'))
+
+ def _get_reservations_for_resource_helper(self):
+ # create three reservation, 1 expired
+ resources_1 = {'goals': 2, 'assists': 1}
+ resources_2 = {'goals': 3, 'bookings': 1}
+ resources_3 = {'bookings': 2, 'assists': 2}
+ exp_date_1 = datetime.datetime(2016, 3, 31, 14, 30)
+ exp_date_2 = datetime.datetime(2015, 3, 31, 14, 30)
+ self._create_reservation(resources_1, expiration=exp_date_1)
+ self._create_reservation(resources_2, expiration=exp_date_1)
+ self._create_reservation(resources_3, expiration=exp_date_2)
+
+ def test_get_reservations_for_resources(self):
+ with mock.patch('neutron.db.quota.api.utcnow') as mock_utcnow:
+ self._get_reservations_for_resource_helper()
+ mock_utcnow.return_value = datetime.datetime(
+ 2015, 5, 20, 0, 0)
+ deltas = quota_api.get_reservations_for_resources(
+ self.context, self.tenant_id, ['goals', 'assists', 'bookings'])
+ self.assertIn('goals', deltas)
+ self.assertEqual(5, deltas['goals'])
+ self.assertIn('assists', deltas)
+ self.assertEqual(1, deltas['assists'])
+ self.assertIn('bookings', deltas)
+ self.assertEqual(1, deltas['bookings'])
+ self.assertEqual(3, len(deltas))
+
+ def test_get_expired_reservations_for_resources(self):
+ with mock.patch('neutron.db.quota.api.utcnow') as mock_utcnow:
+ mock_utcnow.return_value = datetime.datetime(
+ 2015, 5, 20, 0, 0)
+ self._get_reservations_for_resource_helper()
+ deltas = quota_api.get_reservations_for_resources(
+ self.context, self.tenant_id,
+ ['goals', 'assists', 'bookings'],
+ expired=True)
+ self.assertIn('assists', deltas)
+ self.assertEqual(2, deltas['assists'])
+ self.assertIn('bookings', deltas)
+ self.assertEqual(2, deltas['bookings'])
+ self.assertEqual(2, len(deltas))
+
+ def test_get_reservation_for_resources_with_empty_list(self):
+ self.assertIsNone(quota_api.get_reservations_for_resources(
+ self.context, self.tenant_id, []))
+
+ def test_remove_expired_reservations(self):
+ with mock.patch('neutron.db.quota.api.utcnow') as mock_utcnow:
+ mock_utcnow.return_value = datetime.datetime(
+ 2015, 5, 20, 0, 0)
+ resources = {'goals': 2, 'assists': 1}
+ exp_date_1 = datetime.datetime(2016, 3, 31, 14, 30)
+ resv_1 = self._create_reservation(resources, expiration=exp_date_1)
+ exp_date_2 = datetime.datetime(2015, 3, 31, 14, 30)
+ resv_2 = self._create_reservation(resources, expiration=exp_date_2)
+ self.assertEqual(1, quota_api.remove_expired_reservations(
+ self.context, self.tenant_id))
+ self.assertIsNone(quota_api.get_reservation(
+ self.context, resv_2.reservation_id))
+ self.assertIsNotNone(quota_api.get_reservation(
+ self.context, resv_1.reservation_id))
+
+ def test_remove_expired_reservations_no_tenant(self):
+ with mock.patch('neutron.db.quota.api.utcnow') as mock_utcnow:
+ mock_utcnow.return_value = datetime.datetime(
+ 2015, 5, 20, 0, 0)
+ resources = {'goals': 2, 'assists': 1}
+ exp_date_1 = datetime.datetime(2014, 3, 31, 14, 30)
+ resv_1 = self._create_reservation(resources, expiration=exp_date_1)
+ exp_date_2 = datetime.datetime(2015, 3, 31, 14, 30)
+ resv_2 = self._create_reservation(resources,
+ expiration=exp_date_2,
+ tenant_id='Callejon')
+ self.assertEqual(2, quota_api.remove_expired_reservations(
+ self.context))
+ self.assertIsNone(quota_api.get_reservation(
+ self.context, resv_2.reservation_id))
+ self.assertIsNone(quota_api.get_reservation(
+ self.context, resv_1.reservation_id))
+
class TestQuotaDbApiAdminContext(TestQuotaDbApi):
class TestResource(object):
"""Describe a test resource for quota checking."""
- def __init__(self, name, default):
+ def __init__(self, name, default, fake_count=0):
self.name = name
self.quota = default
+ self.fake_count = fake_count
@property
def default(self):
return self.quota
+ def count(self, *args, **kwargs):
+ return self.fake_count
+
+
PROJECT = 'prj_test'
RESOURCE = 'res_test'
+ALT_RESOURCE = 'res_test_meh'
class TestDbQuotaDriver(testlib_api.SqlTestCase):
self.assertRaises(exceptions.InvalidQuotaValue,
self.plugin.limit_check, context.get_admin_context(),
PROJECT, resources, values)
+
+ def _test_make_reservation_success(self, quota_driver,
+ resource_name, deltas):
+ resources = {resource_name: TestResource(resource_name, 2)}
+ self.plugin.update_quota_limit(self.context, PROJECT, resource_name, 2)
+ reservation = quota_driver.make_reservation(
+ self.context,
+ self.context.tenant_id,
+ resources,
+ deltas,
+ self.plugin)
+ self.assertIn(resource_name, reservation.deltas)
+ self.assertEqual(deltas[resource_name],
+ reservation.deltas[resource_name])
+ self.assertEqual(self.context.tenant_id,
+ reservation.tenant_id)
+
+ def test_make_reservation_single_resource(self):
+ quota_driver = driver.DbQuotaDriver()
+ self._test_make_reservation_success(
+ quota_driver, RESOURCE, {RESOURCE: 1})
+
+ def test_make_reservation_fill_quota(self):
+ quota_driver = driver.DbQuotaDriver()
+ self._test_make_reservation_success(
+ quota_driver, RESOURCE, {RESOURCE: 2})
+
+ def test_make_reservation_multiple_resources(self):
+ quota_driver = driver.DbQuotaDriver()
+ resources = {RESOURCE: TestResource(RESOURCE, 2),
+ ALT_RESOURCE: TestResource(ALT_RESOURCE, 2)}
+ deltas = {RESOURCE: 1, ALT_RESOURCE: 2}
+ self.plugin.update_quota_limit(self.context, PROJECT, RESOURCE, 2)
+ self.plugin.update_quota_limit(self.context, PROJECT, ALT_RESOURCE, 2)
+ reservation = quota_driver.make_reservation(
+ self.context,
+ self.context.tenant_id,
+ resources,
+ deltas,
+ self.plugin)
+ self.assertIn(RESOURCE, reservation.deltas)
+ self.assertIn(ALT_RESOURCE, reservation.deltas)
+ self.assertEqual(1, reservation.deltas[RESOURCE])
+ self.assertEqual(2, reservation.deltas[ALT_RESOURCE])
+ self.assertEqual(self.context.tenant_id,
+ reservation.tenant_id)
+
+ def test_make_reservation_over_quota_fails(self):
+ quota_driver = driver.DbQuotaDriver()
+ resources = {RESOURCE: TestResource(RESOURCE, 2,
+ fake_count=2)}
+ deltas = {RESOURCE: 1}
+ self.plugin.update_quota_limit(self.context, PROJECT, RESOURCE, 2)
+ self.assertRaises(exceptions.OverQuota,
+ quota_driver.make_reservation,
+ self.context,
+ self.context.tenant_id,
+ resources,
+ deltas,
+ self.plugin)
extra_environ=env, expect_errors=True)
self.assertEqual(400, res.status_int)
+ def test_make_reservation_resource_unknown_raises(self):
+ tenant_id = 'tenant_id1'
+ self.assertRaises(exceptions.QuotaResourceUnknown,
+ quota.QUOTAS.make_reservation,
+ context.get_admin_context(load_admin_roles=False),
+ tenant_id,
+ {'foobar': 1},
+ plugin=None)
+
+ def test_make_reservation_negative_delta_raises(self):
+ tenant_id = 'tenant_id1'
+ self.assertRaises(exceptions.InvalidQuotaValue,
+ quota.QUOTAS.make_reservation,
+ context.get_admin_context(load_admin_roles=False),
+ tenant_id,
+ {'network': -1},
+ plugin=None)
+
class QuotaExtensionCfgTestCase(QuotaExtensionTestCase):
fmt = 'json'
res.count(self.context, None, self.tenant_id,
resync_usage=True)
mock_set_quota_usage.assert_called_once_with(
- self.context, self.resource, self.tenant_id, in_use=2)
+ self.context, self.resource, self.tenant_id,
+ reserved=0, in_use=2)
def test_count_with_dirty_true_no_usage_info(self):
res = self._create_resource()
self.tenant_id)
res.count(self.context, None, self.tenant_id, resync_usage=True)
mock_set_quota_usage.assert_called_once_with(
- self.context, self.resource, self.tenant_id, in_use=2)
+ self.context, self.resource, self.tenant_id,
+ reserved=0, in_use=2)
def test_add_delete_data_triggers_event(self):
res = self._create_resource()
# and now it should be in sync
self.assertNotIn(self.tenant_id, res._out_of_sync_tenants)
mock_set_quota_usage.assert_called_once_with(
- self.context, self.resource, self.tenant_id, in_use=2)
+ self.context, self.resource, self.tenant_id,
+ reserved=0, in_use=2)