diff --git a/ceilometer/api/app.py b/ceilometer/api/app.py index f121282b6..6db7649e6 100644 --- a/ceilometer/api/app.py +++ b/ceilometer/api/app.py @@ -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) diff --git a/ceilometer/api/controllers/v2.py b/ceilometer/api/controllers/v2.py index 8a660357e..b8d8dc97a 100644 --- a/ceilometer/api/controllers/v2.py +++ b/ceilometer/api/controllers/v2.py @@ -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]) diff --git a/ceilometer/api/hooks.py b/ceilometer/api/hooks.py index d762cea1d..8d7f33b05 100644 --- a/ceilometer/api/hooks.py +++ b/ceilometer/api/hooks.py @@ -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) diff --git a/ceilometer/api/middleware.py b/ceilometer/api/middleware.py index 0faffcb81..202f6c7c9 100644 --- a/ceilometer/api/middleware.py +++ b/ceilometer/api/middleware.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- # +# Copyright 2013 IBM Corp. # Copyright © 2012 New Dream Network, LLC (DreamHost) # # Author: Doug Hellmann @@ -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('' - + '\n'.join(app_iter) - + ''))] + et.ElementTree.fromstring( + '' + + et.ElementTree.tostring(fault) + + ''))] except ParseError as err: LOG.error('Error parsing HTTP response: %s' % err) body = ['%s' % state['status_code'] + ''] 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: diff --git a/ceilometer/service.py b/ceilometer/service.py index f7538468e..ff3019e6d 100644 --- a/ceilometer/service.py +++ b/ceilometer/service.py @@ -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', diff --git a/tests/api/v2/test_app.py b/tests/api/v2/test_app.py index 738022a82..8239c3261 100644 --- a/tests/api/v2/test_app.py +++ b/tests/api/v2/test_app.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- # +# Copyright 2013 IBM Corp. # Copyright © 2013 Julien Danjou # # Author: Julien Danjou @@ -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)