From 08dd30d3fce13972858019255d55486872f2a1f4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 4 Nov 2024 15:08:28 +0000 Subject: [PATCH] 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 --- doc/source/contributor/microversions.rst | 49 +--- nova/api/openstack/compute/aggregates.py | 2 +- nova/api/openstack/compute/baremetal_nodes.py | 12 +- nova/api/openstack/compute/flavors.py | 2 +- .../openstack/compute/floating_ip_pools.py | 2 +- nova/api/openstack/compute/floating_ips.py | 12 +- nova/api/openstack/compute/hosts.py | 12 +- nova/api/openstack/compute/hypervisors.py | 8 +- nova/api/openstack/compute/image_metadata.py | 12 +- nova/api/openstack/compute/images.py | 8 +- nova/api/openstack/compute/multinic.py | 4 +- nova/api/openstack/compute/networks.py | 4 +- nova/api/openstack/compute/quota_sets.py | 6 +- nova/api/openstack/compute/remote_consoles.py | 10 +- nova/api/openstack/compute/security_groups.py | 14 +- nova/api/openstack/compute/server_groups.py | 2 +- .../openstack/compute/server_migrations.py | 8 +- nova/api/openstack/compute/server_shares.py | 11 +- nova/api/openstack/compute/server_tags.py | 12 +- nova/api/openstack/compute/server_topology.py | 2 +- nova/api/openstack/compute/servers.py | 2 +- nova/api/openstack/compute/tenant_networks.py | 4 +- nova/api/openstack/compute/volumes.py | 20 +- nova/api/openstack/versioned_method.py | 35 --- nova/api/openstack/wsgi.py | 217 +++++------------- .../api/openstack/compute/microversions.py | 64 ++---- .../openstack/compute/test_microversions.py | 28 +-- .../api/openstack/compute/test_schemas.py | 29 +-- nova/tests/unit/api/openstack/test_wsgi.py | 98 +++----- 29 files changed, 206 insertions(+), 483 deletions(-) delete mode 100644 nova/api/openstack/versioned_method.py diff --git a/doc/source/contributor/microversions.rst b/doc/source/contributor/microversions.rst index 2f7d1e2006c5..637cc90ba636 100644 --- a/doc/source/contributor/microversions.rst +++ b/doc/source/contributor/microversions.rst @@ -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): diff --git a/nova/api/openstack/compute/aggregates.py b/nova/api/openstack/compute/aggregates.py index 590c5c1da5cf..50ce6835bf42 100644 --- a/nova/api/openstack/compute/aggregates.py +++ b/nova/api/openstack/compute/aggregates.py @@ -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) diff --git a/nova/api/openstack/compute/baremetal_nodes.py b/nova/api/openstack/compute/baremetal_nodes.py index 3ff071496668..84ef320c0ba0 100644 --- a/nova/api/openstack/compute/baremetal_nodes.py +++ b/nova/api/openstack/compute/baremetal_nodes.py @@ -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) diff --git a/nova/api/openstack/compute/flavors.py b/nova/api/openstack/compute/flavors.py index b600cd2e1db1..ba04b8c54475 100644 --- a/nova/api/openstack/compute/flavors.py +++ b/nova/api/openstack/compute/flavors.py @@ -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') diff --git a/nova/api/openstack/compute/floating_ip_pools.py b/nova/api/openstack/compute/floating_ip_pools.py index 85b5f3e8a090..2b95372c3667 100644 --- a/nova/api/openstack/compute/floating_ip_pools.py +++ b/nova/api/openstack/compute/floating_ip_pools.py @@ -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) diff --git a/nova/api/openstack/compute/floating_ips.py b/nova/api/openstack/compute/floating_ips.py index 6b593170966a..1324f94f5efd 100644 --- a/nova/api/openstack/compute/floating_ips.py +++ b/nova/api/openstack/compute/floating_ips.py @@ -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) diff --git a/nova/api/openstack/compute/hosts.py b/nova/api/openstack/compute/hosts.py index e4a9aa0cfe88..559cac4d8d29 100644 --- a/nova/api/openstack/compute/hosts.py +++ b/nova/api/openstack/compute/hosts.py @@ -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): diff --git a/nova/api/openstack/compute/hypervisors.py b/nova/api/openstack/compute/hypervisors.py index 65550ea1d5c3..e9f52fe48d7f 100644 --- a/nova/api/openstack/compute/hypervisors.py +++ b/nova/api/openstack/compute/hypervisors.py @@ -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): diff --git a/nova/api/openstack/compute/image_metadata.py b/nova/api/openstack/compute/image_metadata.py index 029a05f3dcc9..df915c6f14a9 100644 --- a/nova/api/openstack/compute/image_metadata.py +++ b/nova/api/openstack/compute/image_metadata.py @@ -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): diff --git a/nova/api/openstack/compute/images.py b/nova/api/openstack/compute/images.py index e30aa7495185..f6b78ce04dae 100644 --- a/nova/api/openstack/compute/images.py +++ b/nova/api/openstack/compute/images.py @@ -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): diff --git a/nova/api/openstack/compute/multinic.py b/nova/api/openstack/compute/multinic.py index a73e31fea548..fcdfc860568e 100644 --- a/nova/api/openstack/compute/multinic.py +++ b/nova/api/openstack/compute/multinic.py @@ -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)) diff --git a/nova/api/openstack/compute/networks.py b/nova/api/openstack/compute/networks.py index 937664b53733..4a54c3b6585c 100644 --- a/nova/api/openstack/compute/networks.py +++ b/nova/api/openstack/compute/networks.py @@ -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): diff --git a/nova/api/openstack/compute/quota_sets.py b/nova/api/openstack/compute/quota_sets.py index 14bb8c49e0f5..908f790a3c94 100644 --- a/nova/api/openstack/compute/quota_sets.py +++ b/nova/api/openstack/compute/quota_sets.py @@ -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): diff --git a/nova/api/openstack/compute/remote_consoles.py b/nova/api/openstack/compute/remote_consoles.py index acbe4d83fd60..55b4da38fde8 100644 --- a/nova/api/openstack/compute/remote_consoles.py +++ b/nova/api/openstack/compute/remote_consoles.py @@ -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") diff --git a/nova/api/openstack/compute/security_groups.py b/nova/api/openstack/compute/security_groups.py index 3d6577a36011..37543913436b 100644 --- a/nova/api/openstack/compute/security_groups.py +++ b/nova/api/openstack/compute/security_groups.py @@ -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): diff --git a/nova/api/openstack/compute/server_groups.py b/nova/api/openstack/compute/server_groups.py index 5c2bfad7bdba..c69e1c1b0b76 100644 --- a/nova/api/openstack/compute/server_groups.py +++ b/nova/api/openstack/compute/server_groups.py @@ -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") diff --git a/nova/api/openstack/compute/server_migrations.py b/nova/api/openstack/compute/server_migrations.py index 0015bf1455fe..c084be150724 100644 --- a/nova/api/openstack/compute/server_migrations.py +++ b/nova/api/openstack/compute/server_migrations.py @@ -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): diff --git a/nova/api/openstack/compute/server_shares.py b/nova/api/openstack/compute/server_shares.py index 32d455914438..5171eedc1709 100644 --- a/nova/api/openstack/compute/server_shares.py +++ b/nova/api/openstack/compute/server_shares.py @@ -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): diff --git a/nova/api/openstack/compute/server_tags.py b/nova/api/openstack/compute/server_tags.py index 3c029feb09b7..b8600d695c83 100644 --- a/nova/api/openstack/compute/server_tags.py +++ b/nova/api/openstack/compute/server_tags.py @@ -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): diff --git a/nova/api/openstack/compute/server_topology.py b/nova/api/openstack/compute/server_topology.py index 5aec6c3b6642..48cc328012cd 100644 --- a/nova/api/openstack/compute/server_topology.py +++ b/nova/api/openstack/compute/server_topology.py @@ -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): diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index 166f089c9de6..883d7c6a924a 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -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') diff --git a/nova/api/openstack/compute/tenant_networks.py b/nova/api/openstack/compute/tenant_networks.py index ae1e775a46ad..ed00ffb5330e 100644 --- a/nova/api/openstack/compute/tenant_networks.py +++ b/nova/api/openstack/compute/tenant_networks.py @@ -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): diff --git a/nova/api/openstack/compute/volumes.py b/nova/api/openstack/compute/volumes.py index 1ae4cf98da2c..87f51894914a 100644 --- a/nova/api/openstack/compute/volumes.py +++ b/nova/api/openstack/compute/volumes.py @@ -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): diff --git a/nova/api/openstack/versioned_method.py b/nova/api/openstack/versioned_method.py deleted file mode 100644 index b7e30da839b2..000000000000 --- a/nova/api/openstack/versioned_method.py +++ /dev/null @@ -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)) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 00cd36976a95..cf9149297999 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -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.""" diff --git a/nova/tests/unit/api/openstack/compute/microversions.py b/nova/tests/unit/api/openstack/compute/microversions.py index 9cbd610bc1d3..6137471a0858 100644 --- a/nova/tests/unit/api/openstack/compute/microversions.py +++ b/nova/tests/unit/api/openstack/compute/microversions.py @@ -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'] }), diff --git a/nova/tests/unit/api/openstack/compute/test_microversions.py b/nova/tests/unit/api/openstack/compute/test_microversions.py index 9f5dd9088944..d8d7e3e709f7 100644 --- a/nova/tests/unit/api/openstack/compute/test_microversions.py +++ b/nova/tests/unit/api/openstack/compute/test_microversions.py @@ -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): diff --git a/nova/tests/unit/api/openstack/compute/test_schemas.py b/nova/tests/unit/api/openstack/compute/test_schemas.py index b9ad9560c7ec..04c7ea6fd178 100644 --- a/nova/tests/unit/api/openstack/compute/test_schemas.py +++ b/nova/tests/unit/api/openstack/compute/test_schemas.py @@ -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( diff --git a/nova/tests/unit/api/openstack/test_wsgi.py b/nova/tests/unit/api/openstack/test_wsgi.py index 8d72316d350a..50a56a99ee50 100644 --- a/nova/tests/unit/api/openstack/test_wsgi.py +++ b/nova/tests/unit/api/openstack/test_wsgi.py @@ -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):