Make exception translation common and add to pecan
This moves the exception translation logic from the legacy api.v2.resource module to a common module and re-uses it from pecan to bring consistency to the way language and exception translation is handled between the two. This also adjusts the policy enforcement hook to correctly handle the unsupported method case since 'after' hooks are executed after 'on_error' hooks that return an exception. Closes-Bug: #1583844 Change-Id: If3c2d8c94ca6c1615f3b909accf0f718e320d1c2
This commit is contained in:
parent
d2630f2dd4
commit
a1c194cf06
|
@ -15,14 +15,18 @@
|
|||
|
||||
import functools
|
||||
|
||||
import netaddr
|
||||
from neutron_lib import exceptions
|
||||
from oslo_config import cfg
|
||||
import oslo_i18n
|
||||
from oslo_log import log as logging
|
||||
from oslo_policy import policy as oslo_policy
|
||||
from six.moves.urllib import parse
|
||||
from webob import exc
|
||||
|
||||
from neutron._i18n import _, _LW
|
||||
from neutron.common import constants
|
||||
from neutron import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -344,3 +348,82 @@ class NeutronController(object):
|
|||
raise exc.HTTPBadRequest(msg)
|
||||
data[param_name] = param_value or param.get('default-value')
|
||||
return body
|
||||
|
||||
|
||||
def convert_exception_to_http_exc(e, faults, language):
|
||||
serializer = wsgi.JSONDictSerializer()
|
||||
e = translate(e, language)
|
||||
body = serializer.serialize(
|
||||
{'NeutronError': get_exception_data(e)})
|
||||
kwargs = {'body': body, 'content_type': 'application/json'}
|
||||
if isinstance(e, exc.HTTPException):
|
||||
# already an HTTP error, just update with content type and body
|
||||
e.body = body
|
||||
e.content_type = kwargs['content_type']
|
||||
return e
|
||||
if isinstance(e, (exceptions.NeutronException, netaddr.AddrFormatError,
|
||||
oslo_policy.PolicyNotAuthorized)):
|
||||
for fault in faults:
|
||||
if isinstance(e, fault):
|
||||
mapped_exc = faults[fault]
|
||||
break
|
||||
else:
|
||||
mapped_exc = exc.HTTPInternalServerError
|
||||
return mapped_exc(**kwargs)
|
||||
if isinstance(e, NotImplementedError):
|
||||
# NOTE(armando-migliaccio): from a client standpoint
|
||||
# it makes sense to receive these errors, because
|
||||
# extensions may or may not be implemented by
|
||||
# the underlying plugin. So if something goes south,
|
||||
# because a plugin does not implement a feature,
|
||||
# returning 500 is definitely confusing.
|
||||
kwargs['body'] = serializer.serialize(
|
||||
{'NotImplementedError': get_exception_data(e)})
|
||||
return exc.HTTPNotImplemented(**kwargs)
|
||||
# NOTE(jkoelker) Everything else is 500
|
||||
# Do not expose details of 500 error to clients.
|
||||
msg = _('Request Failed: internal server error while '
|
||||
'processing your request.')
|
||||
msg = translate(msg, language)
|
||||
kwargs['body'] = serializer.serialize(
|
||||
{'NeutronError': get_exception_data(exc.HTTPInternalServerError(msg))})
|
||||
return exc.HTTPInternalServerError(**kwargs)
|
||||
|
||||
|
||||
def get_exception_data(e):
|
||||
"""Extract the information about an exception.
|
||||
|
||||
Neutron client for the v2 API expects exceptions to have 'type', 'message'
|
||||
and 'detail' attributes.This information is extracted and converted into a
|
||||
dictionary.
|
||||
|
||||
:param e: the exception to be reraised
|
||||
:returns: a structured dict with the exception data
|
||||
"""
|
||||
err_data = {'type': e.__class__.__name__,
|
||||
'message': e, 'detail': ''}
|
||||
return err_data
|
||||
|
||||
|
||||
def translate(translatable, locale):
|
||||
"""Translates the object to the given locale.
|
||||
|
||||
If the object is an exception its translatable elements are translated
|
||||
in place, if the object is a translatable string it is translated and
|
||||
returned. Otherwise, the object is returned as-is.
|
||||
|
||||
:param translatable: the object to be translated
|
||||
:param locale: the locale to translate to
|
||||
:returns: the translated object, or the object as-is if it
|
||||
was not translated
|
||||
"""
|
||||
localize = oslo_i18n.translate
|
||||
if isinstance(translatable, exceptions.NeutronException):
|
||||
translatable.msg = localize(translatable.msg, locale)
|
||||
elif isinstance(translatable, exc.HTTPError):
|
||||
translatable.detail = localize(translatable.detail, locale)
|
||||
elif isinstance(translatable, Exception):
|
||||
translatable.message = localize(translatable, locale)
|
||||
else:
|
||||
return localize(translatable, locale)
|
||||
return translatable
|
||||
|
|
|
@ -17,18 +17,12 @@
|
|||
Utility methods for working with WSGI servers redux
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import netaddr
|
||||
from neutron_lib import exceptions
|
||||
import oslo_i18n
|
||||
from oslo_log import log as logging
|
||||
from oslo_policy import policy as oslo_policy
|
||||
import six
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from neutron._i18n import _, _LE, _LI
|
||||
from neutron._i18n import _LE, _LI
|
||||
from neutron.api import api_common
|
||||
from neutron import wsgi
|
||||
|
||||
|
||||
|
@ -82,61 +76,15 @@ def Resource(controller, faults=None, deserializers=None, serializers=None,
|
|||
method = getattr(controller, action)
|
||||
|
||||
result = method(request=request, **args)
|
||||
except (exceptions.NeutronException,
|
||||
netaddr.AddrFormatError,
|
||||
oslo_policy.PolicyNotAuthorized) as e:
|
||||
for fault in faults:
|
||||
if isinstance(e, fault):
|
||||
mapped_exc = faults[fault]
|
||||
break
|
||||
else:
|
||||
mapped_exc = webob.exc.HTTPInternalServerError
|
||||
if 400 <= mapped_exc.code < 500:
|
||||
except Exception as e:
|
||||
mapped_exc = api_common.convert_exception_to_http_exc(e, faults,
|
||||
language)
|
||||
if hasattr(mapped_exc, 'code') and 400 <= mapped_exc.code < 500:
|
||||
LOG.info(_LI('%(action)s failed (client error): %(exc)s'),
|
||||
{'action': action, 'exc': e})
|
||||
{'action': action, 'exc': mapped_exc})
|
||||
else:
|
||||
LOG.exception(_LE('%s failed'), action)
|
||||
e = translate(e, language)
|
||||
body = serializer.serialize(
|
||||
{'NeutronError': get_exception_data(e)})
|
||||
kwargs = {'body': body, 'content_type': content_type}
|
||||
raise mapped_exc(**kwargs)
|
||||
except webob.exc.HTTPException as e:
|
||||
type_, value, tb = sys.exc_info()
|
||||
if hasattr(e, 'code') and 400 <= e.code < 500:
|
||||
LOG.info(_LI('%(action)s failed (client error): %(exc)s'),
|
||||
{'action': action, 'exc': e})
|
||||
else:
|
||||
LOG.exception(_LE('%s failed'), action)
|
||||
translate(e, language)
|
||||
value.body = serializer.serialize(
|
||||
{'NeutronError': get_exception_data(e)})
|
||||
value.content_type = content_type
|
||||
six.reraise(type_, value, tb)
|
||||
except NotImplementedError as e:
|
||||
e = translate(e, language)
|
||||
# NOTE(armando-migliaccio): from a client standpoint
|
||||
# it makes sense to receive these errors, because
|
||||
# extensions may or may not be implemented by
|
||||
# the underlying plugin. So if something goes south,
|
||||
# because a plugin does not implement a feature,
|
||||
# returning 500 is definitely confusing.
|
||||
body = serializer.serialize(
|
||||
{'NotImplementedError': get_exception_data(e)})
|
||||
kwargs = {'body': body, 'content_type': content_type}
|
||||
raise webob.exc.HTTPNotImplemented(**kwargs)
|
||||
except Exception:
|
||||
# NOTE(jkoelker) Everything else is 500
|
||||
LOG.exception(_LE('%s failed'), action)
|
||||
# Do not expose details of 500 error to clients.
|
||||
msg = _('Request Failed: internal server error while '
|
||||
'processing your request.')
|
||||
msg = translate(msg, language)
|
||||
body = serializer.serialize(
|
||||
{'NeutronError': get_exception_data(
|
||||
webob.exc.HTTPInternalServerError(msg))})
|
||||
kwargs = {'body': body, 'content_type': content_type}
|
||||
raise webob.exc.HTTPInternalServerError(**kwargs)
|
||||
raise mapped_exc
|
||||
|
||||
status = action_status.get(action, 200)
|
||||
body = serializer.serialize(result)
|
||||
|
@ -154,42 +102,3 @@ def Resource(controller, faults=None, deserializers=None, serializers=None,
|
|||
# extension to rewrite the code for use with pecan.
|
||||
setattr(resource, 'controller', controller)
|
||||
return resource
|
||||
|
||||
|
||||
def get_exception_data(e):
|
||||
"""Extract the information about an exception.
|
||||
|
||||
Neutron client for the v2 API expects exceptions to have 'type', 'message'
|
||||
and 'detail' attributes.This information is extracted and converted into a
|
||||
dictionary.
|
||||
|
||||
:param e: the exception to be reraised
|
||||
:returns: a structured dict with the exception data
|
||||
"""
|
||||
err_data = {'type': e.__class__.__name__,
|
||||
'message': e, 'detail': ''}
|
||||
return err_data
|
||||
|
||||
|
||||
def translate(translatable, locale):
|
||||
"""Translates the object to the given locale.
|
||||
|
||||
If the object is an exception its translatable elements are translated
|
||||
in place, if the object is a translatable string it is translated and
|
||||
returned. Otherwise, the object is returned as-is.
|
||||
|
||||
:param translatable: the object to be translated
|
||||
:param locale: the locale to translate to
|
||||
:returns: the translated object, or the object as-is if it
|
||||
was not translated
|
||||
"""
|
||||
localize = oslo_i18n.translate
|
||||
if isinstance(translatable, exceptions.NeutronException):
|
||||
translatable.msg = localize(translatable.msg, locale)
|
||||
elif isinstance(translatable, webob.exc.HTTPError):
|
||||
translatable.detail = localize(translatable.detail, locale)
|
||||
elif isinstance(translatable, Exception):
|
||||
translatable.message = localize(translatable, locale)
|
||||
else:
|
||||
return localize(translatable, locale)
|
||||
return translatable
|
||||
|
|
|
@ -140,6 +140,8 @@ class PolicyHook(hooks.PecanHook):
|
|||
data = state.response.json
|
||||
except ValueError:
|
||||
return
|
||||
if state.request.method not in pecan_constants.ACTION_MAP:
|
||||
return
|
||||
action = '%s_%s' % (pecan_constants.ACTION_MAP[state.request.method],
|
||||
resource)
|
||||
if not data or (resource not in data and collection not in data):
|
||||
|
|
|
@ -13,11 +13,12 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import oslo_i18n
|
||||
from oslo_log import log as logging
|
||||
from pecan import hooks
|
||||
import webob.exc
|
||||
|
||||
from neutron._i18n import _, _LE
|
||||
from neutron._i18n import _LE, _LI
|
||||
from neutron.api import api_common
|
||||
from neutron.api.v2 import base as v2base
|
||||
|
||||
|
||||
|
@ -26,15 +27,15 @@ LOG = logging.getLogger(__name__)
|
|||
|
||||
class ExceptionTranslationHook(hooks.PecanHook):
|
||||
def on_error(self, state, e):
|
||||
# TODO(kevinbenton): language translation in api.resource.v2
|
||||
# if it's already an http error, just return to let it go through
|
||||
if isinstance(e, webob.exc.WSGIHTTPException):
|
||||
return
|
||||
for exc_class, to_class in v2base.FAULT_MAP.items():
|
||||
if isinstance(e, exc_class):
|
||||
return to_class(getattr(e, 'msg', str(e)))
|
||||
# leaked unexpected exception, convert to boring old 500 error and
|
||||
# hide message from user in case it contained sensitive details
|
||||
LOG.exception(_LE("An unexpected exception was caught: %s"), e)
|
||||
return webob.exc.HTTPInternalServerError(
|
||||
_("An unexpected internal error occurred."))
|
||||
language = None
|
||||
if state.request.accept_language:
|
||||
language = state.request.accept_language.best_match(
|
||||
oslo_i18n.get_available_languages('neutron'))
|
||||
exc = api_common.convert_exception_to_http_exc(e, v2base.FAULT_MAP,
|
||||
language)
|
||||
if hasattr(exc, 'code') and 400 <= exc.code < 500:
|
||||
LOG.info(_LI('%(action)s failed (client error): %(exc)s'),
|
||||
{'action': state.request.method, 'exc': exc})
|
||||
else:
|
||||
LOG.exception(_LE('%s failed.'), state.request.method)
|
||||
return exc
|
||||
|
|
Loading…
Reference in New Issue