From 6acefc6b101401686ef03f672fdc6d5be1b32ae1 Mon Sep 17 00:00:00 2001 From: John Garbutt Date: Tue, 10 Mar 2020 12:31:26 +0000 Subject: [PATCH] Assert quota related API behavior when noop Adding tests so its clear what happens with the noop driver when using the quota APIs. To make the unit tests work, we had to make the caching of the quota driver slightly more dynamic. We verify the current config matches the currently cached driver, and reload the driver if there is a miss-match. It also preserves the ability of some unit tests to pass in a fake quota driver. We also test the current unified limits driver, as it is currently identical in behaviour to the noop driver. As things evolve the tests will diverge, but will show the common approach to what is returned from the API in both cases. blueprint unified-limits-nova Change-Id: If3c58d6cbf0a0aee62766c7142beab165c1fb9a4 --- nova/conf/quota.py | 2 + nova/quota.py | 18 +- .../unit/api/openstack/compute/test_limits.py | 72 ++++++ .../openstack/compute/test_quota_classes.py | 106 +++++++++ .../unit/api/openstack/compute/test_quotas.py | 207 ++++++++++++++++++ nova/tests/unit/test_quota.py | 13 ++ 6 files changed, 414 insertions(+), 4 deletions(-) diff --git a/nova/conf/quota.py b/nova/conf/quota.py index bb37c0c28b85..0d51129d5038 100644 --- a/nova/conf/quota.py +++ b/nova/conf/quota.py @@ -181,6 +181,8 @@ Possible values: 'on-demand.'), ('nova.quota.NoopQuotaDriver', 'Ignores quota and treats all ' 'resources as unlimited.'), + ('nova.quota.UnifiedLimitsDriver', 'Do not use. Still being ' + 'developed.') ], help=""" Provides abstraction for quota checks. Users can configure a specific diff --git a/nova/quota.py b/nova/quota.py index 818c4a47516f..d3f4fc236c51 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -882,13 +882,23 @@ class QuotaEngine(object): } # NOTE(mriedem): quota_driver is ever only supplied in tests with a # fake driver. - self.__driver = quota_driver + self.__driver_override = quota_driver + self.__driver = None + self.__driver_name = None @property def _driver(self): - if self.__driver: - return self.__driver - self.__driver = importutils.import_object(CONF.quota.driver) + if self.__driver_override: + return self.__driver_override + + # NOTE(johngarbutt) to allow unit tests to change the driver by + # simply overriding config, double check if we have the correct + # driver cached before we return the currently cached driver + driver_name_in_config = CONF.quota.driver + if self.__driver_name != driver_name_in_config: + self.__driver = importutils.import_object(driver_name_in_config) + self.__driver_name = driver_name_in_config + return self.__driver def get_defaults(self, context): diff --git a/nova/tests/unit/api/openstack/compute/test_limits.py b/nova/tests/unit/api/openstack/compute/test_limits.py index 31033e111d0c..6f635cc983f4 100644 --- a/nova/tests/unit/api/openstack/compute/test_limits.py +++ b/nova/tests/unit/api/openstack/compute/test_limits.py @@ -477,3 +477,75 @@ class LimitsControllerTestV275(BaseLimitTestSuite): self.assertRaises( exception.ValidationError, self.controller.index, req=req) + + +class NoopLimitsControllerTest(test.NoDBTestCase): + quota_driver = "nova.quota.NoopQuotaDriver" + + def setUp(self): + super(NoopLimitsControllerTest, self).setUp() + self.flags(driver=self.quota_driver, group="quota") + self.controller = limits_v21.LimitsController() + # remove policy checks + patcher = self.mock_can = mock.patch('nova.context.RequestContext.can') + self.mock_can = patcher.start() + self.addCleanup(patcher.stop) + + def test_index_v21(self): + req = fakes.HTTPRequest.blank("/") + response = self.controller.index(req) + expected_response = { + "limits": { + "rate": [], + "absolute": { + 'maxImageMeta': -1, + 'maxPersonality': -1, + 'maxPersonalitySize': -1, + 'maxSecurityGroupRules': -1, + 'maxSecurityGroups': -1, + 'maxServerGroupMembers': -1, + 'maxServerGroups': -1, + 'maxServerMeta': -1, + 'maxTotalCores': -1, + 'maxTotalFloatingIps': -1, + 'maxTotalInstances': -1, + 'maxTotalKeypairs': -1, + 'maxTotalRAMSize': -1, + 'totalCoresUsed': -1, + 'totalFloatingIpsUsed': -1, + 'totalInstancesUsed': -1, + 'totalRAMUsed': -1, + 'totalSecurityGroupsUsed': -1, + 'totalServerGroupsUsed': -1, + }, + }, + } + self.assertEqual(expected_response, response) + + def test_index_v275(self): + req = fakes.HTTPRequest.blank("/?tenant_id=faketenant", + version='2.75') + response = self.controller.index(req) + expected_response = { + "limits": { + "rate": [], + "absolute": { + 'maxServerGroupMembers': -1, + 'maxServerGroups': -1, + 'maxServerMeta': -1, + 'maxTotalCores': -1, + 'maxTotalInstances': -1, + 'maxTotalKeypairs': -1, + 'maxTotalRAMSize': -1, + 'totalCoresUsed': -1, + 'totalInstancesUsed': -1, + 'totalRAMUsed': -1, + 'totalServerGroupsUsed': -1, + }, + }, + } + self.assertEqual(expected_response, response) + + +class UnifiedLimitsControllerTest(NoopLimitsControllerTest): + quota_driver = "nova.quota.UnifiedLimitsDriver" diff --git a/nova/tests/unit/api/openstack/compute/test_quota_classes.py b/nova/tests/unit/api/openstack/compute/test_quota_classes.py index bdb33a7e1ae2..d909db8d6499 100644 --- a/nova/tests/unit/api/openstack/compute/test_quota_classes.py +++ b/nova/tests/unit/api/openstack/compute/test_quota_classes.py @@ -13,11 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. import copy +import mock import webob from nova.api.openstack.compute import quota_classes \ as quota_classes_v21 from nova import exception +from nova import objects from nova import test from nova.tests.unit.api.openstack import fakes @@ -156,3 +158,107 @@ class QuotaClassSetsTestV257(QuotaClassSetsTestV250): for resource in quota_classes_v21.FILTERED_QUOTAS_2_57: self.quota_resources.pop(resource, None) self.filtered_quotas.extend(quota_classes_v21.FILTERED_QUOTAS_2_57) + + +class NoopQuotaClassesTest(test.NoDBTestCase): + quota_driver = "nova.quota.NoopQuotaDriver" + + def setUp(self): + super(NoopQuotaClassesTest, self).setUp() + self.flags(driver=self.quota_driver, group="quota") + self.controller = quota_classes_v21.QuotaClassSetsController() + + def test_show_v21(self): + req = fakes.HTTPRequest.blank("") + response = self.controller.show(req, "test_class") + expected_response = { + 'quota_class_set': { + 'id': 'test_class', + 'cores': -1, + 'fixed_ips': -1, + 'floating_ips': -1, + 'injected_file_content_bytes': -1, + 'injected_file_path_bytes': -1, + 'injected_files': -1, + 'instances': -1, + 'key_pairs': -1, + 'metadata_items': -1, + 'ram': -1, + 'security_group_rules': -1, + 'security_groups': -1 + } + } + self.assertEqual(expected_response, response) + + def test_show_v257(self): + req = fakes.HTTPRequest.blank("", version='2.57') + response = self.controller.show(req, "default") + expected_response = { + 'quota_class_set': { + 'id': 'default', + 'cores': -1, + 'instances': -1, + 'key_pairs': -1, + 'metadata_items': -1, + 'ram': -1, + 'server_group_members': -1, + 'server_groups': -1, + } + } + self.assertEqual(expected_response, response) + + def test_update_v21_still_rejects_badrequests(self): + req = fakes.HTTPRequest.blank("") + body = {'quota_class_set': {'instances': 50, 'cores': 50, + 'ram': 51200, 'unsupported': 12}} + self.assertRaises(exception.ValidationError, self.controller.update, + req, 'test_class', body=body) + + @mock.patch.object(objects.Quotas, "update_class") + def test_update_v21(self, mock_update): + req = fakes.HTTPRequest.blank("") + body = {'quota_class_set': {'ram': 51200}} + response = self.controller.update(req, 'default', body=body) + expected_response = { + 'quota_class_set': { + 'cores': -1, + 'fixed_ips': -1, + 'floating_ips': -1, + 'injected_file_content_bytes': -1, + 'injected_file_path_bytes': -1, + 'injected_files': -1, + 'instances': -1, + 'key_pairs': -1, + 'metadata_items': -1, + 'ram': -1, + 'security_group_rules': -1, + 'security_groups': -1 + } + } + self.assertEqual(expected_response, response) + mock_update.assert_called_once_with(req.environ['nova.context'], + "default", "ram", 51200) + + @mock.patch.object(objects.Quotas, "update_class") + def test_update_v257(self, mock_update): + req = fakes.HTTPRequest.blank("", version='2.57') + body = {'quota_class_set': {'ram': 51200}} + response = self.controller.update(req, 'default', body=body) + expected_response = { + 'quota_class_set': { + 'cores': -1, + 'instances': -1, + 'key_pairs': -1, + 'metadata_items': -1, + 'ram': -1, + 'server_group_members': -1, + 'server_groups': -1, + } + } + self.assertEqual(expected_response, response) + mock_update.assert_called_once_with(req.environ['nova.context'], + "default", "ram", 51200) + + +class UnifiedLimitsQuotaClassesTest(NoopQuotaClassesTest): + quota_driver = "nova.quota.UnifiedLimitsDriver" diff --git a/nova/tests/unit/api/openstack/compute/test_quotas.py b/nova/tests/unit/api/openstack/compute/test_quotas.py index 545bd51e137e..84780b2ca756 100644 --- a/nova/tests/unit/api/openstack/compute/test_quotas.py +++ b/nova/tests/unit/api/openstack/compute/test_quotas.py @@ -15,11 +15,13 @@ # under the License. import mock +from oslo_utils.fixture import uuidsentinel as uuids import webob from nova.api.openstack.compute import quota_sets as quotas_v21 from nova.db import constants as db_const from nova import exception +from nova import objects from nova import quota from nova import test from nova.tests.unit.api.openstack import fakes @@ -660,3 +662,208 @@ class QuotaSetsTestV275(QuotaSetsTestV257): query_string=query_string) self.assertRaises(exception.ValidationError, self.controller.delete, req, 1234) + + +class NoopQuotaSetsTest(test.NoDBTestCase): + quota_driver = "nova.quota.NoopQuotaDriver" + + def setUp(self): + super(NoopQuotaSetsTest, self).setUp() + self.flags(driver=self.quota_driver, group="quota") + self.controller = quotas_v21.QuotaSetsController() + self.stub_out('nova.api.openstack.identity.verify_project_id', + lambda ctx, project_id: True) + + def test_show_v21(self): + req = fakes.HTTPRequest.blank("") + response = self.controller.show(req, uuids.project_id) + expected_response = { + 'quota_set': { + 'id': uuids.project_id, + 'cores': -1, + 'fixed_ips': -1, + 'floating_ips': -1, + 'injected_file_content_bytes': -1, + 'injected_file_path_bytes': -1, + 'injected_files': -1, + 'instances': -1, + 'key_pairs': -1, + 'metadata_items': -1, + 'ram': -1, + 'security_group_rules': -1, + 'security_groups': -1, + 'server_group_members': -1, + 'server_groups': -1, + } + } + self.assertEqual(expected_response, response) + + def test_show_v257(self): + req = fakes.HTTPRequest.blank("", version='2.57') + response = self.controller.show(req, uuids.project_id) + expected_response = { + 'quota_set': { + 'id': uuids.project_id, + 'cores': -1, + 'instances': -1, + 'key_pairs': -1, + 'metadata_items': -1, + 'ram': -1, + 'server_group_members': -1, + 'server_groups': -1}} + self.assertEqual(expected_response, response) + + def test_detail_v21(self): + req = fakes.HTTPRequest.blank("") + response = self.controller.detail(req, uuids.project_id) + expected_detail = {'in_use': -1, 'limit': -1, 'reserved': -1} + expected_response = { + 'quota_set': { + 'id': uuids.project_id, + 'cores': expected_detail, + 'fixed_ips': expected_detail, + 'floating_ips': expected_detail, + 'injected_file_content_bytes': expected_detail, + 'injected_file_path_bytes': expected_detail, + 'injected_files': expected_detail, + 'instances': expected_detail, + 'key_pairs': expected_detail, + 'metadata_items': expected_detail, + 'ram': expected_detail, + 'security_group_rules': expected_detail, + 'security_groups': expected_detail, + 'server_group_members': expected_detail, + 'server_groups': expected_detail, + } + } + self.assertEqual(expected_response, response) + + def test_detail_v21_user(self): + req = fakes.HTTPRequest.blank("?user_id=42") + response = self.controller.detail(req, uuids.project_id) + expected_detail = {'in_use': -1, 'limit': -1, 'reserved': -1} + expected_response = { + 'quota_set': { + 'id': uuids.project_id, + 'cores': expected_detail, + 'fixed_ips': expected_detail, + 'floating_ips': expected_detail, + 'injected_file_content_bytes': expected_detail, + 'injected_file_path_bytes': expected_detail, + 'injected_files': expected_detail, + 'instances': expected_detail, + 'key_pairs': expected_detail, + 'metadata_items': expected_detail, + 'ram': expected_detail, + 'security_group_rules': expected_detail, + 'security_groups': expected_detail, + 'server_group_members': expected_detail, + 'server_groups': expected_detail, + } + } + self.assertEqual(expected_response, response) + + def test_update_still_rejects_badrequests(self): + req = fakes.HTTPRequest.blank("") + body = {'quota_set': {'instances': 50, 'cores': 50, + 'ram': 51200, 'unsupported': 12}} + self.assertRaises(exception.ValidationError, self.controller.update, + req, uuids.project_id, body=body) + + @mock.patch.object(objects.Quotas, "create_limit") + def test_update_v21(self, mock_create): + req = fakes.HTTPRequest.blank("") + body = {'quota_set': {'server_groups': 2}} + response = self.controller.update(req, uuids.project_id, body=body) + expected_response = { + 'quota_set': { + 'cores': -1, + 'fixed_ips': -1, + 'floating_ips': -1, + 'injected_file_content_bytes': -1, + 'injected_file_path_bytes': -1, + 'injected_files': -1, + 'instances': -1, + 'key_pairs': -1, + 'metadata_items': -1, + 'ram': -1, + 'security_group_rules': -1, + 'security_groups': -1, + 'server_group_members': -1, + 'server_groups': -1, + } + } + self.assertEqual(expected_response, response) + mock_create.assert_called_once_with(req.environ['nova.context'], + uuids.project_id, "server_groups", + 2, user_id=None) + + @mock.patch.object(objects.Quotas, "create_limit") + def test_update_v21_user(self, mock_create): + req = fakes.HTTPRequest.blank("?user_id=42") + body = {'quota_set': {'key_pairs': 52}} + response = self.controller.update(req, uuids.project_id, body=body) + expected_response = { + 'quota_set': { + 'cores': -1, + 'fixed_ips': -1, + 'floating_ips': -1, + 'injected_file_content_bytes': -1, + 'injected_file_path_bytes': -1, + 'injected_files': -1, + 'instances': -1, + 'key_pairs': -1, + 'metadata_items': -1, + 'ram': -1, + 'security_group_rules': -1, + 'security_groups': -1, + 'server_group_members': -1, + 'server_groups': -1, + } + } + self.assertEqual(expected_response, response) + mock_create.assert_called_once_with(req.environ['nova.context'], + uuids.project_id, "key_pairs", 52, + user_id="42") + + def test_defaults_v21(self): + req = fakes.HTTPRequest.blank("") + response = self.controller.defaults(req, uuids.project_id) + expected_response = { + 'quota_set': { + 'id': uuids.project_id, + 'cores': -1, + 'fixed_ips': -1, + 'floating_ips': -1, + 'injected_file_content_bytes': -1, + 'injected_file_path_bytes': -1, + 'injected_files': -1, + 'instances': -1, + 'key_pairs': -1, + 'metadata_items': -1, + 'ram': -1, + 'security_group_rules': -1, + 'security_groups': -1, + 'server_group_members': -1, + 'server_groups': -1, + } + } + self.assertEqual(expected_response, response) + + @mock.patch('nova.objects.Quotas.destroy_all_by_project') + def test_quotas_delete(self, mock_destroy_all_by_project): + req = fakes.HTTPRequest.blank("") + self.controller.delete(req, "1234") + mock_destroy_all_by_project.assert_called_once_with( + req.environ['nova.context'], "1234") + + @mock.patch('nova.objects.Quotas.destroy_all_by_project_and_user') + def test_user_quotas_delete(self, mock_destroy_all_by_user): + req = fakes.HTTPRequest.blank("?user_id=42") + self.controller.delete(req, "1234") + mock_destroy_all_by_user.assert_called_once_with( + req.environ['nova.context'], "1234", "42") + + +class UnifiedLimitsQuotaSetsTest(NoopQuotaSetsTest): + quota_driver = "nova.quota.UnifiedLimitsDriver" diff --git a/nova/tests/unit/test_quota.py b/nova/tests/unit/test_quota.py index c9784602a9a2..2e82931cad9d 100644 --- a/nova/tests/unit/test_quota.py +++ b/nova/tests/unit/test_quota.py @@ -340,6 +340,19 @@ class QuotaEngineTestCase(test.TestCase): quota_obj = quota.QuotaEngine(quota_driver=FakeDriver) self.assertEqual(quota_obj._driver, FakeDriver) + def test_init_with_flag_set(self): + quota_obj = quota.QuotaEngine() + self.assertIsInstance(quota_obj._driver, quota.DbQuotaDriver) + + self.flags(group="quota", driver="nova.quota.NoopQuotaDriver") + self.assertIsInstance(quota_obj._driver, quota.NoopQuotaDriver) + + self.flags(group="quota", driver="nova.quota.UnifiedLimitsDriver") + self.assertIsInstance(quota_obj._driver, quota.UnifiedLimitsDriver) + + self.flags(group="quota", driver="nova.quota.DbQuotaDriver") + self.assertIsInstance(quota_obj._driver, quota.DbQuotaDriver) + def _get_quota_engine(self, driver, resources=None): resources = resources or [ quota.AbsoluteResource('test_resource4'),