From 603abeb977d4018963beade5c858b53f990ef32a Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Mon, 20 Sep 2021 09:27:39 +0000 Subject: [PATCH] Execute the quota reservation removal in an isolated DB txn The goal of [1] is to, in case of failing when removing the quota reservation, continue the operation. Any expired reservation will be removed automatically in any driver. If the DB transaction fails, it should affect only to the reservation trying to be deleted. This is why this patch isolates the "remove_reservation" method and guarantees it is called outside an active DB session. That guarantees, in case of failure, no other DB operation will be affected. This patch also partially reverts [2] but still checks the security group rule quota when a new security group is created. Instead of creating and releasing a quota reservation for the security group rules created, now only the available quota limit is checked before creating them. That won't prevent another operation to create security group rules in parallel, exceeding the available quota. However, this is not even guaranteed with the current quota driver. [1]https://review.opendev.org/c/openstack/neutron/+/805031 [2]https://review.opendev.org/c/openstack/neutron/+/701565 Closes-Bug: #1943714 Change-Id: Id73368576a948f78a043d7cf0be16661a65626a9 --- neutron/api/v2/base.py | 11 +- neutron/common/utils.py | 12 +- neutron/db/quota/api.py | 71 ++++++-- neutron/db/quota/driver.py | 34 ++++ neutron/db/quota/driver_nolock.py | 16 +- neutron/db/securitygroups_db.py | 9 +- neutron/pecan_wsgi/hooks/quota_enforcement.py | 12 +- neutron/quota/__init__.py | 4 + neutron/quota/resource.py | 2 +- neutron/tests/functional/db/test_network.py | 6 +- neutron/tests/unit/db/quota/test_driver.py | 160 ++++++++++++------ .../tests/unit/db/quota/test_driver_nolock.py | 24 +++ .../tests/unit/db/test_securitygroups_db.py | 6 +- neutron/tests/unit/plugins/ml2/test_plugin.py | 12 +- ...otaDriverAPI_methods-ed76d167974d6f9d.yaml | 8 + 15 files changed, 278 insertions(+), 109 deletions(-) create mode 100644 neutron/tests/unit/db/quota/test_driver_nolock.py create mode 100644 releasenotes/notes/new_QuotaDriverAPI_methods-ed76d167974d6f9d.yaml diff --git a/neutron/api/v2/base.py b/neutron/api/v2/base.py index 6c54b77d4b3..bbd41fdc39d 100644 --- a/neutron/api/v2/base.py +++ b/neutron/api/v2/base.py @@ -498,12 +498,11 @@ class Controller(object): def notify(create_result): # Ensure usage trackers for all resources affected by this API # operation are marked as dirty - with db_api.CONTEXT_WRITER.using(request.context): - # Commit the reservation(s) - for reservation in reservations: - quota.QUOTAS.commit_reservation( - request.context, reservation.reservation_id) - resource_registry.set_resources_dirty(request.context) + # Commit the reservation(s) + for reservation in reservations: + quota.QUOTAS.commit_reservation( + request.context, reservation.reservation_id) + resource_registry.set_resources_dirty(request.context) notifier_method = self._resource + '.create.end' self._notifier.info(request.context, diff --git a/neutron/common/utils.py b/neutron/common/utils.py index d484f4e42cb..09750fbb644 100644 --- a/neutron/common/utils.py +++ b/neutron/common/utils.py @@ -36,6 +36,7 @@ from eventlet.green import subprocess import netaddr from neutron_lib.api.definitions import availability_zone as az_def from neutron_lib import constants as n_const +from neutron_lib import context as n_context from neutron_lib.db import api as db_api from neutron_lib.services.trunk import constants as trunk_constants from neutron_lib.utils import helpers @@ -675,15 +676,22 @@ def transaction_guard(f): If you receive this error, you must alter your code to handle the fact that the thing you are calling can have side effects so using transactions to undo on failures is not possible. + + This method can be called from a class method or a static method. "inner" + should consider if "self" is passed or not. The next mandatory parameter is + the context ``neutron_lib.context.Context``. """ @functools.wraps(f) - def inner(self, context, *args, **kwargs): + def inner(*args, **kwargs): + context = (args[0] if issubclass(type(args[0]), + n_context.ContextBaseWithSession) else + args[1]) # FIXME(kevinbenton): get rid of all uses of this flag if (context.session.is_active and getattr(context, 'GUARD_TRANSACTION', True)): raise RuntimeError(_("Method %s cannot be called within a " "transaction.") % f) - return f(self, context, *args, **kwargs) + return f(*args, **kwargs) return inner diff --git a/neutron/db/quota/api.py b/neutron/db/quota/api.py index 8f5b0ffa380..4925f3430bc 100644 --- a/neutron/db/quota/api.py +++ b/neutron/db/quota/api.py @@ -19,10 +19,12 @@ import datetime from neutron_lib.db import api as db_api from oslo_db import exception as db_exc +from neutron.common import utils from neutron.objects import quota as quota_obj RESERVATION_EXPIRATION_TIMEOUT = 20 # seconds +UNLIMITED_QUOTA = -1 # Wrapper for utcnow - needed for mocking it in unit tests @@ -201,24 +203,22 @@ def get_reservation(context, reservation_id): for delta in reserv_obj.resource_deltas)) +@utils.transaction_guard +@utils.skip_exceptions(db_exc.DBError) @db_api.CONTEXT_WRITER def remove_reservation(context, reservation_id, set_dirty=False): - try: - 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.project_id - resources = [delta.resource for delta in reservation.resource_deltas] - 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 1 - except db_exc.DBError: - context.session.rollback() + 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.project_id + resources = [delta.resource for delta in reservation.resource_deltas] + 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 1 @db_api.retry_if_session_inactive() @@ -374,6 +374,37 @@ class QuotaDriverAPI(object, metaclass=abc.ABCMeta): quota. """ + @staticmethod + @abc.abstractmethod + def get_resource_usage(context, project_id, resources, resource_name): + """Return the resource current usage + + :param context: The request context, for access checks. + :param project_id: The ID of the project to make the reservations for. + :param resources: A dictionary of the registered resources. + :param resource_name: The name of the resource to retrieve the usage. + :return: The current resource usage. + """ + + @staticmethod + @abc.abstractmethod + def quota_limit_check(context, project_id, resources, deltas): + """Check the current resource usage against a set of deltas. + + This method will check if the provided resource deltas could be + assigned depending on the current resource usage and the quota limits. + If the resource deltas plus the resource usage fit under the quota + limit, the method will pass. If not, a ``OverQuota`` will be raised. + + :param context: The request context, for access checks. + :param project_id: The ID of the project to make the reservations for. + :param resources: A dictionary of the registered resource. + :param deltas: A dictionary of the values to check against the + quota limits. + :return: None if passed; ``OverQuota`` if quota limits are exceeded, + ``InvalidQuotaValue`` if delta values are invalid. + """ + class NullQuotaDriver(QuotaDriverAPI): @@ -416,3 +447,11 @@ class NullQuotaDriver(QuotaDriverAPI): @staticmethod def limit_check(context, project_id, resources, values): pass + + @staticmethod + def get_resource_usage(context, project_id, resources, resource_name): + pass + + @staticmethod + def quota_limit_check(context, project_id, resources, deltas): + pass diff --git a/neutron/db/quota/driver.py b/neutron/db/quota/driver.py index b033b2ca210..1da31ae9f83 100644 --- a/neutron/db/quota/driver.py +++ b/neutron/db/quota/driver.py @@ -310,3 +310,37 @@ class DbQuotaDriver(quota_api.QuotaDriverAPI): overs = [key for key, val in values.items() if 0 <= quotas[key] < val] if overs: raise exceptions.OverQuota(overs=sorted(overs)) + + @staticmethod + def get_resource_usage(context, project_id, resources, resource_name): + tracked_resource = resources.get(resource_name) + if not tracked_resource: + return + + return tracked_resource.count(context, None, project_id, + resync_usage=False) + + def quota_limit_check(self, context, project_id, resources, deltas): + # Ensure no value is less than zero + unders = [key for key, val in deltas.items() if val < 0] + if unders: + raise exceptions.InvalidQuotaValue(unders=sorted(unders)) + + current_limits = self.get_project_quotas(context, resources, + project_id) + overs = set() + for resource_name, delta in deltas.items(): + resource_limit = current_limits.get(resource_name) + if resource_limit in (None, quota_api.UNLIMITED_QUOTA): + continue + + resource_usage = self.get_resource_usage(context, project_id, + resources, resource_name) + if resource_usage is None: + continue + + if resource_usage + delta > resource_limit: + overs.add(resource_name) + + if overs: + raise exceptions.OverQuota(overs=sorted(overs)) diff --git a/neutron/db/quota/driver_nolock.py b/neutron/db/quota/driver_nolock.py index 5190d71e990..28e7dea082e 100644 --- a/neutron/db/quota/driver_nolock.py +++ b/neutron/db/quota/driver_nolock.py @@ -60,12 +60,8 @@ class DbQuotaNoLockDriver(quota_driver.DbQuotaDriver): # Count the number of (1) used and (2) reserved resources for this # project_id. If any resource limit is exceeded, raise exception. for resource_name in requested_resources: - tracked_resource = resources.get(resource_name) - if not tracked_resource: - continue - - used_and_reserved = tracked_resource.count( - context, None, project_id, count_db_registers=True) + used_and_reserved = self.get_resource_usage( + context, project_id, resources, resource_name) resource_num = deltas[resource_name] if limits[resource_name] < (used_and_reserved + resource_num): resources_over_limit.append(resource_name) @@ -77,3 +73,11 @@ class DbQuotaNoLockDriver(quota_driver.DbQuotaDriver): def cancel_reservation(self, context, reservation_id): quota_api.remove_reservation(context, reservation_id, set_dirty=False) + + @staticmethod + def get_resource_usage(context, project_id, resources, resource_name): + tracked_resource = resources.get(resource_name) + if not tracked_resource: + return + return tracked_resource.count(context, None, project_id, + count_db_registers=True) diff --git a/neutron/db/securitygroups_db.py b/neutron/db/securitygroups_db.py index 42c18e01d23..cd60ae880c0 100644 --- a/neutron/db/securitygroups_db.py +++ b/neutron/db/securitygroups_db.py @@ -106,9 +106,8 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, with db_api.CONTEXT_WRITER.using(context): delta = len(ext_sg.sg_supported_ethertypes) delta = delta * 2 if default_sg else delta - reservation = quota.QUOTAS.make_reservation( - context, tenant_id, {'security_group_rule': delta}, - self) + quota.QUOTAS.quota_limit_check(context, tenant_id, + security_group_rule=delta) sg = sg_obj.SecurityGroup( context, id=s.get('id') or uuidutils.generate_uuid(), @@ -135,10 +134,6 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, sg.rules.append(egress_rule) sg.obj_reset_changes(['rules']) - if reservation: - quota.QUOTAS.commit_reservation(context, - reservation.reservation_id) - # fetch sg from db to load the sg rules with sg model. # NOTE(slaweq): With new system/project scopes it may happen that # project admin will try to list security groups for different diff --git a/neutron/pecan_wsgi/hooks/quota_enforcement.py b/neutron/pecan_wsgi/hooks/quota_enforcement.py index 0267145c0f8..14ca67fc0d9 100644 --- a/neutron/pecan_wsgi/hooks/quota_enforcement.py +++ b/neutron/pecan_wsgi/hooks/quota_enforcement.py @@ -77,9 +77,9 @@ class QuotaEnforcementHook(hooks.PecanHook): reservations = state.request.context.get('reservations') or [] if not reservations and state.request.method != 'DELETE': return - with db_api.CONTEXT_WRITER.using(neutron_context): - # Commit the reservation(s) - for reservation in reservations: - quota.QUOTAS.commit_reservation( - neutron_context, reservation.reservation_id) - resource_registry.set_resources_dirty(neutron_context) + + # Commit the reservation(s) + for reservation in reservations: + quota.QUOTAS.commit_reservation( + neutron_context, reservation.reservation_id) + resource_registry.set_resources_dirty(neutron_context) diff --git a/neutron/quota/__init__.py b/neutron/quota/__init__.py index 3a1198a1482..efe6f0a4e8d 100644 --- a/neutron/quota/__init__.py +++ b/neutron/quota/__init__.py @@ -145,5 +145,9 @@ class QuotaEngine(object): return self.get_driver().limit_check( context, tenant_id, resource_registry.get_all_resources(), values) + def quota_limit_check(self, context, project_id, **deltas): + return self.get_driver().quota_limit_check( + context, project_id, resource_registry.get_all_resources(), deltas) + QUOTAS = QuotaEngine.get_instance() diff --git a/neutron/quota/resource.py b/neutron/quota/resource.py index ca45dabe6dd..8db626161da 100644 --- a/neutron/quota/resource.py +++ b/neutron/quota/resource.py @@ -98,7 +98,7 @@ class BaseResource(object, metaclass=abc.ABCMeta): value = getattr(cfg.CONF.QUOTAS, self.flag, cfg.CONF.QUOTAS.default_quota) - return max(value, -1) + return max(value, quota_api.UNLIMITED_QUOTA) @property @abc.abstractmethod diff --git a/neutron/tests/functional/db/test_network.py b/neutron/tests/functional/db/test_network.py index 2160a344bc2..c6f5918d475 100644 --- a/neutron/tests/functional/db/test_network.py +++ b/neutron/tests/functional/db/test_network.py @@ -45,10 +45,8 @@ class NetworkRBACTestCase(testlib_api.SqlTestCase): self.subnet_1_id = uuidutils.generate_uuid() self.subnet_2_id = uuidutils.generate_uuid() self.port_id = uuidutils.generate_uuid() - make_res = mock.patch.object(quota.QuotaEngine, 'make_reservation') - self.mock_quota_make_res = make_res.start() - commit_res = mock.patch.object(quota.QuotaEngine, 'commit_reservation') - self.mock_quota_commit_res = commit_res.start() + quota_check = mock.patch.object(quota.QuotaEngine, 'quota_limit_check') + self.mock_quota_check = quota_check.start() def _create_network(self, tenant_id, network_id, shared, external=False): network = {'tenant_id': tenant_id, diff --git a/neutron/tests/unit/db/quota/test_driver.py b/neutron/tests/unit/db/quota/test_driver.py index c2e5ea0b7a0..f12176fccf6 100644 --- a/neutron/tests/unit/db/quota/test_driver.py +++ b/neutron/tests/unit/db/quota/test_driver.py @@ -92,6 +92,9 @@ class TestDbQuotaDriver(testlib_api.SqlTestCase, self.plugin = FakePlugin() self.context = context.get_admin_context() self.setup_coreplugin(core_plugin=DB_PLUGIN_KLASS) + self.quota_driver = driver.DbQuotaDriver() + self.project_1, self.project_2 = 'prj_test_1', 'prj_test_2' + self.resource_1, self.resource_2 = 'res_test_1', 'res_test_2' def test_create_quota_limit(self): defaults = {RESOURCE: TestResource(RESOURCE, 4)} @@ -142,16 +145,13 @@ class TestDbQuotaDriver(testlib_api.SqlTestCase, self.assertFalse(self.plugin.get_project_quotas(user_ctx, {}, PROJECT)) def test_get_all_quotas(self): - project_1 = 'prj_test_1' - project_2 = 'prj_test_2' - resource_1 = 'res_test_1' - resource_2 = 'res_test_2' + resources = {self.resource_1: TestResource(self.resource_1, 3), + self.resource_2: TestResource(self.resource_2, 5)} - resources = {resource_1: TestResource(resource_1, 3), - resource_2: TestResource(resource_2, 5)} - - self.plugin.update_quota_limit(self.context, project_1, resource_1, 7) - self.plugin.update_quota_limit(self.context, project_2, resource_2, 9) + self.plugin.update_quota_limit(self.context, self.project_1, + self.resource_1, 7) + self.plugin.update_quota_limit(self.context, self.project_2, + self.resource_2, 9) quotas = self.plugin.get_all_quotas(self.context, resources) # Expect two projects' quotas @@ -162,15 +162,15 @@ class TestDbQuotaDriver(testlib_api.SqlTestCase, # Check the expected limits. The quotas can be in any order. for quota in quotas: project = quota['project_id'] - self.assertIn(project, (project_1, project_2)) - if project == project_1: + self.assertIn(project, (self.project_1, self.project_2)) + if project == self.project_1: expected_limit_r1 = 7 expected_limit_r2 = 5 - if project == project_2: + if project == self.project_2: expected_limit_r1 = 3 expected_limit_r2 = 9 - self.assertEqual(expected_limit_r1, quota[resource_1]) - self.assertEqual(expected_limit_r2, quota[resource_2]) + self.assertEqual(expected_limit_r1, quota[self.resource_1]) + self.assertEqual(expected_limit_r2, quota[self.resource_2]) def test_limit_check(self): resources = {RESOURCE: TestResource(RESOURCE, 2)} @@ -222,23 +222,20 @@ class TestDbQuotaDriver(testlib_api.SqlTestCase, reservation.project_id) def test_make_reservation_single_resource(self): - quota_driver = driver.DbQuotaDriver() self._test_make_reservation_success( - quota_driver, RESOURCE, {RESOURCE: 1}) + self.quota_driver, RESOURCE, {RESOURCE: 1}) def test_make_reservation_fill_quota(self): - quota_driver = driver.DbQuotaDriver() self._test_make_reservation_success( - quota_driver, RESOURCE, {RESOURCE: 2}) + self.quota_driver, RESOURCE, {RESOURCE: 2}) def test_make_reservation_multiple_resources(self): - quota_driver = driver.DbQuotaDriver() resources = {RESOURCE: TestResource(RESOURCE, 2), ALT_RESOURCE: TestResource(ALT_RESOURCE, 2)} deltas = {RESOURCE: 1, ALT_RESOURCE: 2} self.plugin.update_quota_limit(self.context, PROJECT, RESOURCE, 2) self.plugin.update_quota_limit(self.context, PROJECT, ALT_RESOURCE, 2) - reservation = quota_driver.make_reservation( + reservation = self.quota_driver.make_reservation( self.context, self.context.project_id, resources, @@ -252,13 +249,12 @@ class TestDbQuotaDriver(testlib_api.SqlTestCase, reservation.project_id) def test_make_reservation_over_quota_fails(self): - quota_driver = driver.DbQuotaDriver() resources = {RESOURCE: TestResource(RESOURCE, 2, fake_count=2)} deltas = {RESOURCE: 1} self.plugin.update_quota_limit(self.context, PROJECT, RESOURCE, 2) self.assertRaises(exceptions.OverQuota, - quota_driver.make_reservation, + self.quota_driver.make_reservation, self.context, self.context.project_id, resources, @@ -269,9 +265,8 @@ class TestDbQuotaDriver(testlib_api.SqlTestCase, res = {RESOURCE: TestTrackedResource(RESOURCE, test_quota.MehModel)} self.plugin.update_quota_limit(self.context, PROJECT, RESOURCE, 6) - quota_driver = driver.DbQuotaDriver() - quota_driver.make_reservation(self.context, PROJECT, res, - {RESOURCE: 1}, self.plugin) + self.quota_driver.make_reservation(self.context, PROJECT, res, + {RESOURCE: 1}, self.plugin) quota_api.set_quota_usage(self.context, RESOURCE, PROJECT, 2) detailed_quota = self.plugin.get_detailed_project_quotas( self.context, res, PROJECT) @@ -279,32 +274,99 @@ class TestDbQuotaDriver(testlib_api.SqlTestCase, self.assertEqual(2, detailed_quota[RESOURCE]['used']) self.assertEqual(1, detailed_quota[RESOURCE]['reserved']) + def _create_resources(self): + return { + self.resource_1: + TestTrackedResource(self.resource_1, test_quota.MehModel), + self.resource_2: + TestCountableResource(self.resource_2, _count_resource)} + def test_get_detailed_project_quotas_multiple_resource(self): - project_1 = 'prj_test_1' - resource_1 = 'res_test_1' - resource_2 = 'res_test_2' - resources = {resource_1: - TestTrackedResource(resource_1, test_quota.MehModel), - resource_2: - TestCountableResource(resource_2, _count_resource)} + resources = self._create_resources() + self.plugin.update_quota_limit(self.context, self.project_1, + self.resource_1, 6) + self.plugin.update_quota_limit(self.context, self.project_1, + self.resource_2, 9) + self.quota_driver.make_reservation( + self.context, self.project_1, resources, + {self.resource_1: 1, self.resource_2: 7}, self.plugin) - self.plugin.update_quota_limit(self.context, project_1, resource_1, 6) - self.plugin.update_quota_limit(self.context, project_1, resource_2, 9) - quota_driver = driver.DbQuotaDriver() - quota_driver.make_reservation(self.context, project_1, - resources, - {resource_1: 1, resource_2: 7}, - self.plugin) - - quota_api.set_quota_usage(self.context, resource_1, project_1, 2) - quota_api.set_quota_usage(self.context, resource_2, project_1, 3) + quota_api.set_quota_usage(self.context, self.resource_1, + self.project_1, 2) + quota_api.set_quota_usage(self.context, self.resource_2, + self.project_1, 3) detailed_quota = self.plugin.get_detailed_project_quotas( - self.context, resources, project_1) + self.context, resources, self.project_1) - self.assertEqual(6, detailed_quota[resource_1]['limit']) - self.assertEqual(1, detailed_quota[resource_1]['reserved']) - self.assertEqual(2, detailed_quota[resource_1]['used']) + self.assertEqual(6, detailed_quota[self.resource_1]['limit']) + self.assertEqual(1, detailed_quota[self.resource_1]['reserved']) + self.assertEqual(2, detailed_quota[self.resource_1]['used']) - self.assertEqual(9, detailed_quota[resource_2]['limit']) - self.assertEqual(7, detailed_quota[resource_2]['reserved']) - self.assertEqual(3, detailed_quota[resource_2]['used']) + self.assertEqual(9, detailed_quota[self.resource_2]['limit']) + self.assertEqual(7, detailed_quota[self.resource_2]['reserved']) + self.assertEqual(3, detailed_quota[self.resource_2]['used']) + + def test_quota_limit_check(self): + resources = self._create_resources() + self.plugin.update_quota_limit(self.context, self.project_1, + self.resource_1, 10) + self.plugin.update_quota_limit(self.context, self.project_1, + self.resource_2, 10) + reservations = {self.resource_1: 8} + self.quota_driver.make_reservation( + self.context, self.project_1, resources, reservations, self.plugin) + resources[self.resource_2]._count_func = lambda x, y, z: 8 + + self.assertIsNone(self.quota_driver.quota_limit_check( + self.context, self.project_1, resources, {self.resource_1: 2})) + self.assertIsNone(self.quota_driver.quota_limit_check( + self.context, self.project_1, resources, {self.resource_2: 2})) + self.assertRaises( + exceptions.OverQuota, self.quota_driver.quota_limit_check, + self.context, self.project_1, resources, {self.resource_1: 3}) + self.assertRaises( + exceptions.OverQuota, self.quota_driver.quota_limit_check, + self.context, self.project_1, resources, {self.resource_2: 3}) + self.assertRaises( + exceptions.OverQuota, self.quota_driver.quota_limit_check, + self.context, self.project_1, resources, + {self.resource_1: 3, self.resource_2: 3}) + + def test_quota_limit_check_unlimited(self): + resources = self._create_resources() + self.plugin.update_quota_limit(self.context, self.project_1, + self.resource_1, -1) + self.plugin.update_quota_limit(self.context, self.project_1, + self.resource_2, -1) + reservations = {self.resource_1: 8} + self.quota_driver.make_reservation( + self.context, self.project_1, resources, reservations, self.plugin) + resources[self.resource_2]._count_func = lambda x, y, z: 8 + + self.assertIsNone(self.quota_driver.quota_limit_check( + self.context, self.project_1, resources, {self.resource_1: 2})) + self.assertIsNone(self.quota_driver.quota_limit_check( + self.context, self.project_1, resources, {self.resource_2: 2})) + self.assertIsNone(self.quota_driver.quota_limit_check( + self.context, self.project_1, resources, + {self.resource_1: 10 ** 9, self.resource_2: 10 ** 9})) + + def test_quota_limit_check_untracked_resource(self): + resources = self._create_resources() + self.plugin.update_quota_limit(self.context, self.project_1, + self.resource_1, -1) + self.plugin.update_quota_limit(self.context, self.project_1, + self.resource_2, -1) + reservations = {self.resource_1: 8} + self.quota_driver.make_reservation( + self.context, self.project_1, resources, reservations, self.plugin) + resources[self.resource_2]._count_func = lambda x, y, z: 8 + + self.assertIsNone(self.quota_driver.quota_limit_check( + self.context, self.project_1, resources, + {self.resource_1: 10 ** 9})) + self.assertIsNone(self.quota_driver.quota_limit_check( + self.context, self.project_1, resources, + {self.resource_2: 10 ** 9})) + self.assertIsNone(self.quota_driver.quota_limit_check( + self.context, self.project_1, resources, {'untracked': 10 ** 9})) diff --git a/neutron/tests/unit/db/quota/test_driver_nolock.py b/neutron/tests/unit/db/quota/test_driver_nolock.py new file mode 100644 index 00000000000..43278efbf65 --- /dev/null +++ b/neutron/tests/unit/db/quota/test_driver_nolock.py @@ -0,0 +1,24 @@ +# Copyright (c) 2021 Red Hat, Inc. +# +# 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 neutron.db.quota import driver_nolock +from neutron.tests.unit.db.quota import test_driver + + +class TestDbQuotaDriverNoLock(test_driver.TestDbQuotaDriver): + + def setUp(self): + super(TestDbQuotaDriverNoLock, self).setUp() + self.quota_driver = driver_nolock.DbQuotaNoLockDriver() diff --git a/neutron/tests/unit/db/test_securitygroups_db.py b/neutron/tests/unit/db/test_securitygroups_db.py index 5eb95b0173f..744d121556b 100644 --- a/neutron/tests/unit/db/test_securitygroups_db.py +++ b/neutron/tests/unit/db/test_securitygroups_db.py @@ -74,10 +74,8 @@ class SecurityGroupDbMixinTestCase(testlib_api.SqlTestCase): self.setup_coreplugin(core_plugin=DB_PLUGIN_KLASS) self.ctx = context.get_admin_context() self.mixin = SecurityGroupDbMixinImpl() - make_res = mock.patch.object(quota.QuotaEngine, 'make_reservation') - self.mock_quota_make_res = make_res.start() - commit_res = mock.patch.object(quota.QuotaEngine, 'commit_reservation') - self.mock_quota_commit_res = commit_res.start() + quota_check = mock.patch.object(quota.QuotaEngine, 'quota_limit_check') + self.mock_quota_check = quota_check.start() is_ext_supported = mock.patch( 'neutron_lib.api.extensions.is_extension_supported') self.is_ext_supported = is_ext_supported.start() diff --git a/neutron/tests/unit/plugins/ml2/test_plugin.py b/neutron/tests/unit/plugins/ml2/test_plugin.py index ab07a243d36..b0a081dac5d 100644 --- a/neutron/tests/unit/plugins/ml2/test_plugin.py +++ b/neutron/tests/unit/plugins/ml2/test_plugin.py @@ -2246,10 +2246,8 @@ class TestMl2PortBinding(Ml2PluginV2TestCase, cfg.CONF.set_override( 'enable_security_group', self.ENABLE_SG, group='SECURITYGROUP') - make_res = mock.patch.object(quota.QuotaEngine, 'make_reservation') - self.mock_quota_make_res = make_res.start() - commit_res = mock.patch.object(quota.QuotaEngine, 'commit_reservation') - self.mock_quota_commit_res = commit_res.start() + quota_check = mock.patch.object(quota.QuotaEngine, 'quota_limit_check') + self.mock_quota_check = quota_check.start() super(TestMl2PortBinding, self).setUp() def _check_port_binding_profile(self, port, profile=None): @@ -2970,10 +2968,8 @@ class TestMl2PortSecurity(Ml2PluginV2TestCase): cfg.CONF.set_override('enable_security_group', False, group='SECURITYGROUP') - make_res = mock.patch.object(quota.QuotaEngine, 'make_reservation') - self.mock_quota_make_res = make_res.start() - commit_res = mock.patch.object(quota.QuotaEngine, 'commit_reservation') - self.mock_quota_commit_res = commit_res.start() + quota_check = mock.patch.object(quota.QuotaEngine, 'quota_limit_check') + self.mock_quota_check = quota_check.start() super(TestMl2PortSecurity, self).setUp() def test_port_update_without_security_groups(self): diff --git a/releasenotes/notes/new_QuotaDriverAPI_methods-ed76d167974d6f9d.yaml b/releasenotes/notes/new_QuotaDriverAPI_methods-ed76d167974d6f9d.yaml new file mode 100644 index 00000000000..12f1e0fe19a --- /dev/null +++ b/releasenotes/notes/new_QuotaDriverAPI_methods-ed76d167974d6f9d.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added two new API methods to ``QuotaDriverAPI`` class. + ``get_resource_usage`` returns the current resource usage. + ``quota_limit_check`` checks the current resource usage of several + resources against a set of deltas (a dictionary of resource names + and resource counters).