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:
Kevin Benton 2016-05-13 14:29:18 -07:00
parent d2630f2dd4
commit a1c194cf06
4 changed files with 108 additions and 113 deletions

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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