diff --git a/nova/api/openstack/compute/quota_sets.py b/nova/api/openstack/compute/quota_sets.py index 7777fc1cf8..616486c3ae 100644 --- a/nova/api/openstack/compute/quota_sets.py +++ b/nova/api/openstack/compute/quota_sets.py @@ -19,7 +19,7 @@ from oslo_utils import strutils import webob from nova.api.openstack import api_version_request -from nova.api.openstack.compute.schemas import quota_sets +from nova.api.openstack.compute.schemas import quota_sets as schema from nova.api.openstack import identity from nova.api.openstack import wsgi from nova.api import validation @@ -31,32 +31,35 @@ from nova import objects from nova.policies import quota_sets as qs_policies from nova import quota - CONF = nova.conf.CONF QUOTAS = quota.QUOTAS -FILTERED_QUOTAS_2_36 = ["fixed_ips", "floating_ips", - "security_group_rules", "security_groups"] +FILTERED_QUOTAS_v236 = [ + 'fixed_ips', 'floating_ips', 'security_group_rules', 'security_groups' +] -FILTERED_QUOTAS_2_57 = list(FILTERED_QUOTAS_2_36) -FILTERED_QUOTAS_2_57.extend(['injected_files', 'injected_file_content_bytes', - 'injected_file_path_bytes']) +FILTERED_QUOTAS_v257 = list(FILTERED_QUOTAS_v236) +FILTERED_QUOTAS_v257.extend([ + 'injected_files', + 'injected_file_content_bytes', + 'injected_file_path_bytes' +]) +@validation.validated class QuotaSetsController(wsgi.Controller): def _format_quota_set(self, project_id, quota_set, filtered_quotas): """Convert the quota object to a result dict.""" + result = {} if project_id: - result = dict(id=str(project_id)) - else: - result = {} + result['id'] = str(project_id) for resource in QUOTAS.resources: - if (resource not in filtered_quotas and - resource in quota_set): + if resource not in filtered_quotas and resource in quota_set: result[resource] = quota_set[resource] - return dict(quota_set=result) + + return {'quota_set': result} def _validate_quota_limit(self, resource, limit, minimum, maximum): def conv_inf(value): @@ -68,6 +71,7 @@ class QuotaSetsController(wsgi.Controller): "reserved %(minimum)s.") % {'limit': limit, 'resource': resource, 'minimum': minimum}) raise webob.exc.HTTPBadRequest(explanation=msg) + if conv_inf(limit) > conv_inf(maximum): msg = (_("Quota limit %(limit)s for %(resource)s must be " "less than or equal to %(maximum)s.") % @@ -104,60 +108,59 @@ class QuotaSetsController(wsgi.Controller): def _get_filtered_quotas(self, req): if api_version_request.is_supported(req, '2.57'): - return FILTERED_QUOTAS_2_57 + return FILTERED_QUOTAS_v257 elif api_version_request.is_supported(req, '2.36'): - return FILTERED_QUOTAS_2_36 + return FILTERED_QUOTAS_v236 else: return [] @wsgi.expected_errors(400) - @validation.query_schema(quota_sets.show_query, '2.0', '2.74') - @validation.query_schema(quota_sets.show_query_v275, '2.75') + @validation.query_schema(schema.show_query, '2.0', '2.74') + @validation.query_schema(schema.show_query_v275, '2.75') + @validation.response_body_schema(schema.show_response, '2.0', '2.35') + @validation.response_body_schema(schema.show_response_v236, '2.36', '2.56') + @validation.response_body_schema(schema.show_response_v257, '2.57') def show(self, req, id): - filtered_quotas = self._get_filtered_quotas(req) - return self._show(req, id, filtered_quotas) - - def _show(self, req, id, filtered_quotas): context = req.environ['nova.context'] context.can(qs_policies.POLICY_ROOT % 'show', {'project_id': id}) identity.verify_project_id(context, id) params = urlparse.parse_qs(req.environ.get('QUERY_STRING', '')) user_id = params.get('user_id', [None])[0] + filtered_quotas = self._get_filtered_quotas(req) return self._format_quota_set( id, self._get_quotas(context, id, user_id=user_id), filtered_quotas=filtered_quotas) @wsgi.expected_errors(400) - @validation.query_schema(quota_sets.show_query, '2.0', '2.74') - @validation.query_schema(quota_sets.show_query_v275, '2.75') + @validation.query_schema(schema.detail_query, '2.0', '2.74') + @validation.query_schema(schema.detail_query_v275, '2.75') + @validation.response_body_schema(schema.detail_response, '2.0', '2.35') + @validation.response_body_schema(schema.detail_response_v236, '2.36', '2.56') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v257, '2.57') def detail(self, req, id): - filtered_quotas = self._get_filtered_quotas(req) - return self._detail(req, id, filtered_quotas) - - def _detail(self, req, id, filtered_quotas): context = req.environ['nova.context'] context.can(qs_policies.POLICY_ROOT % 'detail', {'project_id': id}) identity.verify_project_id(context, id) user_id = req.GET.get('user_id', None) + filtered_quotas = self._get_filtered_quotas(req) return self._format_quota_set( id, self._get_quotas(context, id, user_id=user_id, usages=True), filtered_quotas=filtered_quotas) @wsgi.expected_errors(400) - @validation.schema(quota_sets.update, '2.0', '2.35') - @validation.schema(quota_sets.update_v236, '2.36', '2.56') - @validation.schema(quota_sets.update_v257, '2.57') - @validation.query_schema(quota_sets.show_query, '2.0', '2.74') - @validation.query_schema(quota_sets.show_query_v275, '2.75') + @validation.schema(schema.update, '2.0', '2.35') + @validation.schema(schema.update_v236, '2.36', '2.56') + @validation.schema(schema.update_v257, '2.57') + @validation.query_schema(schema.update_query, '2.0', '2.74') + @validation.query_schema(schema.update_query_v275, '2.75') + @validation.response_body_schema(schema.update_response, '2.0', '2.35') + @validation.response_body_schema(schema.update_response_v236, '2.36', '2.56') # noqa: E501 + @validation.response_body_schema(schema.update_response_v257, '2.57') def update(self, req, id, body): - filtered_quotas = self._get_filtered_quotas(req) - return self._update(req, id, body, filtered_quotas) - - def _update(self, req, id, body, filtered_quotas): context = req.environ['nova.context'] context.can(qs_policies.POLICY_ROOT % 'update', {'project_id': id}) identity.verify_project_id(context, id) @@ -165,6 +168,7 @@ class QuotaSetsController(wsgi.Controller): project_id = id params = urlparse.parse_qs(req.environ.get('QUERY_STRING', '')) user_id = params.get('user_id', [None])[0] + filtered_quotas = self._get_filtered_quotas(req) quota_set = body['quota_set'] @@ -221,27 +225,28 @@ class QuotaSetsController(wsgi.Controller): @wsgi.api_version('2.0') @wsgi.expected_errors(400) - @validation.query_schema(quota_sets.defaults_query) + @validation.query_schema(schema.defaults_query) + @validation.response_body_schema(schema.defaults_response, '2.0', '2.35') + @validation.response_body_schema(schema.defaults_response_v236, '2.36', '2.56') # noqa: E501 + @validation.response_body_schema(schema.defaults_response_v257, '2.57') def defaults(self, req, id): - filtered_quotas = self._get_filtered_quotas(req) - return self._defaults(req, id, filtered_quotas) - - def _defaults(self, req, id, filtered_quotas): context = req.environ['nova.context'] context.can(qs_policies.POLICY_ROOT % 'defaults', {'project_id': id}) identity.verify_project_id(context, id) values = QUOTAS.get_defaults(context) - return self._format_quota_set(id, values, - filtered_quotas=filtered_quotas) + filtered_quotas = self._get_filtered_quotas(req) + return self._format_quota_set( + id, values, filtered_quotas=filtered_quotas) # TODO(oomichi): Here should be 204(No Content) instead of 202 by v2.1 # +microversions because the resource quota-set has been deleted completely # when returning a response. @wsgi.expected_errors(()) - @validation.query_schema(quota_sets.show_query_v275, '2.75') - @validation.query_schema(quota_sets.show_query, '2.0', '2.74') @wsgi.response(202) + @validation.query_schema(schema.delete_query, '2.0', '2.74') + @validation.query_schema(schema.delete_query_v275, '2.75') + @validation.response_body_schema(schema.delete_response) def delete(self, req, id): context = req.environ['nova.context'] context.can(qs_policies.POLICY_ROOT % 'delete', {'project_id': id}) diff --git a/nova/api/openstack/compute/schemas/quota_classes.py b/nova/api/openstack/compute/schemas/quota_classes.py index ce8e30be18..7410a95a2c 100644 --- a/nova/api/openstack/compute/schemas/quota_classes.py +++ b/nova/api/openstack/compute/schemas/quota_classes.py @@ -20,7 +20,7 @@ update = { 'properties': { 'quota_class_set': { 'type': 'object', - 'properties': quota_sets.quota_resources, + 'properties': quota_sets._quota_resources, 'additionalProperties': False, }, }, diff --git a/nova/api/openstack/compute/schemas/quota_sets.py b/nova/api/openstack/compute/schemas/quota_sets.py index 0a84120f94..6dca6c8fb3 100644 --- a/nova/api/openstack/compute/schemas/quota_sets.py +++ b/nova/api/openstack/compute/schemas/quota_sets.py @@ -16,7 +16,7 @@ import copy from nova.api.validation import parameter_types -common_quota = { +_common_quota = { 'type': ['integer', 'string'], 'pattern': '^-?[0-9]+$', # -1 is a flag value for unlimited @@ -26,42 +26,47 @@ common_quota = { 'maximum': 0x7FFFFFFF } -quota_resources = { - 'instances': common_quota, - 'cores': common_quota, - 'ram': common_quota, - 'floating_ips': common_quota, - 'fixed_ips': common_quota, - 'metadata_items': common_quota, - 'key_pairs': common_quota, - 'security_groups': common_quota, - 'security_group_rules': common_quota, - 'injected_files': common_quota, - 'injected_file_content_bytes': common_quota, - 'injected_file_path_bytes': common_quota, - 'server_groups': common_quota, - 'server_group_members': common_quota, +_quota_resources = { + 'instances': _common_quota, + 'cores': _common_quota, + 'ram': _common_quota, + 'floating_ips': _common_quota, + 'fixed_ips': _common_quota, + 'metadata_items': _common_quota, + 'key_pairs': _common_quota, + 'security_groups': _common_quota, + 'security_group_rules': _common_quota, + 'injected_files': _common_quota, + 'injected_file_content_bytes': _common_quota, + 'injected_file_path_bytes': _common_quota, + 'server_groups': _common_quota, + 'server_group_members': _common_quota, # NOTE(stephenfin): This will always be rejected since it was nova-network # only, but we need to allow users to submit it at a minimum - 'networks': common_quota + 'networks': _common_quota } -update_quota_set = copy.deepcopy(quota_resources) -update_quota_set.update({'force': parameter_types.boolean}) +_update_quota_set = copy.deepcopy(_quota_resources) +_update_quota_set.update({'force': parameter_types.boolean}) -update_quota_set_v236 = copy.deepcopy(update_quota_set) -del update_quota_set_v236['fixed_ips'] -del update_quota_set_v236['floating_ips'] -del update_quota_set_v236['security_groups'] -del update_quota_set_v236['security_group_rules'] -del update_quota_set_v236['networks'] +_update_quota_set_v236 = copy.deepcopy(_update_quota_set) +del _update_quota_set_v236['fixed_ips'] +del _update_quota_set_v236['floating_ips'] +del _update_quota_set_v236['security_groups'] +del _update_quota_set_v236['security_group_rules'] +del _update_quota_set_v236['networks'] + +_update_quota_set_v257 = copy.deepcopy(_update_quota_set_v236) +del _update_quota_set_v257['injected_files'] +del _update_quota_set_v257['injected_file_content_bytes'] +del _update_quota_set_v257['injected_file_path_bytes'] update = { 'type': 'object', 'properties': { 'quota_set': { 'type': 'object', - 'properties': update_quota_set, + 'properties': _update_quota_set, 'additionalProperties': False, }, }, @@ -70,15 +75,10 @@ update = { } update_v236 = copy.deepcopy(update) -update_v236['properties']['quota_set']['properties'] = update_quota_set_v236 +update_v236['properties']['quota_set']['properties'] = _update_quota_set_v236 -# 2.57 builds on 2.36 and removes injected_file* quotas. -update_quota_set_v257 = copy.deepcopy(update_quota_set_v236) -del update_quota_set_v257['injected_files'] -del update_quota_set_v257['injected_file_content_bytes'] -del update_quota_set_v257['injected_file_path_bytes'] update_v257 = copy.deepcopy(update_v236) -update_v257['properties']['quota_set']['properties'] = update_quota_set_v257 +update_v257['properties']['quota_set']['properties'] = _update_quota_set_v257 show_query = { 'type': 'object', @@ -95,9 +95,194 @@ show_query = { show_query_v275 = copy.deepcopy(show_query) show_query_v275['additionalProperties'] = False +detail_query = copy.deepcopy(show_query) +detail_query_v275 = copy.deepcopy(show_query_v275) + +update_query = copy.deepcopy(show_query) +update_query_v275 = copy.deepcopy(show_query_v275) + # TODO(stephenfin): Remove additionalProperties in a future API version defaults_query = { 'type': 'object', 'properties': {}, 'additionalProperties': True, } + +delete_query = copy.deepcopy(show_query) +delete_query_v275 = copy.deepcopy(show_query_v275) + +_quota_response = { + 'type': 'object', + 'properties': { + 'cores': {'type': 'integer', 'minimum': -1}, + 'fixed_ips': {'type': 'integer', 'minimum': -1}, + 'floating_ips': {'type': 'integer', 'minimum': -1}, + 'injected_file_content_bytes': {'type': 'integer', 'minimum': -1}, + 'injected_file_path_bytes': {'type': 'integer', 'minimum': -1}, + 'injected_files': {'type': 'integer', 'minimum': -1}, + 'instances': {'type': 'integer', 'minimum': -1}, + 'key_pairs': {'type': 'integer', 'minimum': -1}, + 'metadata_items': {'type': 'integer', 'minimum': -1}, + 'networks': {'type': 'integer', 'minimum': -1}, + 'ram': {'type': 'integer', 'minimum': -1}, + 'security_groups': {'type': 'integer', 'minimum': -1}, + 'security_group_rules': {'type': 'integer', 'minimum': -1}, + 'server_groups': {'type': 'integer', 'minimum': -1}, + 'server_group_members': {'type': 'integer', 'minimum': -1}, + }, + 'required': [ + # only networks is optional (it only appears under nova-network) + 'cores', + 'fixed_ips', + 'floating_ips', + 'injected_file_content_bytes', + 'injected_file_path_bytes', + 'injected_files', + 'instances', + 'key_pairs', + 'metadata_items', + 'ram', + 'security_groups', + 'security_group_rules', + 'server_groups', + 'server_group_members', + ], + 'additionalProperties': False, +} + +_quota_response_v236 = copy.deepcopy(_quota_response) +for field in { + 'fixed_ips', 'floating_ips', 'security_group_rules', 'security_groups' +}: + del _quota_response_v236['properties'][field] + _quota_response_v236['required'].pop( + _quota_response_v236['required'].index(field) + ) + +_quota_response_v257 = copy.deepcopy(_quota_response_v236) +for field in { + 'injected_files', 'injected_file_content_bytes', 'injected_file_path_bytes' +}: + del _quota_response_v257['properties'][field] + _quota_response_v257['required'].pop( + _quota_response_v257['required'].index(field) + ) + +show_response = { + 'type': 'object', + 'properties': { + 'quota_set': copy.deepcopy(_quota_response), + }, + 'required': ['quota_set'], + 'additionalProperties': False, +} +show_response['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +show_response['properties']['quota_set']['required'].append('id') + +show_response_v236 = copy.deepcopy(show_response) +show_response_v236['properties']['quota_set'] = copy.deepcopy( + _quota_response_v236 +) +show_response_v236['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +show_response_v236['properties']['quota_set']['required'].append('id') + +show_response_v257 = copy.deepcopy(show_response_v236) +show_response_v257['properties']['quota_set'] = copy.deepcopy( + _quota_response_v257 +) +show_response_v257['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +show_response_v257['properties']['quota_set']['required'].append('id') + +_detail_quota = { + 'type': 'object', + 'properties': { + 'in_use': {'type': 'integer', 'minimum': -1}, + 'limit': {'type': 'integer', 'minimum': -1}, + 'reserved': {'type': 'integer', 'minimum': -1}, + }, + 'required': ['in_use', 'limit', 'reserved'], + 'additionalProperties': False, +} + +_detail_quota_response = copy.deepcopy(_quota_response) +for field in _detail_quota_response['properties']: + if field == 'id': + continue + + _detail_quota_response['properties'][field] = _detail_quota + +_detail_quota_response_v236 = copy.deepcopy(_detail_quota_response) +for field in { + 'fixed_ips', 'floating_ips', 'security_group_rules', 'security_groups' +}: + del _detail_quota_response_v236['properties'][field] + _detail_quota_response_v236['required'].pop( + _detail_quota_response_v236['required'].index(field) + ) + +_detail_quota_response_v257 = copy.deepcopy(_detail_quota_response_v236) +for field in { + 'injected_files', 'injected_file_content_bytes', 'injected_file_path_bytes' +}: + del _detail_quota_response_v257['properties'][field] + _detail_quota_response_v257['required'].pop( + _detail_quota_response_v257['required'].index(field) + ) + +detail_response = { + 'type': 'object', + 'properties': { + 'quota_set': copy.deepcopy(_detail_quota_response), + }, + 'required': ['quota_set'], + 'additionalProperties': False, +} +detail_response['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +detail_response['properties']['quota_set']['required'].append('id') + +detail_response_v236 = copy.deepcopy(detail_response) +detail_response_v236['properties']['quota_set'] = copy.deepcopy( + _detail_quota_response_v236 +) +detail_response_v236['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +detail_response_v236['properties']['quota_set']['required'].append('id') + +detail_response_v257 = copy.deepcopy(detail_response_v236) +detail_response_v257['properties']['quota_set'] = copy.deepcopy( + _detail_quota_response_v257 +) +detail_response_v257['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +detail_response_v257['properties']['quota_set']['required'].append('id') + +update_response = { + 'type': 'object', + 'properties': { + 'quota_set': _quota_response, + }, + 'required': ['quota_set'], + 'additionalProperties': False, +} + +update_response_v236 = copy.deepcopy(update_response) +update_response_v236['properties']['quota_set'] = _quota_response_v236 + +update_response_v257 = copy.deepcopy(update_response_v236) +update_response_v257['properties']['quota_set'] = _quota_response_v257 + +defaults_response = copy.deepcopy(show_response) +defaults_response_v236 = copy.deepcopy(show_response_v236) +defaults_response_v257 = copy.deepcopy(show_response_v257) + +delete_response = {'type': 'null'} diff --git a/nova/tests/unit/api/openstack/compute/test_quotas.py b/nova/tests/unit/api/openstack/compute/test_quota_sets.py similarity index 84% rename from nova/tests/unit/api/openstack/compute/test_quotas.py rename to nova/tests/unit/api/openstack/compute/test_quota_sets.py index 17813dda1a..bbfdaa9af2 100644 --- a/nova/tests/unit/api/openstack/compute/test_quotas.py +++ b/nova/tests/unit/api/openstack/compute/test_quota_sets.py @@ -20,7 +20,7 @@ from oslo_limit import fixture as limit_fixture from oslo_utils.fixture import uuidsentinel as uuids import webob -from nova.api.openstack.compute import quota_sets as quotas_v21 +from nova.api.openstack.compute import quota_sets from nova.db import constants as db_const from nova import exception from nova.limit import local as local_limit @@ -32,12 +32,14 @@ from nova.tests.unit.api.openstack import fakes def quota_set(id, include_server_group_quotas=True): - res = {'quota_set': {'id': id, 'metadata_items': 128, - 'ram': 51200, 'floating_ips': -1, 'fixed_ips': -1, - 'instances': 10, 'injected_files': 5, 'cores': 20, - 'injected_file_content_bytes': 10240, - 'security_groups': -1, 'security_group_rules': -1, - 'key_pairs': 100, 'injected_file_path_bytes': 255}} + res = { + 'quota_set': { + 'id': id, 'metadata_items': 128, + 'ram': 51200, 'floating_ips': -1, 'fixed_ips': -1, + 'instances': 10, 'injected_files': 5, 'cores': 20, + 'injected_file_content_bytes': 10240, + 'security_groups': -1, 'security_group_rules': -1, + 'key_pairs': 100, 'injected_file_path_bytes': 255}} if include_server_group_quotas: res['quota_set']['server_groups'] = 10 res['quota_set']['server_group_members'] = 10 @@ -47,7 +49,10 @@ def quota_set(id, include_server_group_quotas=True): class BaseQuotaSetsTest(test.TestCase): def setUp(self): - super(BaseQuotaSetsTest, self).setUp() + super().setUp() + + self.controller = quota_sets.QuotaSetsController() + # We need to stub out verify_project_id so that it doesn't # generate an EndpointNotFound exception and result in a # server error. @@ -56,13 +61,11 @@ class BaseQuotaSetsTest(test.TestCase): class QuotaSetsTestV21(BaseQuotaSetsTest): - plugin = quotas_v21 validation_error = exception.ValidationError include_server_group_quotas = True def setUp(self): super(QuotaSetsTestV21, self).setUp() - self._setup_controller() self.default_quotas = { 'instances': 10, 'cores': 20, @@ -81,19 +84,15 @@ class QuotaSetsTestV21(BaseQuotaSetsTest): self.default_quotas['server_groups'] = 10 self.default_quotas['server_group_members'] = 10 - def _setup_controller(self): - self.controller = self.plugin.QuotaSetsController() - def _get_http_request(self, url=''): return fakes.HTTPRequest.blank(url) def test_format_quota_set(self): - quota_set = self.controller._format_quota_set('1234', - self.default_quotas, - []) + quota_set = self.controller._format_quota_set( + uuids.project_id, self.default_quotas, []) qs = quota_set['quota_set'] - self.assertEqual(qs['id'], '1234') + self.assertEqual(qs['id'], uuids.project_id) self.assertEqual(qs['instances'], 10) self.assertEqual(qs['cores'], 20) self.assertEqual(qs['ram'], 51200) @@ -171,9 +170,11 @@ class QuotaSetsTestV21(BaseQuotaSetsTest): def test_quotas_show(self): req = self._get_http_request() - res_dict = self.controller.show(req, 1234) + res_dict = self.controller.show(req, uuids.project_id) - ref_quota_set = quota_set('1234', self.include_server_group_quotas) + ref_quota_set = quota_set( + uuids.project_id, self.include_server_group_quotas + ) self.assertEqual(res_dict, ref_quota_set) def test_quotas_update(self): @@ -197,8 +198,9 @@ class QuotaSetsTestV21(BaseQuotaSetsTest): @mock.patch('nova.api.validation.validators._SchemaValidator.validate') @mock.patch('nova.objects.Quotas.create_limit') - def test_quotas_update_with_bad_data(self, mock_createlimit, - mock_validate): + def test_quotas_update_with_bad_data( + self, mock_createlimit, mock_validate, + ): self.default_quotas.update({ 'instances': 50, 'cores': -50 @@ -278,69 +280,69 @@ class QuotaSetsTestV21(BaseQuotaSetsTest): @mock.patch('nova.objects.Quotas.destroy_all_by_project') def test_quotas_delete(self, mock_destroy_all_by_project): req = self._get_http_request() - self.controller.delete(req, 1234) + self.controller.delete(req, uuids.project_id) self.assertEqual(202, self.controller.delete.wsgi_codes(req)) mock_destroy_all_by_project.assert_called_once_with( - req.environ['nova.context'], 1234) + req.environ['nova.context'], uuids.project_id) def test_duplicate_quota_filter(self): query_string = 'user_id=1&user_id=2' req = fakes.HTTPRequest.blank('', query_string=query_string) - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) def test_quota_filter_negative_int_as_string(self): req = fakes.HTTPRequest.blank('', query_string='user_id=-1') - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) def test_quota_filter_int_as_string(self): req = fakes.HTTPRequest.blank('', query_string='user_id=123') - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) def test_unknown_quota_filter(self): query_string = 'unknown_filter=abc' req = fakes.HTTPRequest.blank('', query_string=query_string) - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) def test_quota_additional_filter(self): query_string = 'user_id=1&additional_filter=2' req = fakes.HTTPRequest.blank('', query_string=query_string) - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) class ExtendedQuotasTestV21(BaseQuotaSetsTest): - plugin = quotas_v21 - - def setUp(self): - super(ExtendedQuotasTestV21, self).setUp() - self._setup_controller() - - fake_quotas = {'ram': {'limit': 51200, - 'in_use': 12800, - 'reserved': 12800}, - 'cores': {'limit': 20, - 'in_use': 10, - 'reserved': 5}, - 'instances': {'limit': 100, - 'in_use': 0, - 'reserved': 0}} - - def _setup_controller(self): - self.controller = self.plugin.QuotaSetsController() + fake_quotas = { + 'cores': {'limit': 20, 'in_use': 10, 'reserved': 5}, + 'fixed_ips': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'floating_ips': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'injected_file_content_bytes': { + 'limit': -1, 'in_use': 0, 'reserved': -1 + }, + 'injected_file_path_bytes': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'injected_files': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'instances': {'limit': 100, 'in_use': 0, 'reserved': 0}, + 'key_pairs': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'metadata_items': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'ram': {'limit': 51200, 'in_use': 12800, 'reserved': 12800}, + 'security_groups': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'security_group_rules': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'server_groups': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'server_group_members': {'limit': -1, 'in_use': 0, 'reserved': -1}, + } def fake_get_quotas(self, context, id, user_id=None, usages=False): if usages: @@ -350,15 +352,10 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest): def fake_get_settable_quotas(self, context, project_id, user_id=None): return { - 'ram': {'minimum': self.fake_quotas['ram']['in_use'] + - self.fake_quotas['ram']['reserved'], - 'maximum': -1}, - 'cores': {'minimum': self.fake_quotas['cores']['in_use'] + - self.fake_quotas['cores']['reserved'], - 'maximum': -1}, - 'instances': {'minimum': self.fake_quotas['instances']['in_use'] + - self.fake_quotas['instances']['reserved'], - 'maximum': -1}, + k: { + 'minimum': v['in_use'] + v['reserved'], + 'maximum': -1, + } for k, v in self.fake_quotas.items() } def _get_http_request(self, url=''): @@ -375,7 +372,7 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest): @mock.patch.object(quota.QUOTAS, 'get_settable_quotas') def test_quotas_force_update_exceed_in_used(self, get_settable_quotas): - with mock.patch.object(self.plugin.QuotaSetsController, + with mock.patch.object(quota_sets.QuotaSetsController, '_get_quotas') as _get_quotas: body = {'quota_set': {'cores': 10, 'force': 'True'}} @@ -413,19 +410,12 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest): class UserQuotasTestV21(BaseQuotaSetsTest): - plugin = quotas_v21 + plugin = quota_sets include_server_group_quotas = True - def setUp(self): - super(UserQuotasTestV21, self).setUp() - self._setup_controller() - def _get_http_request(self, url=''): return fakes.HTTPRequest.blank(url) - def _setup_controller(self): - self.controller = self.plugin.QuotaSetsController() - def test_user_quotas_show(self): req = self._get_http_request( '/v2.1/os-quota-sets/%s?user_id=1' % fakes.FAKE_PROJECT_ID) @@ -496,19 +486,15 @@ class UserQuotasTestV21(BaseQuotaSetsTest): len(mock_createlimit.mock_calls)) -class QuotaSetsTestV236(test.NoDBTestCase): +class QuotaSetsTestV236(BaseQuotaSetsTest): microversion = '2.36' def setUp(self): super(QuotaSetsTestV236, self).setUp() - # We need to stub out verify_project_id so that it doesn't - # generate an EndpointNotFound exception and result in a - # server error. - self.stub_out('nova.api.openstack.identity.verify_project_id', - lambda ctx, project_id: True) self.old_req = fakes.HTTPRequest.blank('', version='2.1') - self.filtered_quotas = ['fixed_ips', 'floating_ips', + self.filtered_quotas = [ + 'fixed_ips', 'floating_ips', 'security_group_rules', 'security_groups'] self.quotas = { 'cores': {'limit': 20}, @@ -542,58 +528,57 @@ class QuotaSetsTestV236(test.NoDBTestCase): 'server_group_members': 10, 'server_groups': 10 } - self.controller = quotas_v21.QuotaSetsController() + self.controller = quota_sets.QuotaSetsController() self.req = fakes.HTTPRequest.blank('', version=self.microversion) - def _ensure_filtered_quotas_existed_in_old_api(self): - res_dict = self.controller.show(self.old_req, 1234) + def test_quotas_show_filtered(self): + res_dict = self.controller.show(self.old_req, uuids.project_id) for filtered in self.filtered_quotas: self.assertIn(filtered, res_dict['quota_set']) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quotas_show_filtered(self, mock_quotas): - mock_quotas.return_value = self.quotas - self._ensure_filtered_quotas_existed_in_old_api() - res_dict = self.controller.show(self.req, 1234) + res_dict = self.controller.show(self.req, uuids.project_id) for filtered in self.filtered_quotas: self.assertNotIn(filtered, res_dict['quota_set']) - @mock.patch('nova.quota.QUOTAS.get_defaults') - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quotas_default_filtered(self, mock_quotas, mock_defaults): - mock_quotas.return_value = self.quotas - self._ensure_filtered_quotas_existed_in_old_api() - res_dict = self.controller.defaults(self.req, 1234) + def test_quotas_default_filtered(self): + res_dict = self.controller.defaults(self.old_req, uuids.project_id) + for filtered in self.filtered_quotas: + self.assertIn(filtered, res_dict['quota_set']) + + res_dict = self.controller.defaults(self.req, uuids.project_id) for filtered in self.filtered_quotas: self.assertNotIn(filtered, res_dict['quota_set']) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quotas_detail_filtered(self, mock_quotas): - mock_quotas.return_value = self.quotas - self._ensure_filtered_quotas_existed_in_old_api() - res_dict = self.controller.detail(self.req, 1234) + def test_quotas_detail_filtered(self): + res_dict = self.controller.detail(self.old_req, uuids.project_id) + for filtered in self.filtered_quotas: + self.assertIn(filtered, res_dict['quota_set']) + + res_dict = self.controller.detail(self.req, uuids.project_id) for filtered in self.filtered_quotas: self.assertNotIn(filtered, res_dict['quota_set']) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quotas_update_input_filtered(self, mock_quotas): - mock_quotas.return_value = self.quotas - self._ensure_filtered_quotas_existed_in_old_api() + def test_quotas_update_input_filtered(self): + self.controller.update( + self.old_req, uuids.project_id, + body={'quota_set': {k: 100 for k in self.filtered_quotas}}) + for filtered in self.filtered_quotas: - self.assertRaises(exception.ValidationError, - self.controller.update, self.req, 1234, + self.assertRaises( + exception.ValidationError, + self.controller.update, self.req, uuids.project_id, body={'quota_set': {filtered: 100}}) - @mock.patch('nova.objects.Quotas.create_limit') - @mock.patch('nova.quota.QUOTAS.get_settable_quotas') - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quotas_update_output_filtered(self, mock_quotas, mock_settable, - mock_create_limit): - mock_quotas.return_value = self.quotas - mock_settable.return_value = {'cores': {'maximum': -1, 'minimum': 0}} - self._ensure_filtered_quotas_existed_in_old_api() - res_dict = self.controller.update(self.req, 1234, - body={'quota_set': {'cores': 100}}) + def test_quotas_update_output_filtered(self): + res_dict = self.controller.update( + self.old_req, uuids.project_id, + body={'quota_set': {'cores': 100}}) + for filtered in self.filtered_quotas: + self.assertIn(filtered, res_dict['quota_set']) + + res_dict = self.controller.update( + self.req, uuids.project_id, + body={'quota_set': {'cores': 101}}) for filtered in self.filtered_quotas: self.assertNotIn(filtered, res_dict['quota_set']) @@ -603,29 +588,20 @@ class QuotaSetsTestV257(QuotaSetsTestV236): def setUp(self): super(QuotaSetsTestV257, self).setUp() - self.filtered_quotas.extend(quotas_v21.FILTERED_QUOTAS_2_57) + self.filtered_quotas.extend(quota_sets.FILTERED_QUOTAS_v257) class QuotaSetsTestV275(QuotaSetsTestV257): microversion = '2.75' - @mock.patch('nova.objects.Quotas.destroy_all_by_project') - @mock.patch('nova.objects.Quotas.create_limit') - @mock.patch('nova.quota.QUOTAS.get_settable_quotas') - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quota_additional_filter_older_version(self, mock_quotas, - mock_settable, - mock_create_limit, - mock_destroy): - mock_quotas.return_value = self.quotas - mock_settable.return_value = {'cores': {'maximum': -1, 'minimum': 0}} + def test_quota_additional_filter_older_version(self): query_string = 'additional_filter=2' req = fakes.HTTPRequest.blank('', version='2.74', query_string=query_string) - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) def test_quota_update_additional_filter(self): query_string = 'user_id=1&additional_filter=2' @@ -639,33 +615,31 @@ class QuotaSetsTestV275(QuotaSetsTestV257): req = fakes.HTTPRequest.blank('', version=self.microversion, query_string=query_string) self.assertRaises(exception.ValidationError, self.controller.show, - req, 1234) + req, uuids.project_id) def test_quota_detail_additional_filter(self): query_string = 'user_id=1&additional_filter=2' req = fakes.HTTPRequest.blank('', version=self.microversion, query_string=query_string) self.assertRaises(exception.ValidationError, self.controller.detail, - req, 1234) + req, uuids.project_id) def test_quota_delete_additional_filter(self): query_string = 'user_id=1&additional_filter=2' req = fakes.HTTPRequest.blank('', version=self.microversion, query_string=query_string) self.assertRaises(exception.ValidationError, self.controller.delete, - req, 1234) + req, uuids.project_id) -class NoopQuotaSetsTest(test.NoDBTestCase): +class NoopQuotaSetsTest(BaseQuotaSetsTest): quota_driver = "nova.quota.NoopQuotaDriver" expected_detail = {'in_use': -1, 'limit': -1, 'reserved': -1} 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) + self.controller = quota_sets.QuotaSetsController() def test_show_v21(self): req = fakes.HTTPRequest.blank("") @@ -845,16 +819,16 @@ class NoopQuotaSetsTest(test.NoDBTestCase): @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") + self.controller.delete(req, uuids.project_id) mock_destroy_all_by_project.assert_called_once_with( - req.environ['nova.context'], "1234") + req.environ['nova.context'], uuids.project_id) @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") + self.controller.delete(req, uuids.project_id) mock_destroy_all_by_user.assert_called_once_with( - req.environ['nova.context'], "1234", "42") + req.environ['nova.context'], uuids.project_id, "42") class UnifiedLimitsQuotaSetsTest(NoopQuotaSetsTest): @@ -1116,13 +1090,13 @@ class UnifiedLimitsQuotaSetsTest(NoopQuotaSetsTest): @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") + self.controller.delete(req, uuids.project_id) # Ensure destroy isn't called for unified limits self.assertEqual(0, mock_destroy_all_by_project.call_count) @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") + self.controller.delete(req, uuids.project_id) # Ensure destroy isn't called for unified limits self.assertEqual(0, mock_destroy_all_by_user.call_count) diff --git a/nova/tests/unit/policies/base.py b/nova/tests/unit/policies/base.py index 8d4aa03142..a725bb43eb 100644 --- a/nova/tests/unit/policies/base.py +++ b/nova/tests/unit/policies/base.py @@ -74,6 +74,7 @@ class BasePolicyTest(test.TestCase): self.admin_project_id = uuids.admin_project_id self.project_id = uuids.project_id self.project_id_other = uuids.project_id_other + self.project_id_unused = uuids.project_id_random # all context are with implied roles. self.legacy_admin_context = nova_context.RequestContext( diff --git a/nova/tests/unit/policies/test_quota_sets.py b/nova/tests/unit/policies/test_quota_sets.py index 84a3cf0b1b..6aa85003f6 100644 --- a/nova/tests/unit/policies/test_quota_sets.py +++ b/nova/tests/unit/policies/test_quota_sets.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock - from nova.api.openstack.compute import quota_sets from nova import exception from nova.policies import quota_sets as policies +from nova.tests import fixtures as nova_fixtures from nova.tests.unit.api.openstack import fakes from nova.tests.unit.policies import base @@ -30,10 +29,11 @@ class QuotaSetsPolicyTest(base.BasePolicyTest): def setUp(self): super(QuotaSetsPolicyTest, self).setUp() self.controller = quota_sets.QuotaSetsController() - self.controller._validate_quota_limit = mock.MagicMock() self.req = fakes.HTTPRequest.blank('') self.project_id = self.req.environ['nova.context'].project_id + self.useFixture(nova_fixtures.NoopQuotaDriverFixture()) + # With legacy rule all admin is able to update or revert their quota # to default or get other project quota. self.project_admin_authorized_contexts = set([ @@ -42,9 +42,7 @@ class QuotaSetsPolicyTest(base.BasePolicyTest): # With legacy rule, everyone is able to get their own quota. self.project_reader_authorized_contexts = set([ self.legacy_admin_context, self.system_admin_context, - self.project_admin_context, - self.system_member_context, self.system_reader_context, - self.system_foo_context, self.project_manager_context, + self.project_admin_context, self.project_manager_context, self.project_member_context, self.project_reader_context, self.project_foo_context, self.other_project_manager_context, @@ -63,20 +61,17 @@ class QuotaSetsPolicyTest(base.BasePolicyTest): self.other_project_member_context, self.other_project_reader_context, self.service_context]) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - @mock.patch('nova.quota.QUOTAS.get_settable_quotas') - def test_update_quota_sets_policy(self, mock_update, mock_get): + def test_update_quota_sets_policy(self): rule_name = policies.POLICY_ROOT % 'update' - body = {'quota_set': { - 'instances': 50, - 'cores': 50} - } + body = {'quota_set': {'instances': 50, 'cores': 50}} + for cxtx in self.project_admin_authorized_contexts: req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx self.controller.update(req, cxtx.project_id, body=body) - for cxtx in (self.all_contexts - - set(self.project_admin_authorized_contexts)): + for cxtx in ( + self.all_contexts - set(self.project_admin_authorized_contexts) + ): req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx exc = self.assertRaises( @@ -86,15 +81,15 @@ class QuotaSetsPolicyTest(base.BasePolicyTest): "Policy doesn't allow %s to be performed." % rule_name, exc.format_message()) - @mock.patch('nova.objects.Quotas.destroy_all_by_project') - def test_delete_quota_sets_policy(self, mock_delete): + def test_delete_quota_sets_policy(self): rule_name = policies.POLICY_ROOT % 'delete' for cxtx in self.project_admin_authorized_contexts: req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx self.controller.delete(req, cxtx.project_id) - for cxtx in (self.all_contexts - - set(self.project_admin_authorized_contexts)): + for cxtx in ( + self.all_contexts - set(self.project_admin_authorized_contexts) + ): req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx exc = self.assertRaises( @@ -104,41 +99,62 @@ class QuotaSetsPolicyTest(base.BasePolicyTest): "Policy doesn't allow %s to be performed." % rule_name, exc.format_message()) - @mock.patch('nova.quota.QUOTAS.get_defaults') - def test_default_quota_sets_policy(self, mock_default): + def test_default_quota_sets_policy(self): rule_name = policies.POLICY_ROOT % 'defaults' self.common_policy_auth(self.everyone_authorized_contexts, rule_name, self.controller.defaults, - self.req, self.project_id) + self.req, self.project_id_unused) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_detail_quota_sets_policy(self, mock_get): + def test_detail_quota_sets_policy(self): rule_name = policies.POLICY_ROOT % 'detail' self.common_policy_auth(self.project_admin_authorized_contexts, rule_name, self.controller.detail, - self.req, 'try-other-project') + self.req, self.project_id_unused) # Check if project reader or higher roles are able to get # their own quota for cxtx in self.project_reader_authorized_contexts: req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx - self.controller.detail(req, cxtx.project_id) + self.controller.detail(req, cxtx.project_id or self.project_id) + for cxtx in ( + self.all_contexts - self.project_reader_authorized_contexts + ): + req = fakes.HTTPRequest.blank('') + req.environ['nova.context'] = cxtx + exc = self.assertRaises( + exception.PolicyNotAuthorized, self.controller.detail, + req, cxtx.project_id or self.project_id) + self.assertEqual( + "Policy doesn't allow %s to be performed." % rule_name, + exc.format_message()) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_show_quota_sets_policy(self, mock_get): + def test_show_quota_sets_policy(self): rule_name = policies.POLICY_ROOT % 'show' self.common_policy_auth(self.project_admin_authorized_contexts, rule_name, self.controller.show, - self.req, 'try-other-project') + self.req, self.project_id_unused) # Check if project reader or higher roles are able to get # their own quota for cxtx in self.project_reader_authorized_contexts: req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx - self.controller.show(req, cxtx.project_id) + self.controller.show(req, cxtx.project_id or self.project_id) + cnt = 0 + for cxtx in ( + self.all_contexts - self.project_reader_authorized_contexts + ): + cnt += 1 + req = fakes.HTTPRequest.blank('') + req.environ['nova.context'] = cxtx + exc = self.assertRaises( + exception.PolicyNotAuthorized, self.controller.show, + req, cxtx.project_id or self.project_id) + self.assertEqual( + "Policy doesn't allow %s to be performed." % rule_name, + exc.format_message()) class QuotaSetsNoLegacyNoScopePolicyTest(QuotaSetsPolicyTest): @@ -154,17 +170,17 @@ class QuotaSetsNoLegacyNoScopePolicyTest(QuotaSetsPolicyTest): # Even with no legacy rule, because any admin requesting # update/revert quota for their own project will be allowed. # And any admin will be able to get other project quota. - self.project_admin_authorized_contexts = [ + self.project_admin_authorized_contexts = set([ self.legacy_admin_context, self.system_admin_context, - self.project_admin_context] - # With no legacy rule, other project and foo role will not be - # able to get the quota. - self.project_reader_authorized_contexts = [ + self.project_admin_context]) + # With no legacy rule, foo role will not be able to get the quota. + self.project_reader_authorized_contexts = set([ self.legacy_admin_context, self.system_admin_context, - self.project_admin_context, - self.system_member_context, self.system_reader_context, - self.project_manager_context, self.project_member_context, - self.project_reader_context] + self.project_admin_context, self.project_manager_context, + self.project_member_context, self.project_reader_context, + self.other_project_manager_context, + self.other_project_member_context, + self.other_project_reader_context]) class QuotaSetsScopeTypePolicyTest(QuotaSetsPolicyTest):