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
This commit is contained in:
Jeremy Freudberg 2018-07-11 11:03:50 -04:00
parent 69d74c1a66
commit 33489a1f9f
6 changed files with 110 additions and 2 deletions

View File

@ -57,6 +57,7 @@ logilab-common==1.4.1
Mako==1.0.7 Mako==1.0.7
MarkupSafe==1.0 MarkupSafe==1.0
mccabe==0.2.1 mccabe==0.2.1
microversion-parse==0.2.1
mock==2.0.0 mock==2.0.0
monotonic==1.4 monotonic==1.4
mox3==0.25.0 mox3==0.25.0

View File

@ -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.

View File

@ -15,6 +15,7 @@ Jinja2>=2.10 # BSD License (3 clause)
jsonschema<3.0.0,>=2.6.0 # MIT jsonschema<3.0.0,>=2.6.0 # MIT
keystoneauth1>=3.4.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0
keystonemiddleware>=4.17.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.config>=5.2.0 # Apache-2.0
oslo.concurrency>=3.26.0 # Apache-2.0 oslo.concurrency>=3.26.0 # Apache-2.0
oslo.context>=2.19.2 # Apache-2.0 oslo.context>=2.19.2 # Apache-2.0

View File

@ -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"

View File

@ -20,6 +20,8 @@ from oslo_serialization import jsonutils
import webob import webob
import webob.dec import webob.dec
from sahara.api import microversion as mv
class VersionResponseMiddlewareV1(base.Middleware): class VersionResponseMiddlewareV1(base.Middleware):
@ -67,7 +69,9 @@ class VersionResponseMiddlewareV2(VersionResponseMiddlewareV1):
version_response["versions"].append( version_response["versions"].append(
{"id": "v2", {"id": "v2",
"status": "EXPERIMENTAL", "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 return version_response

View File

@ -13,14 +13,17 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import re
import traceback import traceback
import flask import flask
import microversion_parse
from oslo_log import log as logging from oslo_log import log as logging
from oslo_middleware import request_id as oslo_req_id from oslo_middleware import request_id as oslo_req_id
import six import six
from werkzeug import datastructures from werkzeug import datastructures
from sahara.api import microversion as mv
from sahara import context from sahara import context
from sahara import exceptions as ex from sahara import exceptions as ex
from sahara.i18n import _ from sahara.i18n import _
@ -114,7 +117,27 @@ class Rest(flask.Blueprint):
return decorator 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): 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): def route(self, rule, **options):
status = options.pop('status_code', None) status = options.pop('status_code', None)
file_upload = options.pop('file_upload', False) file_upload = options.pop('file_upload', False)
@ -266,6 +289,18 @@ def get_request_args():
return flask.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): def abort_and_log(status_code, descr, exc=None):
LOG.error("Request aborted with status code {code} and " LOG.error("Request aborted with status code {code} and "
"message '{message}'".format(code=status_code, message=descr)) "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) 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 = { message = {
"error_code": error_code, "error_code": error_code,
"error_message": error_message, "error_message": error_message,
"error_name": error_name "error_name": error_name
} }
message.update(**msg_kwargs)
resp = render(message) resp = render(message)
resp.status_code = error_code resp.status_code = error_code
return resp 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): def invalid_param_error(status_code, descr, exc=None):
LOG.error("Request aborted with status code {code} and " LOG.error("Request aborted with status code {code} and "
"message '{message}'".format(code=status_code, message=descr)) "message '{message}'".format(code=status_code, message=descr))