diff --git a/etc/reddwarf/reddwarf.conf.sample b/etc/reddwarf/reddwarf.conf.sample index 720c10622c..7399fbc6a0 100644 --- a/etc/reddwarf/reddwarf.conf.sample +++ b/etc/reddwarf/reddwarf.conf.sample @@ -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 diff --git a/etc/reddwarf/reddwarf.conf.test b/etc/reddwarf/reddwarf.conf.test index 5bf787b884..03a0569d01 100644 --- a/etc/reddwarf/reddwarf.conf.test +++ b/etc/reddwarf/reddwarf.conf.test @@ -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 diff --git a/etc/tests/core.test.conf b/etc/tests/core.test.conf index 1a107c0867..f08192f821 100644 --- a/etc/tests/core.test.conf +++ b/etc/tests/core.test.conf @@ -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, diff --git a/reddwarf/common/cfg.py b/reddwarf/common/cfg.py index 319ef7dbfd..937ba7501b 100644 --- a/reddwarf/common/cfg.py +++ b/reddwarf/common/cfg.py @@ -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'), diff --git a/reddwarf/common/exception.py b/reddwarf/common/exception.py index e1e43775ef..f0a3722c3b 100644 --- a/reddwarf/common/exception.py +++ b/reddwarf/common/exception.py @@ -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.") diff --git a/reddwarf/common/wsgi.py b/reddwarf/common/wsgi.py index 775cdc8ab1..673d6f3855 100644 --- a/reddwarf/common/wsgi.py +++ b/reddwarf/common/wsgi.py @@ -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: [ diff --git a/reddwarf/db/sqlalchemy/mappers.py b/reddwarf/db/sqlalchemy/mappers.py index 0ab39e45cb..1e2e5eb0e5 100644 --- a/reddwarf/db/sqlalchemy/mappers.py +++ b/reddwarf/db/sqlalchemy/mappers.py @@ -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): diff --git a/reddwarf/db/sqlalchemy/migrate_repo/versions/011_quota.py b/reddwarf/db/sqlalchemy/migrate_repo/versions/011_quota.py new file mode 100644 index 0000000000..ccbc85ad22 --- /dev/null +++ b/reddwarf/db/sqlalchemy/migrate_repo/versions/011_quota.py @@ -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]) diff --git a/reddwarf/db/sqlalchemy/session.py b/reddwarf/db/sqlalchemy/session.py index 3b197c5224..d7d4c73814 100644 --- a/reddwarf/db/sqlalchemy/session.py +++ b/reddwarf/db/sqlalchemy/session.py @@ -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 = {} diff --git a/reddwarf/extensions/mgmt.py b/reddwarf/extensions/mgmt.py index 21b34a2002..f2c3d679fc 100644 --- a/reddwarf/extensions/mgmt.py +++ b/reddwarf/extensions/mgmt.py @@ -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(), diff --git a/reddwarf/extensions/mgmt/quota/__init__.py b/reddwarf/extensions/mgmt/quota/__init__.py new file mode 100644 index 0000000000..cbf4a45060 --- /dev/null +++ b/reddwarf/extensions/mgmt/quota/__init__.py @@ -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. diff --git a/reddwarf/extensions/mgmt/quota/service.py b/reddwarf/extensions/mgmt/quota/service.py new file mode 100644 index 0000000000..c2cf4a9421 --- /dev/null +++ b/reddwarf/extensions/mgmt/quota/service.py @@ -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) diff --git a/reddwarf/extensions/mgmt/quota/views.py b/reddwarf/extensions/mgmt/quota/views.py new file mode 100644 index 0000000000..b4f799ce76 --- /dev/null +++ b/reddwarf/extensions/mgmt/quota/views.py @@ -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 diff --git a/reddwarf/instance/models.py b/reddwarf/instance/models.py index ed8d7252a0..2de461a36b 100644 --- a/reddwarf/instance/models.py +++ b/reddwarf/instance/models.py @@ -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() diff --git a/reddwarf/instance/service.py b/reddwarf/instance/service.py index 05d45d79b3..cf4c3b67d8 100644 --- a/reddwarf/instance/service.py +++ b/reddwarf/instance/service.py @@ -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): diff --git a/reddwarf/quota/__init__.py b/reddwarf/quota/__init__.py new file mode 100644 index 0000000000..d65c689a83 --- /dev/null +++ b/reddwarf/quota/__init__.py @@ -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. diff --git a/reddwarf/quota/models.py b/reddwarf/quota/models.py new file mode 100644 index 0000000000..1dab7d9b95 --- /dev/null +++ b/reddwarf/quota/models.py @@ -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 diff --git a/reddwarf/quota/quota.py b/reddwarf/quota/quota.py new file mode 100644 index 0000000000..3a7d1d9086 --- /dev/null +++ b/reddwarf/quota/quota.py @@ -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) diff --git a/reddwarf/tests/api/instances.py b/reddwarf/tests/api/instances.py index 7025d96d6f..8d43455be3 100644 --- a/reddwarf/tests/api/instances.py +++ b/reddwarf/tests/api/instances.py @@ -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", diff --git a/reddwarf/tests/unittests/quota/__init__.py b/reddwarf/tests/unittests/quota/__init__.py new file mode 100644 index 0000000000..40d014dd8b --- /dev/null +++ b/reddwarf/tests/unittests/quota/__init__.py @@ -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. diff --git a/reddwarf/tests/unittests/quota/test_quota.py b/reddwarf/tests/unittests/quota/test_quota.py new file mode 100644 index 0000000000..d948c5eab4 --- /dev/null +++ b/reddwarf/tests/unittests/quota/test_quota.py @@ -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)