Add support for API message localization
Using the lazy gettext functionality from oslo gettextutils, it is possible to use the Accept-Language header to translate an exception message to the user requested locale and return that translation from the API. Implements bp user-locale-api Change-Id: I48de5e681827305eef60c7a77e8d3091f9d64be0 Co-authored-by: Mathew Odden <mrodden@us.ibm.com> Co-authored-by: Ben Nemec <openstack@nemebean.com>
This commit is contained in:
parent
4947979514
commit
e5d92994b1
@ -59,7 +59,8 @@ def setup_app(pecan_config=None, extra_hooks=None):
|
|||||||
storage_engine,
|
storage_engine,
|
||||||
storage_engine.get_connection(cfg.CONF),
|
storage_engine.get_connection(cfg.CONF),
|
||||||
),
|
),
|
||||||
hooks.PipelineHook()]
|
hooks.PipelineHook(),
|
||||||
|
hooks.TranslationHook()]
|
||||||
if extra_hooks:
|
if extra_hooks:
|
||||||
app_hooks.extend(extra_hooks)
|
app_hooks.extend(extra_hooks)
|
||||||
|
|
||||||
|
@ -704,9 +704,11 @@ class ResourcesController(rest.RestController):
|
|||||||
# FIXME (flwang): Need to change this to return a 404 error code when
|
# FIXME (flwang): Need to change this to return a 404 error code when
|
||||||
# we get a release of WSME that supports it.
|
# we get a release of WSME that supports it.
|
||||||
if not resources:
|
if not resources:
|
||||||
|
error = _("Unknown resource")
|
||||||
|
pecan.response.translatable_error = error
|
||||||
raise wsme.exc.InvalidInput("resource_id",
|
raise wsme.exc.InvalidInput("resource_id",
|
||||||
resource_id,
|
resource_id,
|
||||||
_("Unknown resource"))
|
error)
|
||||||
return Resource.from_db_and_links(resources[0],
|
return Resource.from_db_and_links(resources[0],
|
||||||
self._resource_links(resource_id))
|
self._resource_links(resource_id))
|
||||||
|
|
||||||
@ -836,14 +838,18 @@ class AlarmsController(rest.RestController):
|
|||||||
alarms = list(conn.get_alarms(name=data.name,
|
alarms = list(conn.get_alarms(name=data.name,
|
||||||
project=data.project_id))
|
project=data.project_id))
|
||||||
if len(alarms) > 0:
|
if len(alarms) > 0:
|
||||||
raise wsme.exc.ClientSideError(_("Alarm with that name exists"))
|
error = _("Alarm with that name exists")
|
||||||
|
pecan.response.translatable_error = error
|
||||||
|
raise wsme.exc.ClientSideError(error)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
kwargs = data.as_dict(storage.models.Alarm)
|
kwargs = data.as_dict(storage.models.Alarm)
|
||||||
alarm_in = storage.models.Alarm(**kwargs)
|
alarm_in = storage.models.Alarm(**kwargs)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.exception(ex)
|
LOG.exception(ex)
|
||||||
raise wsme.exc.ClientSideError(_("Alarm incorrect"))
|
error = _("Alarm incorrect")
|
||||||
|
pecan.response.translatable_error = error
|
||||||
|
raise wsme.exc.ClientSideError(error)
|
||||||
|
|
||||||
alarm = conn.update_alarm(alarm_in)
|
alarm = conn.update_alarm(alarm_in)
|
||||||
return Alarm.from_db_model(alarm)
|
return Alarm.from_db_model(alarm)
|
||||||
@ -860,7 +866,9 @@ class AlarmsController(rest.RestController):
|
|||||||
alarms = list(conn.get_alarms(alarm_id=alarm_id,
|
alarms = list(conn.get_alarms(alarm_id=alarm_id,
|
||||||
project=auth_project))
|
project=auth_project))
|
||||||
if len(alarms) < 1:
|
if len(alarms) < 1:
|
||||||
raise wsme.exc.ClientSideError(_("Unknown alarm"))
|
error = _("Unknown alarm")
|
||||||
|
pecan.response.translatable_error = error
|
||||||
|
raise wsme.exc.ClientSideError(error)
|
||||||
|
|
||||||
# merge the new values from kwargs into the current
|
# merge the new values from kwargs into the current
|
||||||
# alarm "alarm_in".
|
# alarm "alarm_in".
|
||||||
@ -882,7 +890,9 @@ class AlarmsController(rest.RestController):
|
|||||||
alarms = list(conn.get_alarms(alarm_id=alarm_id,
|
alarms = list(conn.get_alarms(alarm_id=alarm_id,
|
||||||
project=auth_project))
|
project=auth_project))
|
||||||
if len(alarms) < 1:
|
if len(alarms) < 1:
|
||||||
raise wsme.exc.ClientSideError(_("Unknown alarm"))
|
error = _("Unknown alarm")
|
||||||
|
pecan.response.translatable_error = error
|
||||||
|
raise wsme.exc.ClientSideError(error)
|
||||||
|
|
||||||
conn.delete_alarm(alarm_id)
|
conn.delete_alarm(alarm_id)
|
||||||
|
|
||||||
@ -896,7 +906,9 @@ class AlarmsController(rest.RestController):
|
|||||||
# FIXME (flwang): Need to change this to return a 404 error code when
|
# FIXME (flwang): Need to change this to return a 404 error code when
|
||||||
# we get a release of WSME that supports it.
|
# we get a release of WSME that supports it.
|
||||||
if len(alarms) < 1:
|
if len(alarms) < 1:
|
||||||
raise wsme.exc.ClientSideError(_("Unknown alarm"))
|
error = _("Unknown alarm")
|
||||||
|
pecan.response.translatable_error = error
|
||||||
|
raise wsme.exc.ClientSideError(error)
|
||||||
|
|
||||||
return Alarm.from_db_model(alarms[0])
|
return Alarm.from_db_model(alarms[0])
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import threading
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
from pecan import hooks
|
from pecan import hooks
|
||||||
|
|
||||||
@ -61,3 +62,18 @@ class PipelineHook(hooks.PecanHook):
|
|||||||
|
|
||||||
def before(self, state):
|
def before(self, state):
|
||||||
state.request.pipeline_manager = self.pipeline_manager
|
state.request.pipeline_manager = self.pipeline_manager
|
||||||
|
|
||||||
|
|
||||||
|
class TranslationHook(hooks.PecanHook):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Use thread local storage to make this thread safe in situations
|
||||||
|
# where one pecan instance is being used to serve multiple request
|
||||||
|
# threads.
|
||||||
|
self.local_error = threading.local()
|
||||||
|
self.local_error.translatable_error = None
|
||||||
|
|
||||||
|
def after(self, state):
|
||||||
|
if hasattr(state.response, 'translatable_error'):
|
||||||
|
self.local_error.translatable_error = (
|
||||||
|
state.response.translatable_error)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
# Copyright 2013 IBM Corp.
|
||||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||||
#
|
#
|
||||||
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
|
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
|
||||||
@ -29,6 +30,8 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from xml.parsers.expat import ExpatError as ParseError
|
from xml.parsers.expat import ExpatError as ParseError
|
||||||
|
|
||||||
|
from ceilometer.api import hooks
|
||||||
|
from ceilometer.openstack.common import gettextutils
|
||||||
from ceilometer.openstack.common import log
|
from ceilometer.openstack.common import log
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
@ -72,13 +75,32 @@ class ParsableErrorMiddleware(object):
|
|||||||
app_iter = self.app(environ, replacement_start_response)
|
app_iter = self.app(environ, replacement_start_response)
|
||||||
if (state['status_code'] / 100) not in (2, 3):
|
if (state['status_code'] / 100) not in (2, 3):
|
||||||
req = webob.Request(environ)
|
req = webob.Request(environ)
|
||||||
|
# Find the first TranslationHook in the array of hooks and use the
|
||||||
|
# translatable_error object from it
|
||||||
|
error = None
|
||||||
|
for hook in self.app.hooks:
|
||||||
|
if isinstance(hook, hooks.TranslationHook):
|
||||||
|
error = hook.local_error.translatable_error
|
||||||
|
break
|
||||||
|
user_locale = req.accept_language.best_match(
|
||||||
|
gettextutils.get_available_languages('ceilometer'),
|
||||||
|
default_match='en_US')
|
||||||
|
|
||||||
if (req.accept.best_match(['application/json', 'application/xml'])
|
if (req.accept.best_match(['application/json', 'application/xml'])
|
||||||
== 'application/xml'):
|
== 'application/xml'):
|
||||||
try:
|
try:
|
||||||
# simple check xml is valid
|
# simple check xml is valid
|
||||||
|
fault = et.ElementTree.fromstring('\n'.join(app_iter))
|
||||||
|
# Add the translated error to the xml data
|
||||||
|
if error is not None:
|
||||||
|
for fault_string in fault.findall('faultstring'):
|
||||||
|
fault_string.text = (
|
||||||
|
gettextutils.get_localized_message(
|
||||||
|
error, user_locale))
|
||||||
body = [et.ElementTree.tostring(
|
body = [et.ElementTree.tostring(
|
||||||
et.ElementTree.fromstring('<error_message>'
|
et.ElementTree.fromstring(
|
||||||
+ '\n'.join(app_iter)
|
'<error_message>'
|
||||||
|
+ et.ElementTree.tostring(fault)
|
||||||
+ '</error_message>'))]
|
+ '</error_message>'))]
|
||||||
except ParseError as err:
|
except ParseError as err:
|
||||||
LOG.error('Error parsing HTTP response: %s' % err)
|
LOG.error('Error parsing HTTP response: %s' % err)
|
||||||
@ -86,6 +108,14 @@ class ParsableErrorMiddleware(object):
|
|||||||
+ '</error_message>']
|
+ '</error_message>']
|
||||||
state['headers'].append(('Content-Type', 'application/xml'))
|
state['headers'].append(('Content-Type', 'application/xml'))
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
|
fault = json.loads('\n'.join(app_iter))
|
||||||
|
if error is not None and 'faultstring' in fault:
|
||||||
|
fault['faultstring'] = (
|
||||||
|
gettextutils.get_localized_message(
|
||||||
|
error, user_locale))
|
||||||
|
body = [json.dumps({'error_message': json.dumps(fault)})]
|
||||||
|
except ValueError as err:
|
||||||
body = [json.dumps({'error_message': '\n'.join(app_iter)})]
|
body = [json.dumps({'error_message': '\n'.join(app_iter)})]
|
||||||
state['headers'].append(('Content-Type', 'application/json'))
|
state['headers'].append(('Content-Type', 'application/json'))
|
||||||
state['headers'].append(('Content-Length', len(body[0])))
|
state['headers'].append(('Content-Length', len(body[0])))
|
||||||
|
@ -75,7 +75,7 @@ cfg.CONF.register_cli_opts(CLI_OPTIONS, group="service_credentials")
|
|||||||
|
|
||||||
def prepare_service(argv=None):
|
def prepare_service(argv=None):
|
||||||
eventlet.monkey_patch()
|
eventlet.monkey_patch()
|
||||||
gettextutils.install('ceilometer')
|
gettextutils.install('ceilometer', True)
|
||||||
rpc.set_defaults(control_exchange='ceilometer')
|
rpc.set_defaults(control_exchange='ceilometer')
|
||||||
cfg.set_defaults(log.log_opts,
|
cfg.set_defaults(log.log_opts,
|
||||||
default_log_levels=['amqplib=WARN',
|
default_log_levels=['amqplib=WARN',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
# Copyright 2013 IBM Corp.
|
||||||
# Copyright © 2013 Julien Danjou
|
# Copyright © 2013 Julien Danjou
|
||||||
#
|
#
|
||||||
# Author: Julien Danjou <julien@danjou.info>
|
# Author: Julien Danjou <julien@danjou.info>
|
||||||
@ -17,6 +18,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
"""Test basic ceilometer-api app
|
"""Test basic ceilometer-api app
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
@ -24,6 +26,7 @@ from oslo.config import cfg
|
|||||||
from ceilometer.api import app
|
from ceilometer.api import app
|
||||||
from ceilometer.api import acl
|
from ceilometer.api import acl
|
||||||
from ceilometer import service
|
from ceilometer import service
|
||||||
|
from ceilometer.openstack.common import gettextutils
|
||||||
from ceilometer.tests import base
|
from ceilometer.tests import base
|
||||||
from ceilometer.tests import db as tests_db
|
from ceilometer.tests import db as tests_db
|
||||||
from .base import FunctionalTest
|
from .base import FunctionalTest
|
||||||
@ -65,6 +68,11 @@ class TestApiMiddleware(FunctionalTest):
|
|||||||
# This doesn't really matter
|
# This doesn't really matter
|
||||||
database_connection = tests_db.MongoDBFakeConnectionUrl()
|
database_connection = tests_db.MongoDBFakeConnectionUrl()
|
||||||
|
|
||||||
|
translated_error = 'Translated error'
|
||||||
|
|
||||||
|
def _fake_get_localized_message(self, message, user_locale):
|
||||||
|
return self.translated_error
|
||||||
|
|
||||||
def test_json_parsable_error_middleware_404(self):
|
def test_json_parsable_error_middleware_404(self):
|
||||||
response = self.get_json('/invalid_path',
|
response = self.get_json('/invalid_path',
|
||||||
expect_errors=True,
|
expect_errors=True,
|
||||||
@ -106,6 +114,21 @@ class TestApiMiddleware(FunctionalTest):
|
|||||||
self.assertEqual(response.content_type, "application/json")
|
self.assertEqual(response.content_type, "application/json")
|
||||||
self.assertTrue(response.json['error_message'])
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
def test_json_parsable_error_middleware_translation_400(self):
|
||||||
|
# Ensure translated messages get placed properly into json faults
|
||||||
|
self.stubs.Set(gettextutils, 'get_localized_message',
|
||||||
|
self._fake_get_localized_message)
|
||||||
|
response = self.get_json('/alarms/-',
|
||||||
|
expect_errors=True,
|
||||||
|
headers={"Accept":
|
||||||
|
"application/json"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_int, 400)
|
||||||
|
self.assertEqual(response.content_type, "application/json")
|
||||||
|
self.assertTrue(response.json['error_message'])
|
||||||
|
fault = json.loads(response.json['error_message'])
|
||||||
|
self.assertEqual(fault['faultstring'], self.translated_error)
|
||||||
|
|
||||||
def test_xml_parsable_error_middleware_404(self):
|
def test_xml_parsable_error_middleware_404(self):
|
||||||
response = self.get_json('/invalid_path',
|
response = self.get_json('/invalid_path',
|
||||||
expect_errors=True,
|
expect_errors=True,
|
||||||
@ -124,3 +147,20 @@ class TestApiMiddleware(FunctionalTest):
|
|||||||
self.assertEqual(response.status_int, 404)
|
self.assertEqual(response.status_int, 404)
|
||||||
self.assertEqual(response.content_type, "application/xml")
|
self.assertEqual(response.content_type, "application/xml")
|
||||||
self.assertEqual(response.xml.tag, 'error_message')
|
self.assertEqual(response.xml.tag, 'error_message')
|
||||||
|
|
||||||
|
def test_xml_parsable_error_middleware_translation_400(self):
|
||||||
|
# Ensure translated messages get placed properly into xml faults
|
||||||
|
self.stubs.Set(gettextutils, 'get_localized_message',
|
||||||
|
self._fake_get_localized_message)
|
||||||
|
|
||||||
|
response = self.get_json('/alarms/-',
|
||||||
|
expect_errors=True,
|
||||||
|
headers={"Accept":
|
||||||
|
"application/xml,*/*"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_int, 400)
|
||||||
|
self.assertEqual(response.content_type, "application/xml")
|
||||||
|
self.assertEqual(response.xml.tag, 'error_message')
|
||||||
|
fault = response.xml.findall('./error/faultstring')
|
||||||
|
for fault_string in fault:
|
||||||
|
self.assertEqual(fault_string.text, self.translated_error)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user