Merge "api: Add response body schemas for quota sets API"

This commit is contained in:
Zuul
2026-01-24 08:29:06 +00:00
committed by Gerrit Code Review
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):