diff --git a/neutron/api/v2/resource.py b/neutron/api/v2/resource.py index 744a7d940d..529f519edb 100644 --- a/neutron/api/v2/resource.py +++ b/neutron/api/v2/resource.py @@ -23,6 +23,7 @@ import webob.exc from neutron.api.v2 import attributes from neutron.common import exceptions +from neutron.openstack.common import gettextutils from neutron.openstack.common import log as logging from neutron import wsgi @@ -70,6 +71,7 @@ def Resource(controller, faults=None, deserializers=None, serializers=None): action = args.pop('action', None) content_type = format_types.get(fmt, request.best_match_content_type()) + language = request.best_match_language() deserializer = deserializers.get(content_type) serializer = serializers.get(content_type) @@ -83,6 +85,7 @@ def Resource(controller, faults=None, deserializers=None, serializers=None): except (exceptions.NeutronException, netaddr.AddrFormatError) as e: LOG.exception(_('%s failed'), action) + e = translate(e, language) body = serializer.serialize({'NeutronError': e}) kwargs = {'body': body, 'content_type': content_type} for fault in faults: @@ -91,10 +94,12 @@ def Resource(controller, faults=None, deserializers=None, serializers=None): raise webob.exc.HTTPInternalServerError(**kwargs) except webob.exc.HTTPException as e: LOG.exception(_('%s failed'), action) + translate(e, language) e.body = serializer.serialize({'NeutronError': e}) e.content_type = content_type raise 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 @@ -111,6 +116,7 @@ def Resource(controller, faults=None, deserializers=None, serializers=None): # 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': msg}) kwargs = {'body': body, 'content_type': content_type} raise webob.exc.HTTPInternalServerError(**kwargs) @@ -126,3 +132,24 @@ def Resource(controller, faults=None, deserializers=None, serializers=None): content_type=content_type, body=body) return resource + + +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 = gettextutils.get_localized_message + if isinstance(translatable, Exception): + translatable.message = localize(translatable.message, locale) + if isinstance(translatable, webob.exc.HTTPError): + translatable.detail = localize(translatable.detail, locale) + return translatable + return localize(translatable, locale) diff --git a/neutron/server/__init__.py b/neutron/server/__init__.py index 72a52b22e0..a31cdbe403 100755 --- a/neutron/server/__init__.py +++ b/neutron/server/__init__.py @@ -27,6 +27,9 @@ from oslo.config import cfg from neutron.common import config from neutron import service +from neutron.openstack.common import gettextutils +gettextutils.install('neutron', lazy=True) + def main(): eventlet.monkey_patch() diff --git a/neutron/tests/unit/test_api_v2_resource.py b/neutron/tests/unit/test_api_v2_resource.py index f4f7289ed9..91ac57117a 100644 --- a/neutron/tests/unit/test_api_v2_resource.py +++ b/neutron/tests/unit/test_api_v2_resource.py @@ -25,6 +25,7 @@ import webtest from neutron.api.v2 import resource as wsgi_resource from neutron.common import exceptions as q_exc from neutron import context +from neutron.openstack.common import gettextutils from neutron.tests import base from neutron import wsgi @@ -98,8 +99,23 @@ class RequestTestCase(base.BaseTestCase): def test_context_without_neutron_context(self): self.assertTrue(self.req.context.is_admin) + def test_best_match_language(self): + # Here we test that we are actually invoking language negotiation + # by webop and also that the default locale always available is en-US + request = wsgi.Request.blank('/') + gettextutils.get_available_languages = mock.MagicMock() + gettextutils.get_available_languages.return_value = ['known-language', + 'es', 'zh'] + request.headers['Accept-Language'] = 'known-language' + language = request.best_match_language() + self.assertEqual(language, 'known-language') + request.headers['Accept-Language'] = 'unknown-language' + language = request.best_match_language() + self.assertEqual(language, 'en_US') + class ResourceTestCase(base.BaseTestCase): + def test_unmapped_neutron_error_with_json(self): msg = u'\u7f51\u7edc' @@ -136,6 +152,29 @@ class ResourceTestCase(base.BaseTestCase): self.assertEqual(wsgi.XMLDeserializer().deserialize(res.body), expected_res) + @mock.patch('neutron.openstack.common.gettextutils.Message.data', + new_callable=mock.PropertyMock) + def test_unmapped_neutron_error_localized(self, mock_translation): + gettextutils.install('blaa', lazy=True) + msg_translation = 'Translated error' + mock_translation.return_value = msg_translation + msg = _('Unmapped error') + + class TestException(q_exc.NeutronException): + message = msg + + controller = mock.MagicMock() + controller.test.side_effect = TestException() + resource = webtest.TestApp(wsgi_resource.Resource(controller)) + + environ = {'wsgiorg.routing_args': (None, {'action': 'test', + 'format': 'json'})} + + res = resource.get('', extra_environ=environ, expect_errors=True) + self.assertEqual(res.status_int, exc.HTTPInternalServerError.code) + self.assertIn(msg_translation, + str(wsgi.JSONDeserializer().deserialize(res.body))) + def test_mapped_neutron_error_with_json(self): msg = u'\u7f51\u7edc' @@ -176,6 +215,31 @@ class ResourceTestCase(base.BaseTestCase): self.assertEqual(wsgi.XMLDeserializer().deserialize(res.body), expected_res) + @mock.patch('neutron.openstack.common.gettextutils.Message.data', + new_callable=mock.PropertyMock) + def test_mapped_neutron_error_localized(self, mock_translation): + gettextutils.install('blaa', lazy=True) + msg_translation = 'Translated error' + mock_translation.return_value = msg_translation + msg = _('Unmapped error') + + class TestException(q_exc.NeutronException): + message = msg + + controller = mock.MagicMock() + controller.test.side_effect = TestException() + faults = {TestException: exc.HTTPGatewayTimeout} + resource = webtest.TestApp(wsgi_resource.Resource(controller, + faults=faults)) + + environ = {'wsgiorg.routing_args': (None, {'action': 'test', + 'format': 'json'})} + + res = resource.get('', extra_environ=environ, expect_errors=True) + self.assertEqual(res.status_int, exc.HTTPGatewayTimeout.code) + self.assertIn(msg_translation, + str(wsgi.JSONDeserializer().deserialize(res.body))) + def test_http_error(self): controller = mock.MagicMock() controller.test.side_effect = exc.HTTPGatewayTimeout() diff --git a/neutron/wsgi.py b/neutron/wsgi.py index 56e909712f..029b7bdfe2 100644 --- a/neutron/wsgi.py +++ b/neutron/wsgi.py @@ -37,6 +37,7 @@ import webob.exc from neutron.common import constants from neutron.common import exceptions as exception from neutron import context +from neutron.openstack.common import gettextutils from neutron.openstack.common import jsonutils from neutron.openstack.common import log as logging @@ -299,6 +300,12 @@ class Request(webob.Request): return _type return None + def best_match_language(self): + """Determine language for returned response.""" + all_languages = gettextutils.get_available_languages('neutron') + return self.accept_language.best_match(all_languages, + default_match='en_US') + @property def context(self): if 'neutron.context' not in self.environ: