From 33489a1f9fc08dd8f844eec31e227736a407f88f Mon Sep 17 00:00:00 2001 From: Jeremy Freudberg Date: Wed, 11 Jul 2018 11:03:50 -0400 Subject: [PATCH] Give the illusion of microversion support Understand and react to microversions in accordance with http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html The actual mechanism allowing for new microversions of APIv2 will come later. Story: 2002178 Task: 20044 Change-Id: I2b664189e45ac4ffd02c3a176787b4bfb78b3871 --- lower-constraints.txt | 1 + .../apiv2-microversion-4c1a58ee8090e5a9.yaml | 5 ++ requirements.txt | 1 + sahara/api/microversion.py | 30 ++++++++ sahara/api/middleware/version_discovery.py | 6 +- sahara/utils/api.py | 69 ++++++++++++++++++- 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/apiv2-microversion-4c1a58ee8090e5a9.yaml create mode 100644 sahara/api/microversion.py diff --git a/lower-constraints.txt b/lower-constraints.txt index 32dc1407cc..9deff5f87e 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -57,6 +57,7 @@ logilab-common==1.4.1 Mako==1.0.7 MarkupSafe==1.0 mccabe==0.2.1 +microversion-parse==0.2.1 mock==2.0.0 monotonic==1.4 mox3==0.25.0 diff --git a/releasenotes/notes/apiv2-microversion-4c1a58ee8090e5a9.yaml b/releasenotes/notes/apiv2-microversion-4c1a58ee8090e5a9.yaml new file mode 100644 index 0000000000..6f4d4c137b --- /dev/null +++ b/releasenotes/notes/apiv2-microversion-4c1a58ee8090e5a9.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Users of Sahara's APIv2 may request a microversion of that API, with + "OpenStack-API-Version: data-processing [version]" in the request headers. diff --git a/requirements.txt b/requirements.txt index e0522288ff..28d95414bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ Jinja2>=2.10 # BSD License (3 clause) jsonschema<3.0.0,>=2.6.0 # MIT keystoneauth1>=3.4.0 # Apache-2.0 keystonemiddleware>=4.17.0 # Apache-2.0 +microversion-parse>=0.2.1 # Apache-2.0 oslo.config>=5.2.0 # Apache-2.0 oslo.concurrency>=3.26.0 # Apache-2.0 oslo.context>=2.19.2 # Apache-2.0 diff --git a/sahara/api/microversion.py b/sahara/api/microversion.py new file mode 100644 index 0000000000..3da881c88a --- /dev/null +++ b/sahara/api/microversion.py @@ -0,0 +1,30 @@ +# Copyright 2018 OpenStack Contributors +# +# 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. + +API_VERSIONS = ["2.0"] + +MIN_API_VERSION = API_VERSIONS[0] +MAX_API_VERSION = API_VERSIONS[-1] + +LATEST = "latest" +VERSION_STRING_REGEX = r"^([1-9]\d*).([1-9]\d*|0)$" + +OPENSTACK_API_VERSION_HEADER = "OpenStack-API-Version" +VARY_HEADER = "Vary" +SAHARA_SERVICE_TYPE = "data-processing" + +BAD_REQUEST_STATUS_CODE = 400 +BAD_REQUEST_STATUS_NAME = "BAD_REQUEST" +NOT_ACCEPTABLE_STATUS_CODE = 406 +NOT_ACCEPTABLE_STATUS_NAME = "NOT_ACCEPTABLE" diff --git a/sahara/api/middleware/version_discovery.py b/sahara/api/middleware/version_discovery.py index f25b4a6f01..72756cc428 100644 --- a/sahara/api/middleware/version_discovery.py +++ b/sahara/api/middleware/version_discovery.py @@ -20,6 +20,8 @@ from oslo_serialization import jsonutils import webob import webob.dec +from sahara.api import microversion as mv + class VersionResponseMiddlewareV1(base.Middleware): @@ -67,7 +69,9 @@ class VersionResponseMiddlewareV2(VersionResponseMiddlewareV1): version_response["versions"].append( {"id": "v2", "status": "EXPERIMENTAL", - "links": self._get_links("2", req) + "links": self._get_links("2", req), + "min_version": mv.MIN_API_VERSION, + "max_version": mv.MAX_API_VERSION } ) return version_response diff --git a/sahara/utils/api.py b/sahara/utils/api.py index ff738a02f4..96ea1f745a 100644 --- a/sahara/utils/api.py +++ b/sahara/utils/api.py @@ -13,14 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re import traceback import flask +import microversion_parse from oslo_log import log as logging from oslo_middleware import request_id as oslo_req_id import six from werkzeug import datastructures +from sahara.api import microversion as mv from sahara import context from sahara import exceptions as ex from sahara.i18n import _ @@ -114,7 +117,27 @@ class Rest(flask.Blueprint): return decorator +def check_microversion_header(): + requested_version = get_requested_microversion() + if not re.match(mv.VERSION_STRING_REGEX, requested_version): + bad_request_microversion(requested_version) + if requested_version not in mv.API_VERSIONS: + not_acceptable_microversion(requested_version) + + +def add_vary_header(response): + response.headers[mv.VARY_HEADER] = mv.OPENSTACK_API_VERSION_HEADER + response.headers[mv.OPENSTACK_API_VERSION_HEADER] = "{} {}".format( + mv.SAHARA_SERVICE_TYPE, get_requested_microversion()) + return response + + class RestV2(Rest): + def __init__(self, *args, **kwargs): + super(RestV2, self).__init__(*args, **kwargs) + self.before_request(check_microversion_header) + self.after_request(add_vary_header) + def route(self, rule, **options): status = options.pop('status_code', None) file_upload = options.pop('file_upload', False) @@ -266,6 +289,18 @@ def get_request_args(): return flask.request.args +def get_requested_microversion(): + requested_version = microversion_parse.get_version( + flask.request.headers, + mv.SAHARA_SERVICE_TYPE + ) + if requested_version is None: + requested_version = mv.MIN_API_VERSION + elif requested_version == mv.LATEST: + requested_version = mv.MAX_API_VERSION + return requested_version + + def abort_and_log(status_code, descr, exc=None): LOG.error("Request aborted with status code {code} and " "message '{message}'".format(code=status_code, message=descr)) @@ -276,19 +311,51 @@ def abort_and_log(status_code, descr, exc=None): flask.abort(status_code, description=descr) -def render_error_message(error_code, error_message, error_name): +def render_error_message(error_code, error_message, error_name, **msg_kwargs): message = { "error_code": error_code, "error_message": error_message, "error_name": error_name } + message.update(**msg_kwargs) + resp = render(message) resp.status_code = error_code return resp +def not_acceptable_microversion(requested_version): + message = ("Version {} is not supported by the API. " + "Minimum is {} and maximum is {}.".format( + requested_version, + mv.MIN_API_VERSION, + mv.MAX_API_VERSION + )) + resp = render_error_message( + mv.NOT_ACCEPTABLE_STATUS_CODE, + message, + mv.NOT_ACCEPTABLE_STATUS_NAME, + max_version=mv.MAX_API_VERSION, + min_version=mv.MIN_API_VERSION + ) + flask.abort(resp) + + +def bad_request_microversion(requested_version): + message = ("API Version String {} is of invalid format. Must be of format" + " MajorNum.MinorNum.").format(requested_version) + resp = render_error_message( + mv.BAD_REQUEST_STATUS_CODE, + message, + mv.BAD_REQUEST_STATUS_NAME, + max_version=mv.MAX_API_VERSION, + min_version=mv.MIN_API_VERSION + ) + flask.abort(resp) + + def invalid_param_error(status_code, descr, exc=None): LOG.error("Request aborted with status code {code} and " "message '{message}'".format(code=status_code, message=descr))