Browse Source

Merge "Give the illusion of microversion support"

tags/10.0.0.0b1
Zuul 5 months ago
parent
commit
ca032db94c

+ 1
- 0
lower-constraints.txt View File

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

+ 5
- 0
releasenotes/notes/apiv2-microversion-4c1a58ee8090e5a9.yaml View File

@@ -0,0 +1,5 @@
1
+---
2
+features:
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.

+ 1
- 0
requirements.txt View File

@@ -15,6 +15,7 @@ Jinja2>=2.10 # BSD License (3 clause)
15 15
 jsonschema<3.0.0,>=2.6.0 # MIT
16 16
 keystoneauth1>=3.4.0 # Apache-2.0
17 17
 keystonemiddleware>=4.17.0 # Apache-2.0
18
+microversion-parse>=0.2.1 # Apache-2.0
18 19
 oslo.config>=5.2.0 # Apache-2.0
19 20
 oslo.concurrency>=3.26.0 # Apache-2.0
20 21
 oslo.context>=2.19.2 # Apache-2.0

+ 30
- 0
sahara/api/microversion.py View File

@@ -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
+
15
+API_VERSIONS = ["2.0"]
16
+
17
+MIN_API_VERSION = API_VERSIONS[0]
18
+MAX_API_VERSION = API_VERSIONS[-1]
19
+
20
+LATEST = "latest"
21
+VERSION_STRING_REGEX = r"^([1-9]\d*).([1-9]\d*|0)$"
22
+
23
+OPENSTACK_API_VERSION_HEADER = "OpenStack-API-Version"
24
+VARY_HEADER = "Vary"
25
+SAHARA_SERVICE_TYPE = "data-processing"
26
+
27
+BAD_REQUEST_STATUS_CODE = 400
28
+BAD_REQUEST_STATUS_NAME = "BAD_REQUEST"
29
+NOT_ACCEPTABLE_STATUS_CODE = 406
30
+NOT_ACCEPTABLE_STATUS_NAME = "NOT_ACCEPTABLE"

+ 5
- 1
sahara/api/middleware/version_discovery.py View File

@@ -20,6 +20,8 @@ from oslo_serialization import jsonutils
20 20
 import webob
21 21
 import webob.dec
22 22
 
23
+from sahara.api import microversion as mv
24
+
23 25
 
24 26
 class VersionResponseMiddlewareV1(base.Middleware):
25 27
 
@@ -67,7 +69,9 @@ class VersionResponseMiddlewareV2(VersionResponseMiddlewareV1):
67 69
         version_response["versions"].append(
68 70
             {"id": "v2",
69 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 77
         return version_response

+ 68
- 1
sahara/utils/api.py View File

@@ -13,14 +13,17 @@
13 13
 # See the License for the specific language governing permissions and
14 14
 # limitations under the License.
15 15
 
16
+import re
16 17
 import traceback
17 18
 
18 19
 import flask
20
+import microversion_parse
19 21
 from oslo_log import log as logging
20 22
 from oslo_middleware import request_id as oslo_req_id
21 23
 import six
22 24
 from werkzeug import datastructures
23 25
 
26
+from sahara.api import microversion as mv
24 27
 from sahara import context
25 28
 from sahara import exceptions as ex
26 29
 from sahara.i18n import _
@@ -114,7 +117,27 @@ class Rest(flask.Blueprint):
114 117
         return decorator
115 118
 
116 119
 
120
+def 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
+
128
+def 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
+
117 135
 class 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 141
     def route(self, rule, **options):
119 142
         status = options.pop('status_code', None)
120 143
         file_upload = options.pop('file_upload', False)
@@ -266,6 +289,18 @@ def get_request_args():
266 289
     return flask.request.args
267 290
 
268 291
 
292
+def 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
+
269 304
 def abort_and_log(status_code, descr, exc=None):
270 305
     LOG.error("Request aborted with status code {code} and "
271 306
               "message '{message}'".format(code=status_code, message=descr))
@@ -276,19 +311,51 @@ def abort_and_log(status_code, descr, exc=None):
276 311
     flask.abort(status_code, description=descr)
277 312
 
278 313
 
279
-def render_error_message(error_code, error_message, error_name):
314
+def render_error_message(error_code, error_message, error_name, **msg_kwargs):
280 315
     message = {
281 316
         "error_code": error_code,
282 317
         "error_message": error_message,
283 318
         "error_name": error_name
284 319
     }
285 320
 
321
+    message.update(**msg_kwargs)
322
+
286 323
     resp = render(message)
287 324
     resp.status_code = error_code
288 325
 
289 326
     return resp
290 327
 
291 328
 
329
+def 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
+
346
+def 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
+
292 359
 def invalid_param_error(status_code, descr, exc=None):
293 360
     LOG.error("Request aborted with status code {code} and "
294 361
               "message '{message}'".format(code=status_code, message=descr))

Loading…
Cancel
Save