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:
parent
69d74c1a66
commit
33489a1f9f
@ -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
|
||||||
|
@ -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.
|
@ -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
|
||||||
|
30
sahara/api/microversion.py
Normal file
30
sahara/api/microversion.py
Normal 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"
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
Loading…
Reference in New Issue
Block a user