summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy Freudberg <jeremyfreudberg@gmail.com>2018-07-11 11:03:50 -0400
committerJeremy Freudberg <jeremyfreudberg@gmail.com>2019-01-10 09:54:56 -0500
commit33489a1f9fc08dd8f844eec31e227736a407f88f (patch)
treea6675414b7d5e5b44f5c74ca163d9c79e51030e6
parent69d74c1a66d1eab260caaa1017ecdbd446cf5263 (diff)
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
Notes
Notes (review): Code-Review+2: Luigi Toscano <ltoscano@redhat.com> Code-Review+2: Telles Mota Vidal Nóbrega <tenobreg@redhat.com> Workflow+1: Telles Mota Vidal Nóbrega <tenobreg@redhat.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Fri, 11 Jan 2019 00:57:41 +0000 Reviewed-on: https://review.openstack.org/581774 Project: openstack/sahara Branch: refs/heads/master
-rw-r--r--lower-constraints.txt1
-rw-r--r--releasenotes/notes/apiv2-microversion-4c1a58ee8090e5a9.yaml5
-rw-r--r--requirements.txt1
-rw-r--r--sahara/api/microversion.py30
-rw-r--r--sahara/api/middleware/version_discovery.py6
-rw-r--r--sahara/utils/api.py69
6 files changed, 110 insertions, 2 deletions
diff --git a/lower-constraints.txt b/lower-constraints.txt
index 32dc140..9deff5f 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -57,6 +57,7 @@ logilab-common==1.4.1
57Mako==1.0.7 57Mako==1.0.7
58MarkupSafe==1.0 58MarkupSafe==1.0
59mccabe==0.2.1 59mccabe==0.2.1
60microversion-parse==0.2.1
60mock==2.0.0 61mock==2.0.0
61monotonic==1.4 62monotonic==1.4
62mox3==0.25.0 63mox3==0.25.0
diff --git a/releasenotes/notes/apiv2-microversion-4c1a58ee8090e5a9.yaml b/releasenotes/notes/apiv2-microversion-4c1a58ee8090e5a9.yaml
new file mode 100644
index 0000000..6f4d4c1
--- /dev/null
+++ b/releasenotes/notes/apiv2-microversion-4c1a58ee8090e5a9.yaml
@@ -0,0 +1,5 @@
1---
2features:
3 - |
4 Users of Sahara's APIv2 may request a microversion of that API, with
5 "OpenStack-API-Version: data-processing [version]" in the request headers.
diff --git a/requirements.txt b/requirements.txt
index e052228..28d9541 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,6 +15,7 @@ Jinja2>=2.10 # BSD License (3 clause)
15jsonschema<3.0.0,>=2.6.0 # MIT 15jsonschema<3.0.0,>=2.6.0 # MIT
16keystoneauth1>=3.4.0 # Apache-2.0 16keystoneauth1>=3.4.0 # Apache-2.0
17keystonemiddleware>=4.17.0 # Apache-2.0 17keystonemiddleware>=4.17.0 # Apache-2.0
18microversion-parse>=0.2.1 # Apache-2.0
18oslo.config>=5.2.0 # Apache-2.0 19oslo.config>=5.2.0 # Apache-2.0
19oslo.concurrency>=3.26.0 # Apache-2.0 20oslo.concurrency>=3.26.0 # Apache-2.0
20oslo.context>=2.19.2 # Apache-2.0 21oslo.context>=2.19.2 # Apache-2.0
diff --git a/sahara/api/microversion.py b/sahara/api/microversion.py
new file mode 100644
index 0000000..3da881c
--- /dev/null
+++ b/sahara/api/microversion.py
@@ -0,0 +1,30 @@
1# Copyright 2018 OpenStack Contributors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15API_VERSIONS = ["2.0"]
16
17MIN_API_VERSION = API_VERSIONS[0]
18MAX_API_VERSION = API_VERSIONS[-1]
19
20LATEST = "latest"
21VERSION_STRING_REGEX = r"^([1-9]\d*).([1-9]\d*|0)$"
22
23OPENSTACK_API_VERSION_HEADER = "OpenStack-API-Version"
24VARY_HEADER = "Vary"
25SAHARA_SERVICE_TYPE = "data-processing"
26
27BAD_REQUEST_STATUS_CODE = 400
28BAD_REQUEST_STATUS_NAME = "BAD_REQUEST"
29NOT_ACCEPTABLE_STATUS_CODE = 406
30NOT_ACCEPTABLE_STATUS_NAME = "NOT_ACCEPTABLE"
diff --git a/sahara/api/middleware/version_discovery.py b/sahara/api/middleware/version_discovery.py
index f25b4a6..72756cc 100644
--- a/sahara/api/middleware/version_discovery.py
+++ b/sahara/api/middleware/version_discovery.py
@@ -20,6 +20,8 @@ from oslo_serialization import jsonutils
20import webob 20import webob
21import webob.dec 21import webob.dec
22 22
23from sahara.api import microversion as mv
24
23 25
24class VersionResponseMiddlewareV1(base.Middleware): 26class VersionResponseMiddlewareV1(base.Middleware):
25 27
@@ -67,7 +69,9 @@ class VersionResponseMiddlewareV2(VersionResponseMiddlewareV1):
67 version_response["versions"].append( 69 version_response["versions"].append(
68 {"id": "v2", 70 {"id": "v2",
69 "status": "EXPERIMENTAL", 71 "status": "EXPERIMENTAL",
70 "links": self._get_links("2", req) 72 "links": self._get_links("2", req),
73 "min_version": mv.MIN_API_VERSION,
74 "max_version": mv.MAX_API_VERSION
71 } 75 }
72 ) 76 )
73 return version_response 77 return version_response
diff --git a/sahara/utils/api.py b/sahara/utils/api.py
index ff738a0..96ea1f7 100644
--- a/sahara/utils/api.py
+++ b/sahara/utils/api.py
@@ -13,14 +13,17 @@
13# See the License for the specific language governing permissions and 13# See the License for the specific language governing permissions and
14# limitations under the License. 14# limitations under the License.
15 15
16import re
16import traceback 17import traceback
17 18
18import flask 19import flask
20import microversion_parse
19from oslo_log import log as logging 21from oslo_log import log as logging
20from oslo_middleware import request_id as oslo_req_id 22from oslo_middleware import request_id as oslo_req_id
21import six 23import six
22from werkzeug import datastructures 24from werkzeug import datastructures
23 25
26from sahara.api import microversion as mv
24from sahara import context 27from sahara import context
25from sahara import exceptions as ex 28from sahara import exceptions as ex
26from sahara.i18n import _ 29from sahara.i18n import _
@@ -114,7 +117,27 @@ class Rest(flask.Blueprint):
114 return decorator 117 return decorator
115 118
116 119
120def check_microversion_header():
121 requested_version = get_requested_microversion()
122 if not re.match(mv.VERSION_STRING_REGEX, requested_version):
123 bad_request_microversion(requested_version)
124 if requested_version not in mv.API_VERSIONS:
125 not_acceptable_microversion(requested_version)
126
127
128def add_vary_header(response):
129 response.headers[mv.VARY_HEADER] = mv.OPENSTACK_API_VERSION_HEADER
130 response.headers[mv.OPENSTACK_API_VERSION_HEADER] = "{} {}".format(
131 mv.SAHARA_SERVICE_TYPE, get_requested_microversion())
132 return response
133
134
117class RestV2(Rest): 135class RestV2(Rest):
136 def __init__(self, *args, **kwargs):
137 super(RestV2, self).__init__(*args, **kwargs)
138 self.before_request(check_microversion_header)
139 self.after_request(add_vary_header)
140
118 def route(self, rule, **options): 141 def route(self, rule, **options):
119 status = options.pop('status_code', None) 142 status = options.pop('status_code', None)
120 file_upload = options.pop('file_upload', False) 143 file_upload = options.pop('file_upload', False)
@@ -266,6 +289,18 @@ def get_request_args():
266 return flask.request.args 289 return flask.request.args
267 290
268 291
292def get_requested_microversion():
293 requested_version = microversion_parse.get_version(
294 flask.request.headers,
295 mv.SAHARA_SERVICE_TYPE
296 )
297 if requested_version is None:
298 requested_version = mv.MIN_API_VERSION
299 elif requested_version == mv.LATEST:
300 requested_version = mv.MAX_API_VERSION
301 return requested_version
302
303
269def abort_and_log(status_code, descr, exc=None): 304def abort_and_log(status_code, descr, exc=None):
270 LOG.error("Request aborted with status code {code} and " 305 LOG.error("Request aborted with status code {code} and "
271 "message '{message}'".format(code=status_code, message=descr)) 306 "message '{message}'".format(code=status_code, message=descr))
@@ -276,19 +311,51 @@ def abort_and_log(status_code, descr, exc=None):
276 flask.abort(status_code, description=descr) 311 flask.abort(status_code, description=descr)
277 312
278 313
279def render_error_message(error_code, error_message, error_name): 314def render_error_message(error_code, error_message, error_name, **msg_kwargs):
280 message = { 315 message = {
281 "error_code": error_code, 316 "error_code": error_code,
282 "error_message": error_message, 317 "error_message": error_message,
283 "error_name": error_name 318 "error_name": error_name
284 } 319 }
285 320
321 message.update(**msg_kwargs)
322
286 resp = render(message) 323 resp = render(message)
287 resp.status_code = error_code 324 resp.status_code = error_code
288 325
289 return resp 326 return resp
290 327
291 328
329def not_acceptable_microversion(requested_version):
330 message = ("Version {} is not supported by the API. "
331 "Minimum is {} and maximum is {}.".format(
332 requested_version,
333 mv.MIN_API_VERSION,
334 mv.MAX_API_VERSION
335 ))
336 resp = render_error_message(
337 mv.NOT_ACCEPTABLE_STATUS_CODE,
338 message,
339 mv.NOT_ACCEPTABLE_STATUS_NAME,
340 max_version=mv.MAX_API_VERSION,
341 min_version=mv.MIN_API_VERSION
342 )
343 flask.abort(resp)
344
345
346def bad_request_microversion(requested_version):
347 message = ("API Version String {} is of invalid format. Must be of format"
348 " MajorNum.MinorNum.").format(requested_version)
349 resp = render_error_message(
350 mv.BAD_REQUEST_STATUS_CODE,
351 message,
352 mv.BAD_REQUEST_STATUS_NAME,
353 max_version=mv.MAX_API_VERSION,
354 min_version=mv.MIN_API_VERSION
355 )
356 flask.abort(resp)
357
358
292def invalid_param_error(status_code, descr, exc=None): 359def invalid_param_error(status_code, descr, exc=None):
293 LOG.error("Request aborted with status code {code} and " 360 LOG.error("Request aborted with status code {code} and "
294 "message '{message}'".format(code=status_code, message=descr)) 361 "message '{message}'".format(code=status_code, message=descr))