Support quota in mogan(part two)

This patch introduces the quota operation in
creating and deleting instances process.

Change-Id: If6573fafc5acb805cf727acdc8f0f9872fc1a717
Implements: blueprint quota-support
This commit is contained in:
wanghao 2017-03-09 15:53:38 +08:00
parent 3736848733
commit efa63af620
9 changed files with 167 additions and 59 deletions

View File

@ -20,6 +20,7 @@ from mogan.common.i18n import _
quota_opts = [ quota_opts = [
cfg.StrOpt('quota_driver', cfg.StrOpt('quota_driver',
default="database",
help=_("Specify the quota driver which is used in Mogan " help=_("Specify the quota driver which is used in Mogan "
"service.")), "service.")),
cfg.IntOpt('reservation_expire', cfg.IntOpt('reservation_expire',

View File

@ -183,7 +183,7 @@ def upgrade():
sa.Column('until_refresh', sa.Integer(), nullable=True), sa.Column('until_refresh', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('resource_name', 'project_id', sa.UniqueConstraint('resource_name', 'project_id',
name='uniq_quotas0resource_name'), name='uniq_quotas_usages0resource_name'),
mysql_ENGINE='InnoDB', mysql_ENGINE='InnoDB',
mysql_DEFAULT_CHARSET='UTF8' mysql_DEFAULT_CHARSET='UTF8'
) )
@ -193,8 +193,8 @@ def upgrade():
sa.Column('updated_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True), sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('usage_id', sa.Integer(), nullable=False), sa.Column('usage_id', sa.Integer(), nullable=True),
sa.Column('allocated_id', sa.Integer(), nullable=False), sa.Column('allocated_id', sa.Integer(), nullable=True),
sa.Column('project_id', sa.String(length=36), nullable=True), sa.Column('project_id', sa.String(length=36), nullable=True),
sa.Column('resource_name', sa.String(length=255), nullable=True), sa.Column('resource_name', sa.String(length=255), nullable=True),
sa.Column('delta', sa.Integer(), nullable=True), sa.Column('delta', sa.Integer(), nullable=True),

View File

@ -239,7 +239,7 @@ class Quota(Base):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
resource_name = Column(String(255), nullable=False) resource_name = Column(String(255), nullable=False)
project_id = Column(String(36), nullable=False) project_id = Column(String(36), nullable=False)
hard_limit = Column(Integer, nullable=False) hard_limit = Column(Integer, default=0)
allocated = Column(Integer, default=0) allocated = Column(Integer, default=0)

View File

@ -28,6 +28,7 @@ from mogan.engine import rpcapi
from mogan import image from mogan import image
from mogan import network from mogan import network
from mogan import objects from mogan import objects
from mogan.objects import quota
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -58,6 +59,8 @@ class API(object):
self.image_api = image_api or image.API() self.image_api = image_api or image.API()
self.engine_rpcapi = rpcapi.EngineAPI() self.engine_rpcapi = rpcapi.EngineAPI()
self.network_api = network.API() self.network_api = network.API()
self.quota = quota.Quota()
self.quota.register_resource(objects.quota.InstanceResource())
def _get_image(self, context, image_uuid): def _get_image(self, context, image_uuid):
return self.image_api.get(context, image_uuid) return self.image_api.get(context, image_uuid)
@ -133,15 +136,32 @@ class API(object):
return instance return instance
def _check_num_instances_quota(self, context, min_count, max_count): def _check_num_instances_quota(self, context, min_count, max_count):
# TODO(little): check quotas and return reserved quotas ins_resource = self.quota.resources['instances']
return max_count, None quotas = self.quota.get_quota_limit_and_usage(context,
{'instances':
ins_resource},
context.tenant)
limit = quotas['instances']['limit']
in_use = quotas['instances']['in_use']
reserved = quotas['instances']['reserved']
available_quota = limit - in_use - reserved
if max_count <= available_quota:
return max_count
elif min_count <= available_quota and max_count > available_quota:
return available_quota
else:
raise exception.OverQuota(overs='instances')
def _provision_instances(self, context, base_options, def _provision_instances(self, context, base_options,
min_count, max_count): min_count, max_count):
# TODO(little): finish to return num_instances according quota # Return num_instances according quota
num_instances, quotas = self._check_num_instances_quota( num_instances = self._check_num_instances_quota(
context, min_count, max_count) context, min_count, max_count)
# Create the instances reservations
reserve_opts = {'instances': num_instances}
reservations = self.quota.reserve(context, **reserve_opts)
LOG.debug("Going to run %s instances...", num_instances) LOG.debug("Going to run %s instances...", num_instances)
instances = [] instances = []
@ -164,8 +184,11 @@ class API(object):
except exception.ObjectActionError: except exception.ObjectActionError:
pass pass
finally: finally:
# TODO(little): quota release self.quota.rollback(context, reservations)
pass
# Commit instances reservations
if reservations:
self.quota.commit(context, reservations)
return instances return instances
@ -261,6 +284,10 @@ class API(object):
LOG.debug("Instance is not found while deleting", LOG.debug("Instance is not found while deleting",
instance=instance) instance=instance)
return return
reserve_opts = {'instances': -1}
reservations = self.quota.reserve(context, **reserve_opts)
if reservations:
self.quota.commit(context, reservations)
self.engine_rpcapi.delete_instance(context, instance) self.engine_rpcapi.delete_instance(context, instance)
@check_instance_lock @check_instance_lock

View File

@ -30,6 +30,7 @@ from mogan.engine.flows import create_instance
from mogan.notifications import base as notifications from mogan.notifications import base as notifications
from mogan import objects from mogan import objects
from mogan.objects import fields from mogan.objects import fields
from mogan.objects import quota
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -41,6 +42,11 @@ class EngineManager(base_manager.BaseEngineManager):
target = messaging.Target(version=RPC_API_VERSION) target = messaging.Target(version=RPC_API_VERSION)
def __init__(self, *args, **kwargs):
super(EngineManager, self).__init__(*args, **kwargs)
self.quota = quota.Quota()
self.quota.register_resource(objects.quota.InstanceResource())
def _get_compute_port(self, context, port_uuid): def _get_compute_port(self, context, port_uuid):
"""Gets compute port by the uuid.""" """Gets compute port by the uuid."""
try: try:
@ -298,6 +304,12 @@ class EngineManager(base_manager.BaseEngineManager):
for port in ports: for port in ports:
self.network_api.delete_port(context, port, instance.uuid) self.network_api.delete_port(context, port, instance.uuid)
def _rollback_instances_quota(self, context, number):
reserve_opts = {'instances': number}
reservations = self.quota.reserve(context, **reserve_opts)
if reservations:
self.quota.commit(context, reservations)
def create_instance(self, context, instance, requested_networks, def create_instance(self, context, instance, requested_networks,
request_spec=None, filter_properties=None): request_spec=None, filter_properties=None):
"""Perform a deployment.""" """Perform a deployment."""
@ -353,6 +365,7 @@ class EngineManager(base_manager.BaseEngineManager):
) )
except Exception: except Exception:
utils.process_event(fsm, instance, event='error') utils.process_event(fsm, instance, event='error')
self._rollback_instances_quota(context, -1)
msg = _("Create manager instance flow failed.") msg = _("Create manager instance flow failed.")
LOG.exception(msg) LOG.exception(msg)
raise exception.MoganException(msg) raise exception.MoganException(msg)
@ -370,6 +383,7 @@ class EngineManager(base_manager.BaseEngineManager):
except Exception as e: except Exception as e:
instance.power_state = states.NOSTATE instance.power_state = states.NOSTATE
utils.process_event(fsm, instance, event='error') utils.process_event(fsm, instance, event='error')
self._rollback_instances_quota(context, -1)
LOG.error("Created instance %(uuid)s failed." LOG.error("Created instance %(uuid)s failed."
"Exception: %(exception)s", "Exception: %(exception)s",
{"uuid": instance.uuid, {"uuid": instance.uuid,
@ -426,6 +440,7 @@ class EngineManager(base_manager.BaseEngineManager):
instance=instance) instance=instance)
instance.power_state = states.NOSTATE instance.power_state = states.NOSTATE
utils.process_event(fsm, instance, event='error') utils.process_event(fsm, instance, event='error')
self._rollback_instances_quota(context, 1)
# Issue delete request to driver only if instance is associated with # Issue delete request to driver only if instance is associated with
# a underlying node. # a underlying node.

View File

@ -19,10 +19,10 @@
import datetime import datetime
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import importutils
from oslo_utils import timeutils from oslo_utils import timeutils
from oslo_versionedobjects import base as object_base from oslo_versionedobjects import base as object_base
import six import six
from stevedore import driver
from mogan.common import exception from mogan.common import exception
from mogan.db import api as dbapi from mogan.db import api as dbapi
@ -50,7 +50,9 @@ class Quota(base.MoganObject, object_base.VersionedObjectDictCompat):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Quota, self).__init__(*args, **kwargs) super(Quota, self).__init__(*args, **kwargs)
self.quota_driver = importutils.import_object(CONF.quota.quota_driver) self.quota_driver = driver.DriverManager(
'mogan.quota.backend_driver', CONF.quota.quota_driver,
invoke_on_load=True).driver
self._resources = {} self._resources = {}
@property @property
@ -106,9 +108,8 @@ class Quota(base.MoganObject, object_base.VersionedObjectDictCompat):
def reserve(self, context, expire=None, project_id=None, **deltas): def reserve(self, context, expire=None, project_id=None, **deltas):
"""reserve the Quota.""" """reserve the Quota."""
return self.quota_driver.reserver(context, self.resources, deltas, return self.quota_driver.reserve(context, self.resources, deltas,
expire=expire, expire=expire, project_id=project_id)
project_id=project_id)
def commit(self, context, reservations, project_id=None): def commit(self, context, reservations, project_id=None):
self.quota_driver.commit(context, reservations, project_id=project_id) self.quota_driver.commit(context, reservations, project_id=project_id)
@ -150,6 +151,10 @@ class Quota(base.MoganObject, object_base.VersionedObjectDictCompat):
for resource in resources: for resource in resources:
self.register_resource(resource) self.register_resource(resource)
def get_quota_limit_and_usage(self, context, resources, project_id):
return self.quota_driver.get_project_quotas(context, resources,
project_id, usages=True)
class DbQuotaDriver(object): class DbQuotaDriver(object):
@ -159,9 +164,9 @@ class DbQuotaDriver(object):
The default driver utilizes the local database. The default driver utilizes the local database.
""" """
def get_project_quotas(self, context, resources, project_id, dbapi = dbapi.get_instance()
quota_class=None, defaults=True,
usages=True): def get_project_quotas(self, context, resources, project_id, usages=True):
"""Retrieve quotas for a project. """Retrieve quotas for a project.
Given a list of resources, retrieve the quotas for the given Given a list of resources, retrieve the quotas for the given
@ -170,32 +175,31 @@ class DbQuotaDriver(object):
:param context: The request context, for access checks. :param context: The request context, for access checks.
:param resources: A dictionary of the registered resources. :param resources: A dictionary of the registered resources.
:param project_id: The ID of the project to return quotas for. :param project_id: The ID of the project to return quotas for.
:param quota_class: If project_id != context.tenant, the
quota class cannot be determined. This
parameter allows it to be specified. It
will be ignored if project_id ==
context.tenant.
:param defaults: If True, the quota class value (or the
default value, if there is no value from the
quota class) will be reported if there is no
specific value for the resource.
:param usages: If True, the current in_use, reserved and allocated :param usages: If True, the current in_use, reserved and allocated
counts will also be returned. counts will also be returned.
""" """
quotas = {} quotas = {}
project_quotas = dbapi.quota_get_all_by_project(context, project_id) project_quotas = {}
res = self.dbapi.quota_get_all_by_project(context, project_id)
for p_quota in res:
project_quotas[p_quota.resource_name] = p_quota.hard_limit
if project_quotas == {}:
self.dbapi.quota_create(context, {'resource_name': 'instances',
'project_id': project_id,
'hard_limit': 10,
'allocated': 0})
project_quotas['instances'] = 10
allocated_quotas = None allocated_quotas = None
if usages: if usages:
project_usages = dbapi.quota_usage_get_all_by_project(context, project_usages = self.dbapi.quota_usage_get_all_by_project(
project_id) context, project_id)
allocated_quotas = dbapi.quota_allocated_get_all_by_project( allocated_quotas = self.dbapi.quota_allocated_get_all_by_project(
context, project_id) context, project_id)
allocated_quotas.pop('project_id') allocated_quotas.pop('project_id')
for resource in resources.values(): for resource in resources.values():
# Omit default/quota class values if resource.name not in project_quotas:
if not defaults and resource.name not in project_quotas:
continue continue
quota_val = project_quotas.get(resource.name) quota_val = project_quotas.get(resource.name)
@ -250,8 +254,7 @@ class DbQuotaDriver(object):
# Grab and return the quotas (without usages) # Grab and return the quotas (without usages)
quotas = self.get_project_quotas(context, sub_resources, quotas = self.get_project_quotas(context, sub_resources,
project_id, project_id, usages=False)
context.quota_class, usages=False)
return {k: v['limit'] for k, v in quotas.items()} return {k: v['limit'] for k, v in quotas.items()}
@ -313,10 +316,11 @@ class DbQuotaDriver(object):
project_id) project_id)
def _reserve(self, context, resources, quotas, deltas, expire, project_id): def _reserve(self, context, resources, quotas, deltas, expire, project_id):
return dbapi.quota_reserve(context, resources, quotas, deltas, expire, return self.dbapi.quota_reserve(context, resources, quotas, deltas,
CONF.quota.until_refresh, expire,
CONF.quota.max_age, CONF.quota.until_refresh,
project_id=project_id) CONF.quota.max_age,
project_id)
def commit(self, context, reservations, project_id=None): def commit(self, context, reservations, project_id=None):
"""Commit reservations. """Commit reservations.
@ -332,7 +336,8 @@ class DbQuotaDriver(object):
if project_id is None: if project_id is None:
project_id = context.tenant project_id = context.tenant
dbapi.reservation_commit(context, reservations, project_id=project_id) self.dbapi.reservation_commit(context, reservations,
project_id=project_id)
def rollback(self, context, reservations, project_id=None): def rollback(self, context, reservations, project_id=None):
"""Roll back reservations. """Roll back reservations.
@ -348,8 +353,8 @@ class DbQuotaDriver(object):
if project_id is None: if project_id is None:
project_id = context.tenant project_id = context.tenant
dbapi.reservation_rollback(context, reservations, self.dbapi.reservation_rollback(context, reservations,
project_id=project_id) project_id=project_id)
def expire(self, context): def expire(self, context):
"""Expire reservations. """Expire reservations.
@ -360,7 +365,7 @@ class DbQuotaDriver(object):
:param context: The request context, for access checks. :param context: The request context, for access checks.
""" """
dbapi.reservation_expire(context) self.dbapi.reservation_expire(context)
class BaseResource(object): class BaseResource(object):

View File

@ -71,7 +71,11 @@ class TestInstances(v1_test.APITestV1):
INSTANCE_UUIDS = ['59f1b681-6ca4-4a17-b784-297a7285004e', INSTANCE_UUIDS = ['59f1b681-6ca4-4a17-b784-297a7285004e',
'2b32fc87-576c-481b-880e-bef8c7351746', '2b32fc87-576c-481b-880e-bef8c7351746',
'482decff-7561-41ad-9bfb-447265b26972', '482decff-7561-41ad-9bfb-447265b26972',
'427693e1-a820-4d7d-8a92-9f5fe2849399'] '427693e1-a820-4d7d-8a92-9f5fe2849399',
'253b2878-ec60-4793-ad19-e65496ec7aab',
'f26f181d-7891-4720-b022-b074ec1733ef',
'02f53bd8-3514-485b-ba60-2722ef09c016',
'8f7495fe-5e44-4f33-81af-4b28e9b2952f']
def setUp(self): def setUp(self):
self.rpc_api = mock.Mock() self.rpc_api = mock.Mock()
@ -113,8 +117,12 @@ class TestInstances(v1_test.APITestV1):
@mock.patch('oslo_utils.uuidutils.generate_uuid') @mock.patch('oslo_utils.uuidutils.generate_uuid')
def _prepare_instance(self, amount, mocked): def _prepare_instance(self, amount, mocked):
mocked.side_effect = self.INSTANCE_UUIDS[:amount] # NOTE(wanghao): Since we added quota reserve in creation option,
# there is one more generate_uuid out of provision_instances, so
# amount should *2 here.
mocked.side_effect = self.INSTANCE_UUIDS[:(amount * 2)]
responses = [] responses = []
headers = self.gen_headers(self.context)
for i in six.moves.xrange(amount): for i in six.moves.xrange(amount):
test_body = { test_body = {
"name": "test_instance_" + str(i), "name": "test_instance_" + str(i),
@ -127,14 +135,15 @@ class TestInstances(v1_test.APITestV1):
'extra': {'fake_key': 'fake_value'} 'extra': {'fake_key': 'fake_value'}
} }
responses.append( responses.append(
self.post_json('/instances', test_body, status=201)) self.post_json('/instances', test_body, headers=headers,
status=201))
return responses return responses
def test_instance_post(self): def test_instance_post(self):
resp = self._prepare_instance(1)[0].json resp = self._prepare_instance(1)[0].json
self.assertEqual('test_instance_0', resp['name']) self.assertEqual('test_instance_0', resp['name'])
self.assertEqual('building', resp['status']) self.assertEqual('building', resp['status'])
self.assertEqual(self.INSTANCE_UUIDS[0], resp['uuid']) self.assertEqual(self.INSTANCE_UUIDS[1], resp['uuid'])
self.assertEqual('just test instance 0', resp['description']) self.assertEqual('just test instance 0', resp['description'])
self.assertEqual(self.INSTANCE_TYPE_UUID, resp['instance_type_uuid']) self.assertEqual(self.INSTANCE_TYPE_UUID, resp['instance_type_uuid'])
self.assertEqual('b8f82429-3a13-4ffe-9398-4d1abdc256a8', self.assertEqual('b8f82429-3a13-4ffe-9398-4d1abdc256a8',
@ -151,10 +160,12 @@ class TestInstances(v1_test.APITestV1):
def test_instance_show(self): def test_instance_show(self):
self._prepare_instance(1) self._prepare_instance(1)
resp = self.get_json('/instances/%s' % self.INSTANCE_UUIDS[0]) headers = self.gen_headers(self.context)
resp = self.get_json('/instances/%s' % self.INSTANCE_UUIDS[1],
headers=headers)
self.assertEqual('test_instance_0', resp['name']) self.assertEqual('test_instance_0', resp['name'])
self.assertEqual('building', resp['status']) self.assertEqual('building', resp['status'])
self.assertEqual(self.INSTANCE_UUIDS[0], resp['uuid']) self.assertEqual(self.INSTANCE_UUIDS[1], resp['uuid'])
self.assertEqual('just test instance 0', resp['description']) self.assertEqual('just test instance 0', resp['description'])
self.assertEqual(self.INSTANCE_TYPE_UUID, resp['instance_type_uuid']) self.assertEqual(self.INSTANCE_TYPE_UUID, resp['instance_type_uuid'])
self.assertEqual('b8f82429-3a13-4ffe-9398-4d1abdc256a8', self.assertEqual('b8f82429-3a13-4ffe-9398-4d1abdc256a8',
@ -171,7 +182,8 @@ class TestInstances(v1_test.APITestV1):
def test_instance_list(self): def test_instance_list(self):
self._prepare_instance(4) self._prepare_instance(4)
resps = self.get_json('/instances')['instances'] headers = self.gen_headers(self.context)
resps = self.get_json('/instances', headers=headers)['instances']
self.assertEqual(4, len(resps)) self.assertEqual(4, len(resps))
self.assertEqual('test_instance_0', resps[0]['name']) self.assertEqual('test_instance_0', resps[0]['name'])
self.assertEqual('just test instance 0', resps[0]['description']) self.assertEqual('just test instance 0', resps[0]['description'])
@ -179,7 +191,9 @@ class TestInstances(v1_test.APITestV1):
def test_instance_list_with_details(self): def test_instance_list_with_details(self):
self._prepare_instance(4) self._prepare_instance(4)
resps = self.get_json('/instances/detail')['instances'] headers = self.gen_headers(self.context)
resps = self.get_json('/instances/detail',
headers=headers)['instances']
self.assertEqual(4, len(resps)) self.assertEqual(4, len(resps))
self.assertEqual(16, len(resps[0].keys())) self.assertEqual(16, len(resps[0].keys()))
self.assertEqual('test_instance_0', resps[0]['name']) self.assertEqual('test_instance_0', resps[0]['name'])
@ -192,6 +206,9 @@ class TestInstances(v1_test.APITestV1):
def test_instance_delete(self): def test_instance_delete(self):
self._prepare_instance(4) self._prepare_instance(4)
self.delete('/instances/' + self.INSTANCE_UUIDS[0], status=204) headers = self.gen_headers(self.context)
resp = self.get_json('/instances/%s' % self.INSTANCE_UUIDS[0]) self.delete('/instances/' + self.INSTANCE_UUIDS[1], headers=headers,
status=204)
resp = self.get_json('/instances/%s' % self.INSTANCE_UUIDS[1],
headers=headers)
self.assertEqual('deleting', resp['status']) self.assertEqual('deleting', resp['status'])

View File

@ -88,12 +88,11 @@ class ComputeAPIUnitTest(base.DbTestCase):
mock_inst_create.assert_has_calls(calls) mock_inst_create.assert_has_calls(calls)
@mock.patch.object(engine_rpcapi.EngineAPI, 'create_instance') @mock.patch.object(engine_rpcapi.EngineAPI, 'create_instance')
@mock.patch('mogan.engine.api.API._provision_instances')
@mock.patch('mogan.engine.api.API._get_image') @mock.patch('mogan.engine.api.API._get_image')
@mock.patch('mogan.engine.api.API._validate_and_build_base_options') @mock.patch('mogan.engine.api.API._validate_and_build_base_options')
@mock.patch.object(engine_rpcapi.EngineAPI, 'list_availability_zones') @mock.patch.object(engine_rpcapi.EngineAPI, 'list_availability_zones')
def test_create(self, mock_list_az, mock_validate, mock_get_image, def test_create(self, mock_list_az, mock_validate, mock_get_image,
mock_provision, mock_create): mock_create):
instance_type = self._create_instance_type() instance_type = self._create_instance_type()
base_options = {'image_uuid': 'fake-uuid', base_options = {'image_uuid': 'fake-uuid',
@ -109,12 +108,15 @@ class ComputeAPIUnitTest(base.DbTestCase):
max_count = 2 max_count = 2
mock_validate.return_value = (base_options, max_count) mock_validate.return_value = (base_options, max_count)
mock_get_image.side_effect = None mock_get_image.side_effect = None
mock_provision.return_value = [mock.MagicMock()
for i in range(max_count)]
mock_create.return_value = mock.MagicMock() mock_create.return_value = mock.MagicMock()
mock_list_az.return_value = {'availability_zones': ['test_az']} mock_list_az.return_value = {'availability_zones': ['test_az']}
requested_networks = [{'uuid': 'fake'}] requested_networks = [{'uuid': 'fake'}]
res = self.dbapi._get_quota_usages(self.context, self.project_id)
before_in_use = 0
if res.get('instances') is not None:
before_in_use = res.get('instances').in_use
self.engine_api.create( self.engine_api.create(
self.context, self.context,
instance_type=instance_type, instance_type=instance_type,
@ -132,10 +134,11 @@ class ComputeAPIUnitTest(base.DbTestCase):
self.context, instance_type, 'fake-uuid', 'fake-name', self.context, instance_type, 'fake-uuid', 'fake-name',
'fake-descritpion', 'test_az', {'k1', 'v1'}, requested_networks, 'fake-descritpion', 'test_az', {'k1', 'v1'}, requested_networks,
max_count) max_count)
mock_provision.assert_called_once_with(self.context, base_options,
min_count, max_count)
self.assertTrue(mock_create.called) self.assertTrue(mock_create.called)
self.assertTrue(mock_get_image.called) self.assertTrue(mock_get_image.called)
res = self.dbapi._get_quota_usages(self.context, self.project_id)
after_in_use = res.get('instances').in_use
self.assertEqual(before_in_use + 2, after_in_use)
@mock.patch.object(engine_rpcapi.EngineAPI, 'list_availability_zones') @mock.patch.object(engine_rpcapi.EngineAPI, 'list_availability_zones')
def test_create_with_invalid_az(self, mock_list_az): def test_create_with_invalid_az(self, mock_list_az):
@ -156,6 +159,43 @@ class ComputeAPIUnitTest(base.DbTestCase):
mock_list_az.assert_called_once_with(self.context) mock_list_az.assert_called_once_with(self.context)
@mock.patch('mogan.engine.api.API._get_image')
@mock.patch('mogan.engine.api.API._validate_and_build_base_options')
@mock.patch.object(engine_rpcapi.EngineAPI, 'list_availability_zones')
def test_create_over_quota_limit(self, mock_list_az, mock_validate,
mock_get_image):
instance_type = self._create_instance_type()
base_options = {'image_uuid': 'fake-uuid',
'status': states.BUILDING,
'user_id': 'fake-user',
'project_id': 'fake-project',
'instance_type_uuid': 'fake-type-uuid',
'name': 'fake-name',
'description': 'fake-description',
'extra': {'k1', 'v1'},
'availability_zone': 'test_az'}
min_count = 11
max_count = 20
mock_validate.return_value = (base_options, max_count)
mock_get_image.side_effect = None
mock_list_az.return_value = {'availability_zones': ['test_az']}
requested_networks = [{'uuid': 'fake'}]
self.assertRaises(
exception.OverQuota,
self.engine_api.create,
self.context,
instance_type,
'fake-uuid',
'fake-name',
'fake-descritpion',
'test_az',
{'k1', 'v1'},
requested_networks,
min_count,
max_count)
def _create_fake_instance_obj(self, fake_instance): def _create_fake_instance_obj(self, fake_instance):
fake_instance_obj = objects.Instance(self.context, **fake_instance) fake_instance_obj = objects.Instance(self.context, **fake_instance)
fake_instance_obj.create(self.context) fake_instance_obj.create(self.context)

View File

@ -51,6 +51,9 @@ mogan.database.migration_backend =
tempest.test_plugins = tempest.test_plugins =
mogan_tests = mogan.tests.tempest.plugin:MoganTempestPlugin mogan_tests = mogan.tests.tempest.plugin:MoganTempestPlugin
mogan.quota.backend_driver =
database = mogan.objects.quota:DbQuotaDriver
[build_sphinx] [build_sphinx]
source-dir = doc/source source-dir = doc/source
build-dir = doc/build build-dir = doc/build