Quota feature

Quota check for create/delete/resize volume
Quota is implemented as a decorator, which can be easy enabled and disabled if wanted.
The quota feature uses a reservation system that reserves resources prior to usage.

BP https://blueprints.launchpad.net/reddwarf/+spec/quotas

Change-Id: I5df82c8f1d7b40311b5d5d7301992607f98e9b2a
This commit is contained in:
Steve Leon 2013-02-25 13:06:36 -08:00
parent 460aa6abde
commit c4848cd472
21 changed files with 1304 additions and 70 deletions

View File

@ -51,6 +51,7 @@ device_path = /dev/vdb
mount_point = /var/lib/mysql
max_accepted_volume_size = 10
max_instances_per_user = 5
max_volumes_per_user = 100
volume_time_out=30
# Reddwarf DNS

View File

@ -66,6 +66,7 @@ device_path = /dev/vdb
mount_point = /var/lib/mysql
max_accepted_volume_size = 25
max_instances_per_user = 55
max_volumes_per_user = 100
volume_time_out=30
# Auth

View File

@ -25,8 +25,9 @@
"reddwarf_must_have_volume":false,
"reddwarf_can_have_volume":true,
"reddwarf_main_instance_has_volume": true,
"reddwarf_max_accepted_volume_size": 1000,
"reddwarf_max_accepted_volume_size": 25,
"reddwarf_max_instances_per_user": 55,
"reddwarf_max_volumes_per_user": 100,
"use_reaper":false,
"root_removed_from_instance_api": true,
"root_timestamp_disabled": false,

View File

@ -75,8 +75,15 @@ common_opts = [
cfg.StrOpt('format_options', default='-m 5'),
cfg.IntOpt('volume_format_timeout', default=120),
cfg.StrOpt('mount_options', default='defaults,noatime'),
cfg.IntOpt('max_instances_per_user', default=5),
cfg.IntOpt('max_accepted_volume_size', default=5),
cfg.IntOpt('max_instances_per_user', default=5,
help='default maximum number of instances per tenant'),
cfg.IntOpt('max_accepted_volume_size', default=5,
help='default maximum volume size for an instance'),
cfg.IntOpt('max_volumes_per_user', default=20,
help='default maximum for total volume used by a tenant'),
cfg.StrOpt('quota_driver',
default='reddwarf.quota.quota.DbQuotaDriver',
help='default driver to use for quota checks'),
cfg.StrOpt('taskmanager_queue', default='taskmanager'),
cfg.BoolOpt('use_nova_server_volume', default=False),
cfg.StrOpt('fake_mode_events', default='simulated'),

View File

@ -96,7 +96,7 @@ class OverLimit(ReddwarfError):
class QuotaExceeded(ReddwarfError):
message = _("User instance quota exceeded.")
message = _("Quota exceeded for resources: %(overs)s")
class VolumeQuotaExceeded(QuotaExceeded):
@ -201,3 +201,15 @@ class ConfigNotFound(NotFound):
class PasteAppNotFound(NotFound):
message = _("Paste app not found.")
class QuotaNotFound(NotFound):
message = _("Quota could not be found")
class TenantQuotaNotFound(QuotaNotFound):
message = _("Quota for tenant %(tenant_id)s could not be found.")
class QuotaResourceUnknown(QuotaNotFound):
message = _("Unknown quota resources %(unknown)s.")

View File

@ -91,6 +91,8 @@ CUSTOM_SERIALIZER_METADATA = {
'device': {'used': '', 'name': '', 'type': ''},
# mgmt/account
'account': {'id': '', 'num_instances': ''},
# mgmt/quotas
'quotas': {'instances': '', 'volumes': ''},
#mgmt/instance
'guest_status': {'state_description': ''},
#mgmt/instance/diagnostics
@ -319,6 +321,7 @@ class Controller(object):
exception.ModelNotFoundError,
exception.UserNotFound,
exception.DatabaseNotFound,
exception.QuotaResourceUnknown
],
webob.exc.HTTPConflict: [],
webob.exc.HTTPRequestEntityTooLarge: [

View File

@ -38,6 +38,12 @@ def map(engine, models):
Table('dns_records', meta, autoload=True))
orm.mapper(models['agent_heartbeats'],
Table('agent_heartbeats', meta, autoload=True))
orm.mapper(models['quotas'],
Table('quotas', meta, autoload=True))
orm.mapper(models['quota_usages'],
Table('quota_usages', meta, autoload=True))
orm.mapper(models['reservations'],
Table('reservations', meta, autoload=True))
def mapping_exists(model):

View File

@ -0,0 +1,67 @@
#Copyright [2013] Hewlett-Packard Development Company, L.P.
#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.
from sqlalchemy.schema import Column
from sqlalchemy.schema import MetaData
from sqlalchemy.schema import UniqueConstraint
from reddwarf.db.sqlalchemy.migrate_repo.schema import create_tables
from reddwarf.db.sqlalchemy.migrate_repo.schema import DateTime
from reddwarf.db.sqlalchemy.migrate_repo.schema import drop_tables
from reddwarf.db.sqlalchemy.migrate_repo.schema import Integer
from reddwarf.db.sqlalchemy.migrate_repo.schema import String
from reddwarf.db.sqlalchemy.migrate_repo.schema import Table
meta = MetaData()
quotas = Table('quotas', meta,
Column('id', String(36),
primary_key=True, nullable=False),
Column('created', DateTime()),
Column('updated', DateTime()),
Column('tenant_id', String(36)),
Column('resource', String(length=255), nullable=False),
Column('hard_limit', Integer()),
UniqueConstraint('tenant_id', 'resource'))
quota_usages = Table('quota_usages', meta,
Column('id', String(36),
primary_key=True, nullable=False),
Column('created', DateTime()),
Column('updated', DateTime()),
Column('tenant_id', String(36)),
Column('in_use', Integer(), default=0),
Column('reserved', Integer(), default=0),
Column('resource', String(length=255), nullable=False),
UniqueConstraint('tenant_id', 'resource'))
reservations = Table('reservations', meta,
Column('created', DateTime()),
Column('updated', DateTime()),
Column('id', String(36),
primary_key=True, nullable=False),
Column('usage_id', String(36)),
Column('delta', Integer(), nullable=False),
Column('status', String(length=36)))
def upgrade(migrate_engine):
meta.bind = migrate_engine
create_tables([quotas, quota_usages, reservations])
def downgrade(migrate_engine):
meta.bind = migrate_engine
drop_tables([quotas, quota_usages, reservations])

View File

@ -45,12 +45,14 @@ def configure_db(options, models_mapper=None):
from reddwarf.dns import models as dns_models
from reddwarf.extensions.mysql import models as mysql_models
from reddwarf.guestagent import models as agent_models
from reddwarf.quota import models as quota_models
model_modules = [
base_models,
dns_models,
mysql_models,
agent_models,
quota_models,
]
models = {}

View File

@ -21,6 +21,7 @@ from reddwarf.common import extensions
from reddwarf.common import wsgi
from reddwarf.extensions.mgmt.instances.service import MgmtInstanceController
from reddwarf.extensions.mgmt.host.service import HostController
from reddwarf.extensions.mgmt.quota.service import QuotaController
from reddwarf.extensions.mgmt.host.instance import service as hostservice
from reddwarf.extensions.mgmt.volume.service import StorageController
@ -69,6 +70,14 @@ class Mgmt(extensions.ExtensionsDescriptor):
member_actions={})
resources.append(hosts)
quota = extensions.ResourceExtension(
'{tenant_id}/mgmt/quotas',
QuotaController(),
deserializer=wsgi.RequestDeserializer(),
serializer=serializer,
member_actions={})
resources.append(quota)
storage = extensions.ResourceExtension(
'{tenant_id}/mgmt/storage',
StorageController(),

View File

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

View File

@ -0,0 +1,61 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from reddwarf.common import wsgi
from reddwarf.common import exception
from reddwarf.common.auth import admin_context
from reddwarf.extensions.mgmt.quota import views
from reddwarf.openstack.common import log as logging
from reddwarf.quota.quota import QUOTAS as quota_engine
from reddwarf.quota.models import Quota
LOG = logging.getLogger(__name__)
class QuotaController(wsgi.Controller):
"""Controller for quota functionality"""
@admin_context
def show(self, req, tenant_id, id):
"""Return all quotas for this tenant."""
LOG.info(_("req : '%s'\n\n") % req)
LOG.info(_("Indexing quota info for tenant '%s'") % id)
quotas = quota_engine.get_all_quotas_by_tenant(id)
return wsgi.Result(views.QuotaView(quotas).data(), 200)
@admin_context
def update(self, req, body, tenant_id, id):
LOG.info("req : '%s'\n\n" % req)
LOG.info("Updating quota limits for tenant '%s'" % id)
if not body:
raise exception.BadRequest(_("Invalid request body."))
quotas = {}
quota = None
for resource, limit in body['quotas'].items():
try:
quota = Quota.find_by(tenant_id=id, resource=resource)
quota.hard_limit = limit
quota.save()
except exception.ModelNotFoundError:
quota = Quota.create(tenant_id=id,
resource=resource,
hard_limit=limit)
quotas[resource] = quota
return wsgi.Result(views.QuotaView(quotas).data(), 200)

View File

@ -0,0 +1,28 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class QuotaView(object):
def __init__(self, quotas):
self.quotas = quotas
def data(self):
rtn = {}
for resource_name, quota in self.quotas.items():
rtn[resource_name] = quota.hard_limit
return rtn

View File

@ -71,6 +71,31 @@ class InstanceStatus(object):
ERROR = "ERROR"
def validate_volume_size(size):
max_size = CONF.max_accepted_volume_size
if long(size) > max_size:
msg = ("Volume 'size' cannot exceed maximum "
"of %d Gb, %s cannot be accepted."
% (max_size, size))
raise exception.VolumeQuotaExceeded(msg)
def run_with_quotas(tenant_id, deltas, f):
""" Quota wrapper """
from reddwarf.quota.quota import QUOTAS as quota_engine
reservations = quota_engine.reserve(tenant_id, **deltas)
result = None
try:
result = f()
except:
quota_engine.rollback(reservations)
raise
else:
quota_engine.commit(reservations)
return result
def load_simple_instance_server_status(context, db_info):
"""Loads a server or raises an exception."""
if 'BUILDING' == db_info.task_status.action:
@ -317,14 +342,20 @@ class BaseInstance(SimpleInstance):
return create_guest_client(self.context, self.db_info.id)
def delete(self):
if self.is_building:
raise exception.UnprocessableEntity("Instance %s is not ready." %
self.id)
LOG.debug(_(" ... deleting compute id = %s") %
self.db_info.compute_instance_id)
LOG.debug(_(" ... setting status to DELETING."))
self.update_db(task_status=InstanceTasks.DELETING)
task_api.API(self.context).delete_instance(self.id)
def _delete_resources():
if self.is_building:
raise exception.UnprocessableEntity("Instance %s is not ready."
% self.id)
LOG.debug(_(" ... deleting compute id = %s") %
self.db_info.compute_instance_id)
LOG.debug(_(" ... setting status to DELETING."))
self.update_db(task_status=InstanceTasks.DELETING)
task_api.API(self.context).delete_instance(self.id)
return run_with_quotas(self.tenant_id,
{'instances': -1,
'volumes': -self.volume_size},
_delete_resources)
def _delete_resources(self):
pass
@ -395,35 +426,41 @@ class Instance(BuiltInstance):
@classmethod
def create(cls, context, name, flavor_id, image_id,
databases, users, service_type, volume_size):
client = create_nova_client(context)
try:
flavor = client.flavors.get(flavor_id)
except nova_exceptions.NotFound:
raise exception.FlavorNotFound(uuid=flavor_id)
def _create_resources():
client = create_nova_client(context)
try:
flavor = client.flavors.get(flavor_id)
except nova_exceptions.NotFound:
raise exception.FlavorNotFound(uuid=flavor_id)
db_info = DBInstance.create(name=name, flavor_id=flavor_id,
tenant_id=context.tenant,
volume_size=volume_size,
task_status=InstanceTasks.BUILDING)
LOG.debug(_("Tenant %s created new Reddwarf instance %s...")
% (context.tenant, db_info.id))
db_info = DBInstance.create(name=name, flavor_id=flavor_id,
tenant_id=context.tenant,
volume_size=volume_size,
task_status=InstanceTasks.BUILDING)
LOG.debug(_("Tenant %s created new Reddwarf instance %s...")
% (context.tenant, db_info.id))
service_status = InstanceServiceStatus.create(
instance_id=db_info.id,
status=ServiceStatuses.NEW)
service_status = InstanceServiceStatus.create(
instance_id=db_info.id,
status=ServiceStatuses.NEW)
if CONF.reddwarf_dns_support:
dns_client = create_dns_client(context)
hostname = dns_client.determine_hostname(db_info.id)
db_info.hostname = hostname
db_info.save()
if CONF.reddwarf_dns_support:
dns_client = create_dns_client(context)
hostname = dns_client.determine_hostname(db_info.id)
db_info.hostname = hostname
db_info.save()
task_api.API(context).create_instance(db_info.id, name, flavor_id,
flavor.ram, image_id, databases,
users, service_type,
volume_size)
task_api.API(context).create_instance(db_info.id, name, flavor_id,
flavor.ram, image_id,
databases, users,
service_type, volume_size)
return SimpleInstance(context, db_info, service_status)
return SimpleInstance(context, db_info, service_status)
validate_volume_size(volume_size)
return run_with_quotas(context.tenant,
{'instances': 1, 'volumes': volume_size},
_create_resources)
def resize_flavor(self, new_flavor_id):
self._validate_can_perform_action()
@ -450,18 +487,26 @@ class Instance(BuiltInstance):
new_flavor_size)
def resize_volume(self, new_size):
self._validate_can_perform_action()
LOG.info("Resizing volume of instance %s..." % self.id)
if not self.volume_size:
raise exception.BadRequest("Instance %s has no volume." % self.id)
old_size = self.volume_size
if int(new_size) <= old_size:
msg = ("The new volume 'size' must be larger than the current "
"volume size of '%s'")
raise exception.BadRequest(msg % old_size)
# Set the task to Resizing before sending off to the taskmanager
self.update_db(task_status=InstanceTasks.RESIZING)
task_api.API(self.context).resize_volume(new_size, self.id)
def _resize_resources():
self._validate_can_perform_action()
LOG.info("Resizing volume of instance %s..." % self.id)
if not self.volume_size:
raise exception.BadRequest("Instance %s has no volume."
% self.id)
old_size = self.volume_size
if int(new_size) <= old_size:
msg = ("The new volume 'size' must be larger than the current "
"volume size of '%s'")
raise exception.BadRequest(msg % old_size)
# Set the task to Resizing before sending off to the taskmanager
self.update_db(task_status=InstanceTasks.RESIZING)
task_api.API(self.context).resize_volume(new_size, self.id)
new_size_l = long(new_size)
validate_volume_size(new_size_l)
return run_with_quotas(self.tenant_id,
{'volumes': new_size_l - self.volume_size},
_resize_resources)
def reboot(self):
self._validate_can_perform_action()

View File

@ -189,16 +189,6 @@ class InstanceController(wsgi.Controller):
else:
volume_size = None
instance_max = CONF.max_instances_per_user
number_instances = models.DBInstance.find_all(tenant_id=tenant_id,
deleted=False).count()
if number_instances >= instance_max:
# That's too many, pal. Got to cut you off.
LOG.error(_("New instance would exceed user instance quota."))
msg = "User instance quota of %d would be exceeded."
raise exception.QuotaExceeded(msg % instance_max)
instance = models.Instance.create(context, name, flavor_id,
image_id, databases, users,
service_type, volume_size)
@ -238,12 +228,6 @@ class InstanceController(wsgi.Controller):
"integer value, %s cannot be accepted."
% volume_size)
raise exception.ReddwarfError(msg)
max_size = CONF.max_accepted_volume_size
if int(volume_size) > max_size:
msg = ("Volume 'size' cannot exceed maximum "
"of %d Gb, %s cannot be accepted."
% (max_size, volume_size))
raise exception.VolumeQuotaExceeded(msg)
@staticmethod
def _validate(body):

View File

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

108
reddwarf/quota/models.py Normal file
View File

@ -0,0 +1,108 @@
# Copyright 2011 OpenStack LLC
#
# 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.
from reddwarf.common import cfg
from reddwarf.common import utils
from reddwarf.db import models as dbmodels
from reddwarf.openstack.common import log as logging
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
def enum(**enums):
return type('Enum', (), enums)
class Quota(dbmodels.DatabaseModelBase):
"""Defines the base model class for a quota."""
_data_fields = ['created', 'updated', 'tenant_id', 'resource',
'hard_limit', 'id']
def __init__(self, tenant_id, resource, hard_limit,
id=utils.generate_uuid(), created=utils.utcnow(),
update=utils.utcnow()):
self.tenant_id = tenant_id
self.resource = resource
self.hard_limit = hard_limit
self.id = id
self.created = created
self.update = update
class QuotaUsage(dbmodels.DatabaseModelBase):
"""Defines the quota usage for a tenant."""
_data_fields = ['created', 'updated', 'tenant_id', 'resource',
'in_use', 'reserved', 'id']
class Reservation(dbmodels.DatabaseModelBase):
"""Defines the reservation for a quota."""
_data_fields = ['created', 'updated', 'usage_id',
'id', 'delta', 'status']
Statuses = enum(NEW='New',
RESERVED='Reserved',
COMMITTED='Committed',
ROLLEDBACK='Rolled Back')
def persisted_models():
return {
'quotas': Quota,
'quota_usages': QuotaUsage,
'reservations': Reservation,
}
class Resource(object):
"""Describe a single resource for quota checking."""
INSTANCES = 'instances'
VOLUMES = 'volumes'
def __init__(self, name, flag=None):
"""
Initializes a Resource.
:param name: The name of the resource, i.e., "volumes".
:param flag: The name of the flag or configuration option
which specifies the default value of the quota
for this resource.
"""
self.name = name
self.flag = flag
def __str__(self):
return self.name
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return (isinstance(other, Resource) and
self.name == other.name and
self.flag == other.flag)
@property
def default(self):
"""Return the default value of the quota."""
return CONF[self.flag] if self.flag else -1

317
reddwarf/quota/quota.py Normal file
View File

@ -0,0 +1,317 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Quotas for DB instances and resources."""
from reddwarf.openstack.common import log as logging
from reddwarf.openstack.common.gettextutils import _
from reddwarf.openstack.common import cfg
import datetime
from reddwarf.common import exception
from reddwarf.openstack.common import importutils
from reddwarf.openstack.common import timeutils
from reddwarf.quota.models import Quota
from reddwarf.quota.models import QuotaUsage
from reddwarf.quota.models import Reservation
from reddwarf.quota.models import Resource
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class DbQuotaDriver(object):
"""
Driver to perform necessary checks to enforce quotas and obtain
quota information. The default driver utilizes the local
database.
"""
def __init__(self, resources):
self.resources = resources
def get_quota_by_tenant(self, tenant_id, resource):
"""Get a specific quota by tenant."""
quotas = Quota.find_all(tenant_id=tenant_id, resource=resource).all()
if len(quotas) == 0:
return Quota(tenant_id, resource, self.resources[resource].default)
return quotas[0]
def get_all_quotas_by_tenant(self, tenant_id, resources):
"""
Retrieve the quotas for the given tenant.
:param resources: A list of the registered resource to get.
:param tenant_id: The ID of the tenant to return quotas for.
"""
all_quotas = Quota.find_all(tenant_id=tenant_id).all()
result_quotas = dict((quota.resource, quota)
for quota in all_quotas
if quota.resource in resources)
if len(result_quotas) != len(resources):
for resource in resources:
# Not in the DB, return default value
if resource not in result_quotas:
quota = Quota(tenant_id,
resource,
self.resources[resource].default)
result_quotas[resource] = quota
return result_quotas
def get_quota_usage_by_tenant(self, tenant_id, resource):
"""Get a specific quota usage by tenant."""
quotas = QuotaUsage.find_all(tenant_id=tenant_id,
resource=resource).all()
if len(quotas) == 0:
return QuotaUsage.create(tenant_id=tenant_id,
in_use=0,
reserved=0,
resource=resource)
return quotas[0]
def get_all_quota_usages_by_tenant(self, tenant_id, resources):
"""
Retrieve the quota usagess for the given tenant.
:param tenant_id: The ID of the tenant to return quotas for.
:param resources: A list of the registered resources to get.
"""
all_usages = QuotaUsage.find_all(tenant_id=tenant_id).all()
result_usages = dict((usage.resource, usage)
for usage in all_usages
if usage.resource in resources)
if len(result_usages) != len(resources):
for resource in resources:
# Not in the DB, return default value
if resource not in result_usages:
usage = QuotaUsage.create(tenant_id=tenant_id,
in_use=0,
reserved=0,
resource=resource)
result_usages[resource] = usage
return result_usages
def get_defaults(self, resources):
"""Given a list of resources, retrieve the default quotas.
:param resources: A list of the registered resources.
"""
quotas = {}
for resource in resources.values():
quotas[resource.name] = resource.default
return quotas
def reserve(self, tenant_id, resources, deltas):
"""Check quotas and reserve resources for a tenant.
This method checks quotas against current usage,
reserved resources and the desired deltas.
If any of the proposed values is over the defined quota, an
QuotaExceeded exception will be raised with the sorted list of the
resources which are too high. Otherwise, the method returns a
list of reservation objects which were created.
:param tenant_id: The ID of the tenant reserving the resources.
:param resources: A dictionary of the registered resources.
:param deltas: A dictionary of the proposed delta changes.
"""
unregistered_resources = [delta for delta in deltas
if delta not in resources]
if unregistered_resources:
raise exception.QuotaResourceUnknown(unknown=
unregistered_resources)
quotas = self.get_all_quotas_by_tenant(tenant_id, deltas.keys())
quota_usages = self.get_all_quota_usages_by_tenant(tenant_id,
deltas.keys())
overs = [resource for resource in deltas
if (quota_usages[resource].in_use +
quota_usages[resource].reserved +
int(deltas[resource])) > quotas[resource].hard_limit]
if overs:
raise exception.QuotaExceeded(overs=sorted(overs))
reservations = []
for resource in deltas:
reserved = deltas[resource]
usage = quota_usages[resource]
usage.reserved = reserved
usage.save()
resv = Reservation.create(usage_id=usage.id,
delta=usage.reserved,
status=Reservation.Statuses.RESERVED)
reservations.append(resv)
return reservations
def commit(self, reservations):
"""Commit reservations.
:param reservations: A list of the reservation UUIDs, as
returned by the reserve() method.
"""
for reservation in reservations:
usage = QuotaUsage.find_by(id=reservation.usage_id)
usage.in_use += reservation.delta
usage.reserved -= reservation.delta
reservation.status = Reservation.Statuses.COMMITTED
usage.save()
reservation.save()
def rollback(self, reservations):
"""Roll back reservations.
:param reservations: A list of the reservation UUIDs, as
returned by the reserve() method.
"""
for reservation in reservations:
usage = QuotaUsage.find_by(id=reservation.usage_id)
usage.reserved -= reservation.delta
reservation.status = Reservation.Statuses.ROLLEDBACK
usage.save()
reservation.save()
class QuotaEngine(object):
"""Represent the set of recognized quotas."""
def __init__(self, quota_driver_class=None):
"""Initialize a Quota object."""
self._resources = {}
if not quota_driver_class:
quota_driver_class = CONF.quota_driver
if isinstance(quota_driver_class, basestring):
quota_driver_class = importutils.import_object(quota_driver_class,
self._resources)
self._driver = quota_driver_class
def __contains__(self, resource):
return resource in self._resources
def register_resource(self, resource):
"""Register a resource."""
self._resources[resource.name] = resource
def register_resources(self, resources):
"""Register a dictionary of resources."""
for resource in resources:
self.register_resource(resource)
def get_quota_by_tenant(self, tenant_id, resource):
"""Get a specific quota by tenant."""
return self._driver.get_quota_by_tenant(tenant_id, resource)
def get_defaults(self):
"""Retrieve the default quotas."""
return self._driver.get_defaults(self._resources)
def get_all_quotas_by_tenant(self, tenant_id):
"""Retrieve the quotas for the given tenant.
:param tenant_id: The ID of the tenant to return quotas for.
"""
return self._driver.get_all_quotas_by_tenant(tenant_id,
self._resources)
def reserve(self, tenant_id, **deltas):
"""Check quotas and reserve resources.
For counting quotas--those quotas for which there is a usage
synchronization function--this method checks quotas against
current usage and the desired deltas. The deltas are given as
keyword arguments, and current usage and other reservations
are factored into the quota check.
This method will raise a QuotaResourceUnknown exception if a
given resource is unknown or if it does not have a usage
synchronization function.
If any of the proposed values is over the defined quota, an
QuotaExceeded exception will be raised with the sorted list of the
resources which are too high. Otherwise, the method returns a
list of reservation UUIDs which were created.
:param tenant_id: The ID of the tenant to reserve quotas for.
"""
reservations = self._driver.reserve(tenant_id, self._resources, deltas)
LOG.debug(_("Created reservations %(reservations)s") % locals())
return reservations
def commit(self, reservations):
"""Commit reservations.
:param reservations: A list of the reservation UUIDs, as
returned by the reserve() method.
"""
try:
self._driver.commit(reservations)
except Exception:
LOG.exception(_("Failed to commit reservations "
"%(reservations)s") % locals())
def rollback(self, reservations):
"""Roll back reservations.
:param reservations: A list of the reservation UUIDs, as
returned by the reserve() method.
"""
try:
self._driver.rollback(reservations)
except Exception:
LOG.exception(_("Failed to roll back reservations "
"%(reservations)s") % locals())
@property
def resources(self):
return sorted(self._resources.keys())
QUOTAS = QuotaEngine()
''' Define all kind of resources here '''
resources = [Resource(Resource.INSTANCES, 'max_instances_per_user'),
Resource(Resource.VOLUMES, 'max_volumes_per_user')]
QUOTAS.register_resources(resources)

View File

@ -229,14 +229,14 @@ def test_delete_instance_not_found():
@test(depends_on_classes=[InstanceSetup],
groups=[GROUP, GROUP_START, GROUP_START_SIMPLE, tests.INSTANCES],
groups=[GROUP, GROUP_START, GROUP_START_SIMPLE, 'dbaas_quotas'],
runs_after_groups=[tests.PRE_INSTANCES])
class CreateInstance(unittest.TestCase):
"""Test to create a Database Instance
class CreateInstanceQuotaTest(unittest.TestCase):
If the call returns without raising an exception this test passes.
"""
def tearDown(self):
dbaas_admin.quota.update(instance_info.user.tenant,
CONFIG.reddwarf_max_instances_per_user,
CONFIG.reddwarf_max_volumes_per_user)
def test_instance_size_too_big(self):
vol_ok = CONFIG.get('reddwarf_volume_support', False)
@ -247,6 +247,51 @@ class CreateInstance(unittest.TestCase):
{'size': too_big + 1}, [])
assert_equal(413, dbaas.last_http_code)
def test_create_too_many_instances(self):
instance_quota = 0
new_quotas = dbaas_admin.quota.update(instance_info.user.tenant,
instance_quota,
CONFIG
.reddwarf_max_volumes_per_user)
verify_quota = dbaas_admin.quota.show(instance_info.user.tenant)
assert_equal(new_quotas._info, verify_quota._info)
assert_equal(0, verify_quota._info['instances'])
assert_equal(CONFIG.reddwarf_max_volumes_per_user,
verify_quota._info['volumes'])
assert_raises(exceptions.OverLimit, dbaas.instances.create,
"too_many_instances", instance_info.dbaas_flavor_href,
{'size': instance_quota + 1}, [])
assert_equal(413, dbaas.last_http_code)
def test_create_instances_total_volume_exceeded(self):
volume_quota = 3
new_quotas = dbaas_admin.quota.update(instance_info.user.tenant,
CONFIG
.reddwarf_max_instances_per_user,
volume_quota)
assert_equal(CONFIG.reddwarf_max_instances_per_user,
new_quotas._info['instances'])
assert_equal(volume_quota,
new_quotas._info['volumes'])
assert_raises(exceptions.OverLimit, dbaas.instances.create,
"too_many_instances", instance_info.dbaas_flavor_href,
{'size': volume_quota + 1}, [])
assert_equal(413, dbaas.last_http_code)
@test(depends_on_classes=[InstanceSetup],
groups=[GROUP, GROUP_START, GROUP_START_SIMPLE, tests.INSTANCES],
runs_after_groups=[tests.PRE_INSTANCES, 'dbaas_quotas'])
class CreateInstance(unittest.TestCase):
"""Test to create a Database Instance
If the call returns without raising an exception this test passes.
"""
def test_create(self):
databases = []
databases.append({"name": "firstdb", "character_set": "latin2",

View File

@ -0,0 +1,13 @@
# Copyright 2011 OpenStack LLC
#
# 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.

View File

@ -0,0 +1,492 @@
# Copyright 2012 OpenStack LLC
#
# 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.
import testtools
from mock import Mock
from reddwarf.quota.quota import DbQuotaDriver
from reddwarf.quota.models import Resource
from reddwarf.quota.models import Quota
from reddwarf.quota.models import QuotaUsage
from reddwarf.quota.models import Reservation
from reddwarf.common import exception
from reddwarf.common import cfg
from reddwarf.instance.models import run_with_quotas
from reddwarf.quota.quota import QUOTAS
"""
Unit tests for the classes and functions in DbQuotaDriver.py.
"""
CONF = cfg.CONF
resources = {
Resource.INSTANCES: Resource(Resource.INSTANCES, 'max_instances_per_user'),
Resource.VOLUMES: Resource(Resource.VOLUMES, 'max_volumes_per_user'),
}
FAKE_TENANT1 = "123456"
FAKE_TENANT2 = "654321"
class Run_with_quotasTest(testtools.TestCase):
def setUp(self):
super(Run_with_quotasTest, self).setUp()
self.quota_reserve_orig = QUOTAS.reserve
self.quota_rollback_orig = QUOTAS.rollback
self.quota_commit_orig = QUOTAS.commit
QUOTAS.reserve = Mock()
QUOTAS.rollback = Mock()
QUOTAS.commit = Mock()
def tearDown(self):
super(Run_with_quotasTest, self).tearDown()
QUOTAS.reserve = self.quota_reserve_orig
QUOTAS.rollback = self.quota_rollback_orig
QUOTAS.commit = self.quota_commit_orig
def test_run_with_quotas(self):
f = Mock()
run_with_quotas(FAKE_TENANT1, {'instances': 1, 'volumes': 5}, f)
self.assertTrue(QUOTAS.reserve.called)
self.assertTrue(QUOTAS.commit.called)
self.assertFalse(QUOTAS.rollback.called)
self.assertTrue(f.called)
def test_run_with_quotas_error(self):
f = Mock(side_effect=Exception())
self.assertRaises(Exception, run_with_quotas, FAKE_TENANT1,
{'instances': 1, 'volumes': 5}, f)
self.assertTrue(QUOTAS.reserve.called)
self.assertTrue(QUOTAS.rollback.called)
self.assertFalse(QUOTAS.commit.called)
self.assertTrue(f.called)
class DbQuotaDriverTest(testtools.TestCase):
def setUp(self):
super(DbQuotaDriverTest, self).setUp()
self.driver = DbQuotaDriver(resources)
self.orig_Quota_find_all = Quota.find_all
self.orig_QuotaUsage_find_all = QuotaUsage.find_all
self.orig_QuotaUsage_find_by = QuotaUsage.find_by
self.orig_Reservation_create = Reservation.create
self.orig_QuotaUsage_create = QuotaUsage.create
self.orig_QuotaUsage_save = QuotaUsage.save
self.orig_Reservation_save = Reservation.save
self.mock_quota_result = Mock()
self.mock_usage_result = Mock()
Quota.find_all = Mock(return_value=self.mock_quota_result)
QuotaUsage.find_all = Mock(return_value=self.mock_usage_result)
def tearDown(self):
super(DbQuotaDriverTest, self).tearDown()
Quota.find_all = self.orig_Quota_find_all
QuotaUsage.find_all = self.orig_QuotaUsage_find_all
QuotaUsage.find_by = self.orig_QuotaUsage_find_by
Reservation.create = self.orig_Reservation_create
QuotaUsage.create = self.orig_QuotaUsage_create
QuotaUsage.save = self.orig_QuotaUsage_save
Reservation.save = self.orig_Reservation_save
def test_get_defaults(self):
defaults = self.driver.get_defaults(resources)
self.assertEqual(CONF.max_instances_per_user,
defaults[Resource.INSTANCES])
self.assertEqual(CONF.max_volumes_per_user,
defaults[Resource.VOLUMES])
def test_get_quota_by_tenant(self):
FAKE_QUOTAS = [Quota(tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
hard_limit=12)]
self.mock_quota_result.all = Mock(return_value=FAKE_QUOTAS)
quota = self.driver.get_quota_by_tenant(FAKE_TENANT1,
Resource.VOLUMES)
self.assertEquals(FAKE_TENANT1, quota.tenant_id)
self.assertEquals(Resource.INSTANCES, quota.resource)
self.assertEquals(12, quota.hard_limit)
def test_get_quota_by_tenant_default(self):
self.mock_quota_result.all = Mock(return_value=[])
quota = self.driver.get_quota_by_tenant(FAKE_TENANT1,
Resource.VOLUMES)
self.assertEquals(FAKE_TENANT1, quota.tenant_id)
self.assertEquals(Resource.VOLUMES, quota.resource)
self.assertEquals(CONF.max_volumes_per_user, quota.hard_limit)
def test_get_all_quotas_by_tenant(self):
FAKE_QUOTAS = [Quota(tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
hard_limit=22),
Quota(tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
hard_limit=15)]
self.mock_quota_result.all = Mock(return_value=FAKE_QUOTAS)
quotas = self.driver.get_all_quotas_by_tenant(FAKE_TENANT1,
resources.keys())
self.assertEquals(FAKE_TENANT1, quotas[Resource.INSTANCES].tenant_id)
self.assertEquals(Resource.INSTANCES,
quotas[Resource.INSTANCES].resource)
self.assertEquals(22, quotas[Resource.INSTANCES].hard_limit)
self.assertEquals(FAKE_TENANT1, quotas[Resource.VOLUMES].tenant_id)
self.assertEquals(Resource.VOLUMES, quotas[Resource.VOLUMES].resource)
self.assertEquals(15, quotas[Resource.VOLUMES].hard_limit)
def test_get_all_quotas_by_tenant_with_all_default(self):
self.mock_quota_result.all = Mock(return_value=[])
quotas = self.driver.get_all_quotas_by_tenant(FAKE_TENANT1,
resources.keys())
self.assertEquals(FAKE_TENANT1, quotas[Resource.INSTANCES].tenant_id)
self.assertEquals(Resource.INSTANCES,
quotas[Resource.INSTANCES].resource)
self.assertEquals(CONF.max_instances_per_user,
quotas[Resource.INSTANCES].hard_limit)
self.assertEquals(FAKE_TENANT1, quotas[Resource.VOLUMES].tenant_id)
self.assertEquals(Resource.VOLUMES, quotas[Resource.VOLUMES].resource)
self.assertEquals(CONF.max_volumes_per_user,
quotas[Resource.VOLUMES].hard_limit)
def test_get_all_quotas_by_tenant_with_one_default(self):
FAKE_QUOTAS = [Quota(tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
hard_limit=22)]
self.mock_quota_result.all = Mock(return_value=FAKE_QUOTAS)
quotas = self.driver.get_all_quotas_by_tenant(FAKE_TENANT1,
resources.keys())
self.assertEquals(FAKE_TENANT1, quotas[Resource.INSTANCES].tenant_id)
self.assertEquals(Resource.INSTANCES,
quotas[Resource.INSTANCES].resource)
self.assertEquals(22, quotas[Resource.INSTANCES].hard_limit)
self.assertEquals(FAKE_TENANT1, quotas[Resource.VOLUMES].tenant_id)
self.assertEquals(Resource.VOLUMES, quotas[Resource.VOLUMES].resource)
self.assertEquals(CONF.max_volumes_per_user,
quotas[Resource.VOLUMES].hard_limit)
def test_get_quota_usage_by_tenant(self):
FAKE_QUOTAS = [QuotaUsage(tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=3,
reserved=1)]
self.mock_usage_result.all = Mock(return_value=FAKE_QUOTAS)
usage = self.driver.get_quota_usage_by_tenant(FAKE_TENANT1,
Resource.VOLUMES)
self.assertEquals(FAKE_TENANT1, usage.tenant_id)
self.assertEquals(Resource.VOLUMES, usage.resource)
self.assertEquals(3, usage.in_use)
self.assertEquals(1, usage.reserved)
def test_get_quota_usage_by_tenant_default(self):
FAKE_QUOTA = QuotaUsage(tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=0,
reserved=0)
self.mock_usage_result.all = Mock(return_value=[])
QuotaUsage.create = Mock(return_value=FAKE_QUOTA)
usage = self.driver.get_quota_usage_by_tenant(FAKE_TENANT1,
Resource.VOLUMES)
self.assertEquals(FAKE_TENANT1, usage.tenant_id)
self.assertEquals(Resource.VOLUMES, usage.resource)
self.assertEquals(0, usage.in_use)
self.assertEquals(0, usage.reserved)
def test_get_all_quota_usages_by_tenant(self):
FAKE_QUOTAS = [QuotaUsage(tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
in_use=2,
reserved=1),
QuotaUsage(tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=1,
reserved=1)]
self.mock_usage_result.all = Mock(return_value=FAKE_QUOTAS)
usages = self.driver.get_all_quota_usages_by_tenant(FAKE_TENANT1,
resources.keys())
self.assertEquals(FAKE_TENANT1, usages[Resource.INSTANCES].tenant_id)
self.assertEquals(Resource.INSTANCES,
usages[Resource.INSTANCES].resource)
self.assertEquals(2, usages[Resource.INSTANCES].in_use)
self.assertEquals(1, usages[Resource.INSTANCES].reserved)
self.assertEquals(FAKE_TENANT1, usages[Resource.VOLUMES].tenant_id)
self.assertEquals(Resource.VOLUMES, usages[Resource.VOLUMES].resource)
self.assertEquals(1, usages[Resource.VOLUMES].in_use)
self.assertEquals(1, usages[Resource.VOLUMES].reserved)
def test_get_all_quota_usages_by_tenant_with_all_default(self):
FAKE_QUOTAS = [QuotaUsage(tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
in_use=0,
reserved=0),
QuotaUsage(tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=0,
reserved=0)]
self.mock_usage_result.all = Mock(return_value=[])
QuotaUsage.create = Mock(side_effect=FAKE_QUOTAS)
usages = self.driver.get_all_quota_usages_by_tenant(FAKE_TENANT1,
resources.keys())
self.assertEquals(FAKE_TENANT1, usages[Resource.INSTANCES].tenant_id)
self.assertEquals(Resource.INSTANCES,
usages[Resource.INSTANCES].resource)
self.assertEquals(0, usages[Resource.INSTANCES].in_use)
self.assertEquals(0, usages[Resource.INSTANCES].reserved)
self.assertEquals(FAKE_TENANT1, usages[Resource.VOLUMES].tenant_id)
self.assertEquals(Resource.VOLUMES, usages[Resource.VOLUMES].resource)
self.assertEquals(0, usages[Resource.VOLUMES].in_use)
self.assertEquals(0, usages[Resource.VOLUMES].reserved)
def test_get_all_quota_usages_by_tenant_with_one_default(self):
FAKE_QUOTAS = [QuotaUsage(tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
in_use=0,
reserved=0)]
NEW_FAKE_QUOTA = QuotaUsage(tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=0,
reserved=0)
self.mock_usage_result.all = Mock(return_value=FAKE_QUOTAS)
QuotaUsage.create = Mock(return_value=NEW_FAKE_QUOTA)
usages = self.driver.get_all_quota_usages_by_tenant(FAKE_TENANT1,
resources.keys())
self.assertEquals(FAKE_TENANT1, usages[Resource.INSTANCES].tenant_id)
self.assertEquals(Resource.INSTANCES,
usages[Resource.INSTANCES].resource)
self.assertEquals(0, usages[Resource.INSTANCES].in_use)
self.assertEquals(0, usages[Resource.INSTANCES].reserved)
self.assertEquals(FAKE_TENANT1, usages[Resource.VOLUMES].tenant_id)
self.assertEquals(Resource.VOLUMES, usages[Resource.VOLUMES].resource)
self.assertEquals(0, usages[Resource.VOLUMES].in_use)
self.assertEquals(0, usages[Resource.VOLUMES].reserved)
def test_reserve(self):
FAKE_QUOTAS = [QuotaUsage(id=1,
tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
in_use=1,
reserved=2),
QuotaUsage(id=2,
tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=1,
reserved=1)]
self.mock_quota_result.all = Mock(return_value=[])
self.mock_usage_result.all = Mock(return_value=FAKE_QUOTAS)
QuotaUsage.save = Mock()
Reservation.create = Mock()
delta = {'instances': 2, 'volumes': 3}
self.driver.reserve(FAKE_TENANT1, resources, delta)
_, kw = Reservation.create.call_args_list[0]
self.assertEquals(1, kw['usage_id'])
self.assertEquals(2, kw['delta'])
self.assertEquals(Reservation.Statuses.RESERVED, kw['status'])
_, kw = Reservation.create.call_args_list[1]
self.assertEquals(2, kw['usage_id'])
self.assertEquals(3, kw['delta'])
self.assertEquals(Reservation.Statuses.RESERVED, kw['status'])
def test_reserve_resource_unknown(self):
delta = {'instances': 10, 'volumes': 2000, 'Fake_resource': 123}
self.assertRaises(exception.QuotaResourceUnknown,
self.driver.reserve,
FAKE_TENANT1,
resources,
delta)
def test_reserve_over_quota(self):
FAKE_QUOTAS = [QuotaUsage(id=1,
tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
in_use=0,
reserved=0),
QuotaUsage(id=2,
tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=0,
reserved=0)]
self.mock_quota_result.all = Mock(return_value=[])
self.mock_usage_result.all = Mock(return_value=FAKE_QUOTAS)
delta = {'instances': 1, 'volumes': CONF.max_volumes_per_user + 1}
self.assertRaises(exception.QuotaExceeded,
self.driver.reserve,
FAKE_TENANT1,
resources,
delta)
def test_reserve_over_quota_with_usage(self):
FAKE_QUOTAS = [QuotaUsage(id=1,
tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
in_use=1,
reserved=0),
QuotaUsage(id=2,
tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=0,
reserved=0)]
self.mock_quota_result.all = Mock(return_value=[])
self.mock_usage_result.all = Mock(return_value=FAKE_QUOTAS)
delta = {'instances': 5, 'volumes': 3}
self.assertRaises(exception.QuotaExceeded,
self.driver.reserve,
FAKE_TENANT1,
resources,
delta)
def test_reserve_over_quota_with_reserved(self):
FAKE_QUOTAS = [QuotaUsage(id=1,
tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
in_use=1,
reserved=2),
QuotaUsage(id=2,
tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=0,
reserved=0)]
self.mock_quota_result.all = Mock(return_value=[])
self.mock_usage_result.all = Mock(return_value=FAKE_QUOTAS)
delta = {'instances': 4, 'volumes': 2}
self.assertRaises(exception.QuotaExceeded,
self.driver.reserve,
FAKE_TENANT1,
resources,
delta)
def test_commit(self):
Reservation.save = Mock()
QuotaUsage.save = Mock()
FAKE_QUOTAS = [QuotaUsage(id=1,
tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
in_use=5,
reserved=2),
QuotaUsage(id=2,
tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=1,
reserved=2)]
FAKE_RESERVATIONS = [Reservation(usage_id=1,
delta=1,
status=Reservation.Statuses.RESERVED),
Reservation(usage_id=2,
delta=2,
status=Reservation.Statuses.RESERVED)]
QuotaUsage.find_by = Mock(side_effect=FAKE_QUOTAS)
self.driver.commit(FAKE_RESERVATIONS)
self.assertEqual(6, FAKE_QUOTAS[0].in_use)
self.assertEqual(1, FAKE_QUOTAS[0].reserved)
self.assertEqual(Reservation.Statuses.COMMITTED,
FAKE_RESERVATIONS[0].status)
self.assertEqual(3, FAKE_QUOTAS[1].in_use)
self.assertEqual(0, FAKE_QUOTAS[1].reserved)
self.assertEqual(Reservation.Statuses.COMMITTED,
FAKE_RESERVATIONS[1].status)
def test_rollback(self):
Reservation.save = Mock()
QuotaUsage.save = Mock()
FAKE_QUOTAS = [QuotaUsage(id=1,
tenant_id=FAKE_TENANT1,
resource=Resource.INSTANCES,
in_use=5,
reserved=2),
QuotaUsage(id=2,
tenant_id=FAKE_TENANT1,
resource=Resource.VOLUMES,
in_use=1,
reserved=2)]
FAKE_RESERVATIONS = [Reservation(usage_id=1,
delta=1,
status=Reservation.Statuses.RESERVED),
Reservation(usage_id=2,
delta=2,
status=Reservation.Statuses.RESERVED)]
QuotaUsage.find_by = Mock(side_effect=FAKE_QUOTAS)
self.driver.rollback(FAKE_RESERVATIONS)
self.assertEqual(5, FAKE_QUOTAS[0].in_use)
self.assertEqual(1, FAKE_QUOTAS[0].reserved)
self.assertEqual(Reservation.Statuses.ROLLEDBACK,
FAKE_RESERVATIONS[0].status)
self.assertEqual(1, FAKE_QUOTAS[1].in_use)
self.assertEqual(0, FAKE_QUOTAS[1].reserved)
self.assertEqual(Reservation.Statuses.ROLLEDBACK,
FAKE_RESERVATIONS[1].status)