0ac4017325
The Reponse type is only used to wrap passthru responses, so this change renames it to PassthruResponse and changes how it is used to make it more clear what is happening. The 'return_type' and 'error' fields are removed as they are not used by passthru. Now that no Unset usage is required, future removal of core WSME logic won't affect PassthruResponse. If the passthru response has 'attach' set then the return value will be an io.BytesIO object which is handled by the expose decorator. This is preferable to having utils.vendor_passthru have the side-effect of setting the pecan Response.app_iter - now all the response handling is done in the expose decorator. Change-Id: Ibcf7860c4a67f0ec8747e5710d8c1ef7281573d1
223 lines
7.0 KiB
Python
223 lines
7.0 KiB
Python
#
|
|
# 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 datetime
|
|
import functools
|
|
from http import client as http_client
|
|
import inspect
|
|
import json
|
|
import sys
|
|
import traceback
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log
|
|
import pecan
|
|
from webob import static
|
|
|
|
from ironic.api import args as api_args
|
|
from ironic.api import functions
|
|
from ironic.api import types as atypes
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
class JSonRenderer(object):
|
|
@staticmethod
|
|
def __init__(path, extra_vars):
|
|
pass
|
|
|
|
@staticmethod
|
|
def render(template_path, namespace):
|
|
if 'faultcode' in namespace:
|
|
return encode_error(None, namespace)
|
|
result = encode_result(
|
|
namespace['result'],
|
|
namespace['datatype']
|
|
)
|
|
return result
|
|
|
|
|
|
pecan.templating._builtin_renderers['wsmejson'] = JSonRenderer
|
|
|
|
pecan_json_decorate = pecan.expose(
|
|
template='wsmejson:',
|
|
content_type='application/json',
|
|
generic=False)
|
|
|
|
|
|
def expose(*args, **kwargs):
|
|
sig = functions.signature(*args, **kwargs)
|
|
|
|
def decorate(f):
|
|
sig(f)
|
|
funcdef = functions.FunctionDefinition.get(f)
|
|
funcdef.resolve_types(atypes.registry)
|
|
|
|
@functools.wraps(f)
|
|
def callfunction(self, *args, **kwargs):
|
|
return_type = funcdef.return_type
|
|
|
|
try:
|
|
args, kwargs = api_args.get_args(
|
|
funcdef, args, kwargs, pecan.request.params,
|
|
pecan.request.body, pecan.request.content_type
|
|
)
|
|
result = f(self, *args, **kwargs)
|
|
|
|
# NOTE: Support setting of status_code with default 201
|
|
pecan.response.status = funcdef.status_code
|
|
if isinstance(result, atypes.PassthruResponse):
|
|
pecan.response.status = result.status_code
|
|
|
|
# NOTE(lucasagomes): If the return code is 204
|
|
# (No Response) we have to make sure that we are not
|
|
# returning anything in the body response and the
|
|
# content-length is 0
|
|
if result.status_code == 204:
|
|
return_type = None
|
|
|
|
if callable(getattr(result.obj, 'read', None)):
|
|
# Stream the files-like data directly to the response
|
|
pecan.response.app_iter = static.FileIter(result.obj)
|
|
return_type = None
|
|
result = None
|
|
else:
|
|
result = result.obj
|
|
|
|
except Exception:
|
|
try:
|
|
exception_info = sys.exc_info()
|
|
orig_exception = exception_info[1]
|
|
orig_code = getattr(orig_exception, 'code', None)
|
|
data = 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 data
|
|
|
|
if return_type is None:
|
|
pecan.request.pecan['content_type'] = None
|
|
pecan.response.content_type = None
|
|
return ''
|
|
|
|
return dict(
|
|
datatype=return_type,
|
|
result=result
|
|
)
|
|
|
|
pecan_json_decorate(callfunction)
|
|
pecan.util._cfg(callfunction)['argspec'] = inspect.getfullargspec(f)
|
|
callfunction._wsme_definition = funcdef
|
|
return callfunction
|
|
|
|
return decorate
|
|
|
|
|
|
def tojson(datatype, value):
|
|
"""A generic converter from python to jsonify-able datatypes.
|
|
|
|
"""
|
|
if value is None:
|
|
return None
|
|
if isinstance(datatype, atypes.ArrayType):
|
|
return [tojson(datatype.item_type, item) for item in value]
|
|
if isinstance(datatype, atypes.DictType):
|
|
return dict((
|
|
(tojson(datatype.key_type, item[0]),
|
|
tojson(datatype.value_type, item[1]))
|
|
for item in value.items()
|
|
))
|
|
if isinstance(value, datetime.datetime):
|
|
return value.isoformat()
|
|
if atypes.iscomplex(datatype):
|
|
d = dict()
|
|
for attr in atypes.list_attributes(datatype):
|
|
attr_value = getattr(value, attr.key)
|
|
if attr_value is not atypes.Unset:
|
|
d[attr.name] = tojson(attr.datatype, attr_value)
|
|
return d
|
|
if isinstance(datatype, atypes.UserType):
|
|
return tojson(datatype.basetype, datatype.tobasetype(value))
|
|
return value
|
|
|
|
|
|
def encode_result(value, datatype, **options):
|
|
jsondata = tojson(datatype, value)
|
|
return json.dumps(jsondata)
|
|
|
|
|
|
def encode_error(context, errordetail):
|
|
return json.dumps(errordetail)
|
|
|
|
|
|
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
|
|
|
|
|
|
class validate(object):
|
|
"""Decorator that define the arguments types of a function.
|
|
|
|
|
|
Example::
|
|
|
|
class MyController(object):
|
|
@expose(str)
|
|
@validate(datetime.date, datetime.time)
|
|
def format(self, d, t):
|
|
return d.isoformat() + ' ' + t.isoformat()
|
|
"""
|
|
def __init__(self, *param_types):
|
|
self.param_types = param_types
|
|
|
|
def __call__(self, func):
|
|
argspec = functions.getargspec(func)
|
|
fd = functions.FunctionDefinition.get(func)
|
|
fd.set_arg_types(argspec, self.param_types)
|
|
return func
|