Files
ironic/ironic/api/expose.py
T
Steve Baker 0ac4017325 Rename Response to PassthruResponse
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
2020-07-20 14:09:53 +12:00

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