Convert root path / to use plain JSON

This change replaces the Version and Root types with simple functions
that return data as a dict.

A new @expose decorator is defined in ironic.api.method which
currently only needs to format exceptions. In the future it may also
do input validation.

Change-Id: I4e3b84fb652a74463b44c3ae6c75cdac4d2ab6db
Story: 1651346
Task: 10551
This commit is contained in:
Steve Baker
2020-07-09 13:11:54 +12:00
parent 0ac4017325
commit 1e28a056b0
5 changed files with 136 additions and 79 deletions
+13 -38
View File
@@ -17,53 +17,28 @@
import pecan
from pecan import rest
from ironic.api.controllers import base
from ironic.api.controllers import v1
from ironic.api.controllers import version
from ironic.api import expose
from ironic.api import method
class Root(base.Base):
name = str
"""The name of the API"""
description = str
"""Some information about this API"""
versions = [version.Version]
"""Links to all the versions available in this API"""
default_version = version.Version
"""A link to the default version of the API"""
@staticmethod
def convert():
root = Root()
root.name = "OpenStack Ironic API"
root.description = ("Ironic is an OpenStack project which aims to "
"provision baremetal machines.")
root.default_version = version.default_version()
root.versions = [root.default_version]
return root
def root():
return {
'name': "OpenStack Ironic API",
'description': ("Ironic is an OpenStack project which aims to "
"provision baremetal machines."),
'default_version': version.default_version(),
'versions': version.all_versions()
}
class RootController(rest.RestController):
_versions = [version.ID_VERSION1]
"""All supported API versions"""
_default_version = version.ID_VERSION1
"""The default API version"""
v1 = v1.Controller()
@expose.expose(Root)
@method.expose()
def get(self):
# NOTE: The reason why convert() it's being called for every
# request is because we need to get the host url from
# the request object to make the links.
return Root.convert()
return root()
@pecan.expose()
def _route(self, args, request=None):
@@ -73,6 +48,6 @@ class RootController(rest.RestController):
if the version number is not specified in the url.
"""
if args[0] and args[0] not in self._versions:
args = [self._default_version] + args
if args[0] and args[0] != version.ID_VERSION1:
args = [version.ID_VERSION1] + args
return super(RootController, self)._route(args, request)
+1 -1
View File
@@ -113,7 +113,7 @@ class V1(base.Base):
deploy_templates = None
"""Links to the deploy_templates resource"""
version = version.Version
version = None
"""Version discovery information."""
events = None
+26 -39
View File
@@ -11,53 +11,40 @@
# under the License.
from ironic import api
from ironic.api.controllers import base
from ironic.api.controllers import link
ID_VERSION1 = 'v1'
class Version(base.Base):
"""An API version representation.
This class represents an API version, including the minimum and
maximum minor versions that are supported within the major version.
"""
id = str
"""The ID of the (major) version, also acts as the release number"""
links = None
"""A Link that point to a specific version of the API"""
status = str
"""Status of the version.
One of:
* CURRENT - the latest version of API,
* SUPPORTED - supported, but not latest, version of API,
* DEPRECATED - supported, but deprecated, version of API.
"""
version = str
"""The current, maximum supported (major.minor) version of API."""
min_version = str
"""Minimum supported (major.minor) version of API."""
def __init__(self, id, min_version, version, status='CURRENT'):
self.id = id
self.links = [link.make_link('self', api.request.public_url,
self.id, '', bookmark=True)]
self.status = status
self.version = version
self.min_version = min_version
def all_versions():
return [default_version()]
def default_version():
"""Return a dict representing the current default version
id: The ID of the (major) version, also acts as the release number
links: A list containing one link that points to the current version
of the API
status: Status of the version, one of CURRENT, SUPPORTED, DEPRECATED
min_version: The current, maximum supported (major.minor) version of API.
version: Minimum supported (major.minor) version of API.
"""
# NOTE(dtantsur): avoid circular imports
from ironic.api.controllers.v1 import versions
return Version(ID_VERSION1,
versions.min_version_string(),
versions.max_version_string())
return {
'id': ID_VERSION1,
'links': [
link.make_link('self',
api.request.public_url,
ID_VERSION1, '', bookmark=True)
],
'status': 'CURRENT',
'min_version': versions.min_version_string(),
'version': versions.max_version_string()
}
+95
View File
@@ -0,0 +1,95 @@
#
# Copyright 2015 Rackspace, Inc
# 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 functools
from http import client as http_client
import json
import sys
import traceback
from oslo_config import cfg
from oslo_log import log
import pecan
LOG = log.getLogger(__name__)
pecan_json_decorate = pecan.expose(
content_type='application/json',
generic=False)
def expose():
def decorate(f):
@functools.wraps(f)
def callfunction(self, *args, **kwargs):
try:
result = f(self, *args, **kwargs)
except Exception:
try:
exception_info = sys.exc_info()
orig_exception = exception_info[1]
orig_code = getattr(orig_exception, 'code', None)
result = format_exception(
exception_info,
cfg.CONF.debug_tracebacks_in_api
)
finally:
del exception_info
if orig_code and orig_code in http_client.responses:
pecan.response.status = orig_code
else:
pecan.response.status = 500
return json.dumps(result)
pecan_json_decorate(callfunction)
return callfunction
return decorate
def format_exception(excinfo, debug=False):
"""Extract informations that can be sent to the client."""
error = excinfo[1]
code = getattr(error, 'code', None)
if code and code in http_client.responses and (400 <= code < 500):
faultstring = (error.faultstring if hasattr(error, 'faultstring')
else str(error))
faultcode = getattr(error, 'faultcode', 'Client')
r = dict(faultcode=faultcode,
faultstring=faultstring)
LOG.debug("Client-side error: %s", r['faultstring'])
r['debuginfo'] = None
return r
else:
faultstring = str(error)
debuginfo = "\n".join(traceback.format_exception(*excinfo))
LOG.error('Server-side error: "%s". Detail: \n%s',
faultstring, debuginfo)
faultcode = getattr(error, 'faultcode', 'Server')
r = dict(faultcode=faultcode, faultstring=faultstring)
if debug:
r['debuginfo'] = debuginfo
else:
r['debuginfo'] = None
return r
+1 -1
View File
@@ -103,7 +103,7 @@ class TestNoExceptionTracebackHook(base.BaseApiTest):
def setUp(self):
super(TestNoExceptionTracebackHook, self).setUp()
p = mock.patch.object(root.Root, 'convert')
p = mock.patch.object(root, 'root')
self.root_convert_mock = p.start()
self.addCleanup(p.stop)