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: I446fdf4b9843155e0c818c271114b1e2e4f43b64
This commit is contained in:
Matt Odden 2013-05-24 05:37:17 +00:00
parent 7fa2264625
commit ee53124ccb
12 changed files with 131 additions and 17 deletions

View File

@ -1,5 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp.
# Copyright 2011 OpenStack Foundation
# All Rights Reserved.
#
@ -25,6 +26,7 @@ import webob
from nova.api.openstack import xmlutil
from nova import exception
from nova.openstack.common import gettextutils
from nova.openstack.common.gettextutils import _
from nova.openstack.common import jsonutils
from nova.openstack.common import log as logging
@ -176,6 +178,12 @@ class Request(webob.Request):
return content_type
def best_match_language(self):
"""Determine language for returned response."""
return self.accept_language.best_match(
gettextutils.get_available_languages('nova'),
default_match='en_US')
class ActionDispatcher(object):
"""Maps method name to local methods through action name."""
@ -925,7 +933,7 @@ class Resource(wsgi.Application):
"%(body)s") % {'action': action,
'body': unicode(body, 'utf-8')}
LOG.debug(msg)
LOG.debug(_("Calling method %s") % meth)
LOG.debug(_("Calling method %s") % str(meth))
# Now, deserialize the request body...
try:
@ -1180,6 +1188,8 @@ class Fault(webob.exc.HTTPException):
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, req):
"""Generate a WSGI response based on the exception passed to ctor."""
user_locale = req.best_match_language()
# Replace the body with fault details.
code = self.wrapped_exc.status_int
fault_name = self._fault_names.get(code, "computeFault")
@ -1187,6 +1197,8 @@ class Fault(webob.exc.HTTPException):
LOG.debug(_("Returning %(code)s to user: %(explanation)s"),
{'code': code, 'explanation': explanation})
explanation = gettextutils.get_localized_message(explanation,
user_locale)
fault_data = {
fault_name: {
'code': code,
@ -1250,9 +1262,19 @@ class RateLimitFault(webob.exc.HTTPException):
Return the wrapped exception with a serialized body conforming to our
error format.
"""
user_locale = request.best_match_language()
content_type = request.best_match_content_type()
metadata = {"attributes": {"overLimit": ["code", "retryAfter"]}}
self.content['overLimit']['message'] = \
gettextutils.get_localized_message(
self.content['overLimit']['message'],
user_locale)
self.content['overLimit']['details'] = \
gettextutils.get_localized_message(
self.content['overLimit']['details'],
user_locale)
xml_serializer = XMLDictSerializer(metadata, XMLNS_V11)
serializer = {
'application/xml': xml_serializer,

View File

@ -32,3 +32,6 @@ os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
import eventlet
eventlet.monkey_patch(os=False)
from nova.openstack.common import gettextutils
gettextutils.enable_lazy()

View File

@ -911,7 +911,7 @@ class ComputeManager(manager.SchedulerDependentManager):
info = extra_usage_info.copy()
if not msg:
msg = ""
info['message'] = msg
info['message'] = unicode(msg)
self._notify_about_instance_usage(context, instance, type_,
extra_usage_info=info, **kwargs)

View File

@ -27,6 +27,7 @@ import eventlet
eventlet.monkey_patch(os=False)
import copy
import gettext
import os
import shutil
import sys
@ -191,6 +192,17 @@ class MoxStubout(fixtures.Fixture):
self.addCleanup(self.mox.VerifyAll)
class TranslationFixture(fixtures.Fixture):
"""Use gettext NullTranslation objects in tests."""
def setUp(self):
super(TranslationFixture, self).setUp()
nulltrans = gettext.NullTranslations()
gettext_fixture = fixtures.MonkeyPatch('gettext.translation',
lambda *x, **y: nulltrans)
self.gettext_patcher = self.useFixture(gettext_fixture)
class TestingException(Exception):
pass
@ -216,6 +228,7 @@ class TestCase(testtools.TestCase):
self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
self.useFixture(fixtures.NestedTempfile())
self.useFixture(fixtures.TempHomeDir())
self.useFixture(TranslationFixture())
if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
os.environ.get('OS_STDOUT_CAPTURE') == '1'):

View File

@ -17,7 +17,6 @@
import uuid
from lxml import etree
import testtools
import webob
from nova.api.openstack.compute.contrib import floating_ips
@ -306,9 +305,10 @@ class FloatingIpTest(test.TestCase):
self.stubs.Set(network.api.API, "allocate_floating_ip", fake_allocate)
req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips')
with testtools.ExpectedException(webob.exc.HTTPNotFound,
'No more floating ips'):
self.controller.create(req)
ex = self.assertRaises(webob.exc.HTTPNotFound,
self.controller.create, req)
self.assertIn('No more floating ips', ex.explanation)
def test_floating_ip_allocate_no_free_ips_pool(self):
def fake_allocate(*args, **kwargs):
@ -317,10 +317,11 @@ class FloatingIpTest(test.TestCase):
self.stubs.Set(network.api.API, "allocate_floating_ip", fake_allocate)
req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips')
with testtools.ExpectedException(
webob.exc.HTTPNotFound,
'No more floating ips in pool non_existant_pool'):
self.controller.create(req, {'pool': 'non_existant_pool'})
ex = self.assertRaises(webob.exc.HTTPNotFound,
self.controller.create, req, {'pool': 'non_existant_pool'})
self.assertIn('No more floating ips in pool non_existant_pool',
ex.explanation)
def test_floating_ip_allocate(self):
def fake1(*args, **kwargs):

View File

@ -699,3 +699,8 @@ def stub_snapshot_get_all(self, context):
def stub_bdm_get_all_by_instance(context, instance_uuid):
return [{'source_type': 'volume', 'volume_id': 'volume_id1'},
{'source_type': 'volume', 'volume_id': 'volume_id2'}]
def fake_get_available_languages(domain):
existing_translations = ['en_GB', 'en_AU', 'de', 'zh_CN', 'en_US']
return existing_translations

View File

@ -1,5 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp.
# Copyright 2010 OpenStack Foundation
# All Rights Reserved.
#
@ -23,6 +24,7 @@ import webob.exc
from nova.api.openstack import common
from nova.api.openstack import wsgi
from nova.openstack.common import gettextutils
from nova.openstack.common import jsonutils
from nova import test
@ -138,6 +140,22 @@ class TestFaults(test.TestCase):
self.assertTrue('resizeNotAllowed' not in resp.body)
self.assertTrue('forbidden' in resp.body)
def test_raise_localize_explanation(self):
msgid = "String with params: %s"
params = ('blah', )
lazy_gettext = gettextutils._
expl = lazy_gettext(msgid) % params
@webob.dec.wsgify
def raiser(req):
raise wsgi.Fault(webob.exc.HTTPNotFound(explanation=expl))
req = webob.Request.blank('/.xml')
resp = req.get_response(raiser)
self.assertEqual(resp.content_type, "application/xml")
self.assertEqual(resp.status_int, 404)
self.assertTrue((msgid % params) in resp.body)
def test_fault_has_status_int(self):
# Ensure the status_int is set correctly on faults.
fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='what?'))

View File

@ -5,6 +5,7 @@ import webob
from nova.api.openstack import wsgi
from nova import exception
from nova.openstack.common import gettextutils
from nova import test
from nova.tests.api.openstack import fakes
from nova.tests import utils
@ -97,6 +98,53 @@ class RequestTest(test.TestCase):
'uuid1': instances[1],
'uuid2': instances[2]})
def test_from_request(self):
self.stubs.Set(gettextutils, 'get_available_languages',
fakes.fake_get_available_languages)
request = wsgi.Request.blank('/')
accepted = 'bogus;q=1.1, en-gb;q=0.7,en-us,en;q=.5,*;q=.7'
request.headers = {'Accept-Language': accepted}
self.assertEqual(request.best_match_language(), 'en_US')
def test_asterisk(self):
# asterisk should match first available if there
# are not any other available matches
self.stubs.Set(gettextutils, 'get_available_languages',
fakes.fake_get_available_languages)
request = wsgi.Request.blank('/')
accepted = '*,es;q=.5'
request.headers = {'Accept-Language': accepted}
self.assertEqual(request.best_match_language(), 'en_GB')
def test_prefix(self):
self.stubs.Set(gettextutils, 'get_available_languages',
fakes.fake_get_available_languages)
request = wsgi.Request.blank('/')
accepted = 'zh'
request.headers = {'Accept-Language': accepted}
self.assertEqual(request.best_match_language(), 'zh_CN')
def test_secondary(self):
self.stubs.Set(gettextutils, 'get_available_languages',
fakes.fake_get_available_languages)
request = wsgi.Request.blank('/')
accepted = 'nn,en-gb;q=.5'
request.headers = {'Accept-Language': accepted}
self.assertEqual(request.best_match_language(), 'en_GB')
def test_none_found(self):
self.stubs.Set(gettextutils, 'get_available_languages',
fakes.fake_get_available_languages)
request = wsgi.Request.blank('/')
accepted = 'nb-no'
request.headers = {'Accept-Language': accepted}
self.assertEqual(request.best_match_language(), 'en_US')
class ActionDispatcherTest(test.TestCase):
def test_dispatch(self):

View File

@ -78,9 +78,9 @@ class TestKeystoneMiddlewareRoles(test.TestCase):
context = req.environ['nova.context']
if "knight" in context.roles and "bad" not in context.roles:
return webob.Response(status=_("200 Role Match"))
return webob.Response(status="200 Role Match")
elif context.roles == ['']:
return webob.Response(status=_("200 No Roles"))
return webob.Response(status="200 No Roles")
else:
raise webob.exc.HTTPBadRequest(_("unexpected role header"))

View File

@ -43,6 +43,7 @@ from oslo.config import cfg
from nova import exception
from nova.openstack.common import excutils
from nova.openstack.common import gettextutils
from nova.openstack.common.gettextutils import _
from nova.openstack.common import importutils
from nova.openstack.common import lockutils
@ -476,6 +477,8 @@ def utf8(value):
"""
if isinstance(value, unicode):
return value.encode('utf-8')
elif isinstance(value, gettextutils.Message):
return unicode(value).encode('utf-8')
assert isinstance(value, str)
return value

View File

@ -1004,7 +1004,7 @@ class ComputeDriver(object):
"""
if not self._compute_event_callback:
LOG.debug(_("Discarding event %s") % event)
LOG.debug(_("Discarding event %s") % str(event))
return
if not isinstance(event, virtevent.Event):
@ -1012,7 +1012,7 @@ class ComputeDriver(object):
_("Event must be an instance of nova.virt.event.Event"))
try:
LOG.debug(_("Emitting event %s") % event)
LOG.debug(_("Emitting event %s") % str(event))
self._compute_event_callback(event)
except Exception as ex:
LOG.error(_("Exception dispatching event %(event)s: %(ex)s"),

View File

@ -583,7 +583,8 @@ class LibvirtDriver(driver.ComputeDriver):
self._wrapped_conn = wrapped_conn
try:
LOG.debug(_("Registering for lifecycle events %s") % self)
LOG.debug(_("Registering for lifecycle events %s") %
str(self))
wrapped_conn.domainEventRegisterAny(
None,
libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE,
@ -595,8 +596,8 @@ class LibvirtDriver(driver.ComputeDriver):
if self.has_min_version(MIN_LIBVIRT_CLOSE_CALLBACK_VERSION):
try:
LOG.debug(_("Registering for connection events: %s")
% self)
LOG.debug(_("Registering for connection events: %s") %
str(self))
wrapped_conn.registerCloseCallback(
self._close_callback, None)
except libvirt.libvirtError: