api: Add response body schemas for quota sets API

This exposes a minor issue in our policy checks. We reportedly assert
that a token scoped for one project cannot fetch quotas for another
project, but we weren't actually checking this. If we were, it wouldn't
have worked since our tests attempt to fetch quotas for the project
specified in the token. We add negative tests cases and update the
comments to clarify this.

Change-Id: I93c60a6bb110ab70f8821d0fbd6e8c5f87453582
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2025-07-29 16:24:05 +01:00
parent c63a2bba9d
commit 407434154e
6 changed files with 448 additions and 267 deletions
+50 -45
View File
@@ -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})
@@ -20,7 +20,7 @@ update = {
'properties': {
'quota_class_set': {
'type': 'object',
'properties': quota_sets.quota_resources,
'properties': quota_sets._quota_resources,
'additionalProperties': False,
},
},
+218 -33
View File
@@ -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'}
@@ -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)
+1
View File
@@ -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(
+55 -39
View File
@@ -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):