diff --git a/magnum/api/controllers/root.py b/magnum/api/controllers/root.py index 311ceb93fb..59166a0709 100644 --- a/magnum/api/controllers/root.py +++ b/magnum/api/controllers/root.py @@ -31,12 +31,24 @@ class Version(base.APIBase): links = [link.Link] """A Link that point to a specific version of the API""" + status = wtypes.text + """The current status of the version: CURRENT, SUPPORTED, UNSUPPORTED""" + + max_version = wtypes.text + """The max microversion supported by this version""" + + min_version = wtypes.text + """The min microversion supported by this version""" + @staticmethod - def convert(id): + def convert(id, status, max, min): version = Version() version.id = id version.links = [link.Link.make_link('self', pecan.request.host_url, id, '', bookmark=True)] + version.status = status + version.max_version = max + version.min_version = min return version @@ -51,17 +63,13 @@ class Root(base.APIBase): versions = [Version] """Links to all the versions available in this API""" - default_version = Version - """A link to the default version of the API""" - @staticmethod def convert(): root = Root() root.name = "OpenStack Magnum API" root.description = ("Magnum is an OpenStack project which aims to " "provide container management.") - root.versions = [Version.convert('v1')] - root.default_version = Version.convert('v1') + root.versions = [Version.convert('v1', "CURRENT", "1.1", "1.1")] return root diff --git a/magnum/api/controllers/v1/__init__.py b/magnum/api/controllers/v1/__init__.py index c032f9f53e..4af563e774 100644 --- a/magnum/api/controllers/v1/__init__.py +++ b/magnum/api/controllers/v1/__init__.py @@ -21,7 +21,6 @@ NOTE: IN PROGRESS AND NOT FULLY IMPLEMENTED. from oslo_log import log as logging import pecan from pecan import rest -from webob import exc from wsme import types as wtypes from magnum.api.controllers import base as controllers_base @@ -31,6 +30,7 @@ from magnum.api.controllers.v1 import baymodel from magnum.api.controllers.v1 import certificate from magnum.api.controllers.v1 import magnum_services from magnum.api import expose +from magnum.api import http_error from magnum.i18n import _ @@ -157,32 +157,41 @@ class Controller(rest.RestController): headers = {} # ensure that major version in the URL matches the header if version.major != BASE_VERSION: - raise exc.HTTPNotAcceptable(_( + raise http_error.HTTPNotAcceptableAPIVersion(_( "Mutually exclusive versions requested. Version %(ver)s " "requested but not supported by this service." "The supported version range is: " "[%(min)s, %(max)s].") % {'ver': version, 'min': MIN_VER_STR, 'max': MAX_VER_STR}, - headers=headers) + headers=headers, + max_version=str(MAX_VER), + min_version=str(MIN_VER)) # ensure the minor version is within the supported range if version < MIN_VER or version > MAX_VER: - raise exc.HTTPNotAcceptable(_( + raise http_error.HTTPNotAcceptableAPIVersion(_( "Version %(ver)s was requested but the minor version is not " "supported by this service. The supported version range is: " "[%(min)s, %(max)s].") % {'ver': version, 'min': MIN_VER_STR, - 'max': MAX_VER_STR}, headers=headers) + 'max': MAX_VER_STR}, + headers=headers, + max_version=str(MAX_VER), + min_version=str(MIN_VER)) @pecan.expose() def _route(self, args): version = controllers_base.Version( pecan.request.headers, MIN_VER_STR, MAX_VER_STR) - # Always set the min and max headers + # Always set the basic version headers pecan.response.headers[ controllers_base.Version.min_string] = MIN_VER_STR pecan.response.headers[ controllers_base.Version.max_string] = MAX_VER_STR + pecan.response.headers[ + controllers_base.Version.string] = " ".join( + [controllers_base.Version.service_string, str(version)]) + pecan.response.headers["vary"] = controllers_base.Version.string # assert that requested version is supported self._check_version(version, pecan.response.headers) diff --git a/magnum/api/http_error.py b/magnum/api/http_error.py new file mode 100644 index 0000000000..21e8aa0f9d --- /dev/null +++ b/magnum/api/http_error.py @@ -0,0 +1,70 @@ +# All Rights Reserved. +# +# 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. + +import json +import six +from webob import exc + + +class HTTPNotAcceptableAPIVersion(exc.HTTPNotAcceptable): + # subclass of :class:`~HTTPNotAcceptable` + # + # This indicates the resource identified by the request is only + # capable of generating response entities which have content + # characteristics not acceptable according to the accept headers + # sent in the request. + # + # code: 406, title: Not Acceptable + # + # differences from webob.exc.HTTPNotAcceptable: + # + # - additional max and min version paramters + # - additional error info for code, title, and links + code = 406 + title = 'Not Acceptable' + max_version = '' + min_version = '' + + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, max_version='', min_version='', **kw): + + super(HTTPNotAcceptableAPIVersion, self).__init__( + detail=detail, headers=headers, comment=comment, + body_template=body_template, **kw) + + self.max_version = max_version + self.min_version = min_version + + def __call__(self, environ, start_response): + for err_str in self.app_iter: + err = {} + try: + err = json.loads(err_str.decode('utf-8')) + except ValueError: + pass + + links = {'rel': 'help', 'href': 'http://developer.openstack.org' + '/api-guide/compute/microversions.html'} + + err['max_version'] = self.max_version + err['min_version'] = self.min_version + err['code'] = "magnum.microversion-unsupported" + err['links'] = [links] + err['title'] = "Requested microversion is unsupported" + + self.app_iter = [six.b(json.dumps(err))] + self.headers['Content-Length'] = str(len(self.app_iter[0])) + + return super(HTTPNotAcceptableAPIVersion, self).__call__( + environ, start_response) diff --git a/magnum/api/middleware/parsable_error.py b/magnum/api/middleware/parsable_error.py index bcfc3fac0c..e3cff0475d 100644 --- a/magnum/api/middleware/parsable_error.py +++ b/magnum/api/middleware/parsable_error.py @@ -29,6 +29,41 @@ class ParsableErrorMiddleware(object): def __init__(self, app): self.app = app + def _update_errors(self, app_iter, status_code): + errs = [] + for err_str in app_iter: + err = {} + try: + err = json.loads(err_str.decode('utf-8')) + except ValueError: + pass + + if 'title' in err and 'description' in err: + title = err['title'] + desc = err['description'] + elif 'faultstring' in err: + title = err['faultstring'].split('.', 1)[0] + desc = err['faultstring'] + else: + title = '' + desc = '' + + code = err['faultcode'].lower() if 'faultcode' in err else '' + + # if already formatted by custom exception, don't update + if 'min_version' in err: + errs.append(err) + else: + errs.append({ + 'request_id': '', + 'code': code, + 'status': status_code, + 'title': title, + 'detail': desc, + 'links': []}) + + return errs + def __call__(self, environ, start_response): # Request for this state, modified by replace_start_response() # and used when an error is being reported. @@ -54,44 +89,17 @@ class ParsableErrorMiddleware(object): ] # Save the headers in case we need to modify them. state['headers'] = headers + return start_response(status, headers, exc_info) app_iter = self.app(environ, replacement_start_response) if (state['status_code'] // 100) not in (2, 3): - errs = [] - for err_str in app_iter: - err = {} - try: - err = json.loads(err_str.decode('utf-8')) - except ValueError: - pass - - if 'title' in err and 'description' in err: - title = err['title'] - desc = err['description'] - elif 'faultstring' in err: - title = err['faultstring'].split('.', 1)[0] - desc = err['faultstring'] - else: - title = '' - desc = '' - - code = err['faultcode'].lower() if 'faultcode' in err else '' - - errs.append({ - 'request_id': '', - 'code': code, - 'status': state['status_code'], - 'title': title, - 'detail': desc, - 'links': [] - }) - + errs = self._update_errors(app_iter, state['status_code']) body = [six.b(json.dumps({'errors': errs}))] - state['headers'].append(('Content-Type', 'application/json')) state['headers'].append(('Content-Length', str(len(body[0])))) + else: body = app_iter return body diff --git a/magnum/tests/unit/api/controllers/test_root.py b/magnum/tests/unit/api/controllers/test_root.py index 18c787536f..ac6dd11c62 100644 --- a/magnum/tests/unit/api/controllers/test_root.py +++ b/magnum/tests/unit/api/controllers/test_root.py @@ -32,16 +32,16 @@ class TestRootController(api_base.FunctionalTest): def setUp(self): super(TestRootController, self).setUp() self.root_expected = { - u'default_version': - {u'id': u'v1', u'links': - [{u'href': u'http://localhost/v1/', u'rel': u'self'}]}, u'description': u'Magnum is an OpenStack project which ' 'aims to provide container management.', u'name': u'OpenStack Magnum API', u'versions': [{u'id': u'v1', u'links': [{u'href': u'http://localhost/v1/', - u'rel': u'self'}]}]} + u'rel': u'self'}], + u'status': u'CURRENT', + u'max_version': u'1.1', + u'min_version': u'1.1'}]} self.v1_expected = { u'media_types':