From 93d8376c44cdba58b1db88137b3dcb3a1ad1b2e6 Mon Sep 17 00:00:00 2001 From: Manjeet Singh Bhatia Date: Thu, 7 Jul 2016 01:11:24 +0000 Subject: [PATCH] OVO for Quotas and Reservation This patch introduces and integrates Oslo-Versioned Object for ResourceDelta, Reservation, Quota and QuotaUsage model classes. Partially-Implements: blueprint adopt-oslo-versioned-objects-for-db Co-Authored-By: Victor Morales Change-Id: Ic058e66e6780e1f6ff25a5b2bbe7294959905765 --- neutron/db/quota/api.py | 179 ++++++++------------- neutron/db/quota/driver.py | 41 ++--- neutron/objects/quota.py | 152 +++++++++++++++++ neutron/tests/unit/db/quota/test_api.py | 16 +- neutron/tests/unit/db/quota/test_driver.py | 8 +- neutron/tests/unit/objects/test_objects.py | 4 + neutron/tests/unit/objects/test_quota.py | 126 +++++++++++++++ neutron/tests/unit/quota/test_resource.py | 4 + 8 files changed, 392 insertions(+), 138 deletions(-) create mode 100644 neutron/objects/quota.py create mode 100644 neutron/tests/unit/objects/test_quota.py diff --git a/neutron/db/quota/api.py b/neutron/db/quota/api.py index 646d297bc6a..b35767b44ce 100644 --- a/neutron/db/quota/api.py +++ b/neutron/db/quota/api.py @@ -15,13 +15,8 @@ import collections import datetime -import sqlalchemy as sa -from sqlalchemy.orm import exc as orm_exc -from sqlalchemy import sql - -from neutron.db import _utils as db_utils from neutron.db import api as db_api -from neutron.db.quota import models as quota_models +from neutron.objects import quota as quota_obj # Wrapper for utcnow - needed for mocking it in unit tests @@ -50,40 +45,30 @@ def get_quota_usage_by_resource_and_tenant(context, resource, tenant_id): :returns: a QuotaUsageInfo instance """ - query = db_utils.model_query(context, quota_models.QuotaUsage) - query = query.filter_by(resource=resource, tenant_id=tenant_id) - - # NOTE(manjeets) as lock mode was just for protecting dirty bits - # an update on dirty will prevent the race. - query.filter_by(dirty=True).update({'dirty': True}) - - result = query.first() + result = quota_obj.QuotaUsage.get_object_dirty_protected( + context, resource=resource, project_id=tenant_id) if not result: return - return QuotaUsageInfo(result.resource, - result.tenant_id, - result.in_use, + return QuotaUsageInfo(result.resource, result.project_id, result.in_use, result.dirty) @db_api.retry_if_session_inactive() def get_quota_usage_by_resource(context, resource): - query = db_utils.model_query(context, quota_models.QuotaUsage) - query = query.filter_by(resource=resource) + objs = quota_obj.QuotaUsage.get_objects(context, resource=resource) return [QuotaUsageInfo(item.resource, - item.tenant_id, + item.project_id, item.in_use, - item.dirty) for item in query] + item.dirty) for item in objs] @db_api.retry_if_session_inactive() def get_quota_usage_by_tenant_id(context, tenant_id): - query = db_utils.model_query(context, quota_models.QuotaUsage) - query = query.filter_by(tenant_id=tenant_id) + objs = quota_obj.QuotaUsage.get_objects(context, project_id=tenant_id) return [QuotaUsageInfo(item.resource, - item.tenant_id, + tenant_id, item.in_use, - item.dirty) for item in query] + item.dirty) for item in objs] @db_api.retry_if_session_inactive() @@ -101,16 +86,13 @@ def set_quota_usage(context, resource, tenant_id, or a delta (default to False) """ with db_api.autonested_transaction(context.session): - query = db_utils.model_query(context, quota_models.QuotaUsage) - query = query.filter_by(resource=resource).filter_by( - tenant_id=tenant_id) - usage_data = query.first() + usage_data = quota_obj.QuotaUsage.get_object( + context, resource=resource, project_id=tenant_id) if not usage_data: # Must create entry - usage_data = quota_models.QuotaUsage( - resource=resource, - tenant_id=tenant_id) - context.session.add(usage_data) + usage_data = quota_obj.QuotaUsage( + context, resource=resource, project_id=tenant_id) + usage_data.create() # Perform explicit comparison with None as 0 is a valid value if in_use is not None: if delta: @@ -118,10 +100,9 @@ def set_quota_usage(context, resource, tenant_id, usage_data.in_use = in_use # After an explicit update the dirty bit should always be reset usage_data.dirty = False - return QuotaUsageInfo(usage_data.resource, - usage_data.tenant_id, - usage_data.in_use, - usage_data.dirty) + usage_data.update() + return QuotaUsageInfo(usage_data.resource, usage_data.project_id, + usage_data.in_use, usage_data.dirty) @db_api.retry_if_session_inactive() @@ -134,9 +115,13 @@ def set_quota_usage_dirty(context, resource, tenant_id, dirty=True): :param dirty: the desired value for the dirty bit (defaults to True) :returns: 1 if the quota usage data were updated, 0 otherwise. """ - query = db_utils.model_query(context, quota_models.QuotaUsage) - query = query.filter_by(resource=resource).filter_by(tenant_id=tenant_id) - return query.update({'dirty': dirty}) + obj = quota_obj.QuotaUsage.get_object( + context, resource=resource, project_id=tenant_id) + if obj: + obj.dirty = dirty + obj.update() + return 1 + return 0 @db_api.retry_if_session_inactive() @@ -150,12 +135,14 @@ def set_resources_quota_usage_dirty(context, resources, tenant_id, dirty=True): :param dirty: the desired value for the dirty bit (defaults to True) :returns: the number of records for which the bit was actually set. """ - query = db_utils.model_query(context, quota_models.QuotaUsage) - query = query.filter_by(tenant_id=tenant_id) + filters = {'project_id': tenant_id} if resources: - query = query.filter(quota_models.QuotaUsage.resource.in_(resources)) - # synchronize_session=False needed because of the IN condition - return query.update({'dirty': dirty}, synchronize_session=False) + filters['resource'] = resources + objs = quota_obj.QuotaUsage.get_objects(context, **filters) + for obj in objs: + obj.dirty = dirty + obj.update() + return len(objs) @db_api.retry_if_session_inactive() @@ -167,66 +154,62 @@ def set_all_quota_usage_dirty(context, resource, dirty=True): :returns: the number of tenants for which the dirty bit was actually updated """ - query = db_utils.model_query(context, quota_models.QuotaUsage) - query = query.filter_by(resource=resource) - return query.update({'dirty': dirty}) + # TODO(manjeets) consider squashing this method with + # set_resources_quota_usage_dirty + objs = quota_obj.QuotaUsage.get_objects(context, resource=resource) + for obj in objs: + obj.dirty = dirty + obj.update() + return len(objs) @db_api.retry_if_session_inactive() def create_reservation(context, tenant_id, deltas, expiration=None): # This method is usually called from within another transaction. # Consider using begin_nested - with context.session.begin(subtransactions=True): - expiration = expiration or (utcnow() + datetime.timedelta(0, 120)) - resv = quota_models.Reservation(tenant_id=tenant_id, - expiration=expiration) - context.session.add(resv) - for (resource, delta) in deltas.items(): - context.session.add( - quota_models.ResourceDelta(resource=resource, - amount=delta, - reservation=resv)) - return ReservationInfo(resv['id'], - resv['tenant_id'], - resv['expiration'], + expiration = expiration or (utcnow() + datetime.timedelta(0, 120)) + delta_objs = [] + for (resource, delta) in deltas.items(): + delta_objs.append(quota_obj.ResourceDelta( + context, resource=resource, amount=delta)) + reserv_obj = quota_obj.Reservation( + context, project_id=tenant_id, expiration=expiration, + resource_deltas=delta_objs) + reserv_obj.create() + return ReservationInfo(reserv_obj['id'], + reserv_obj['project_id'], + reserv_obj['expiration'], dict((delta.resource, delta.amount) - for delta in resv.resource_deltas)) + for delta in reserv_obj.resource_deltas)) @db_api.retry_if_session_inactive() def get_reservation(context, reservation_id): - query = context.session.query(quota_models.Reservation).filter_by( - id=reservation_id) - resv = query.first() - if not resv: + reserv_obj = quota_obj.Reservation.get_object(context, id=reservation_id) + if not reserv_obj: return - return ReservationInfo(resv['id'], - resv['tenant_id'], - resv['expiration'], + return ReservationInfo(reserv_obj['id'], + reserv_obj['project_id'], + reserv_obj['expiration'], dict((delta.resource, delta.amount) - for delta in resv.resource_deltas)) + for delta in reserv_obj.resource_deltas)) @db_api.retry_if_session_inactive() @db_api.context_manager.writer def remove_reservation(context, reservation_id, set_dirty=False): - delete_query = context.session.query(quota_models.Reservation).filter_by( - id=reservation_id) - # Not handling MultipleResultsFound as the query is filtering by primary - # key - try: - reservation = delete_query.one() - except orm_exc.NoResultFound: + reservation = quota_obj.Reservation.get_object(context, id=reservation_id) + if not reservation: # TODO(salv-orlando): Raise here and then handle the exception? return - tenant_id = reservation.tenant_id + tenant_id = reservation.project_id resources = [delta.resource for delta in reservation.resource_deltas] - num_deleted = delete_query.delete() + reservation.delete() if set_dirty: # quota_usage for all resource involved in this reservation must # be marked as dirty set_resources_quota_usage_dirty(context, resources, tenant_id) - return num_deleted + return 1 @db_api.retry_if_session_inactive() @@ -241,38 +224,14 @@ def get_reservations_for_resources(context, tenant_id, resources, reservations (defaults to False) :returns: a dictionary mapping resources with corresponding deltas """ - if not resources: - # Do not waste time - return - now = utcnow() - resv_query = context.session.query( - quota_models.ResourceDelta.resource, - quota_models.Reservation.expiration, - sql.func.sum(quota_models.ResourceDelta.amount)).join( - quota_models.Reservation) - if expired: - exp_expr = (quota_models.Reservation.expiration < now) - else: - exp_expr = (quota_models.Reservation.expiration >= now) - resv_query = resv_query.filter(sa.and_( - quota_models.Reservation.tenant_id == tenant_id, - quota_models.ResourceDelta.resource.in_(resources), - exp_expr)).group_by( - quota_models.ResourceDelta.resource, - quota_models.Reservation.expiration) - return dict((resource, total_reserved) - for (resource, exp, total_reserved) in resv_query) + # NOTE(manjeets) we are using utcnow() here because it + # can be mocked easily where as datetime is built in type + # mock.path does not allow mocking built in types. + return quota_obj.Reservation.get_total_reservations_map( + context, utcnow(), tenant_id, resources, expired) @db_api.retry_if_session_inactive() @db_api.context_manager.writer def remove_expired_reservations(context, tenant_id=None): - now = utcnow() - resv_query = context.session.query(quota_models.Reservation) - if tenant_id: - tenant_expr = (quota_models.Reservation.tenant_id == tenant_id) - else: - tenant_expr = sql.true() - resv_query = resv_query.filter(sa.and_( - tenant_expr, quota_models.Reservation.expiration < now)) - return resv_query.delete() + return quota_obj.Reservation.delete_expired(context, utcnow(), tenant_id) diff --git a/neutron/db/quota/driver.py b/neutron/db/quota/driver.py index 156570915c4..a07a9d284f9 100644 --- a/neutron/db/quota/driver.py +++ b/neutron/db/quota/driver.py @@ -17,10 +17,9 @@ from neutron_lib import exceptions from oslo_log import log from neutron.common import exceptions as n_exc -from neutron.db import _utils as db_utils from neutron.db import api as db_api from neutron.db.quota import api as quota_api -from neutron.db.quota import models as quota_models +from neutron.objects import quota as quota_obj LOG = log.getLogger(__name__) @@ -66,9 +65,8 @@ class DbQuotaDriver(object): for key, resource in resources.items()) # update with tenant specific limits - q_qry = db_utils.model_query(context, quota_models.Quota).filter_by( - tenant_id=tenant_id) - for item in q_qry: + quota_objs = quota_obj.Quota.get_objects(context, project_id=tenant_id) + for item in quota_objs: tenant_quota[item['resource']] = item['limit'] return tenant_quota @@ -82,12 +80,10 @@ class DbQuotaDriver(object): Raise a "not found" error if the quota for the given tenant was never defined. """ - with context.session.begin(): - tenant_quotas = context.session.query(quota_models.Quota) - tenant_quotas = tenant_quotas.filter_by(tenant_id=tenant_id) - if not tenant_quotas.delete(): - # No record deleted means the quota was not found - raise n_exc.TenantQuotaNotFound(tenant_id=tenant_id) + if quota_obj.Quota.delete_objects( + context, project_id=tenant_id) < 1: + # No record deleted means the quota was not found + raise n_exc.TenantQuotaNotFound(tenant_id=tenant_id) @staticmethod @db_api.retry_if_session_inactive() @@ -104,8 +100,8 @@ class DbQuotaDriver(object): all_tenant_quotas = {} - for quota in context.session.query(quota_models.Quota): - tenant_id = quota['tenant_id'] + for quota in quota_obj.Quota.get_objects(context): + tenant_id = quota['project_id'] # avoid setdefault() because only want to copy when actually # required @@ -124,17 +120,14 @@ class DbQuotaDriver(object): @staticmethod @db_api.retry_if_session_inactive() def update_quota_limit(context, tenant_id, resource, limit): - with context.session.begin(): - tenant_quota = context.session.query(quota_models.Quota).filter_by( - tenant_id=tenant_id, resource=resource).first() - - if tenant_quota: - tenant_quota.update({'limit': limit}) - else: - tenant_quota = quota_models.Quota(tenant_id=tenant_id, - resource=resource, - limit=limit) - context.session.add(tenant_quota) + tenant_quotas = quota_obj.Quota.get_objects( + context, project_id=tenant_id, resource=resource) + if tenant_quotas: + tenant_quotas[0].limit = limit + tenant_quotas[0].update() + else: + quota_obj.Quota(context, project_id=tenant_id, resource=resource, + limit=limit).create() def _get_quotas(self, context, tenant_id, resources): """Retrieves the quotas for specific resources. diff --git a/neutron/objects/quota.py b/neutron/objects/quota.py new file mode 100644 index 00000000000..29b5a150cf6 --- /dev/null +++ b/neutron/objects/quota.py @@ -0,0 +1,152 @@ +# Copyright (c) 2016 Intel Corporation. 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 oslo_versionedobjects import base as obj_base +from oslo_versionedobjects import fields as obj_fields +import sqlalchemy as sa +from sqlalchemy import sql + +from neutron.db import api as db_api +from neutron.db.quota import models +from neutron.objects import base +from neutron.objects import common_types + + +@obj_base.VersionedObjectRegistry.register +class ResourceDelta(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = models.ResourceDelta + + primary_keys = ['resource', 'reservation_id'] + + foreign_keys = {'Reservation': {'reservation_id': 'id'}} + + fields = { + 'resource': obj_fields.StringField(), + 'reservation_id': common_types.UUIDField(), + 'amount': obj_fields.IntegerField(nullable=True), + } + + +@obj_base.VersionedObjectRegistry.register +class Reservation(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = models.Reservation + + fields = { + 'id': common_types.UUIDField(), + 'project_id': obj_fields.StringField(nullable=True), + 'expiration': obj_fields.DateTimeField(tzinfo_aware=False, + nullable=True), + 'resource_deltas': obj_fields.ListOfObjectsField( + ResourceDelta.__name__, nullable=True), + } + + synthetic_fields = ['resource_deltas'] + + def create(self): + deltas = self.resource_deltas + with db_api.autonested_transaction(self.obj_context.session): + super(Reservation, self).create() + if deltas: + for delta in deltas: + delta.reservation_id = self.id + delta.create() + self.resource_deltas.append(delta) + self.obj_reset_changes(['resource_deltas']) + + @classmethod + def delete_expired(cls, context, now, project_id): + resv_query = context.session.query(models.Reservation) + if project_id: + project_expr = (models.Reservation.project_id == project_id) + else: + project_expr = sql.true() + # TODO(manjeets) Fetch and delete objects using + # object/db/api.py once comparison operations are + # supported + resv_query = resv_query.filter(sa.and_( + project_expr, models.Reservation.expiration < now)) + return resv_query.delete() + + @classmethod + def get_total_reservations_map(cls, context, now, project_id, + resources, expired): + if not resources: + return + resv_query = context.session.query( + models.ResourceDelta.resource, + models.Reservation.expiration, + sql.func.sum(models.ResourceDelta.amount)).join( + models.Reservation) + if expired: + exp_expr = (models.Reservation.expiration < now) + else: + exp_expr = (models.Reservation.expiration >= now) + resv_query = resv_query.filter(sa.and_( + models.Reservation.project_id == project_id, + models.ResourceDelta.resource.in_(resources), + exp_expr)).group_by( + models.ResourceDelta.resource, + models.Reservation.expiration) + return dict((resource, total_reserved) + for (resource, exp, total_reserved) in resv_query) + + +@obj_base.VersionedObjectRegistry.register +class Quota(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = models.Quota + + fields = { + 'id': common_types.UUIDField(), + 'project_id': obj_fields.StringField(nullable=True), + 'resource': obj_fields.StringField(nullable=True), + 'limit': obj_fields.IntegerField(nullable=True), + } + + +@obj_base.VersionedObjectRegistry.register +class QuotaUsage(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = models.QuotaUsage + + primary_keys = ['resource', 'project_id'] + + fields = { + 'resource': obj_fields.StringField(), + 'project_id': obj_fields.StringField(), + 'dirty': obj_fields.BooleanField(default=False), + 'in_use': obj_fields.IntegerField(default=0), + 'reserved': obj_fields.IntegerField(default=0), + } + + @classmethod + def get_object_dirty_protected(cls, context, **kwargs): + query = context.session.query(cls.db_model) + query = query.filter_by(**cls.modify_fields_to_db(kwargs)) + # NOTE(manjeets) as lock mode was just for protecting dirty bits + # an update on dirty will prevent the race. + query.filter_by(dirty=True).update({'dirty': True}) + res = query.first() + if res: + return cls._load_object(context, res) diff --git a/neutron/tests/unit/db/quota/test_api.py b/neutron/tests/unit/db/quota/test_api.py index 839a9468742..5b4c1746962 100644 --- a/neutron/tests/unit/db/quota/test_api.py +++ b/neutron/tests/unit/db/quota/test_api.py @@ -15,12 +15,19 @@ import datetime import mock +from neutron_lib import constants as const from neutron_lib import context +from neutron_lib.plugins import directory +from oslo_config import cfg from neutron.db.quota import api as quota_api +from neutron.tests.unit.db.quota import test_driver from neutron.tests.unit import testlib_api +DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2' + + class TestQuotaDbApi(testlib_api.SqlTestCaseLight): def _set_context(self): @@ -36,8 +43,8 @@ class TestQuotaDbApi(testlib_api.SqlTestCaseLight): def _create_quota_usage(self, resource, used, tenant_id=None): tenant_id = tenant_id or self.tenant_id - return quota_api.set_quota_usage( - self.context, resource, tenant_id, in_use=used) + return quota_api.set_quota_usage(context.get_admin_context(), + resource, tenant_id, in_use=used) def _verify_quota_usage(self, usage_info, expected_resource=None, @@ -54,6 +61,9 @@ class TestQuotaDbApi(testlib_api.SqlTestCaseLight): def setUp(self): super(TestQuotaDbApi, self).setUp() self._set_context() + self.plugin = test_driver.FakePlugin() + directory.add_plugin(const.CORE, self.plugin) + cfg.CONF.set_override("core_plugin", DB_PLUGIN_KLASS) def test_create_quota_usage(self): usage_info = self._create_quota_usage('goals', 26) @@ -291,7 +301,7 @@ class TestQuotaDbApi(testlib_api.SqlTestCaseLight): expiration=exp_date_2, tenant_id='Callejon') self.assertEqual(2, quota_api.remove_expired_reservations( - self.context)) + context.get_admin_context())) self.assertIsNone(quota_api.get_reservation( self.context, resv_2.reservation_id)) self.assertIsNone(quota_api.get_reservation( diff --git a/neutron/tests/unit/db/quota/test_driver.py b/neutron/tests/unit/db/quota/test_driver.py index 6156ed308cb..20c502ebdcb 100644 --- a/neutron/tests/unit/db/quota/test_driver.py +++ b/neutron/tests/unit/db/quota/test_driver.py @@ -19,9 +19,13 @@ from neutron_lib import exceptions as lib_exc from neutron.common import exceptions from neutron.db import db_base_plugin_v2 as base_plugin from neutron.db.quota import driver +from neutron.tests import base from neutron.tests.unit import testlib_api +DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2' + + class FakePlugin(base_plugin.NeutronDbPluginV2, driver.DbQuotaDriver): """A fake plugin class containing all DB methods.""" @@ -47,11 +51,13 @@ RESOURCE = 'res_test' ALT_RESOURCE = 'res_test_meh' -class TestDbQuotaDriver(testlib_api.SqlTestCase): +class TestDbQuotaDriver(testlib_api.SqlTestCase, + base.BaseTestCase): def setUp(self): super(TestDbQuotaDriver, self).setUp() self.plugin = FakePlugin() self.context = context.get_admin_context() + self.setup_coreplugin(core_plugin=DB_PLUGIN_KLASS) def test_create_quota_limit(self): defaults = {RESOURCE: TestResource(RESOURCE, 4)} diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 324917d12eb..72fa9db028e 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -64,6 +64,10 @@ object_data = { 'QosMinimumBandwidthRule': '1.2-314c3419f4799067cc31cc319080adff', 'QosRuleType': '1.2-e6fd08fcca152c339cbd5e9b94b1b8e7', 'QosPolicy': '1.4-50460f619c34428ec5651916e938e5a0', + 'Quota': '1.0-6bb6a0f1bd5d66a2134ffa1a61873097', + 'QuotaUsage': '1.0-6fbf820368681aac7c5d664662605cf9', + 'Reservation': '1.0-49929fef8e82051660342eed51b48f2a', + 'ResourceDelta': '1.0-a980b37e0a52618b5af8db29af18be76', 'Route': '1.0-a9883a63b416126f9e345523ec09483b', 'RouterExtraAttributes': '1.0-ef8d61ae2864f0ec9af0ab7939cab318', 'RouterL3AgentBinding': '1.0-c5ba6c95e3a4c1236a55f490cd67da82', diff --git a/neutron/tests/unit/objects/test_quota.py b/neutron/tests/unit/objects/test_quota.py new file mode 100644 index 00000000000..b13ba8e50da --- /dev/null +++ b/neutron/tests/unit/objects/test_quota.py @@ -0,0 +1,126 @@ +# Copyright (c) 2016 Intel Corporation. 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. + +import datetime +from oslo_utils import uuidutils + +from neutron.objects import quota +from neutron.tests.unit.objects import test_base as obj_test_base +from neutron.tests.unit import testlib_api + + +class ResourceDeltaObjectIfaceTestCase( + obj_test_base.BaseObjectIfaceTestCase): + + _test_class = quota.ResourceDelta + + +class ResourceDeltaDbObjectTestCase(obj_test_base.BaseDbObjectTestCase, + testlib_api.SqlTestCase): + + _test_class = quota.ResourceDelta + + def setUp(self): + super(ResourceDeltaDbObjectTestCase, self).setUp() + for obj in self.obj_fields: + self._create_test_reservation(res_id=obj['reservation_id']) + + def _create_test_reservation(self, res_id): + self._reservation = quota.Reservation(self.context, id=res_id) + self._reservation.create() + + +class ReservationObjectIfaceTestCase( + obj_test_base.BaseObjectIfaceTestCase): + + _test_class = quota.Reservation + + +class ReservationDbObjectTestCase(obj_test_base.BaseDbObjectTestCase, + testlib_api.SqlTestCase): + + _test_class = quota.Reservation + + def _create_test_reservation(self, res=None, exp=None): + res_id = uuidutils.generate_uuid() + reservation = self._test_class(self.context, + id=res_id, resource=res, expiration=exp) + reservation.create() + return reservation + + def test_delete_expired(self): + dt = datetime.datetime.utcnow() + resources = {'goals': 2, 'assists': 1} + exp_date1 = datetime.datetime(2016, 3, 31, 14, 30) + res1 = self._create_test_reservation(resources, exp_date1) + exp_date2 = datetime.datetime(2015, 3, 31, 14, 30) + res2 = self._create_test_reservation(resources, exp_date2) + self.assertEqual(2, self._test_class.delete_expired( + self.context, dt, None)) + objs = self._test_class.get_objects(self.context, + id=[res1.id, res2.id]) + self.assertEqual([], objs) + + def test_reservation_synthetic_field(self): + res = self._create_test_reservation() + resource = 'test-res' + res_delta = quota.ResourceDelta(self.context, + resource=resource, reservation_id=res.id, amount='10') + res_delta.create() + obj = self._test_class.get_object(self.context, id=res.id) + self.assertEqual(res_delta, obj.resource_deltas[0]) + res_delta.delete() + obj.update() + # NOTE(manjeets) update on reservation should reflect + # changes on synthetic field when it is deleted. + obj = self._test_class.get_object(self.context, id=res.id) + self.assertEqual([], obj.resource_deltas) + + +class QuotaObjectIfaceTestCase(obj_test_base.BaseObjectIfaceTestCase): + + _test_class = quota.Quota + + +class QuotaDbObjectTestCase(obj_test_base.BaseDbObjectTestCase, + testlib_api.SqlTestCase): + + _test_class = quota.Quota + + +class QuotaUsageObjectIfaceTestCase(obj_test_base.BaseObjectIfaceTestCase): + + _test_class = quota.QuotaUsage + + +class QuotaUsageDbObjectTestCase(obj_test_base.BaseDbObjectTestCase, + testlib_api.SqlTestCase): + + _test_class = quota.QuotaUsage + + def _test_get_object_dirty_protected(self, obj, dirty=True): + obj.create() + obj.dirty = dirty + obj.update() + new = self._test_class.get_object_dirty_protected( + self.context, + **obj._get_composite_keys()) + self.assertEqual(obj, new) + self.assertEqual(dirty, new.dirty) + + def test_get_object_dirty_protected(self): + obj = self._make_object(self.obj_fields[0]) + obj1 = self._make_object(self.obj_fields[1]) + self._test_get_object_dirty_protected(obj, dirty=False) + self._test_get_object_dirty_protected(obj1) diff --git a/neutron/tests/unit/quota/test_resource.py b/neutron/tests/unit/quota/test_resource.py index a7c0681f6fe..014e4318c6a 100644 --- a/neutron/tests/unit/quota/test_resource.py +++ b/neutron/tests/unit/quota/test_resource.py @@ -26,6 +26,9 @@ from neutron.tests.unit import quota as test_quota from neutron.tests.unit import testlib_api +DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2' + + meh_quota_flag = 'quota_meh' meh_quota_opts = [cfg.IntOpt(meh_quota_flag, default=99)] @@ -90,6 +93,7 @@ class TestTrackedResource(testlib_api.SqlTestCaseLight): def setUp(self): base.BaseTestCase.config_parse() cfg.CONF.register_opts(meh_quota_opts, 'QUOTAS') + cfg.CONF.set_override("core_plugin", DB_PLUGIN_KLASS) self.addCleanup(cfg.CONF.reset) self.resource = 'meh' self.other_resource = 'othermeh'