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:
parent
bf800f9c6c
commit
574b25b857
|
@ -416,13 +416,15 @@ class Controller(object):
|
||||||
if self._collection in body:
|
if self._collection in body:
|
||||||
# Have to account for bulk create
|
# Have to account for bulk create
|
||||||
items = body[self._collection]
|
items = body[self._collection]
|
||||||
deltas = {}
|
|
||||||
bulk = True
|
|
||||||
else:
|
else:
|
||||||
items = [body]
|
items = [body]
|
||||||
bulk = False
|
|
||||||
# Ensure policy engine is initialized
|
# Ensure policy engine is initialized
|
||||||
policy.init()
|
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:
|
for item in items:
|
||||||
self._validate_network_tenant_ownership(request,
|
self._validate_network_tenant_ownership(request,
|
||||||
item[self._resource])
|
item[self._resource])
|
||||||
|
@ -433,29 +435,33 @@ class Controller(object):
|
||||||
if 'tenant_id' not in item[self._resource]:
|
if 'tenant_id' not in item[self._resource]:
|
||||||
# no tenant_id - no quota check
|
# no tenant_id - no quota check
|
||||||
continue
|
continue
|
||||||
try:
|
|
||||||
tenant_id = item[self._resource]['tenant_id']
|
tenant_id = item[self._resource]['tenant_id']
|
||||||
count = quota.QUOTAS.count(request.context, self._resource,
|
delta = request_deltas.get(tenant_id, 0)
|
||||||
self._plugin, tenant_id)
|
delta = delta + 1
|
||||||
if bulk:
|
request_deltas[tenant_id] = delta
|
||||||
delta = deltas.get(tenant_id, 0) + 1
|
# Quota enforcement
|
||||||
deltas[tenant_id] = delta
|
reservations = []
|
||||||
else:
|
try:
|
||||||
delta = 1
|
for tenant in request_deltas:
|
||||||
kwargs = {self._resource: count + delta}
|
reservation = quota.QUOTAS.make_reservation(
|
||||||
|
request.context,
|
||||||
|
tenant,
|
||||||
|
{self._resource:
|
||||||
|
request_deltas[tenant]},
|
||||||
|
self._plugin)
|
||||||
|
reservations.append(reservation)
|
||||||
except exceptions.QuotaResourceUnknown as e:
|
except exceptions.QuotaResourceUnknown as e:
|
||||||
# We don't want to quota this resource
|
# We don't want to quota this resource
|
||||||
LOG.debug(e)
|
LOG.debug(e)
|
||||||
else:
|
|
||||||
quota.QUOTAS.limit_check(request.context,
|
|
||||||
item[self._resource]['tenant_id'],
|
|
||||||
**kwargs)
|
|
||||||
|
|
||||||
def notify(create_result):
|
def notify(create_result):
|
||||||
# Ensure usage trackers for all resources affected by this API
|
# Ensure usage trackers for all resources affected by this API
|
||||||
# operation are marked as dirty
|
# operation are marked as dirty
|
||||||
# TODO(salv-orlando): This operation will happen in a single
|
with request.context.session.begin():
|
||||||
# transaction with reservation commit once that is implemented
|
# Commit the reservation(s)
|
||||||
|
for reservation in reservations:
|
||||||
|
quota.QUOTAS.commit_reservation(
|
||||||
|
request.context, reservation.reservation_id)
|
||||||
resource_registry.set_resources_dirty(request.context)
|
resource_registry.set_resources_dirty(request.context)
|
||||||
|
|
||||||
notifier_method = self._resource + '.create.end'
|
notifier_method = self._resource + '.create.end'
|
||||||
|
@ -467,11 +473,35 @@ class Controller(object):
|
||||||
notifier_method)
|
notifier_method)
|
||||||
return create_result
|
return create_result
|
||||||
|
|
||||||
|
def do_create(body, bulk=False, emulated=False):
|
||||||
kwargs = {self._parent_id_name: parent_id} if parent_id else {}
|
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:
|
if self._collection in body and self._native_bulk:
|
||||||
# plugin does atomic bulk create operations
|
# plugin does atomic bulk create operations
|
||||||
obj_creator = getattr(self._plugin, "%s_bulk" % action)
|
objs = do_create(body, bulk=True)
|
||||||
objs = obj_creator(request.context, body, **kwargs)
|
|
||||||
# Use first element of list to discriminate attributes which
|
# Use first element of list to discriminate attributes which
|
||||||
# should be removed because of authZ policies
|
# should be removed because of authZ policies
|
||||||
fields_to_strip = self._exclude_attributes_by_policy(
|
fields_to_strip = self._exclude_attributes_by_policy(
|
||||||
|
@ -480,15 +510,12 @@ class Controller(object):
|
||||||
request.context, obj, fields_to_strip=fields_to_strip)
|
request.context, obj, fields_to_strip=fields_to_strip)
|
||||||
for obj in objs]})
|
for obj in objs]})
|
||||||
else:
|
else:
|
||||||
obj_creator = getattr(self._plugin, action)
|
|
||||||
if self._collection in body:
|
if self._collection in body:
|
||||||
# Emulate atomic bulk behavior
|
# Emulate atomic bulk behavior
|
||||||
objs = self._emulate_bulk_create(obj_creator, request,
|
objs = do_create(body, bulk=True, emulated=True)
|
||||||
body, parent_id)
|
|
||||||
return notify({self._collection: objs})
|
return notify({self._collection: objs})
|
||||||
else:
|
else:
|
||||||
kwargs.update({self._resource: body})
|
obj = do_create(body)
|
||||||
obj = obj_creator(request.context, **kwargs)
|
|
||||||
self._send_nova_notification(action, {},
|
self._send_nova_notification(action, {},
|
||||||
{self._resource: obj})
|
{self._resource: obj})
|
||||||
return notify({self._resource: self._view(request.context,
|
return notify({self._resource: self._view(request.context,
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
2a16083502f3
|
2a16083502f3
|
||||||
48153cb5f051
|
9859ac9c136
|
||||||
kilo
|
kilo
|
||||||
|
|
|
@ -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'))
|
|
@ -13,11 +13,21 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import collections
|
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 import common_db_mixin as common_db_api
|
||||||
from neutron.db.quota import models as quota_models
|
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(
|
class QuotaUsageInfo(collections.namedtuple(
|
||||||
'QuotaUsageInfo', ['resource', 'tenant_id', 'used', 'reserved', 'dirty'])):
|
'QuotaUsageInfo', ['resource', 'tenant_id', 'used', 'reserved', 'dirty'])):
|
||||||
|
|
||||||
|
@ -27,6 +37,32 @@ class QuotaUsageInfo(collections.namedtuple(
|
||||||
return self.reserved + self.used
|
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,
|
def get_quota_usage_by_resource_and_tenant(context, resource, tenant_id,
|
||||||
lock_for_update=False):
|
lock_for_update=False):
|
||||||
"""Return usage info for a given resource and tenant.
|
"""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 = common_db_api.model_query(context, quota_models.QuotaUsage)
|
||||||
query = query.filter_by(resource=resource)
|
query = query.filter_by(resource=resource)
|
||||||
return query.update({'dirty': dirty})
|
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()
|
||||||
|
|
|
@ -13,9 +13,16 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_db import api as oslo_db_api
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
from neutron.common import exceptions
|
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
|
from neutron.db.quota import models as quota_models
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DbQuotaDriver(object):
|
class DbQuotaDriver(object):
|
||||||
"""Driver to perform necessary checks to enforce quotas and obtain quota
|
"""Driver to perform necessary checks to enforce quotas and obtain quota
|
||||||
|
@ -42,7 +49,8 @@ class DbQuotaDriver(object):
|
||||||
# update with tenant specific limits
|
# update with tenant specific limits
|
||||||
q_qry = context.session.query(quota_models.Quota).filter_by(
|
q_qry = context.session.query(quota_models.Quota).filter_by(
|
||||||
tenant_id=tenant_id)
|
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 tenant_quota
|
||||||
|
|
||||||
|
@ -116,6 +124,112 @@ class DbQuotaDriver(object):
|
||||||
|
|
||||||
return dict((k, v) for k, v in quotas.items())
|
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):
|
def limit_check(self, context, tenant_id, resources, values):
|
||||||
"""Check simple quota limits.
|
"""Check simple quota limits.
|
||||||
|
|
||||||
|
|
|
@ -13,12 +13,33 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import orm
|
||||||
from sqlalchemy import sql
|
from sqlalchemy import sql
|
||||||
|
|
||||||
from neutron.db import model_base
|
from neutron.db import model_base
|
||||||
from neutron.db import models_v2
|
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):
|
class Quota(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant):
|
||||||
"""Represent a single quota override for a tenant.
|
"""Represent a single quota override for a tenant.
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import six
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from neutron.common import exceptions
|
from neutron.common import exceptions
|
||||||
|
from neutron.db.quota import api as quota_api
|
||||||
from neutron.i18n import _LI, _LW
|
from neutron.i18n import _LI, _LW
|
||||||
from neutron.quota import resource_registry
|
from neutron.quota import resource_registry
|
||||||
|
|
||||||
|
@ -152,6 +153,33 @@ class ConfDriver(object):
|
||||||
msg = _('Access to this resource was denied.')
|
msg = _('Access to this resource was denied.')
|
||||||
raise webob.exc.HTTPForbidden(msg)
|
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):
|
class QuotaEngine(object):
|
||||||
"""Represent the set of recognized quotas."""
|
"""Represent the set of recognized quotas."""
|
||||||
|
@ -210,6 +238,39 @@ class QuotaEngine(object):
|
||||||
|
|
||||||
return res.count(context, *args, **kwargs)
|
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):
|
def limit_check(self, context, tenant_id, **values):
|
||||||
"""Check simple quota limits.
|
"""Check simple quota limits.
|
||||||
|
|
||||||
|
@ -232,6 +293,7 @@ class QuotaEngine(object):
|
||||||
:param tenant_id: Tenant for which the quota limit is being checked
|
:param tenant_id: Tenant for which the quota limit is being checked
|
||||||
:param values: Dict specifying requested deltas for each resource
|
: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
|
# Verify that resources are managed by the quota engine
|
||||||
requested_resources = set(values.keys())
|
requested_resources = set(values.keys())
|
||||||
managed_resources = set([res for res in
|
managed_resources = set([res for res in
|
||||||
|
|
|
@ -208,14 +208,15 @@ class TrackedResource(BaseResource):
|
||||||
max_retries=db_api.MAX_RETRIES,
|
max_retries=db_api.MAX_RETRIES,
|
||||||
exception_checker=lambda exc:
|
exception_checker=lambda exc:
|
||||||
isinstance(exc, oslo_db_exception.DBDuplicateEntry))
|
isinstance(exc, oslo_db_exception.DBDuplicateEntry))
|
||||||
def _set_quota_usage(self, context, tenant_id, in_use):
|
def _set_quota_usage(self, context, tenant_id, in_use, reserved):
|
||||||
return quota_api.set_quota_usage(context, self.name,
|
return quota_api.set_quota_usage(context, self.name, tenant_id,
|
||||||
tenant_id, in_use=in_use)
|
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
|
# Update quota usage
|
||||||
usage_info = self._set_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._dirty_tenants.discard(tenant_id)
|
||||||
self._out_of_sync_tenants.discard(tenant_id)
|
self._out_of_sync_tenants.discard(tenant_id)
|
||||||
LOG.debug(("Unset dirty status for tenant:%(tenant_id)s on "
|
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})
|
{'tenant_id': tenant_id, 'resource': self.name})
|
||||||
in_use = context.session.query(self._model_class).filter_by(
|
in_use = context.session.query(self._model_class).filter_by(
|
||||||
tenant_id=tenant_id).count()
|
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
|
# 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):
|
def count(self, context, _plugin, tenant_id, resync_usage=False):
|
||||||
"""Return the current usage count for the resource."""
|
"""Return the current usage count for the resource.
|
||||||
# Load current usage data
|
|
||||||
|
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(
|
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
|
# If dirty or missing, calculate actual resource usage querying
|
||||||
# the database and set/create usage info data
|
# the database and set/create usage info data
|
||||||
# NOTE: this routine "trusts" usage counters at service startup. This
|
# NOTE: this routine "trusts" usage counters at service startup. This
|
||||||
# assumption is generally valid, but if the database is tampered with,
|
# assumption is generally valid, but if the database is tampered with,
|
||||||
# or if data migrations do not take care of usage counters, the
|
# or if data migrations do not take care of usage counters, the
|
||||||
# assumption will not hold anymore
|
# assumption will not hold anymore
|
||||||
if (tenant_id in self._dirty_tenants or not usage_info
|
if (tenant_id in self._dirty_tenants or
|
||||||
or usage_info.dirty):
|
not usage_info or usage_info.dirty):
|
||||||
LOG.debug(("Usage tracker for resource:%(resource)s and tenant:"
|
LOG.debug(("Usage tracker for resource:%(resource)s and tenant:"
|
||||||
"%(tenant_id)s is out of sync, need to count used "
|
"%(tenant_id)s is out of sync, need to count used "
|
||||||
"quota"), {'resource': self.name,
|
"quota"), {'resource': self.name,
|
||||||
'tenant_id': tenant_id})
|
'tenant_id': tenant_id})
|
||||||
in_use = context.session.query(self._model_class).filter_by(
|
in_use = context.session.query(self._model_class).filter_by(
|
||||||
tenant_id=tenant_id).count()
|
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
|
# Update quota usage, if requested (by default do not do that, as
|
||||||
# typically one counts before adding a record, and that would mark
|
# typically one counts before adding a record, and that would mark
|
||||||
# the usage counter as dirty again)
|
# the usage counter as dirty again)
|
||||||
if resync_usage or not usage_info:
|
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:
|
else:
|
||||||
usage_info = quota_api.QuotaUsageInfo(usage_info.resource,
|
usage_info = quota_api.QuotaUsageInfo(usage_info.resource,
|
||||||
usage_info.tenant_id,
|
usage_info.tenant_id,
|
||||||
in_use,
|
in_use,
|
||||||
usage_info.reserved,
|
reserved,
|
||||||
usage_info.dirty)
|
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
|
return usage_info.total
|
||||||
|
|
||||||
def register_events(self):
|
def register_events(self):
|
||||||
|
|
|
@ -65,7 +65,7 @@ def set_resources_dirty(context):
|
||||||
return
|
return
|
||||||
|
|
||||||
for res in get_all_resources().values():
|
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:
|
if is_tracked(res.name) and res.dirty:
|
||||||
res.mark_dirty(context, nested=True)
|
res.mark_dirty(context, nested=True)
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
from neutron import context
|
from neutron import context
|
||||||
from neutron.db.quota import api as quota_api
|
from neutron.db.quota import api as quota_api
|
||||||
from neutron.tests.unit import testlib_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,
|
self.context = context.Context('Gonzalo', self.tenant_id,
|
||||||
is_admin=False, is_advsvc=False)
|
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):
|
def _create_quota_usage(self, resource, used, reserved, tenant_id=None):
|
||||||
tenant_id = tenant_id or self.tenant_id
|
tenant_id = tenant_id or self.tenant_id
|
||||||
return quota_api.set_quota_usage(
|
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.assertIsNone(quota_api.get_quota_usage_by_resource_and_tenant(
|
||||||
self.context, 'goals', self.tenant_id))
|
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 TestQuotaDbApiAdminContext(TestQuotaDbApi):
|
||||||
|
|
||||||
|
|
|
@ -27,16 +27,22 @@ class FakePlugin(base_plugin.NeutronDbPluginV2, driver.DbQuotaDriver):
|
||||||
class TestResource(object):
|
class TestResource(object):
|
||||||
"""Describe a test resource for quota checking."""
|
"""Describe a test resource for quota checking."""
|
||||||
|
|
||||||
def __init__(self, name, default):
|
def __init__(self, name, default, fake_count=0):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.quota = default
|
self.quota = default
|
||||||
|
self.fake_count = fake_count
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default(self):
|
def default(self):
|
||||||
return self.quota
|
return self.quota
|
||||||
|
|
||||||
|
def count(self, *args, **kwargs):
|
||||||
|
return self.fake_count
|
||||||
|
|
||||||
|
|
||||||
PROJECT = 'prj_test'
|
PROJECT = 'prj_test'
|
||||||
RESOURCE = 'res_test'
|
RESOURCE = 'res_test'
|
||||||
|
ALT_RESOURCE = 'res_test_meh'
|
||||||
|
|
||||||
|
|
||||||
class TestDbQuotaDriver(testlib_api.SqlTestCase):
|
class TestDbQuotaDriver(testlib_api.SqlTestCase):
|
||||||
|
@ -132,3 +138,63 @@ class TestDbQuotaDriver(testlib_api.SqlTestCase):
|
||||||
self.assertRaises(exceptions.InvalidQuotaValue,
|
self.assertRaises(exceptions.InvalidQuotaValue,
|
||||||
self.plugin.limit_check, context.get_admin_context(),
|
self.plugin.limit_check, context.get_admin_context(),
|
||||||
PROJECT, resources, values)
|
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)
|
||||||
|
|
|
@ -344,6 +344,24 @@ class QuotaExtensionDbTestCase(QuotaExtensionTestCase):
|
||||||
extra_environ=env, expect_errors=True)
|
extra_environ=env, expect_errors=True)
|
||||||
self.assertEqual(400, res.status_int)
|
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):
|
class QuotaExtensionCfgTestCase(QuotaExtensionTestCase):
|
||||||
fmt = 'json'
|
fmt = 'json'
|
||||||
|
|
|
@ -165,7 +165,8 @@ class TestTrackedResource(testlib_api.SqlTestCaseLight):
|
||||||
res.count(self.context, None, self.tenant_id,
|
res.count(self.context, None, self.tenant_id,
|
||||||
resync_usage=True)
|
resync_usage=True)
|
||||||
mock_set_quota_usage.assert_called_once_with(
|
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):
|
def test_count_with_dirty_true_no_usage_info(self):
|
||||||
res = self._create_resource()
|
res = self._create_resource()
|
||||||
|
@ -184,7 +185,8 @@ class TestTrackedResource(testlib_api.SqlTestCaseLight):
|
||||||
self.tenant_id)
|
self.tenant_id)
|
||||||
res.count(self.context, None, self.tenant_id, resync_usage=True)
|
res.count(self.context, None, self.tenant_id, resync_usage=True)
|
||||||
mock_set_quota_usage.assert_called_once_with(
|
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):
|
def test_add_delete_data_triggers_event(self):
|
||||||
res = self._create_resource()
|
res = self._create_resource()
|
||||||
|
@ -251,4 +253,5 @@ class TestTrackedResource(testlib_api.SqlTestCaseLight):
|
||||||
# and now it should be in sync
|
# and now it should be in sync
|
||||||
self.assertNotIn(self.tenant_id, res._out_of_sync_tenants)
|
self.assertNotIn(self.tenant_id, res._out_of_sync_tenants)
|
||||||
mock_set_quota_usage.assert_called_once_with(
|
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)
|
||||||
|
|
Loading…
Reference in New Issue