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
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 IBM Corp.
|
||||||
# Copyright 2011 OpenStack Foundation
|
# Copyright 2011 OpenStack Foundation
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
@ -25,6 +26,7 @@ import webob
|
|||||||
|
|
||||||
from nova.api.openstack import xmlutil
|
from nova.api.openstack import xmlutil
|
||||||
from nova import exception
|
from nova import exception
|
||||||
|
from nova.openstack.common import gettextutils
|
||||||
from nova.openstack.common.gettextutils import _
|
from nova.openstack.common.gettextutils import _
|
||||||
from nova.openstack.common import jsonutils
|
from nova.openstack.common import jsonutils
|
||||||
from nova.openstack.common import log as logging
|
from nova.openstack.common import log as logging
|
||||||
@ -176,6 +178,12 @@ class Request(webob.Request):
|
|||||||
|
|
||||||
return content_type
|
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):
|
class ActionDispatcher(object):
|
||||||
"""Maps method name to local methods through action name."""
|
"""Maps method name to local methods through action name."""
|
||||||
@ -925,7 +933,7 @@ class Resource(wsgi.Application):
|
|||||||
"%(body)s") % {'action': action,
|
"%(body)s") % {'action': action,
|
||||||
'body': unicode(body, 'utf-8')}
|
'body': unicode(body, 'utf-8')}
|
||||||
LOG.debug(msg)
|
LOG.debug(msg)
|
||||||
LOG.debug(_("Calling method %s") % meth)
|
LOG.debug(_("Calling method %s") % str(meth))
|
||||||
|
|
||||||
# Now, deserialize the request body...
|
# Now, deserialize the request body...
|
||||||
try:
|
try:
|
||||||
@ -1180,6 +1188,8 @@ class Fault(webob.exc.HTTPException):
|
|||||||
@webob.dec.wsgify(RequestClass=Request)
|
@webob.dec.wsgify(RequestClass=Request)
|
||||||
def __call__(self, req):
|
def __call__(self, req):
|
||||||
"""Generate a WSGI response based on the exception passed to ctor."""
|
"""Generate a WSGI response based on the exception passed to ctor."""
|
||||||
|
|
||||||
|
user_locale = req.best_match_language()
|
||||||
# Replace the body with fault details.
|
# Replace the body with fault details.
|
||||||
code = self.wrapped_exc.status_int
|
code = self.wrapped_exc.status_int
|
||||||
fault_name = self._fault_names.get(code, "computeFault")
|
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"),
|
LOG.debug(_("Returning %(code)s to user: %(explanation)s"),
|
||||||
{'code': code, 'explanation': explanation})
|
{'code': code, 'explanation': explanation})
|
||||||
|
|
||||||
|
explanation = gettextutils.get_localized_message(explanation,
|
||||||
|
user_locale)
|
||||||
fault_data = {
|
fault_data = {
|
||||||
fault_name: {
|
fault_name: {
|
||||||
'code': code,
|
'code': code,
|
||||||
@ -1250,9 +1262,19 @@ class RateLimitFault(webob.exc.HTTPException):
|
|||||||
Return the wrapped exception with a serialized body conforming to our
|
Return the wrapped exception with a serialized body conforming to our
|
||||||
error format.
|
error format.
|
||||||
"""
|
"""
|
||||||
|
user_locale = request.best_match_language()
|
||||||
content_type = request.best_match_content_type()
|
content_type = request.best_match_content_type()
|
||||||
metadata = {"attributes": {"overLimit": ["code", "retryAfter"]}}
|
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)
|
xml_serializer = XMLDictSerializer(metadata, XMLNS_V11)
|
||||||
serializer = {
|
serializer = {
|
||||||
'application/xml': xml_serializer,
|
'application/xml': xml_serializer,
|
||||||
|
@ -32,3 +32,6 @@ os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
|
|||||||
import eventlet
|
import eventlet
|
||||||
|
|
||||||
eventlet.monkey_patch(os=False)
|
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()
|
info = extra_usage_info.copy()
|
||||||
if not msg:
|
if not msg:
|
||||||
msg = ""
|
msg = ""
|
||||||
info['message'] = msg
|
info['message'] = unicode(msg)
|
||||||
self._notify_about_instance_usage(context, instance, type_,
|
self._notify_about_instance_usage(context, instance, type_,
|
||||||
extra_usage_info=info, **kwargs)
|
extra_usage_info=info, **kwargs)
|
||||||
|
|
||||||
|
13
nova/test.py
13
nova/test.py
@ -27,6 +27,7 @@ import eventlet
|
|||||||
eventlet.monkey_patch(os=False)
|
eventlet.monkey_patch(os=False)
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import gettext
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
@ -191,6 +192,17 @@ class MoxStubout(fixtures.Fixture):
|
|||||||
self.addCleanup(self.mox.VerifyAll)
|
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):
|
class TestingException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -216,6 +228,7 @@ class TestCase(testtools.TestCase):
|
|||||||
self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
|
self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
|
||||||
self.useFixture(fixtures.NestedTempfile())
|
self.useFixture(fixtures.NestedTempfile())
|
||||||
self.useFixture(fixtures.TempHomeDir())
|
self.useFixture(fixtures.TempHomeDir())
|
||||||
|
self.useFixture(TranslationFixture())
|
||||||
|
|
||||||
if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
|
if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
|
||||||
os.environ.get('OS_STDOUT_CAPTURE') == '1'):
|
os.environ.get('OS_STDOUT_CAPTURE') == '1'):
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import testtools
|
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from nova.api.openstack.compute.contrib import floating_ips
|
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)
|
self.stubs.Set(network.api.API, "allocate_floating_ip", fake_allocate)
|
||||||
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips')
|
req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips')
|
||||||
with testtools.ExpectedException(webob.exc.HTTPNotFound,
|
ex = self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
'No more floating ips'):
|
self.controller.create, req)
|
||||||
self.controller.create(req)
|
|
||||||
|
self.assertIn('No more floating ips', ex.explanation)
|
||||||
|
|
||||||
def test_floating_ip_allocate_no_free_ips_pool(self):
|
def test_floating_ip_allocate_no_free_ips_pool(self):
|
||||||
def fake_allocate(*args, **kwargs):
|
def fake_allocate(*args, **kwargs):
|
||||||
@ -317,10 +317,11 @@ class FloatingIpTest(test.TestCase):
|
|||||||
self.stubs.Set(network.api.API, "allocate_floating_ip", fake_allocate)
|
self.stubs.Set(network.api.API, "allocate_floating_ip", fake_allocate)
|
||||||
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips')
|
req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips')
|
||||||
with testtools.ExpectedException(
|
ex = self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
webob.exc.HTTPNotFound,
|
self.controller.create, req, {'pool': 'non_existant_pool'})
|
||||||
'No more floating ips in pool non_existant_pool'):
|
|
||||||
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 test_floating_ip_allocate(self):
|
||||||
def fake1(*args, **kwargs):
|
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):
|
def stub_bdm_get_all_by_instance(context, instance_uuid):
|
||||||
return [{'source_type': 'volume', 'volume_id': 'volume_id1'},
|
return [{'source_type': 'volume', 'volume_id': 'volume_id1'},
|
||||||
{'source_type': 'volume', 'volume_id': 'volume_id2'}]
|
{'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
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 IBM Corp.
|
||||||
# Copyright 2010 OpenStack Foundation
|
# Copyright 2010 OpenStack Foundation
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
@ -23,6 +24,7 @@ import webob.exc
|
|||||||
|
|
||||||
from nova.api.openstack import common
|
from nova.api.openstack import common
|
||||||
from nova.api.openstack import wsgi
|
from nova.api.openstack import wsgi
|
||||||
|
from nova.openstack.common import gettextutils
|
||||||
from nova.openstack.common import jsonutils
|
from nova.openstack.common import jsonutils
|
||||||
from nova import test
|
from nova import test
|
||||||
|
|
||||||
@ -138,6 +140,22 @@ class TestFaults(test.TestCase):
|
|||||||
self.assertTrue('resizeNotAllowed' not in resp.body)
|
self.assertTrue('resizeNotAllowed' not in resp.body)
|
||||||
self.assertTrue('forbidden' 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):
|
def test_fault_has_status_int(self):
|
||||||
# Ensure the status_int is set correctly on faults.
|
# Ensure the status_int is set correctly on faults.
|
||||||
fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='what?'))
|
fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='what?'))
|
||||||
|
@ -5,6 +5,7 @@ import webob
|
|||||||
|
|
||||||
from nova.api.openstack import wsgi
|
from nova.api.openstack import wsgi
|
||||||
from nova import exception
|
from nova import exception
|
||||||
|
from nova.openstack.common import gettextutils
|
||||||
from nova import test
|
from nova import test
|
||||||
from nova.tests.api.openstack import fakes
|
from nova.tests.api.openstack import fakes
|
||||||
from nova.tests import utils
|
from nova.tests import utils
|
||||||
@ -97,6 +98,53 @@ class RequestTest(test.TestCase):
|
|||||||
'uuid1': instances[1],
|
'uuid1': instances[1],
|
||||||
'uuid2': instances[2]})
|
'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):
|
class ActionDispatcherTest(test.TestCase):
|
||||||
def test_dispatch(self):
|
def test_dispatch(self):
|
||||||
|
@ -78,9 +78,9 @@ class TestKeystoneMiddlewareRoles(test.TestCase):
|
|||||||
context = req.environ['nova.context']
|
context = req.environ['nova.context']
|
||||||
|
|
||||||
if "knight" in context.roles and "bad" not in context.roles:
|
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 == ['']:
|
elif context.roles == ['']:
|
||||||
return webob.Response(status=_("200 No Roles"))
|
return webob.Response(status="200 No Roles")
|
||||||
else:
|
else:
|
||||||
raise webob.exc.HTTPBadRequest(_("unexpected role header"))
|
raise webob.exc.HTTPBadRequest(_("unexpected role header"))
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ from oslo.config import cfg
|
|||||||
|
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova.openstack.common import excutils
|
from nova.openstack.common import excutils
|
||||||
|
from nova.openstack.common import gettextutils
|
||||||
from nova.openstack.common.gettextutils import _
|
from nova.openstack.common.gettextutils import _
|
||||||
from nova.openstack.common import importutils
|
from nova.openstack.common import importutils
|
||||||
from nova.openstack.common import lockutils
|
from nova.openstack.common import lockutils
|
||||||
@ -476,6 +477,8 @@ def utf8(value):
|
|||||||
"""
|
"""
|
||||||
if isinstance(value, unicode):
|
if isinstance(value, unicode):
|
||||||
return value.encode('utf-8')
|
return value.encode('utf-8')
|
||||||
|
elif isinstance(value, gettextutils.Message):
|
||||||
|
return unicode(value).encode('utf-8')
|
||||||
assert isinstance(value, str)
|
assert isinstance(value, str)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@ -1004,7 +1004,7 @@ class ComputeDriver(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not self._compute_event_callback:
|
if not self._compute_event_callback:
|
||||||
LOG.debug(_("Discarding event %s") % event)
|
LOG.debug(_("Discarding event %s") % str(event))
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(event, virtevent.Event):
|
if not isinstance(event, virtevent.Event):
|
||||||
@ -1012,7 +1012,7 @@ class ComputeDriver(object):
|
|||||||
_("Event must be an instance of nova.virt.event.Event"))
|
_("Event must be an instance of nova.virt.event.Event"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
LOG.debug(_("Emitting event %s") % event)
|
LOG.debug(_("Emitting event %s") % str(event))
|
||||||
self._compute_event_callback(event)
|
self._compute_event_callback(event)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(_("Exception dispatching event %(event)s: %(ex)s"),
|
LOG.error(_("Exception dispatching event %(event)s: %(ex)s"),
|
||||||
|
@ -583,7 +583,8 @@ class LibvirtDriver(driver.ComputeDriver):
|
|||||||
self._wrapped_conn = wrapped_conn
|
self._wrapped_conn = wrapped_conn
|
||||||
|
|
||||||
try:
|
try:
|
||||||
LOG.debug(_("Registering for lifecycle events %s") % self)
|
LOG.debug(_("Registering for lifecycle events %s") %
|
||||||
|
str(self))
|
||||||
wrapped_conn.domainEventRegisterAny(
|
wrapped_conn.domainEventRegisterAny(
|
||||||
None,
|
None,
|
||||||
libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE,
|
libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE,
|
||||||
@ -595,8 +596,8 @@ class LibvirtDriver(driver.ComputeDriver):
|
|||||||
|
|
||||||
if self.has_min_version(MIN_LIBVIRT_CLOSE_CALLBACK_VERSION):
|
if self.has_min_version(MIN_LIBVIRT_CLOSE_CALLBACK_VERSION):
|
||||||
try:
|
try:
|
||||||
LOG.debug(_("Registering for connection events: %s")
|
LOG.debug(_("Registering for connection events: %s") %
|
||||||
% self)
|
str(self))
|
||||||
wrapped_conn.registerCloseCallback(
|
wrapped_conn.registerCloseCallback(
|
||||||
self._close_callback, None)
|
self._close_callback, None)
|
||||||
except libvirt.libvirtError:
|
except libvirt.libvirtError:
|
||||||
|
Loading…
Reference in New Issue
Block a user