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:
parent
7fa2264625
commit
ee53124ccb
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
13
nova/test.py
13
nova/test.py
|
@ -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'):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?'))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue