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:
parent
460aa6abde
commit
c4848cd472
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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'),
|
||||
|
@ -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.")
|
||||
|
@ -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: [
|
||||
|
@ -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):
|
||||
|
67
reddwarf/db/sqlalchemy/migrate_repo/versions/011_quota.py
Normal file
67
reddwarf/db/sqlalchemy/migrate_repo/versions/011_quota.py
Normal 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])
|
@ -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 = {}
|
||||
|
@ -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(),
|
||||
|
16
reddwarf/extensions/mgmt/quota/__init__.py
Normal file
16
reddwarf/extensions/mgmt/quota/__init__.py
Normal 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.
|
61
reddwarf/extensions/mgmt/quota/service.py
Normal file
61
reddwarf/extensions/mgmt/quota/service.py
Normal 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)
|
28
reddwarf/extensions/mgmt/quota/views.py
Normal file
28
reddwarf/extensions/mgmt/quota/views.py
Normal 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
|
@ -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()
|
||||
|
@ -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):
|
||||
|
16
reddwarf/quota/__init__.py
Normal file
16
reddwarf/quota/__init__.py
Normal 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
108
reddwarf/quota/models.py
Normal 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
317
reddwarf/quota/quota.py
Normal 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)
|
@ -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",
|
||||
|
13
reddwarf/tests/unittests/quota/__init__.py
Normal file
13
reddwarf/tests/unittests/quota/__init__.py
Normal 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.
|
492
reddwarf/tests/unittests/quota/test_quota.py
Normal file
492
reddwarf/tests/unittests/quota/test_quota.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user