tests: Ensure all APIs have a response body schema

Well, in a manner of speaking :) We add a new decorator to highlight the
resources that we have fully annotated. This will prevent regressions
and give us a quick sentinel for identifying resources that have been
updated.

Change-Id: Ic2cf231a01b0f053faf40409ae047c8bf9990fd2
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2024-11-04 19:10:15 +00:00
parent baf310ac28
commit 02a6c48b38
30 changed files with 70 additions and 35 deletions

View File

@@ -32,6 +32,7 @@ It was removed in the 23.0.0 (Wallaby) release.
"""
@validation.validated
class AdminActionsController(wsgi.Controller):
def __init__(self):
super(AdminActionsController, self).__init__()

View File

@@ -24,6 +24,7 @@ from nova.i18n import _
from nova.policies import admin_password as ap_policies
@validation.validated
class AdminPasswordController(wsgi.Controller):
def __init__(self):

View File

@@ -40,6 +40,7 @@ def _get_context(req):
return req.environ['nova.context']
@validation.validated
class AggregateController(wsgi.Controller):
"""The Host Aggregates API controller for the OpenStack API."""

View File

@@ -27,6 +27,7 @@ from nova import exception
from nova.policies import assisted_volume_snapshots as avs_policies
@validation.validated
class AssistedVolumeSnapshotsController(wsgi.Controller):
"""The Assisted volume snapshots API controller for the OpenStack API."""

View File

@@ -55,6 +55,7 @@ def _translate_interface_attachment_view(context, port_info, show_tag=False):
return info
@validation.validated
class InterfaceAttachmentController(wsgi.Controller):
"""The interface attachment API controller for the OpenStack API."""

View File

@@ -25,6 +25,7 @@ CONF = nova.conf.CONF
ATTRIBUTE_NAME = "availability_zone"
@validation.validated
class AvailabilityZoneController(wsgi.Controller):
"""The Availability Zone API controller for the OpenStack API."""

View File

@@ -40,6 +40,7 @@ def _no_ironic_proxy(cmd):
raise webob.exc.HTTPBadRequest(explanation=msg % {'cmd': cmd})
@validation.validated
class BareMetalNodeController(wsgi.Controller):
"""The Bare-Metal Node API controller for the OpenStack API."""

View File

@@ -28,6 +28,7 @@ from nova.policies import console_auth_tokens as cat_policies
CONF = nova.conf.CONF
@validation.validated
class ConsoleAuthTokensController(wsgi.Controller):
@wsgi.expected_errors((400, 401, 404), '2.1', '2.30')

View File

@@ -27,6 +27,7 @@ from nova import exception
from nova.policies import console_output as co_policies
@validation.validated
class ConsoleOutputController(wsgi.Controller):
def __init__(self):
super(ConsoleOutputController, self).__init__()

View File

@@ -25,6 +25,7 @@ from nova import exception
from nova.policies import create_backup as cb_policies
@validation.validated
class CreateBackupController(wsgi.Controller):
def __init__(self):
super(CreateBackupController, self).__init__()

View File

@@ -26,6 +26,7 @@ from nova import exception
from nova.policies import deferred_delete as dd_policies
@validation.validated
class DeferredDeleteController(wsgi.Controller):
def __init__(self):
super(DeferredDeleteController, self).__init__()

View File

@@ -38,6 +38,7 @@ LOG = logging.getLogger(__name__)
MIN_VER_NOVA_COMPUTE_EVACUATE_STOPPED = 62
@validation.validated
class EvacuateController(wsgi.Controller):
def __init__(self):
super(EvacuateController, self).__init__()

View File

@@ -852,6 +852,7 @@ EXTENSION_LIST_LEGACY_V2_COMPATIBLE = sorted(
EXTENSION_LIST_LEGACY_V2_COMPATIBLE, key=lambda x: x['alias'])
@validation.validated
class ExtensionInfoController(wsgi.Controller):
@wsgi.expected_errors(())

View File

@@ -37,6 +37,7 @@ def _marshall_flavor_access(flavor):
return {'flavor_access': rval}
@validation.validated
class FlavorAccessController(wsgi.Controller):
"""The flavor access API controller for the OpenStack API."""
@@ -58,6 +59,7 @@ class FlavorAccessController(wsgi.Controller):
return _marshall_flavor_access(flavor)
@validation.validated
class FlavorActionController(wsgi.Controller):
"""The flavor access API controller for the OpenStack API."""

View File

@@ -31,6 +31,7 @@ from nova.policies import flavor_manage as fm_policies
from nova import utils
@validation.validated
class FlavorsController(wsgi.Controller):
"""Flavor controller for the OpenStack API."""

View File

@@ -27,6 +27,7 @@ from nova.policies import flavor_extra_specs as fes_policies
from nova import utils
@validation.validated
class FlavorExtraSpecsController(wsgi.Controller):
"""The flavor extra specs API controller for the OpenStack API."""

View File

@@ -34,6 +34,7 @@ def _translate_floating_ip_pools_view(pools):
}
@validation.validated
class FloatingIPPoolsController(wsgi.Controller):
"""The Floating IP Pool API controller for the OpenStack API."""

View File

@@ -178,6 +178,7 @@ class FloatingIPController(wsgi.Controller):
raise webob.exc.HTTPNotFound(explanation=exc.format_message())
@validation.validated
class FloatingIPActionController(wsgi.Controller):
"""This API is deprecated from the Microversion '2.44'."""

View File

@@ -22,6 +22,7 @@ from nova.compute import api as compute
from nova.policies import lock_server as ls_policies
@validation.validated
class LockServerController(wsgi.Controller):
def __init__(self):
super(LockServerController, self).__init__()

View File

@@ -31,6 +31,7 @@ from nova.policies import migrate_server as ms_policies
LOG = logging.getLogger(__name__)
@validation.validated
class MigrateServerController(wsgi.Controller):
def __init__(self):
super(MigrateServerController, self).__init__()

View File

@@ -26,6 +26,7 @@ from nova import exception
from nova.policies import multinic as multinic_policies
@validation.validated
class MultinicController(wsgi.Controller):
"""This API is deprecated from Microversion '2.44'."""

View File

@@ -24,6 +24,7 @@ from nova import exception
from nova.policies import pause_server as ps_policies
@validation.validated
class PauseServerController(wsgi.Controller):
def __init__(self):
super(PauseServerController, self).__init__()

View File

@@ -30,6 +30,7 @@ from nova import utils
CONF = nova.conf.CONF
@validation.validated
class RescueController(wsgi.Controller):
def __init__(self):
super(RescueController, self).__init__()

View File

@@ -385,6 +385,7 @@ class ServerSecurityGroupController(
key=lambda k: (k['tenant_id'], k['name'])))}
@validation.validated
class SecurityGroupActionController(wsgi.Controller):
def __init__(self):
super(SecurityGroupActionController, self).__init__()

View File

@@ -29,6 +29,7 @@ from nova.policies import shelve as shelve_policies
LOG = logging.getLogger(__name__)
@validation.validated
class ShelveController(wsgi.Controller):
def __init__(self):
super(ShelveController, self).__init__()

View File

@@ -23,6 +23,7 @@ from nova import exception
from nova.policies import suspend_server as ss_policies
@validation.validated
class SuspendServerController(wsgi.Controller):
def __init__(self):
super(SuspendServerController, self).__init__()

View File

@@ -74,6 +74,7 @@ VERSIONS = {
}
@validation.validated
class Versions(wsgi.Resource):
# The root version API isn't under the microversion control.
@@ -108,6 +109,7 @@ class Versions(wsgi.Resource):
return args
@validation.validated
class VersionsV2(wsgi.Resource):
def __init__(self):

View File

@@ -22,6 +22,7 @@ from nova.api.openstack import wsgi
from nova.api import validation
@validation.validated
class VersionsController(wsgi.Controller):
@wsgi.expected_errors(404)

View File

@@ -35,6 +35,11 @@ CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
def validated(cls):
cls._validated = True
return cls
class Schemas:
"""A microversion-aware schema container.

View File

@@ -30,53 +30,42 @@ class SchemaTest(test.NoDBTestCase):
def test_schemas(self):
missing_request_schemas = set()
missing_query_schemas = set()
missing_response_schemas = set()
invalid_schemas = set()
def _validate_func(func, method):
def _validate_schema(func, schema):
try:
self.meta_schema.check_schema(schema)
except jsonschema.exceptions.SchemaError:
LOG.exception(
'schema validation failed for %s',
func.__qualname__,
)
invalid_schemas.add(func.__qualname__)
def _validate_func(func, method, validated):
if method in ("POST", "PUT", "PATCH"):
# request body validation
if not hasattr(func, 'request_body_schemas'):
missing_request_schemas.add(func.__qualname__)
else:
for schema, _, _ in func.request_body_schemas._schemas:
try:
self.meta_schema.check_schema(schema)
except jsonschema.exceptions.SchemaError:
LOG.exception(
"Invalid request body schema for %s",
func.__qualname__,
)
invalid_schemas.add(func.__qualname__)
break
_validate_schema(func, schema)
elif method in ("GET",):
# request query string validation
if not hasattr(func, 'request_query_schemas'):
missing_request_schemas.add(func.__qualname__)
else:
for schema, _, _ in func.request_query_schemas._schemas:
try:
self.meta_schema.check_schema(schema)
except jsonschema.exceptions.SchemaError:
LOG.exception(
"Invalid request query schema for %s",
func.__qualname__,
)
invalid_schemas.add(func.__qualname__)
break
_validate_schema(func, schema)
# TODO(stephenfin): Check for missing schemas once we have added
# them all
if hasattr(func, 'response_body_schemas'):
# response body validation
if not hasattr(func, 'response_body_schemas'):
if validated:
missing_response_schemas.add(func.__qualname__)
else:
for schema, _, _ in func.response_body_schemas._schemas:
try:
self.meta_schema.check_schema(schema)
except jsonschema.exceptions.SchemaError:
LOG.exception(
"Invalid response body schema for %s",
func.__qualname__,
)
invalid_schemas.add(func.__qualname__)
break
_validate_schema(func, schema)
for route in self.router.map.matchlist:
if 'controller' not in route.defaults:
@@ -84,6 +73,11 @@ class SchemaTest(test.NoDBTestCase):
controller = route.defaults['controller']
validated = getattr(controller.controller, '_validated', False)
# NOTE: This is effectively a reimplementation of
# 'routes.route.Route.make_full_route' that uses OpenAPI-compatible
# template strings instead of regexes for parameters
path = ""
for part in route.routelist:
if isinstance(part, dict):
@@ -117,24 +111,30 @@ class SchemaTest(test.NoDBTestCase):
) in wsgi_actions:
func = controller.wsgi_actions[wsgi_action]
# method will always be POST for actions
_validate_func(func, method)
_validate_func(func, method, validated)
else:
# body validation
func = getattr(controller.controller, action)
_validate_func(func, method)
_validate_func(func, method, validated)
if missing_request_schemas:
raise test.TestingException(
f"Found API resources without schemas: "
f"Found API resources without request body schemas: "
f"{sorted(missing_request_schemas)}"
)
if missing_query_schemas:
raise test.TestingException(
f"Found API resources without query schemas: "
f"Found API resources without request query schemas: "
f"{sorted(missing_query_schemas)}"
)
if missing_response_schemas:
raise test.TestingException(
f"Found API resources without response body schemas: "
f"{sorted(missing_response_schemas)}"
)
if invalid_schemas:
raise test.TestingException(
f"Found API resources with invalid schemas: "