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:
Brad Pokorny 2013-08-01 02:31:36 +00:00
parent 4947979514
commit e5d92994b1
6 changed files with 111 additions and 12 deletions

View File

@ -59,7 +59,8 @@ def setup_app(pecan_config=None, extra_hooks=None):
storage_engine,
storage_engine.get_connection(cfg.CONF),
),
hooks.PipelineHook()]
hooks.PipelineHook(),
hooks.TranslationHook()]
if extra_hooks:
app_hooks.extend(extra_hooks)

View File

@ -704,9 +704,11 @@ class ResourcesController(rest.RestController):
# FIXME (flwang): Need to change this to return a 404 error code when
# we get a release of WSME that supports it.
if not resources:
error = _("Unknown resource")
pecan.response.translatable_error = error
raise wsme.exc.InvalidInput("resource_id",
resource_id,
_("Unknown resource"))
error)
return Resource.from_db_and_links(resources[0],
self._resource_links(resource_id))
@ -836,14 +838,18 @@ class AlarmsController(rest.RestController):
alarms = list(conn.get_alarms(name=data.name,
project=data.project_id))
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:
kwargs = data.as_dict(storage.models.Alarm)
alarm_in = storage.models.Alarm(**kwargs)
except Exception as 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)
return Alarm.from_db_model(alarm)
@ -860,7 +866,9 @@ class AlarmsController(rest.RestController):
alarms = list(conn.get_alarms(alarm_id=alarm_id,
project=auth_project))
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
# alarm "alarm_in".
@ -882,7 +890,9 @@ class AlarmsController(rest.RestController):
alarms = list(conn.get_alarms(alarm_id=alarm_id,
project=auth_project))
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)
@ -896,7 +906,9 @@ class AlarmsController(rest.RestController):
# FIXME (flwang): Need to change this to return a 404 error code when
# we get a release of WSME that supports it.
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])

View File

@ -17,6 +17,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import threading
from oslo.config import cfg
from pecan import hooks
@ -61,3 +62,18 @@ class PipelineHook(hooks.PecanHook):
def before(self, state):
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)

View File

@ -1,5 +1,6 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2013 IBM Corp.
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
@ -29,6 +30,8 @@ try:
except ImportError:
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
LOG = log.getLogger(__name__)
@ -72,13 +75,32 @@ class ParsableErrorMiddleware(object):
app_iter = self.app(environ, replacement_start_response)
if (state['status_code'] / 100) not in (2, 3):
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'])
== 'application/xml'):
try:
# 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(
et.ElementTree.fromstring('<error_message>'
+ '\n'.join(app_iter)
et.ElementTree.fromstring(
'<error_message>'
+ et.ElementTree.tostring(fault)
+ '</error_message>'))]
except ParseError as err:
LOG.error('Error parsing HTTP response: %s' % err)
@ -86,6 +108,14 @@ class ParsableErrorMiddleware(object):
+ '</error_message>']
state['headers'].append(('Content-Type', 'application/xml'))
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)})]
state['headers'].append(('Content-Type', 'application/json'))
state['headers'].append(('Content-Length', len(body[0])))

View File

@ -75,7 +75,7 @@ cfg.CONF.register_cli_opts(CLI_OPTIONS, group="service_credentials")
def prepare_service(argv=None):
eventlet.monkey_patch()
gettextutils.install('ceilometer')
gettextutils.install('ceilometer', True)
rpc.set_defaults(control_exchange='ceilometer')
cfg.set_defaults(log.log_opts,
default_log_levels=['amqplib=WARN',

View File

@ -1,5 +1,6 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2013 IBM Corp.
# Copyright © 2013 Julien Danjou
#
# Author: Julien Danjou <julien@danjou.info>
@ -17,6 +18,7 @@
# under the License.
"""Test basic ceilometer-api app
"""
import json
import os
from oslo.config import cfg
@ -24,6 +26,7 @@ from oslo.config import cfg
from ceilometer.api import app
from ceilometer.api import acl
from ceilometer import service
from ceilometer.openstack.common import gettextutils
from ceilometer.tests import base
from ceilometer.tests import db as tests_db
from .base import FunctionalTest
@ -65,6 +68,11 @@ class TestApiMiddleware(FunctionalTest):
# This doesn't really matter
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):
response = self.get_json('/invalid_path',
expect_errors=True,
@ -106,6 +114,21 @@ class TestApiMiddleware(FunctionalTest):
self.assertEqual(response.content_type, "application/json")
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):
response = self.get_json('/invalid_path',
expect_errors=True,
@ -124,3 +147,20 @@ class TestApiMiddleware(FunctionalTest):
self.assertEqual(response.status_int, 404)
self.assertEqual(response.content_type, "application/xml")
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)