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.get_connection(cfg.CONF),
|
||||
),
|
||||
hooks.PipelineHook()]
|
||||
hooks.PipelineHook(),
|
||||
hooks.TranslationHook()]
|
||||
if 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
|
||||
# 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])
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,21 +75,48 @@ 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)
|
||||
+ '</error_message>'))]
|
||||
et.ElementTree.fromstring(
|
||||
'<error_message>'
|
||||
+ et.ElementTree.tostring(fault)
|
||||
+ '</error_message>'))]
|
||||
except ParseError as err:
|
||||
LOG.error('Error parsing HTTP response: %s' % err)
|
||||
body = ['<error_message>%s' % state['status_code']
|
||||
+ '</error_message>']
|
||||
state['headers'].append(('Content-Type', 'application/xml'))
|
||||
else:
|
||||
body = [json.dumps({'error_message': '\n'.join(app_iter)})]
|
||||
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])))
|
||||
else:
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user