api: Add new, simpler api_version decorator

Get rid of the whole API version switching madness and make our schema
generation _significantly_ simpler.

This looks a lot larger than it actually is. In most cases, this is
simply 's/wsgi.Controller.api_version/wsgi.api_version/'.

Change-Id: I180bfad84c38653709c216282099d9b3fb64c5a7
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2024-11-04 15:08:28 +00:00
parent a722640b2f
commit 08dd30d3fc
29 changed files with 206 additions and 483 deletions

View File

@@ -240,9 +240,9 @@ Adding a new API method
In the controller class::
@wsgi.Controller.api_version("2.4")
@wsgi.api_version("2.4")
def my_api_method(self, req, id):
....
...
This method would only be available if the caller had specified an
``OpenStack-API-Version`` of >= ``2.4``. If they had specified a
@@ -254,36 +254,14 @@ Removing an API method
In the controller class::
@wsgi.Controller.api_version("2.1", "2.4")
@wsgi.api_version("2.1", "2.4")
def my_api_method(self, req, id):
....
...
This method would only be available if the caller had specified an
``OpenStack-API-Version`` of <= ``2.4``. If ``2.5`` or later
is specified the server will respond with ``HTTP/404``.
Changing a method's behavior
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In the controller class::
@wsgi.Controller.api_version("2.1", "2.3")
def my_api_method(self, req, id):
.... method_1 ...
@wsgi.Controller.api_version("2.4") # noqa
def my_api_method(self, req, id):
.... method_2 ...
If a caller specified ``2.1``, ``2.2`` or ``2.3`` (or received the
default of ``2.1``) they would see the result from ``method_1``,
``2.4`` or later ``method_2``.
It is vital that the two methods have the same name, so the second of
them will need ``# noqa`` to avoid failing flake8's ``F811`` rule. The
two methods may be different in any kind of semantics (schema
validation, return values, response codes, etc)
A change in schema only
~~~~~~~~~~~~~~~~~~~~~~~
@@ -291,26 +269,23 @@ If there is no change to the method, only to the schema that is used for
validation, you can add a version range to the ``validation.schema``
decorator::
@wsgi.Controller.api_version("2.1")
@wsgi.api_version("2.1")
@validation.schema(dummy_schema.dummy, "2.3", "2.8")
@validation.schema(dummy_schema.dummy2, "2.9")
def update(self, req, id, body):
....
...
This method will be available from version ``2.1``, validated according to
``dummy_schema.dummy`` from ``2.3`` to ``2.8``, and validated according to
``dummy_schema.dummy2`` from ``2.9`` onward.
Other API method changes
~~~~~~~~~~~~~~~~~~~~~~~~
When not using decorators
~~~~~~~~~~~~~~~~~~~~~~~~~
When you don't want to use the ``@api_version`` decorator on a method
or you want to change behavior within a method (say it leads to
simpler or simply a lot less code) you can directly test for the
requested version with a method as long as you have access to the api
request object (commonly called ``req``). Every API method has an
api_version_request object attached to the req object and that can be
When you want to change more than the API request or response schema, you can
directly test for the requested version with a method as long as you have
access to the api request object (commonly called ``req``). Every API method
has an api_version_request object attached to the req object and that can be
used to modify behavior based on its value::
def index(self, req):

View File

@@ -287,7 +287,7 @@ class AggregateController(wsgi.Controller):
(show_uuid or key != 'uuid')):
yield key, getattr(aggregate, key)
@wsgi.Controller.api_version('2.81')
@wsgi.api_version('2.81')
@wsgi.response(202)
@wsgi.expected_errors((400, 404))
@validation.schema(aggregate_images.aggregate_images)

View File

@@ -61,7 +61,7 @@ class BareMetalNodeController(wsgi.Controller):
)
return self._ironic_connection
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((404, 501))
@validation.query_schema(schema.index_query)
@validation.response_body_schema(schema.index_response)
@@ -86,7 +86,7 @@ class BareMetalNodeController(wsgi.Controller):
return {'nodes': nodes}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((404, 501))
@validation.query_schema(schema.show_query)
@validation.response_body_schema(schema.show_response)
@@ -117,20 +117,20 @@ class BareMetalNodeController(wsgi.Controller):
return {'node': node}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(400)
@validation.schema(schema.create)
@validation.response_body_schema(schema.create_response)
def create(self, req, body):
_no_ironic_proxy("node-create")
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(400)
@validation.response_body_schema(schema.delete_response)
def delete(self, req, id):
_no_ironic_proxy("node-delete")
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.action('add_interface')
@wsgi.expected_errors(400)
@validation.schema(schema.add_interface)
@@ -138,7 +138,7 @@ class BareMetalNodeController(wsgi.Controller):
def _add_interface(self, req, id, body):
_no_ironic_proxy("port-create")
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.action('remove_interface')
@wsgi.expected_errors(400)
@validation.schema(schema.remove_interface)

View File

@@ -108,7 +108,7 @@ class FlavorsController(wsgi.Controller):
return self._view_builder.show(req, flavor, include_description,
include_extra_specs=include_extra_specs)
@wsgi.Controller.api_version('2.55')
@wsgi.api_version('2.55')
@wsgi.expected_errors((400, 404))
@validation.schema(schema.update, '2.55')
@validation.response_body_schema(schema.update_response, '2.55', '2.60')

View File

@@ -41,7 +41,7 @@ class FloatingIPPoolsController(wsgi.Controller):
super(FloatingIPPoolsController, self).__init__()
self.network_api = neutron.API()
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(())
@validation.query_schema(schema.index_query)
@validation.response_body_schema(schema.index_response)

View File

@@ -82,7 +82,7 @@ class FloatingIPController(wsgi.Controller):
self.compute_api = compute.API()
self.network_api = neutron.API()
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((400, 404))
@validation.query_schema(schema.show_query)
def show(self, req, id):
@@ -101,7 +101,7 @@ class FloatingIPController(wsgi.Controller):
return _translate_floating_ip_view(floating_ip)
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(())
@validation.query_schema(schema.index_query)
def index(self, req):
@@ -115,7 +115,7 @@ class FloatingIPController(wsgi.Controller):
return {'floating_ips': [_translate_floating_ip_view(ip)['floating_ip']
for ip in floating_ips]}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((400, 403, 404))
@validation.schema(schema.create)
def create(self, req, body=None):
@@ -148,7 +148,7 @@ class FloatingIPController(wsgi.Controller):
return _translate_floating_ip_view(ip)
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.response(202)
@wsgi.expected_errors((400, 403, 404, 409))
def delete(self, req, id):
@@ -186,7 +186,7 @@ class FloatingIPActionController(wsgi.Controller):
self.compute_api = compute.API()
self.network_api = neutron.API()
@wsgi.Controller.api_version("2.1", "2.43")
@wsgi.api_version("2.1", "2.43")
@wsgi.expected_errors((400, 403, 404))
@wsgi.action('addFloatingIp')
@validation.schema(schema.add_floating_ip)
@@ -267,7 +267,7 @@ class FloatingIPActionController(wsgi.Controller):
return webob.Response(status_int=202)
@wsgi.Controller.api_version("2.1", "2.43")
@wsgi.api_version("2.1", "2.43")
@wsgi.expected_errors((400, 403, 404, 409))
@wsgi.action('removeFloatingIp')
@validation.schema(schema.remove_floating_ip)

View File

@@ -38,7 +38,7 @@ class HostController(wsgi.Controller):
super(HostController, self).__init__()
self.api = compute.HostAPI()
@wsgi.Controller.api_version("2.1", "2.42")
@wsgi.api_version("2.1", "2.42")
@validation.query_schema(hosts.index_query)
@wsgi.expected_errors(())
def index(self, req):
@@ -88,7 +88,7 @@ class HostController(wsgi.Controller):
'zone': service['availability_zone']})
return {'hosts': hosts}
@wsgi.Controller.api_version("2.1", "2.42")
@wsgi.api_version("2.1", "2.42")
@wsgi.expected_errors((400, 404, 501))
@validation.schema(hosts.update)
def update(self, req, id, body):
@@ -180,7 +180,7 @@ class HostController(wsgi.Controller):
raise webob.exc.HTTPBadRequest(explanation=e.format_message())
return {"host": host_name, "power_action": result}
@wsgi.Controller.api_version("2.1", "2.42")
@wsgi.api_version("2.1", "2.42")
@wsgi.expected_errors((400, 404, 501))
@validation.query_schema(hosts.startup_query)
def startup(self, req, id):
@@ -189,7 +189,7 @@ class HostController(wsgi.Controller):
target={})
return self._host_power_action(req, host_name=id, action="startup")
@wsgi.Controller.api_version("2.1", "2.42")
@wsgi.api_version("2.1", "2.42")
@wsgi.expected_errors((400, 404, 501))
@validation.query_schema(hosts.shutdown_query)
def shutdown(self, req, id):
@@ -198,7 +198,7 @@ class HostController(wsgi.Controller):
target={})
return self._host_power_action(req, host_name=id, action="shutdown")
@wsgi.Controller.api_version("2.1", "2.42")
@wsgi.api_version("2.1", "2.42")
@wsgi.expected_errors((400, 404, 501))
@validation.query_schema(hosts.reboot_query)
def reboot(self, req, id):
@@ -256,7 +256,7 @@ class HostController(wsgi.Controller):
instance['ephemeral_gb'])
return project_map
@wsgi.Controller.api_version("2.1", "2.42")
@wsgi.api_version("2.1", "2.42")
@wsgi.expected_errors(404)
@validation.query_schema(hosts.show_query)
def show(self, req, id):

View File

@@ -359,7 +359,7 @@ class HypervisorsController(wsgi.Controller):
),
}
@wsgi.Controller.api_version('2.1', '2.87')
@wsgi.api_version('2.1', '2.87')
@wsgi.expected_errors((400, 404, 501))
@validation.query_schema(schema.uptime_query)
def uptime(self, req, id):
@@ -412,7 +412,7 @@ class HypervisorsController(wsgi.Controller):
return {'hypervisor': hypervisor}
@wsgi.Controller.api_version('2.1', '2.52')
@wsgi.api_version('2.1', '2.52')
@wsgi.expected_errors(404)
@validation.query_schema(schema.search_query)
def search(self, req, id):
@@ -451,7 +451,7 @@ class HypervisorsController(wsgi.Controller):
return {'hypervisors': hypervisors}
@wsgi.Controller.api_version('2.1', '2.52')
@wsgi.api_version('2.1', '2.52')
@wsgi.expected_errors(404)
@validation.query_schema(schema.servers_query)
def servers(self, req, id):
@@ -497,7 +497,7 @@ class HypervisorsController(wsgi.Controller):
return {'hypervisors': hypervisors}
@wsgi.Controller.api_version('2.1', '2.87')
@wsgi.api_version('2.1', '2.87')
@wsgi.expected_errors(())
@validation.query_schema(schema.statistics_query)
def statistics(self, req):

View File

@@ -43,7 +43,7 @@ class ImageMetadataController(wsgi.Controller):
msg = _("Image not found.")
raise exc.HTTPNotFound(explanation=msg)
@wsgi.Controller.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.expected_errors((403, 404))
@validation.query_schema(image_metadata.index_query)
def index(self, req, image_id):
@@ -52,7 +52,7 @@ class ImageMetadataController(wsgi.Controller):
metadata = self._get_image(context, image_id)['properties']
return dict(metadata=metadata)
@wsgi.Controller.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.expected_errors((403, 404))
@validation.query_schema(image_metadata.show_query)
def show(self, req, image_id, id):
@@ -63,7 +63,7 @@ class ImageMetadataController(wsgi.Controller):
else:
raise exc.HTTPNotFound()
@wsgi.Controller.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.expected_errors((400, 403, 404))
@validation.schema(image_metadata.create)
def create(self, req, image_id, body):
@@ -80,7 +80,7 @@ class ImageMetadataController(wsgi.Controller):
raise exc.HTTPForbidden(explanation=e.format_message())
return dict(metadata=image['properties'])
@wsgi.Controller.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.expected_errors((400, 403, 404))
@validation.schema(image_metadata.update)
def update(self, req, image_id, id, body):
@@ -103,7 +103,7 @@ class ImageMetadataController(wsgi.Controller):
raise exc.HTTPForbidden(explanation=e.format_message())
return dict(meta=meta)
@wsgi.Controller.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.expected_errors((400, 403, 404))
@validation.schema(image_metadata.update_all)
def update_all(self, req, image_id, body):
@@ -119,7 +119,7 @@ class ImageMetadataController(wsgi.Controller):
raise exc.HTTPForbidden(explanation=e.format_message())
return dict(metadata=metadata)
@wsgi.Controller.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.api_version("2.1", MAX_IMAGE_META_PROXY_API_VERSION)
@wsgi.expected_errors((403, 404))
@wsgi.response(204)
def delete(self, req, image_id, id):

View File

@@ -74,7 +74,7 @@ class ImagesController(wsgi.Controller):
return filters
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
def show(self, req, id):
@@ -93,7 +93,7 @@ class ImagesController(wsgi.Controller):
return self._view_builder.show(req, image)
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((403, 404))
@wsgi.response(204)
def delete(self, req, id):
@@ -114,7 +114,7 @@ class ImagesController(wsgi.Controller):
explanation = _("You are not allowed to delete the image.")
raise webob.exc.HTTPForbidden(explanation=explanation)
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(400)
@validation.query_schema(schema.index_query)
def index(self, req):
@@ -134,7 +134,7 @@ class ImagesController(wsgi.Controller):
raise webob.exc.HTTPBadRequest(explanation=e.format_message())
return self._view_builder.index(req, images)
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(400)
@validation.query_schema(schema.detail_query)
def detail(self, req):

View File

@@ -33,7 +33,7 @@ class MultinicController(wsgi.Controller):
super(MultinicController, self).__init__()
self.compute_api = compute.API()
@wsgi.Controller.api_version("2.1", "2.43")
@wsgi.api_version("2.1", "2.43")
@wsgi.response(202)
@wsgi.action('addFixedIp')
@wsgi.expected_errors((400, 404))
@@ -52,7 +52,7 @@ class MultinicController(wsgi.Controller):
except exception.NoMoreFixedIps as e:
raise exc.HTTPBadRequest(explanation=e.format_message())
@wsgi.Controller.api_version("2.1", "2.43")
@wsgi.api_version("2.1", "2.43")
@wsgi.response(202)
@wsgi.action('removeFixedIp')
@wsgi.expected_errors((400, 404))

View File

@@ -77,7 +77,7 @@ class NetworkController(wsgi.Controller):
# TODO(stephenfin): 'network_api' is only being passed for use by tests
self.network_api = network_api or neutron.API()
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(())
@validation.query_schema(schema.index_query)
def index(self, req):
@@ -88,7 +88,7 @@ class NetworkController(wsgi.Controller):
result = [network_dict(context, net_ref) for net_ref in networks]
return {'networks': result}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
def show(self, req, id):

View File

@@ -110,7 +110,7 @@ class QuotaSetsController(wsgi.Controller):
else:
return []
@wsgi.Controller.api_version('2.1')
@wsgi.api_version('2.1')
@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')
@@ -148,7 +148,7 @@ class QuotaSetsController(wsgi.Controller):
self._get_quotas(context, id, user_id=user_id, usages=True),
filtered_quotas=filtered_quotas)
@wsgi.Controller.api_version('2.1')
@wsgi.api_version('2.1')
@wsgi.expected_errors(400)
@validation.schema(quota_sets.update, '2.0', '2.35')
@validation.schema(quota_sets.update_v236, '2.36', '2.56')
@@ -221,7 +221,7 @@ class QuotaSetsController(wsgi.Controller):
self._get_quotas(context, id, user_id=user_id),
filtered_quotas=filtered_quotas)
@wsgi.Controller.api_version('2.0')
@wsgi.api_version('2.0')
@wsgi.expected_errors(400)
@validation.query_schema(quota_sets.defaults_query)
def defaults(self, req, id):

View File

@@ -37,7 +37,7 @@ class RemoteConsolesController(wsgi.Controller):
'serial': self.compute_api.get_serial_console,
'mks': self.compute_api.get_mks_console}
@wsgi.Controller.api_version("2.1", "2.5")
@wsgi.api_version("2.1", "2.5")
@wsgi.expected_errors((400, 404, 409, 501))
@wsgi.action('os-getVNCConsole')
@validation.schema(schema.get_vnc_console)
@@ -69,7 +69,7 @@ class RemoteConsolesController(wsgi.Controller):
return {'console': {'type': console_type, 'url': output['url']}}
@wsgi.Controller.api_version("2.1", "2.5")
@wsgi.api_version("2.1", "2.5")
@wsgi.expected_errors((400, 404, 409, 501))
@wsgi.action('os-getSPICEConsole')
@validation.schema(schema.get_spice_console)
@@ -98,7 +98,7 @@ class RemoteConsolesController(wsgi.Controller):
return {'console': {'type': console_type, 'url': output['url']}}
@wsgi.Controller.api_version("2.1", "2.5")
@wsgi.api_version("2.1", "2.5")
@wsgi.expected_errors((400, 404, 409, 501))
@wsgi.action('os-getRDPConsole')
@wsgi.removed('29.0.0', _rdp_console_removal_reason)
@@ -109,7 +109,7 @@ class RemoteConsolesController(wsgi.Controller):
"""
raise webob.exc.HTTPBadRequest()
@wsgi.Controller.api_version("2.1", "2.5")
@wsgi.api_version("2.1", "2.5")
@wsgi.expected_errors((400, 404, 409, 501))
@wsgi.action('os-getSerialConsole')
@validation.schema(schema.get_serial_console)
@@ -140,7 +140,7 @@ class RemoteConsolesController(wsgi.Controller):
return {'console': {'type': console_type, 'url': output['url']}}
@wsgi.Controller.api_version("2.6")
@wsgi.api_version("2.6")
@wsgi.expected_errors((400, 404, 409, 501))
@validation.schema(schema.create_v26, "2.6", "2.7")
@validation.schema(schema.create_v28, "2.8", "2.98")

View File

@@ -134,7 +134,7 @@ class SecurityGroupControllerBase(object):
class SecurityGroupController(SecurityGroupControllerBase, wsgi.Controller):
"""The Security group API controller for the OpenStack API."""
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((400, 404))
@validation.query_schema(schema.show_query)
def show(self, req, id):
@@ -154,7 +154,7 @@ class SecurityGroupController(SecurityGroupControllerBase, wsgi.Controller):
return {'security_group': self._format_security_group(context,
security_group)}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((400, 404))
@wsgi.response(202)
def delete(self, req, id):
@@ -172,7 +172,7 @@ class SecurityGroupController(SecurityGroupControllerBase, wsgi.Controller):
except exception.Invalid as exp:
raise exc.HTTPBadRequest(explanation=exp.format_message())
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@validation.query_schema(schema.index_query)
@wsgi.expected_errors(404)
def index(self, req):
@@ -196,7 +196,7 @@ class SecurityGroupController(SecurityGroupControllerBase, wsgi.Controller):
list(sorted(result,
key=lambda k: (k['tenant_id'], k['name'])))}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((400, 403))
@validation.schema(schema.create)
def create(self, req, body):
@@ -219,7 +219,7 @@ class SecurityGroupController(SecurityGroupControllerBase, wsgi.Controller):
return {'security_group': self._format_security_group(context,
group_ref)}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((400, 404))
@validation.schema(schema.update)
def update(self, req, id, body):
@@ -254,7 +254,7 @@ class SecurityGroupController(SecurityGroupControllerBase, wsgi.Controller):
class SecurityGroupRulesController(SecurityGroupControllerBase,
wsgi.Controller):
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((400, 403, 404))
@validation.schema(schema.create_rules)
def create(self, req, body):
@@ -327,7 +327,7 @@ class SecurityGroupRulesController(SecurityGroupControllerBase,
return security_group_api.new_cidr_ingress_rule(
cidr, ip_protocol, from_port, to_port)
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((400, 404, 409))
@wsgi.response(202)
def delete(self, req, id):

View File

@@ -175,7 +175,7 @@ class ServerGroupController(wsgi.Controller):
for group in limited_list]
return {'server_groups': result}
@wsgi.Controller.api_version("2.1")
@wsgi.api_version("2.1")
@wsgi.expected_errors((400, 403, 409))
@validation.schema(schema.create, "2.0", "2.14")
@validation.schema(schema.create_v215, "2.15", "2.63")

View File

@@ -65,7 +65,7 @@ class ServerMigrationsController(wsgi.Controller):
super(ServerMigrationsController, self).__init__()
self.compute_api = compute.API()
@wsgi.Controller.api_version("2.22")
@wsgi.api_version("2.22")
@wsgi.response(202)
@wsgi.expected_errors((400, 403, 404, 409))
@wsgi.action('force_complete')
@@ -91,7 +91,7 @@ class ServerMigrationsController(wsgi.Controller):
common.raise_http_conflict_for_instance_invalid_state(
state_error, 'force_complete', server_id)
@wsgi.Controller.api_version("2.23")
@wsgi.api_version("2.23")
@wsgi.expected_errors(404)
@validation.query_schema(schema.index_query)
def index(self, req, server_id):
@@ -114,7 +114,7 @@ class ServerMigrationsController(wsgi.Controller):
output(migration, include_uuid, include_user_project)
for migration in migrations]}
@wsgi.Controller.api_version("2.23")
@wsgi.api_version("2.23")
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
def show(self, req, server_id, id):
@@ -153,7 +153,7 @@ class ServerMigrationsController(wsgi.Controller):
return {'migration': output(migration, include_uuid,
include_user_project)}
@wsgi.Controller.api_version("2.24")
@wsgi.api_version("2.24")
@wsgi.response(202)
@wsgi.expected_errors((400, 404, 409))
def delete(self, req, server_id, id):

View File

@@ -70,7 +70,7 @@ class ServerSharesController(wsgi.Controller):
)
return instance
@wsgi.Controller.api_version("2.97")
@wsgi.api_version("2.97")
@wsgi.response(200)
@wsgi.expected_errors((400, 403, 404))
@validation.query_schema(schema.index_query)
@@ -91,7 +91,7 @@ class ServerSharesController(wsgi.Controller):
return self._view_builder._list_view(db_shares)
@wsgi.Controller.api_version("2.97")
@wsgi.api_version("2.97")
@wsgi.response(201)
@wsgi.expected_errors((400, 403, 404, 409))
@validation.schema(schema.create, '2.97')
@@ -104,7 +104,8 @@ class ServerSharesController(wsgi.Controller):
Prevent user from using the same tag twice on the same instance.
"""
try:
objects.ShareMapping.get_by_instance_uuid_and_share_id(context,
objects.ShareMapping.get_by_instance_uuid_and_share_id(
context,
share_mapping.instance_uuid, share_mapping.share_id
)
raise exception.ShareMappingAlreadyExists(
@@ -196,7 +197,7 @@ class ServerSharesController(wsgi.Controller):
return view
@wsgi.Controller.api_version("2.97")
@wsgi.api_version("2.97")
@wsgi.response(200)
@wsgi.expected_errors((400, 403, 404))
@validation.query_schema(schema.show_query)
@@ -227,7 +228,7 @@ class ServerSharesController(wsgi.Controller):
return view
@wsgi.Controller.api_version("2.97")
@wsgi.api_version("2.97")
@wsgi.response(200)
@wsgi.expected_errors((400, 403, 404, 409))
def delete(self, req, server_id, id):

View File

@@ -60,7 +60,7 @@ class ServerTagsController(wsgi.Controller):
server_id)
return instance
@wsgi.Controller.api_version("2.26")
@wsgi.api_version("2.26")
@wsgi.response(204)
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@@ -81,7 +81,7 @@ class ServerTagsController(wsgi.Controller):
% {'server_id': server_id, 'tag': id})
raise webob.exc.HTTPNotFound(explanation=msg)
@wsgi.Controller.api_version("2.26")
@wsgi.api_version("2.26")
@wsgi.expected_errors(404)
@validation.query_schema(schema.index_query)
def index(self, req, server_id):
@@ -98,7 +98,7 @@ class ServerTagsController(wsgi.Controller):
return {'tags': _get_tags_names(tags)}
@wsgi.Controller.api_version("2.26")
@wsgi.api_version("2.26")
@wsgi.expected_errors((400, 404, 409))
@validation.schema(schema.update)
def update(self, req, server_id, id, body):
@@ -151,7 +151,7 @@ class ServerTagsController(wsgi.Controller):
req, server_id, id)
return response
@wsgi.Controller.api_version("2.26")
@wsgi.api_version("2.26")
@wsgi.expected_errors((404, 409))
@validation.schema(schema.update_all)
def update_all(self, req, server_id, body):
@@ -176,7 +176,7 @@ class ServerTagsController(wsgi.Controller):
return {'tags': _get_tags_names(tags)}
@wsgi.Controller.api_version("2.26")
@wsgi.api_version("2.26")
@wsgi.response(204)
@wsgi.expected_errors((404, 409))
def delete(self, req, server_id, id):
@@ -201,7 +201,7 @@ class ServerTagsController(wsgi.Controller):
notifications_base.send_instance_update_notification(
context, instance, service="nova-api")
@wsgi.Controller.api_version("2.26")
@wsgi.api_version("2.26")
@wsgi.response(204)
@wsgi.expected_errors((404, 409))
def delete_all(self, req, server_id):

View File

@@ -25,7 +25,7 @@ class ServerTopologyController(wsgi.Controller):
super(ServerTopologyController, self).__init__(*args, **kwargs)
self.compute_api = compute.API()
@wsgi.Controller.api_version("2.78")
@wsgi.api_version("2.78")
@wsgi.expected_errors(404)
@validation.query_schema(schema.query_params_v21)
def index(self, req, server_id):

View File

@@ -1502,7 +1502,7 @@ class ServersController(wsgi.Controller):
state_error, 'stop', id
)
@wsgi.Controller.api_version("2.17")
@wsgi.api_version("2.17")
@wsgi.response(202)
@wsgi.expected_errors((400, 404, 409))
@wsgi.action('trigger_crash_dump')

View File

@@ -74,7 +74,7 @@ class TenantNetworkController(wsgi.Controller):
project_id=project_id)
return self.network_api.get_all(ctx)
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(())
@validation.query_schema(schema.index_query)
def index(self, req):
@@ -87,7 +87,7 @@ class TenantNetworkController(wsgi.Controller):
networks.extend(self._default_networks)
return {'networks': [network_dict(n) for n in networks]}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
def show(self, req, id):

View File

@@ -109,7 +109,7 @@ class VolumeController(wsgi.Controller):
super(VolumeController, self).__init__()
self.volume_api = cinder.API()
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(404)
@validation.query_schema(volumes_schema.show_query)
def show(self, req, id):
@@ -125,7 +125,7 @@ class VolumeController(wsgi.Controller):
return {'volume': _translate_volume_detail_view(context, vol)}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.response(202)
@wsgi.expected_errors((400, 404))
def delete(self, req, id):
@@ -141,7 +141,7 @@ class VolumeController(wsgi.Controller):
except exception.VolumeNotFound as e:
raise exc.HTTPNotFound(explanation=e.format_message())
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(())
@validation.query_schema(volumes_schema.index_query)
def index(self, req):
@@ -151,7 +151,7 @@ class VolumeController(wsgi.Controller):
target={'project_id': context.project_id})
return self._items(req, entity_maker=_translate_volume_summary_view)
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(())
@validation.query_schema(volumes_schema.detail_query)
def detail(self, req):
@@ -170,7 +170,7 @@ class VolumeController(wsgi.Controller):
res = [entity_maker(context, vol) for vol in limited_list]
return {'volumes': res}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((400, 403, 404))
@validation.schema(volumes_schema.create)
def create(self, req, body):
@@ -607,7 +607,7 @@ class SnapshotController(wsgi.Controller):
self.volume_api = cinder.API()
super(SnapshotController, self).__init__()
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(404)
@validation.query_schema(volumes_schema.snapshot_show_query)
def show(self, req, id):
@@ -623,7 +623,7 @@ class SnapshotController(wsgi.Controller):
return {'snapshot': _translate_snapshot_detail_view(context, vol)}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.response(202)
@wsgi.expected_errors(404)
def delete(self, req, id):
@@ -637,7 +637,7 @@ class SnapshotController(wsgi.Controller):
except exception.SnapshotNotFound as e:
raise exc.HTTPNotFound(explanation=e.format_message())
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(())
@validation.query_schema(volumes_schema.index_query)
def index(self, req):
@@ -647,7 +647,7 @@ class SnapshotController(wsgi.Controller):
target={'project_id': context.project_id})
return self._items(req, entity_maker=_translate_snapshot_summary_view)
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(())
@validation.query_schema(volumes_schema.detail_query)
def detail(self, req):
@@ -666,7 +666,7 @@ class SnapshotController(wsgi.Controller):
res = [entity_maker(context, snapshot) for snapshot in limited_list]
return {'snapshots': res}
@wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((400, 403))
@validation.schema(volumes_schema.snapshot_create)
def create(self, req, body):

View File

@@ -1,35 +0,0 @@
# Copyright 2014 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class VersionedMethod(object):
def __init__(self, name, start_version, end_version, func):
"""Versioning information for a single method
@name: Name of the method
@start_version: Minimum acceptable version
@end_version: Maximum acceptable_version
@func: Method to call
Minimum and maximums are inclusive
"""
self.name = name
self.start_version = start_version
self.end_version = end_version
self.func = func
def __str__(self):
return ("Version Method %s: min: %s, max: %s"
% (self.name, self.start_version, self.end_version))

View File

@@ -24,8 +24,7 @@ from oslo_utils import encodeutils
from oslo_utils import strutils
import webob
from nova.api.openstack import api_version_request as api_version
from nova.api.openstack import versioned_method
from nova.api.openstack import api_version_request
from nova.api import wsgi
from nova import exception
from nova import i18n
@@ -59,9 +58,6 @@ _METHODS_WITH_BODY = [
# support is fully merged. It does not affect the V2 API.
DEFAULT_API_VERSION = "2.1"
# name of attribute to keep version method information
VER_METHOD_ATTR = 'versioned_methods'
# Names of headers used by clients to request a specific version
# of the REST API
API_VERSION_REQUEST_HEADER = 'OpenStack-API-Version'
@@ -81,7 +77,7 @@ class Request(wsgi.Request):
def __init__(self, *args, **kwargs):
super(Request, self).__init__(*args, **kwargs)
if not hasattr(self, 'api_version_request'):
self.api_version_request = api_version.APIVersionRequest()
self.api_version_request = api_version_request.APIVersionRequest()
def best_match_content_type(self):
"""Determine the requested response content-type."""
@@ -158,25 +154,25 @@ class Request(wsgi.Request):
legacy_headers=[LEGACY_API_VERSION_REQUEST_HEADER])
if hdr_string is None:
self.api_version_request = api_version.APIVersionRequest(
api_version.DEFAULT_API_VERSION)
self.api_version_request = api_version_request.APIVersionRequest(
api_version_request.DEFAULT_API_VERSION)
elif hdr_string == 'latest':
# 'latest' is a special keyword which is equivalent to
# requesting the maximum version of the API supported
self.api_version_request = api_version.max_api_version()
self.api_version_request = api_version_request.max_api_version()
else:
self.api_version_request = api_version.APIVersionRequest(
self.api_version_request = api_version_request.APIVersionRequest(
hdr_string)
# Check that the version requested is within the global
# minimum/maximum of supported API versions
if not self.api_version_request.matches(
api_version.min_api_version(),
api_version.max_api_version()):
api_version_request.min_api_version(),
api_version_request.max_api_version()):
raise exception.InvalidGlobalAPIVersion(
req_ver=self.api_version_request.get_string(),
min_ver=api_version.min_api_version().get_string(),
max_ver=api_version.max_api_version().get_string())
min_ver=api_version_request.min_api_version().get_string(),
max_ver=api_version_request.max_api_version().get_string())
def set_legacy_v2(self):
self.environ[ENV_LEGACY_V2] = True
@@ -243,8 +239,8 @@ class WSGICodes:
ver = req.api_version_request
for code, min_version, max_version in self._codes:
min_ver = api_version.APIVersionRequest(min_version)
max_ver = api_version.APIVersionRequest(max_version)
min_ver = api_version_request.APIVersionRequest(min_version)
max_ver = api_version_request.APIVersionRequest(max_version)
if ver.matches(min_ver, max_ver):
return code
@@ -700,6 +696,46 @@ def removed(version: str, reason: str):
return decorator
def api_version(
min_version: ty.Optional[str] = None,
max_version: ty.Optional[str] = None,
):
"""Mark an API as supporting lower and upper version bounds.
:param min_version: A string of two numerals. X.Y indicating the minimum
version of the JSON-Schema to validate against.
:param max_version: A string of two numerals. X.Y indicating the maximum
version of the JSON-Schema against to.
"""
def decorator(f):
@functools.wraps(f)
def wrapped(*args, **kwargs):
min_ver = api_version_request.APIVersionRequest(min_version)
max_ver = api_version_request.APIVersionRequest(max_version)
# The request object is always the second argument.
# However numerous unittests pass in the request object
# via kwargs instead so we handle that as well.
# TODO(cyeoh): cleanup unittests so we don't have to
# to do this
if 'req' in kwargs:
ver = kwargs['req'].api_version_request
else:
ver = args[1].api_version_request
if not ver.matches(min_ver, max_ver):
raise exception.VersionNotFoundForAPIMethod(version=ver)
return f(*args, **kwargs)
wrapped.min_version = min_version
wrapped.max_version = max_version
return wrapped
return decorator
def expected_errors(
errors: ty.Union[int, tuple[int, ...]],
min_version: ty.Optional[str] = None,
@@ -714,8 +750,8 @@ def expected_errors(
def decorator(f):
@functools.wraps(f)
def wrapped(*args, **kwargs):
min_ver = api_version.APIVersionRequest(min_version)
max_ver = api_version.APIVersionRequest(max_version)
min_ver = api_version_request.APIVersionRequest(min_version)
max_ver = api_version_request.APIVersionRequest(max_version)
# The request object is always the second argument.
# However numerous unittests pass in the request object
@@ -791,36 +827,22 @@ class ControllerMetaclass(type):
def __new__(mcs, name, bases, cls_dict):
"""Adds the wsgi_actions dictionary to the class."""
# Find all actions
actions = {}
versioned_methods = None
# start with wsgi actions from base classes
for base in bases:
actions.update(getattr(base, 'wsgi_actions', {}))
if base.__name__ == "Controller":
# NOTE(cyeoh): This resets the VER_METHOD_ATTR attribute
# between API controller class creations. This allows us
# to use a class decorator on the API methods that doesn't
# require naming explicitly what method is being versioned as
# it can be implicit based on the method decorated. It is a bit
# ugly.
if VER_METHOD_ATTR in base.__dict__:
versioned_methods = getattr(base, VER_METHOD_ATTR)
delattr(base, VER_METHOD_ATTR)
for key, value in cls_dict.items():
if not callable(value):
continue
if getattr(value, 'wsgi_action', None):
actions[value.wsgi_action] = key
# Add the actions to the class dict
cls_dict['wsgi_actions'] = actions
if versioned_methods:
cls_dict[VER_METHOD_ATTR] = versioned_methods
return super(ControllerMetaclass, mcs).__new__(mcs, name, bases,
cls_dict)
@@ -837,103 +859,6 @@ class Controller(metaclass=ControllerMetaclass):
else:
self._view_builder = None
def __getattribute__(self, key):
def version_select(*args, **kwargs):
"""Look for the method which matches the name supplied and version
constraints and calls it with the supplied arguments.
@return: Returns the result of the method called
@raises: VersionNotFoundForAPIMethod if there is no method which
matches the name and version constraints
"""
# The first arg to all versioned methods is always the request
# object. The version for the request is attached to the
# request object
if len(args) == 0:
ver = kwargs['req'].api_version_request
else:
ver = args[0].api_version_request
func_list = self.versioned_methods[key]
for func in func_list:
if ver.matches(func.start_version, func.end_version):
# Update the version_select wrapper function so
# other decorator attributes like wsgi.response
# are still respected.
functools.update_wrapper(version_select, func.func)
return func.func(self, *args, **kwargs)
# No version match
raise exception.VersionNotFoundForAPIMethod(version=ver)
try:
version_meth_dict = object.__getattribute__(self, VER_METHOD_ATTR)
except AttributeError:
# No versioning on this class
return object.__getattribute__(self, key)
if version_meth_dict and \
key in object.__getattribute__(self, VER_METHOD_ATTR):
return version_select
return object.__getattribute__(self, key)
# NOTE(cyeoh): This decorator MUST appear first (the outermost
# decorator) on an API method for it to work correctly
@classmethod
def api_version(cls, min_ver, max_ver=None):
"""Decorator for versioning api methods.
Add the decorator to any method which takes a request object
as the first parameter and belongs to a class which inherits from
wsgi.Controller.
@min_ver: string representing minimum version
@max_ver: optional string representing maximum version
"""
def decorator(f):
obj_min_ver = api_version.APIVersionRequest(min_ver)
if max_ver:
obj_max_ver = api_version.APIVersionRequest(max_ver)
else:
obj_max_ver = api_version.APIVersionRequest()
# Add to list of versioned methods registered
func_name = f.__name__
new_func = versioned_method.VersionedMethod(
func_name, obj_min_ver, obj_max_ver, f)
func_dict = getattr(cls, VER_METHOD_ATTR, {})
if not func_dict:
setattr(cls, VER_METHOD_ATTR, func_dict)
func_list = func_dict.get(func_name, [])
if not func_list:
func_dict[func_name] = func_list
func_list.append(new_func)
# Ensure the list is sorted by minimum version (reversed)
# so later when we work through the list in order we find
# the method which has the latest version which supports
# the version requested.
is_intersect = Controller.check_for_versions_intersection(
func_list)
if is_intersect:
raise exception.ApiVersionsIntersect(
name=new_func.name,
min_ver=new_func.start_version,
max_ver=new_func.end_version,
)
func_list.sort(key=lambda f: f.start_version, reverse=True)
return f
return decorator
@staticmethod
def is_valid_body(body, entity_name):
if not (body and entity_name in body):
@@ -948,36 +873,6 @@ class Controller(metaclass=ControllerMetaclass):
return is_dict(body[entity_name])
@staticmethod
def check_for_versions_intersection(func_list):
"""Determines whether function list contains version intervals
intersections or not. General algorithm:
https://en.wikipedia.org/wiki/Intersection_algorithm
:param func_list: list of VersionedMethod objects
:return: boolean
"""
pairs = []
counter = 0
for f in func_list:
pairs.append((f.start_version, 1, f))
pairs.append((f.end_version, -1, f))
def compare(x):
return x[0]
pairs.sort(key=compare)
for p in pairs:
counter += p[1]
if counter > 1:
return True
return False
class Fault(webob.exc.HTTPException):
"""Wrap webob.exc.HTTPException to provide API friendly response."""

View File

@@ -17,6 +17,7 @@ import functools
import webob
from nova.api.openstack import api_version_request
from nova.api.openstack.compute import routes
from nova.api.openstack import wsgi
from nova.api import validation
@@ -25,54 +26,50 @@ from nova.tests.unit.api.openstack.compute import dummy_schema
class MicroversionsController(wsgi.Controller):
@wsgi.Controller.api_version("2.1")
@wsgi.api_version("2.1")
def index(self, req):
data = {'param': 'val'}
return data
if api_version_request.is_supported(req, '3.0'):
raise webob.exc.HTTPBadRequest()
@wsgi.Controller.api_version("2.2") # noqa
def index(self, req): # noqa
data = {'param': 'val2'}
if api_version_request.is_supported(req, '2.2'):
data = {'param': 'val2'}
else:
data = {'param': 'val'}
return data
@wsgi.Controller.api_version("3.0") # noqa
def index(self, req): # noqa
raise webob.exc.HTTPBadRequest()
# We have a second example controller here to help check
# for accidental dependencies between API controllers
# due to base class changes
class MicroversionsController2(wsgi.Controller):
@wsgi.Controller.api_version("2.2", "2.5")
@wsgi.api_version("2.2", "3.1")
@wsgi.response(200, "2.2", "2.5")
@wsgi.response(202, "2.5", "3.1")
def index(self, req):
data = {'param': 'controller2_val1'}
return data
@wsgi.Controller.api_version("2.5", "3.1") # noqa
@wsgi.response(202)
def index(self, req): # noqa
data = {'param': 'controller2_val2'}
if api_version_request.is_supported(req, '2.5'):
data = {'param': 'controller2_val2'}
else:
data = {'param': 'controller2_val1'}
return data
class MicroversionsController3(wsgi.Controller):
@wsgi.Controller.api_version("2.1")
@wsgi.api_version("2.1")
@validation.schema(dummy_schema.dummy)
def create(self, req, body):
data = {'param': 'create_val1'}
return data
@wsgi.Controller.api_version("2.1")
@wsgi.api_version("2.1")
@validation.schema(dummy_schema.dummy, "2.3", "2.8")
@validation.schema(dummy_schema.dummy2, "2.9")
def update(self, req, id, body):
data = {'param': 'update_val1'}
return data
@wsgi.Controller.api_version("2.1", "2.2")
@wsgi.api_version("2.1", "2.2")
@wsgi.response(202)
@wsgi.action('foo')
def _foo(self, req, id, body):
@@ -80,24 +77,8 @@ class MicroversionsController3(wsgi.Controller):
return data
class MicroversionsController4(wsgi.Controller):
@wsgi.Controller.api_version("2.1")
def _create(self, req):
data = {'param': 'controller4_val1'}
return data
@wsgi.Controller.api_version("2.2") # noqa
def _create(self, req): # noqa
data = {'param': 'controller4_val2'}
return data
def create(self, req, body):
return self._create(req)
class MicroversionsExtendsBaseController(wsgi.Controller):
@wsgi.Controller.api_version("2.1")
@wsgi.api_version("2.1")
def show(self, req, id):
return {'base_param': 'base_val'}
@@ -114,10 +95,6 @@ mv3_controller = functools.partial(routes._create_controller,
MicroversionsController3, [])
mv4_controller = functools.partial(routes._create_controller,
MicroversionsController4, [])
mv5_controller = functools.partial(routes._create_controller,
MicroversionsExtendsBaseController, [])
@@ -138,9 +115,6 @@ ROUTES = (
('/microversions3/{id}/action', {
'POST': [mv3_controller, 'action']
}),
('/microversions4', {
'POST': [mv4_controller, 'create']
}),
('/microversions5/{id}', {
'GET': [mv5_controller, 'show']
}),

View File

@@ -72,8 +72,7 @@ class LegacyMicroversionsTest(test.NoDBTestCase):
self.assertIn(self.header_name, res.headers.getall('Vary'))
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
def test_microversions_return_header_non_default(self,
mock_maxver):
def test_microversions_return_header_non_default(self, mock_maxver):
mock_maxver.return_value = api_version.APIVersionRequest("2.3")
req = fakes.HTTPRequest.blank(
@@ -255,31 +254,6 @@ class LegacyMicroversionsTest(test.NoDBTestCase):
else:
self.assertEqual("compute 2.10", res.headers[self.header_name])
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
def _test_microversions_inner_function(self, version, expected_resp,
mock_maxver):
mock_maxver.return_value = api_version.APIVersionRequest("2.2")
req = fakes.HTTPRequest.blank(
'/v2/%s/microversions4' % fakes.FAKE_PROJECT_ID)
req.headers = self._make_header(version)
req.environ['CONTENT_TYPE'] = "application/json"
req.method = 'POST'
req.body = b''
res = req.get_response(self.app)
self.assertEqual(200, res.status_int)
resp_json = jsonutils.loads(res.body)
self.assertEqual(expected_resp, resp_json['param'])
if 'nova' not in self.header_name.lower():
version = 'compute %s' % version
self.assertEqual(version, res.headers[self.header_name])
def test_microversions_inner_function_v22(self):
self._test_microversions_inner_function('2.2', 'controller4_val2')
def test_microversions_inner_function_v21(self):
self._test_microversions_inner_function('2.1', 'controller4_val1')
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
def _test_microversions_actions(self, ret_code, ret_header, req_header,
mock_maxver):

View File

@@ -116,37 +116,12 @@ class SchemaTest(test.NoDBTestCase):
wsgi_action, wsgi_method, action_controller
) in wsgi_actions:
func = controller.wsgi_actions[wsgi_action]
if hasattr(action_controller, 'versioned_methods'):
if wsgi_method in action_controller.versioned_methods:
# currently all our actions are unversioned and if
# this changes then we need to fix this
funcs = action_controller.versioned_methods[
wsgi_method
]
assert len(funcs) == 1
func = funcs[0].func
# method will always be POST for actions
_validate_func(func, method)
else:
# body validation
versioned_methods = getattr(
controller.controller, 'versioned_methods', {}
)
if action in versioned_methods:
# versioned method
for versioned_method in sorted(
versioned_methods[action],
key=lambda v: v.start_version
):
func = versioned_method.func
_validate_func(func, method)
else:
# unversioned method
func = getattr(controller.controller, action)
_validate_func(func, method)
func = getattr(controller.controller, action)
_validate_func(func, method)
if missing_request_schemas:
raise test.TestingException(

View File

@@ -17,7 +17,6 @@ import testscenarios
import webob
from nova.api.openstack import api_version_request as api_version
from nova.api.openstack import versioned_method
from nova.api.openstack import wsgi
from nova import exception
from nova import test
@@ -854,77 +853,42 @@ class ValidBodyTest(test.NoDBTestCase):
self.assertFalse(self.controller.is_valid_body(body, 'foo'))
class TestController(test.NoDBTestCase):
def test_check_for_versions_intersection_negative(self):
func_list = \
[versioned_method.VersionedMethod('foo',
api_version.APIVersionRequest(
'2.1'),
api_version.APIVersionRequest(
'2.4'),
None),
versioned_method.VersionedMethod('foo',
api_version.APIVersionRequest(
'2.11'),
api_version.APIVersionRequest(
'3.1'),
None),
versioned_method.VersionedMethod('foo',
api_version.APIVersionRequest(
'2.8'),
api_version.APIVersionRequest(
'2.9'),
None),
]
class APIVersionTestCase(test.NoDBTestCase):
result = wsgi.Controller.check_for_versions_intersection(func_list=
func_list)
self.assertFalse(result)
def test_api_version(self):
class FakeController(wsgi.Controller):
@wsgi.api_version('2.10', '2.19')
def fake_func(self, req):
return {'resources': []}
func_list = \
[versioned_method.VersionedMethod('foo',
api_version.APIVersionRequest(
'2.12'),
api_version.APIVersionRequest(
'2.14'),
None),
versioned_method.VersionedMethod('foo',
api_version.APIVersionRequest(
'3.0'),
api_version.APIVersionRequest(
'3.4'),
None)
]
controller = FakeController()
result = wsgi.Controller.check_for_versions_intersection(func_list=
func_list)
self.assertFalse(result)
req = fakes.HTTPRequest.blank('', version='2.10')
self.assertEqual({'resources': []}, controller.fake_func(req))
def test_check_for_versions_intersection_positive(self):
func_list = \
[versioned_method.VersionedMethod('foo',
api_version.APIVersionRequest(
'2.1'),
api_version.APIVersionRequest(
'2.4'),
None),
versioned_method.VersionedMethod('foo',
api_version.APIVersionRequest(
'2.3'),
api_version.APIVersionRequest(
'3.0'),
None),
versioned_method.VersionedMethod('foo',
api_version.APIVersionRequest(
'2.8'),
api_version.APIVersionRequest(
'2.9'),
None),
]
req = fakes.HTTPRequest.blank('', version='2.19')
self.assertEqual({'resources': []}, controller.fake_func(req))
result = wsgi.Controller.check_for_versions_intersection(func_list=
func_list)
self.assertTrue(result)
req = fakes.HTTPRequest.blank('', version='2.9')
self.assertRaises(
exception.VersionNotFoundForAPIMethod, controller.fake_func, req
)
req = fakes.HTTPRequest.blank('', version='2.20')
self.assertRaises(
exception.VersionNotFoundForAPIMethod, controller.fake_func, req
)
def test_api_version_legacy(self):
class FakeController(wsgi.Controller):
@wsgi.api_version('2.0', '2.10')
def fake_func(self, req):
return {'resources': []}
controller = FakeController()
req = fakes.HTTPRequest.blank('')
req.set_legacy_v2()
self.assertEqual({'resources': []}, controller.fake_func(req))
class ExpectedErrorTestCase(test.NoDBTestCase):