diff --git a/etc/murano/murano-paste.ini b/etc/murano/murano-paste.ini index 612ef203..a820f93e 100644 --- a/etc/murano/murano-paste.ini +++ b/etc/murano/murano-paste.ini @@ -1,5 +1,5 @@ [pipeline:murano] -pipeline = versionnegotiation authtoken context rootapp +pipeline = versionnegotiation faultwrap authtoken context rootapp [filter:context] paste.filter_factory = murano.api.middleware.context:ContextMiddleware.factory @@ -22,3 +22,6 @@ paste.app_factory = murano.api.v1.router:API.factory [filter:versionnegotiation] paste.filter_factory = murano.api.middleware.version_negotiation:VersionNegotiationFilter.factory + +[filter:faultwrap] +paste.filter_factory = murano.api.middleware.fault:FaultWrapper.factory diff --git a/murano/api/middleware/fault.py b/murano/api/middleware/fault.py new file mode 100644 index 00000000..af3e2f7d --- /dev/null +++ b/murano/api/middleware/fault.py @@ -0,0 +1,131 @@ +# 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. + +"""A middleware that turns exceptions into parsable string. Inspired by +Cinder's faultwrapper +""" + +import sys +import traceback + +from oslo.config import cfg +import webob + +from murano.openstack.common import wsgi +from murano.packages import exceptions as pkg_exc + +cfg.CONF.import_opt('debug', 'murano.openstack.common.log') + + +class HTTPExceptionDisguise(Exception): + """Disguises HTTP exceptions so they can be handled by the webob fault + application in the wsgi pipeline. + """ + + def __init__(self, exception): + self.exc = exception + self.tb = sys.exc_info()[2] + + +class Fault(object): + + def __init__(self, error): + self.error = error + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + if req.content_type == 'application/xml': + serializer = wsgi.XMLDictSerializer() + else: + serializer = wsgi.JSONDictSerializer() + resp = webob.Response(request=req) + default_webob_exc = webob.exc.HTTPInternalServerError() + resp.status_code = self.error.get('code', default_webob_exc.code) + serializer.default(resp, self.error) + return resp + + +class FaultWrapper(wsgi.Middleware): + """Replace error body with something the client can parse.""" + + @classmethod + def factory(cls, global_conf, **local_conf): + def filter(app): + return cls(app) + return filter + + error_map = { + 'ValueError': webob.exc.HTTPBadRequest, + 'LookupError': webob.exc.HTTPNotFound, + 'PackageClassLoadError': webob.exc.HTTPBadRequest, + 'PackageUILoadError': webob.exc.HTTPBadRequest, + 'PackageLoadError': webob.exc.HTTPBadRequest, + 'PackageFormatError': webob.exc.HTTPBadRequest, + } + + def _map_exception_to_error(self, class_exception): + if class_exception == Exception: + return webob.exc.HTTPInternalServerError + + if class_exception.__name__ not in self.error_map: + return self._map_exception_to_error(class_exception.__base__) + + return self.error_map[class_exception.__name__] + + def _error(self, ex): + + trace = None + webob_exc = None + if isinstance(ex, HTTPExceptionDisguise): + # An HTTP exception was disguised so it could make it here + # let's remove the disguise and set the original HTTP exception + if cfg.CONF.debug: + trace = ''.join(traceback.format_tb(ex.tb)) + ex = ex.exc + webob_exc = ex + + ex_type = ex.__class__.__name__ + + full_message = unicode(ex) + if full_message.find('\n') > -1: + message, msg_trace = full_message.split('\n', 1) + else: + msg_trace = traceback.format_exc() + message = full_message + + if isinstance(ex, pkg_exc.PackageException): + message = ex.message + + if cfg.CONF.debug and not trace: + trace = msg_trace + + if not webob_exc: + webob_exc = self._map_exception_to_error(ex.__class__) + + error = { + 'code': webob_exc.code, + 'title': webob_exc.title, + 'explanation': webob_exc.explanation, + 'error': { + 'message': message, + 'type': ex_type, + 'traceback': trace, + } + } + + return error + + def process_request(self, req): + try: + return req.get_response(self.application) + except Exception as exc: + return req.get_response(Fault(self._error(exc))) diff --git a/murano/openstack/common/wsgi.py b/murano/openstack/common/wsgi.py index 0e5ce590..5bf7575f 100644 --- a/murano/openstack/common/wsgi.py +++ b/murano/openstack/common/wsgi.py @@ -452,13 +452,14 @@ class DictSerializer(ActionDispatcher): class JSONDictSerializer(DictSerializer): """Default JSON request body serialization""" - def default(self, data): + def default(self, data, result=None): def sanitizer(obj): if isinstance(obj, datetime.datetime): _dtime = obj - datetime.timedelta(microseconds=obj.microsecond) return _dtime.isoformat() return unicode(obj) - + if result: + data.body = jsonutils.dumps(result) return jsonutils.dumps(data, default=sanitizer) @@ -473,7 +474,7 @@ class XMLDictSerializer(DictSerializer): self.metadata = metadata or {} self.xmlns = xmlns - def default(self, data): + def default(self, data, result=None): # We expect data to contain a single key which is the XML root. root_key = data.keys()[0] doc = minidom.Document() diff --git a/murano/packages/exceptions.py b/murano/packages/exceptions.py index 2ac50209..93bed0f2 100644 --- a/murano/packages/exceptions.py +++ b/murano/packages/exceptions.py @@ -16,7 +16,12 @@ import murano.openstack.common.exception as e -class PackageClassLoadError(e.Error): +class PackageException(e.Error): + def __str__(self): + return unicode(self.message).encode('UTF-8') + + +class PackageClassLoadError(PackageException): def __init__(self, class_name, message=None): msg = 'Unable to load class "{0}" from package'.format(class_name) if message: @@ -24,7 +29,7 @@ class PackageClassLoadError(e.Error): super(PackageClassLoadError, self).__init__(msg) -class PackageUILoadError(e.Error): +class PackageUILoadError(PackageException): def __init__(self, message=None): msg = 'Unable to load ui definition from package' if message: @@ -32,7 +37,7 @@ class PackageUILoadError(e.Error): super(PackageUILoadError, self).__init__(msg) -class PackageLoadError(e.Error): +class PackageLoadError(PackageException): pass