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'),