Reservations support

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
This commit is contained in:
Salvatore Orlando 2015-03-11 17:28:43 -07:00
parent bf800f9c6c
commit 574b25b857
13 changed files with 697 additions and 49 deletions

View File

@ -416,13 +416,15 @@ class Controller(object):
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])
@ -433,30 +435,34 @@ class Controller(object):
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,
@ -467,11 +473,35 @@ class Controller(object):
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(
@ -480,15 +510,12 @@ class Controller(object):
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,

View File

@ -1,3 +1,3 @@
2a16083502f3
48153cb5f051
9859ac9c136
kilo

View File

@ -0,0 +1,47 @@
# 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'))

View File

@ -13,11 +13,21 @@
# 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'])):
@ -27,6 +37,32 @@ class QuotaUsageInfo(collections.namedtuple(
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.
@ -157,3 +193,105 @@ def set_all_quota_usage_dirty(context, resource, dirty=True):
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()

View File

@ -13,9 +13,16 @@
# 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
@ -42,7 +49,8 @@ class DbQuotaDriver(object):
# 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
@ -116,6 +124,112 @@ class DbQuotaDriver(object):
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.

View File

@ -13,12 +13,33 @@
# 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.

View File

@ -24,6 +24,7 @@ import six
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
@ -152,6 +153,33 @@ class ConfDriver(object):
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."""
@ -210,6 +238,39 @@ class QuotaEngine(object):
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.
@ -232,6 +293,7 @@ class QuotaEngine(object):
: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

View File

@ -208,14 +208,15 @@ class TrackedResource(BaseResource):
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 "
@ -231,40 +232,62 @@ class TrackedResource(BaseResource):
{'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):

View File

@ -65,7 +65,7 @@ def set_resources_dirty(context):
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)

View File

@ -12,6 +12,10 @@
# 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
@ -24,6 +28,12 @@ class TestQuotaDbApi(testlib_api.SqlTestCaseLight):
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(
@ -203,6 +213,125 @@ class TestQuotaDbApi(testlib_api.SqlTestCaseLight):
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):

View File

@ -27,16 +27,22 @@ class FakePlugin(base_plugin.NeutronDbPluginV2, driver.DbQuotaDriver):
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):
@ -132,3 +138,63 @@ 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)

View File

@ -344,6 +344,24 @@ class QuotaExtensionDbTestCase(QuotaExtensionTestCase):
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'

View File

@ -165,7 +165,8 @@ class TestTrackedResource(testlib_api.SqlTestCaseLight):
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()
@ -184,7 +185,8 @@ class TestTrackedResource(testlib_api.SqlTestCaseLight):
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()
@ -251,4 +253,5 @@ class TestTrackedResource(testlib_api.SqlTestCaseLight):
# 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)