api: Add response body schemas for limits API

Change-Id: Iec1c43dc2ee34488afd337eb65f4245def460e16
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2024-11-25 16:47:46 +00:00
parent acf48c768a
commit 8eae0ecdd9
6 changed files with 236 additions and 202 deletions

View File

@@ -14,7 +14,7 @@
# under the License.
from nova.api.openstack import api_version_request
from nova.api.openstack.compute.schemas import limits
from nova.api.openstack.compute.schemas import limits as schema
from nova.api.openstack.compute.views import limits as limits_views
from nova.api.openstack import wsgi
from nova.api import validation
@@ -27,26 +27,32 @@ QUOTAS = quota.QUOTAS
# This is a list of limits which needs to filter out from the API response.
# This is due to the deprecation of network related proxy APIs, the related
# limit should be removed from the API also.
FILTERED_LIMITS_2_36 = ['floating_ips', 'security_groups',
'security_group_rules']
FILTERED_LIMITS_v236 = [
'floating_ips', 'security_groups', 'security_group_rules'
]
FILTERED_LIMITS_2_57 = list(FILTERED_LIMITS_2_36)
FILTERED_LIMITS_2_57.extend(['injected_files', 'injected_file_content_bytes'])
FILTERED_LIMITS_v257 = list(FILTERED_LIMITS_v236)
FILTERED_LIMITS_v257.extend(['injected_files', 'injected_file_content_bytes'])
@validation.validated
class LimitsController(wsgi.Controller):
"""Controller for accessing limits in the OpenStack API."""
@wsgi.expected_errors(())
@validation.query_schema(limits.limits_query_schema, '2.1', '2.56')
@validation.query_schema(limits.limits_query_schema, '2.57', '2.74')
@validation.query_schema(limits.limits_query_schema_275, '2.75')
@validation.query_schema(schema.index_query, '2.1', '2.56')
@validation.query_schema(schema.index_query, '2.57', '2.74')
@validation.query_schema(schema.index_query_v275, '2.75')
@validation.response_body_schema(schema.index_response, '2.1', '2.35')
@validation.response_body_schema(schema.index_response_v236, '2.36', '2.38') # noqa: E501
@validation.response_body_schema(schema.index_response_v239, '2.39', '2.56') # noqa: E501
@validation.response_body_schema(schema.index_response_v257, '2.57')
def index(self, req):
filtered_limits = []
if api_version_request.is_supported(req, '2.57'):
filtered_limits = FILTERED_LIMITS_2_57
filtered_limits = FILTERED_LIMITS_v257
elif api_version_request.is_supported(req, '2.36'):
filtered_limits = FILTERED_LIMITS_2_36
filtered_limits = FILTERED_LIMITS_v236
max_image_meta = True
if api_version_request.is_supported(req, '2.39'):

View File

@@ -16,7 +16,7 @@ import copy
from nova.api.validation import parameter_types
limits_query_schema = {
index_query = {
'type': 'object',
'properties': {
'tenant_id': parameter_types.common_query_param,
@@ -27,5 +27,128 @@ limits_query_schema = {
'additionalProperties': True
}
limits_query_schema_275 = copy.deepcopy(limits_query_schema)
limits_query_schema_275['additionalProperties'] = False
index_query_v275 = copy.deepcopy(index_query)
index_query_v275['additionalProperties'] = False
_absolute_quota_response = {
'type': 'object',
'properties': {
'maxImageMeta': {'type': 'integer', 'minimum': -1},
'maxPersonality': {'type': 'integer', 'minimum': -1},
'maxPersonalitySize': {'type': 'integer', 'minimum': -1},
'maxSecurityGroups': {'type': 'integer', 'minimum': -1},
'maxSecurityGroupRules': {'type': 'integer', 'minimum': -1},
'maxServerMeta': {'type': 'integer', 'minimum': -1},
'maxServerGroups': {'type': 'integer', 'minimum': -1},
'maxServerGroupMembers': {'type': 'integer', 'minimum': -1},
'maxTotalCores': {'type': 'integer', 'minimum': -1},
'maxTotalFloatingIps': {'type': 'integer', 'minimum': -1},
'maxTotalInstances': {'type': 'integer', 'minimum': -1},
'maxTotalKeypairs': {'type': 'integer', 'minimum': -1},
'maxTotalRAMSize': {'type': 'integer', 'minimum': -1},
'totalCoresUsed': {'type': 'integer', 'minimum': -1},
'totalFloatingIpsUsed': {'type': 'integer', 'minimum': -1},
'totalInstancesUsed': {'type': 'integer', 'minimum': -1},
'totalRAMUsed': {'type': 'integer', 'minimum': -1},
'totalSecurityGroupsUsed': {'type': 'integer', 'minimum': -1},
'totalServerGroupsUsed': {'type': 'integer', 'minimum': -1},
},
'required': [
'maxImageMeta',
'maxPersonality',
'maxPersonalitySize',
'maxSecurityGroups',
'maxSecurityGroupRules',
'maxServerMeta',
'maxServerGroups',
'maxServerGroupMembers',
'maxTotalCores',
'maxTotalFloatingIps',
'maxTotalInstances',
'maxTotalKeypairs',
'maxTotalRAMSize',
'totalCoresUsed',
'totalFloatingIpsUsed',
'totalInstancesUsed',
'totalRAMUsed',
'totalSecurityGroupsUsed',
'totalServerGroupsUsed',
],
'additionalProperties': False,
}
_absolute_quota_response_v236 = copy.deepcopy(_absolute_quota_response)
del _absolute_quota_response_v236['properties']['maxSecurityGroups']
del _absolute_quota_response_v236['properties']['maxSecurityGroupRules']
del _absolute_quota_response_v236['properties']['maxTotalFloatingIps']
del _absolute_quota_response_v236['properties']['totalFloatingIpsUsed']
del _absolute_quota_response_v236['properties']['totalSecurityGroupsUsed']
_absolute_quota_response_v236['required'].pop(
_absolute_quota_response_v236['required'].index('maxSecurityGroups')
)
_absolute_quota_response_v236['required'].pop(
_absolute_quota_response_v236['required'].index('maxSecurityGroupRules')
)
_absolute_quota_response_v236['required'].pop(
_absolute_quota_response_v236['required'].index('maxTotalFloatingIps')
)
_absolute_quota_response_v236['required'].pop(
_absolute_quota_response_v236['required'].index('totalFloatingIpsUsed')
)
_absolute_quota_response_v236['required'].pop(
_absolute_quota_response_v236['required'].index('totalSecurityGroupsUsed')
)
_absolute_quota_response_v239 = copy.deepcopy(_absolute_quota_response_v236)
del _absolute_quota_response_v239['properties']['maxImageMeta']
_absolute_quota_response_v239['required'].pop(
_absolute_quota_response_v239['required'].index('maxImageMeta')
)
_absolute_quota_response_v257 = copy.deepcopy(_absolute_quota_response_v239)
del _absolute_quota_response_v257['properties']['maxPersonality']
del _absolute_quota_response_v257['properties']['maxPersonalitySize']
_absolute_quota_response_v257['required'].pop(
_absolute_quota_response_v257['required'].index('maxPersonality')
)
_absolute_quota_response_v257['required'].pop(
_absolute_quota_response_v257['required'].index('maxPersonalitySize')
)
index_response = {
'type': 'object',
'properties': {
'limits': {
'type': 'object',
'properties': {
'absolute': _absolute_quota_response,
'rate': {
'type': 'array',
# Yes, this is an empty array
'items': {},
'maxItems': 0,
'additionalItems': False,
},
},
'required': ['absolute', 'rate'],
'additionalProperties': False,
},
},
'required': ['limits'],
'additionalProperties': False,
}
index_response_v236 = copy.deepcopy(index_response)
index_response_v236['properties']['limits']['properties']['absolute'] = (
_absolute_quota_response_v236
)
index_response_v239 = copy.deepcopy(index_response)
index_response_v239['properties']['limits']['properties']['absolute'] = (
_absolute_quota_response_v239
)
index_response_v257 = copy.deepcopy(index_response_v236)
index_response_v257['properties']['limits']['properties']['absolute'] = (
_absolute_quota_response_v257
)

View File

@@ -33,7 +33,7 @@ class ViewBuilder(object):
"security_group_rules": ["maxSecurityGroupRules"],
"server_groups": ["maxServerGroups"],
"server_group_members": ["maxServerGroupMembers"]
}
}
def build(self, request, quotas, filtered_limits=None,
max_image_meta=True):

View File

@@ -1418,22 +1418,22 @@ def _server_group_count(context, project_id, user_id=None):
QUOTAS = QuotaEngine(
resources=[
CountableResource(
'instances', _instances_cores_ram_count, 'instances'),
CountableResource(
'cores', _instances_cores_ram_count, 'cores'),
CountableResource(
'ram', _instances_cores_ram_count, 'ram'),
AbsoluteResource(
'metadata_items', 'metadata_items'),
AbsoluteResource(
'injected_files', 'injected_files'),
AbsoluteResource(
'injected_file_content_bytes', 'injected_file_content_bytes'),
AbsoluteResource(
'injected_file_path_bytes', 'injected_file_path_length'),
CountableResource(
'instances', _instances_cores_ram_count, 'instances'),
CountableResource(
'key_pairs', _keypair_get_count_by_user, 'key_pairs'),
AbsoluteResource(
'metadata_items', 'metadata_items'),
CountableResource(
'ram', _instances_cores_ram_count, 'ram'),
CountableResource(
'server_groups', _server_group_count, 'server_groups'),
CountableResource(

View File

@@ -39,22 +39,40 @@ from nova.tests.unit.api.openstack import fakes
from nova.tests.unit import matchers
def fake_get_project_quotas(context, project_id, usages=True):
absolute_limits = {
'cores': -1,
'floating_ips': 10,
'injected_files': -1,
'injected_file_content_bytes': -1,
'injected_file_path_bytes': -1,
'instances': 5,
'key_pairs': -1,
'metadata_items': -1,
'ram': 512,
'server_groups': 10,
'server_group_members': -1,
'security_groups': 10,
'security_group_rules': 20,
}
return {
k: {'limit': v, 'in_use': v // 2}
for k, v in absolute_limits.items()
}
class BaseLimitTestSuite(test.NoDBTestCase):
"""Base test suite which provides relevant stubs and time abstraction."""
def setUp(self):
super(BaseLimitTestSuite, self).setUp()
super().setUp()
self.time = 0.0
self.absolute_limits = {}
def stub_get_project_quotas(context, project_id, usages=True):
return {k: dict(limit=v, in_use=v // 2)
for k, v in self.absolute_limits.items()}
patcher_get_project_quotas = mock.patch.object(
nova.quota.QUOTAS,
"get_project_quotas",
side_effect=stub_get_project_quotas)
side_effect=fake_get_project_quotas)
self.mock_get_project_quotas = patcher_get_project_quotas.start()
self.addCleanup(patcher_get_project_quotas.stop)
patcher = self.mock_can = mock.patch('nova.context.RequestContext.can')
@@ -94,19 +112,6 @@ class LimitsControllerTestV21(BaseLimitTestSuite):
request.environ["nova.context"] = context
return request
def test_empty_index_json(self):
# Test getting empty limit details in JSON.
request = self._get_index_request()
response = request.get_response(self.controller)
expected = {
"limits": {
"rate": [],
"absolute": {},
},
}
body = jsonutils.loads(response.body)
self.assertEqual(expected, body)
def test_index_json(self):
self._test_index_json()
@@ -120,41 +125,33 @@ class LimitsControllerTestV21(BaseLimitTestSuite):
if tenant_id is None:
tenant_id = context.project_id
self.absolute_limits = {
'ram': 512,
'instances': 5,
'cores': 21,
'key_pairs': 10,
'floating_ips': 10,
'security_groups': 10,
'security_group_rules': 20,
}
expected = {
"limits": {
"rate": [],
"absolute": {
"maxTotalRAMSize": 512,
"maxTotalInstances": 5,
"maxTotalCores": 21,
"maxTotalKeypairs": 10,
"maxTotalFloatingIps": 10,
"maxImageMeta": -1,
"maxPersonality": -1,
"maxPersonalitySize": -1,
"maxSecurityGroups": 10,
"maxSecurityGroupRules": 20,
"totalRAMUsed": 256,
"totalCoresUsed": 10,
"totalInstancesUsed": 2,
"maxServerGroups": 10,
"maxServerGroupMembers": -1,
"maxServerMeta": -1,
"maxTotalCores": -1,
"maxTotalFloatingIps": 10,
"maxTotalInstances": 5,
"maxTotalKeypairs": -1,
"maxTotalRAMSize": 512,
"totalCoresUsed": -1,
"totalFloatingIpsUsed": 5,
"totalInstancesUsed": 2,
"totalRAMUsed": 256,
"totalSecurityGroupsUsed": 5,
},
"totalServerGroupsUsed": 5,
},
"rate": [],
},
}
def _get_project_quotas(context, project_id, usages=True):
return {k: dict(limit=v, in_use=v // 2)
for k, v in self.absolute_limits.items()}
self.mock_get_project_quotas.side_effect = _get_project_quotas
response = request.get_response(self.controller)
body = jsonutils.loads(response.body)
@@ -162,43 +159,6 @@ class LimitsControllerTestV21(BaseLimitTestSuite):
self.mock_get_project_quotas.assert_called_once_with(
context, tenant_id, usages=True)
def _do_test_used_limits(self, reserved):
request = self._get_index_request(tenant_id=None)
quota_map = {
'totalRAMUsed': 'ram',
'totalCoresUsed': 'cores',
'totalInstancesUsed': 'instances',
'totalFloatingIpsUsed': 'floating_ips',
'totalSecurityGroupsUsed': 'security_groups',
'totalServerGroupsUsed': 'server_groups',
}
limits = {}
expected_abs_limits = []
for display_name, q in quota_map.items():
limits[q] = {'limit': len(display_name),
'in_use': len(display_name) // 2,
'reserved': 0}
expected_abs_limits.append(display_name)
def stub_get_project_quotas(context, project_id, usages=True):
return limits
self.mock_get_project_quotas.side_effect = stub_get_project_quotas
res = request.get_response(self.controller)
body = jsonutils.loads(res.body)
abs_limits = body['limits']['absolute']
for limit in expected_abs_limits:
value = abs_limits[limit]
r = limits[quota_map[limit]]['reserved'] if reserved else 0
self.assertEqual(limits[quota_map[limit]]['in_use'] + r, value)
def test_used_limits_basic(self):
self._do_test_used_limits(False)
def test_used_limits_with_reserved(self):
self._do_test_used_limits(True)
def test_admin_can_fetch_limits_for_a_given_tenant_id(self):
project_id = "123456"
user_id = "A1234"
@@ -214,8 +174,8 @@ class LimitsControllerTestV21(BaseLimitTestSuite):
self.assertEqual(2, self.mock_can.call_count)
self.mock_can.assert_called_with(
l_policies.OTHER_PROJECT_LIMIT_POLICY_NAME)
self.mock_get_project_quotas.assert_called_once_with(context,
tenant_id, usages=True)
self.mock_get_project_quotas.assert_called_once_with(
context, tenant_id, usages=True)
def _test_admin_can_fetch_used_limits_for_own_project(self, req_get):
project_id = "123456"
@@ -268,33 +228,6 @@ class LimitsControllerTestV21(BaseLimitTestSuite):
self.mock_get_project_quotas.assert_called_once_with(
context, project_id, usages=True)
def test_used_ram_added(self):
fake_req = self._get_index_request()
def stub_get_project_quotas(context, project_id, usages=True):
return {'ram': {'limit': 512, 'in_use': 256}}
self.mock_get_project_quotas.side_effect = stub_get_project_quotas
res = fake_req.get_response(self.controller)
body = jsonutils.loads(res.body)
abs_limits = body['limits']['absolute']
self.assertIn('totalRAMUsed', abs_limits)
self.assertEqual(256, abs_limits['totalRAMUsed'])
self.assertEqual(1, self.mock_get_project_quotas.call_count)
def test_no_ram_quota(self):
fake_req = self._get_index_request()
self.mock_get_project_quotas.side_effect = None
self.mock_get_project_quotas.return_value = {}
res = fake_req.get_response(self.controller)
body = jsonutils.loads(res.body)
abs_limits = body['limits']['absolute']
self.assertNotIn('totalRAMUsed', abs_limits)
self.assertEqual(1, self.mock_get_project_quotas.call_count)
class FakeHttplibSocket(object):
"""Fake `httplib.HTTPResponse` replacement."""
@@ -346,28 +279,32 @@ class LimitsViewBuilderTest(test.NoDBTestCase):
super(LimitsViewBuilderTest, self).setUp()
self.view_builder = views.limits.ViewBuilder()
self.req = fakes.HTTPRequest.blank('/?tenant_id=None')
self.rate_limits = []
self.absolute_limits = {"metadata_items": {'limit': 1, 'in_use': 1},
"injected_files": {'limit': 5, 'in_use': 1},
"injected_file_content_bytes":
{'limit': 5, 'in_use': 1}}
def test_build_limits(self):
expected_limits = {"limits": {
quotas = {
"metadata_items": {'limit': 1, 'in_use': 1},
"injected_files": {'limit': 5, 'in_use': 1},
"injected_file_content_bytes": {'limit': 5, 'in_use': 1},
}
expected_limits = {
"limits": {
"rate": [],
"absolute": {"maxServerMeta": 1,
"maxImageMeta": 1,
"maxPersonality": 5,
"maxPersonalitySize": 5}}}
"absolute": {
"maxServerMeta": 1,
"maxImageMeta": 1,
"maxPersonality": 5,
"maxPersonalitySize": 5,
}
},
}
output = self.view_builder.build(self.req, self.absolute_limits)
output = self.view_builder.build(self.req, quotas)
self.assertThat(output, matchers.DictMatches(expected_limits))
def test_build_limits_empty_limits(self):
expected_limits = {"limits": {"rate": [],
"absolute": {}}}
quotas = {}
expected_limits = {"limits": {"rate": [], "absolute": {}}}
output = self.view_builder.build(self.req, quotas)
self.assertThat(output, matchers.DictMatches(expected_limits))
@@ -381,34 +318,25 @@ class LimitsControllerTestV236(BaseLimitTestSuite):
version='2.36')
def test_index_filtered(self):
absolute_limits = {
'ram': 512,
'instances': 5,
'cores': 21,
'key_pairs': 10,
'floating_ips': 10,
'security_groups': 10,
'security_group_rules': 20,
}
def _get_project_quotas(context, project_id, usages=True):
return {k: dict(limit=v, in_use=v // 2)
for k, v in absolute_limits.items()}
self.mock_get_project_quotas.side_effect = _get_project_quotas
response = self.controller.index(self.req)
expected_response = {
"limits": {
"rate": [],
"absolute": {
"maxTotalRAMSize": 512,
"maxImageMeta": -1,
"maxPersonality": -1,
"maxPersonalitySize": -1,
"maxServerGroups": 10,
"maxServerGroupMembers": -1,
"maxServerMeta": -1,
"maxTotalCores": -1,
"maxTotalInstances": 5,
"maxTotalCores": 21,
"maxTotalKeypairs": 10,
"totalRAMUsed": 256,
"totalCoresUsed": 10,
"maxTotalKeypairs": -1,
"maxTotalRAMSize": 512,
"totalCoresUsed": -1,
"totalInstancesUsed": 2,
"totalRAMUsed": 256,
"totalServerGroupsUsed": 5,
},
},
}
@@ -424,28 +352,11 @@ class LimitsControllerTestV239(BaseLimitTestSuite):
version='2.39')
def test_index_filtered_no_max_image_meta(self):
absolute_limits = {
"metadata_items": 1,
}
def _get_project_quotas(context, project_id, usages=True):
return {k: dict(limit=v, in_use=v // 2)
for k, v in absolute_limits.items()}
self.mock_get_project_quotas.side_effect = _get_project_quotas
response = self.controller.index(self.req)
# starting from version 2.39 there is no 'maxImageMeta' field
# in response after removing 'image-metadata' proxy API
expected_response = {
"limits": {
"rate": [],
"absolute": {
"maxServerMeta": 1,
},
},
}
self.assertEqual(expected_response, response)
self.assertNotIn('maxImageMeta', response['limits']['absolute'])
self.assertIn('maxServerMeta', response['limits']['absolute'])
class LimitsControllerTestV275(BaseLimitTestSuite):
@@ -454,23 +365,12 @@ class LimitsControllerTestV275(BaseLimitTestSuite):
self.controller = limits_v21.LimitsController()
def test_index_additional_query_param_old_version(self):
absolute_limits = {
"metadata_items": 1,
}
req = fakes.HTTPRequest.blank("/?unknown=fake",
version='2.74')
def _get_project_quotas(context, project_id, usages=True):
return {k: dict(limit=v, in_use=v // 2)
for k, v in absolute_limits.items()}
self.mock_get_project_quotas.side_effect = _get_project_quotas
req = fakes.HTTPRequest.blank("/?unknown=fake", version='2.74')
self.controller.index(req)
self.controller.index(req)
def test_index_additional_query_param(self):
req = fakes.HTTPRequest.blank("/?unknown=fake",
version='2.75')
req = fakes.HTTPRequest.blank("/?unknown=fake", version='2.75')
self.assertRaises(
exception.ValidationError,
self.controller.index, req=req)

View File

@@ -39,13 +39,18 @@ class LimitsPolicyTest(base.BasePolicyTest):
self.req = fakes.HTTPRequest.blank('')
self.absolute_limits = {
'ram': 512,
'instances': 5,
'cores': 21,
'key_pairs': 10,
'floating_ips': 10,
'security_groups': 10,
'injected_file_content_bytes': 1024,
'injected_files': 20,
'instances': 5,
'key_pairs': 10,
'metadata_items': 10,
'ram': 512,
'security_group_rules': 20,
'security_groups': 10,
'server_group_members': 5,
'server_groups': 10,
}
def stub_get_project_quotas(context, project_id, usages=True):