From f70b72b78b16d569f469f2cd6381dee6c90ed229 Mon Sep 17 00:00:00 2001 From: Fei Long Wang Date: Fri, 24 Jan 2014 11:06:29 +0800 Subject: [PATCH] Add support for API message localization Add support for doing language resolution for a request, based on the Accept-Language HTTP header. For example, an HTTP client can receive API messages in Chinese even if the locale language of the server is English. The underscore (_) method is initialized so that it returns openstack.common.gettextutils.Message objects. The locale of these Message objects is set when exceptions with which they are associated are raised in the context of an HTTP request. docImpact Partially implements bp i18n-messages Signed-off-by: Fei Long Wang Signed-off-by: John Warren Change-Id: I352cda57fe119022c59c6c813b5c8053765b2d3c --- glance/api/v1/images.py | 2 +- glance/api/v1/members.py | 51 +++++----- glance/api/v2/image_data.py | 16 ++-- glance/api/v2/image_members.py | 22 ++--- glance/api/v2/image_tags.py | 10 +- glance/api/v2/images.py | 36 +++---- glance/api/v2/tasks.py | 12 +-- glance/cmd/__init__.py | 2 +- glance/cmd/api.py | 4 +- glance/common/exception.py | 12 ++- glance/common/rpc.py | 4 +- glance/common/wsgi.py | 47 ++++++++-- glance/image_cache/drivers/xattr.py | 6 +- glance/notifier.py | 5 +- glance/schema.py | 4 +- glance/store/__init__.py | 15 +-- glance/store/filesystem.py | 7 +- glance/store/location.py | 3 +- glance/tests/unit/common/test_exception.py | 6 ++ glance/tests/unit/common/test_wsgi.py | 104 +++++++++++++++++++++ glance/tests/unit/test_swift_store.py | 2 +- 21 files changed, 260 insertions(+), 110 deletions(-) diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py index 560cc532b8..8cca001b9f 100644 --- a/glance/api/v1/images.py +++ b/glance/api/v1/images.py @@ -1066,7 +1066,7 @@ class ImageDeserializer(wsgi.JSONRequestDeserializer): except exception.InvalidParameterValue as e: msg = unicode(e) LOG.warn(msg, exc_info=True) - raise HTTPBadRequest(explanation=msg, request=request) + raise HTTPBadRequest(explanation=e.msg, request=request) image_meta = result['image_meta'] image_meta = validate_image_meta(request, image_meta) diff --git a/glance/api/v1/members.py b/glance/api/v1/members.py index 2f47a7c6e4..061c045ed4 100644 --- a/glance/api/v1/members.py +++ b/glance/api/v1/members.py @@ -15,6 +15,7 @@ # under the License. from oslo.config import cfg +import six import webob.exc from glance.api import policy @@ -94,13 +95,11 @@ class Controller(controller.BaseController): registry.delete_member(req.context, image_id, id) self._update_store_acls(req, image_id) except exception.NotFound as e: - msg = "%s" % e - LOG.debug(msg) - raise webob.exc.HTTPNotFound(msg) + LOG.debug(six.text_type(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - msg = "%s" % e - LOG.debug(msg) - raise webob.exc.HTTPNotFound(msg) + LOG.debug(six.text_type(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) return webob.exc.HTTPNoContent() @@ -152,17 +151,14 @@ class Controller(controller.BaseController): registry.add_member(req.context, image_id, id, can_share) self._update_store_acls(req, image_id) except exception.Invalid as e: - msg = "%s" % e - LOG.debug(msg) - raise webob.exc.HTTPBadRequest(explanation=msg) + LOG.debug(six.text_type(e)) + raise webob.exc.HTTPBadRequest(explanation=e.msg) except exception.NotFound as e: - msg = "%s" % e - LOG.debug(msg) - raise webob.exc.HTTPNotFound(msg) + LOG.debug(six.text_type(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - msg = "%s" % e - LOG.debug(msg) - raise webob.exc.HTTPNotFound(msg) + LOG.debug(six.text_type(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) return webob.exc.HTTPNoContent() @@ -190,17 +186,14 @@ class Controller(controller.BaseController): registry.replace_members(req.context, image_id, body) self._update_store_acls(req, image_id) except exception.Invalid as e: - msg = "%s" % e - LOG.debug(msg) - raise webob.exc.HTTPBadRequest(explanation=msg) + LOG.debug(six.text_type(e)) + raise webob.exc.HTTPBadRequest(explanation=e.msg) except exception.NotFound as e: - msg = "%s" % e - LOG.debug(msg) - raise webob.exc.HTTPNotFound(msg) + LOG.debug(six.text_type(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - msg = "%s" % e - LOG.debug(msg) - raise webob.exc.HTTPNotFound(msg) + LOG.debug(six.text_type(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) return webob.exc.HTTPNoContent() @@ -220,13 +213,11 @@ class Controller(controller.BaseController): try: members = registry.get_member_images(req.context, id) except exception.NotFound as e: - msg = "%s" % e - LOG.debug(msg) - raise webob.exc.HTTPNotFound(msg) + LOG.debug(six.text_type(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - msg = "%s" % e - LOG.debug(msg) - raise webob.exc.HTTPForbidden(msg) + LOG.debug(six.text_type(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) return dict(shared_images=members) def _update_store_acls(self, req, image_id): diff --git a/glance/api/v2/image_data.py b/glance/api/v2/image_data.py index fbc61c8336..1bff3d7c5c 100644 --- a/glance/api/v2/image_data.py +++ b/glance/api/v2/image_data.py @@ -92,7 +92,7 @@ class ImageDataController(object): except exception.InvalidImageStatusTransition as e: msg = unicode(e) LOG.debug(msg) - raise webob.exc.HTTPConflict(explanation=msg, request=req) + raise webob.exc.HTTPConflict(explanation=e.msg, request=req) except exception.Forbidden as e: msg = (_("Not allowed to upload image data for image %s") % @@ -101,7 +101,7 @@ class ImageDataController(object): raise webob.exc.HTTPForbidden(explanation=msg, request=req) except exception.NotFound as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.StorageFull as e: msg = _("Image storage media is full: %s") % e @@ -149,11 +149,11 @@ class ImageDataController(object): if not image.locations: raise exception.ImageDataNotFound() except exception.ImageDataNotFound as e: - raise webob.exc.HTTPNoContent(explanation=unicode(e)) + raise webob.exc.HTTPNoContent(explanation=e.msg) except exception.NotFound as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) return image @@ -162,8 +162,8 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer): def upload(self, request): try: request.get_content_type(('application/octet-stream',)) - except exception.InvalidContentType: - raise webob.exc.HTTPUnsupportedMediaType() + except exception.InvalidContentType as e: + raise webob.exc.HTTPUnsupportedMediaType(explanation=e.msg) image_size = request.content_length or None return {'size': image_size, 'data': request.body_file} @@ -178,7 +178,7 @@ class ResponseSerializer(wsgi.JSONResponseSerializer): # an iterator very strange response.app_iter = iter(image.get_data()) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) #NOTE(saschpe): "response.app_iter = ..." currently resets Content-MD5 # (https://github.com/Pylons/webob/issues/86), so it should be set # afterwards for the time being. diff --git a/glance/api/v2/image_members.py b/glance/api/v2/image_members.py index 3b69322a00..4f5d513dd3 100644 --- a/glance/api/v2/image_members.py +++ b/glance/api/v2/image_members.py @@ -67,13 +67,13 @@ class ImageMembersController(object): return new_member except exception.NotFound as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) except exception.Duplicate as e: - raise webob.exc.HTTPConflict(explanation=unicode(e)) + raise webob.exc.HTTPConflict(explanation=e.msg) except exception.ImageMemberLimitExceeded as e: - raise webob.exc.HTTPRequestEntityTooLarge(explanation=unicode(e)) + raise webob.exc.HTTPRequestEntityTooLarge(explanation=e.msg) @utils.mutating def update(self, req, image_id, member_id, status): @@ -100,9 +100,9 @@ class ImageMembersController(object): member_repo.save(member) return member except exception.NotFound as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) except ValueError as e: raise webob.exc.HTTPBadRequest(explanation=unicode(e)) @@ -132,9 +132,9 @@ class ImageMembersController(object): members.append(member) return dict(members=members) except exception.NotFound as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) def show(self, req, image_id, member_id): """ @@ -157,7 +157,7 @@ class ImageMembersController(object): member = member_repo.get(member_id) return member except (exception.NotFound, exception.Forbidden) as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) @utils.mutating def delete(self, req, image_id, member_id): @@ -173,9 +173,9 @@ class ImageMembersController(object): member_repo.remove(member) return webob.Response(body='', status=204) except exception.NotFound as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) class RequestDeserializer(wsgi.JSONRequestDeserializer): diff --git a/glance/api/v2/image_tags.py b/glance/api/v2/image_tags.py index b4c47780c6..9663a1e0eb 100644 --- a/glance/api/v2/image_tags.py +++ b/glance/api/v2/image_tags.py @@ -43,11 +43,11 @@ class Controller(object): image.tags.add(tag_value) image_repo.save(image) except exception.NotFound as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) except exception.ImageTagLimitExceeded as e: - raise webob.exc.HTTPRequestEntityTooLarge(explanation=unicode(e)) + raise webob.exc.HTTPRequestEntityTooLarge(explanation=e.msg) @utils.mutating def delete(self, req, image_id, tag_value): @@ -59,9 +59,9 @@ class Controller(object): image.tags.remove(tag_value) image_repo.save(image) except exception.NotFound as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) class ResponseSerializer(wsgi.JSONResponseSerializer): diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py index 9b6e7e5408..0b92403d37 100644 --- a/glance/api/v2/images.py +++ b/glance/api/v2/images.py @@ -60,15 +60,15 @@ class ImagesController(object): tags=tags, **image) image_repo.add(image) except exception.DuplicateLocation as dup: - raise webob.exc.HTTPBadRequest(explanation=unicode(dup)) + raise webob.exc.HTTPBadRequest(explanation=dup.msg) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) except exception.InvalidParameterValue as e: - raise webob.exc.HTTPBadRequest(explanation=unicode(e)) + raise webob.exc.HTTPBadRequest(explanation=e.msg) except exception.LimitExceeded as e: LOG.info(unicode(e)) raise webob.exc.HTTPRequestEntityTooLarge( - explanation=unicode(e), request=req, content_type='text/plain') + explanation=e.msg, request=req, content_type='text/plain') return image @@ -93,9 +93,9 @@ class ImagesController(object): result['next_marker'] = images[-1].image_id except (exception.NotFound, exception.InvalidSortKey, exception.InvalidFilterRangeValue) as e: - raise webob.exc.HTTPBadRequest(explanation=unicode(e)) + raise webob.exc.HTTPBadRequest(explanation=e.msg) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) result['images'] = images return result @@ -104,9 +104,9 @@ class ImagesController(object): try: return image_repo.get(image_id) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) except exception.NotFound as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) @utils.mutating def update(self, req, image_id, changes): @@ -123,11 +123,11 @@ class ImagesController(object): if changes: image_repo.save(image) except exception.NotFound as e: - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) except exception.InvalidParameterValue as e: - raise webob.exc.HTTPBadRequest(explanation=unicode(e)) + raise webob.exc.HTTPBadRequest(explanation=e.msg) except exception.StorageQuotaFull as e: msg = (_("Denying attempt to upload image because it exceeds the ." "quota: %s") % e) @@ -137,7 +137,7 @@ class ImagesController(object): except exception.LimitExceeded as e: LOG.info(unicode(e)) raise webob.exc.HTTPRequestEntityTooLarge( - explanation=unicode(e), request=req, content_type='text/plain') + explanation=e.msg, request=req, content_type='text/plain') return image @@ -192,7 +192,7 @@ class ImagesController(object): image.delete() image_repo.remove(image) except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) except exception.NotFound as e: msg = (_("Failed to find image %(image_id)s to delete") % {'image_id': image_id}) @@ -228,7 +228,7 @@ class ImagesController(object): if image.status == 'queued': image.status = 'active' except (exception.BadStoreUri, exception.DuplicateLocation) as bse: - raise webob.exc.HTTPBadRequest(explanation=unicode(bse)) + raise webob.exc.HTTPBadRequest(explanation=bse.msg) except ValueError as ve: # update image status failed. raise webob.exc.HTTPBadRequest(explanation=unicode(ve)) @@ -243,7 +243,7 @@ class ImagesController(object): if image.status == 'queued': image.status = 'active' except (exception.BadStoreUri, exception.DuplicateLocation) as bse: - raise webob.exc.HTTPBadRequest(explanation=unicode(bse)) + raise webob.exc.HTTPBadRequest(explanation=bse.msg) except ValueError as ve: # update image status failed. raise webob.exc.HTTPBadRequest(explanation=unicode(ve)) @@ -301,7 +301,7 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer): try: self.schema.validate(body) except exception.InvalidObject as e: - raise webob.exc.HTTPBadRequest(explanation=unicode(e)) + raise webob.exc.HTTPBadRequest(explanation=e.msg) image = {} properties = body tags = properties.pop('tags', None) @@ -418,7 +418,7 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer): try: self.schema.validate(partial_image) except exception.InvalidObject as e: - raise webob.exc.HTTPBadRequest(explanation=unicode(e)) + raise webob.exc.HTTPBadRequest(explanation=e.msg) def _validate_path(self, op, path): path_root = path[0] @@ -595,7 +595,7 @@ class ResponseSerializer(wsgi.JSONResponseSerializer): image_view['schema'] = '/v2/schemas/image' image_view = self.schema.filter(image_view) # domain except exception.Forbidden as e: - raise webob.exc.HTTPForbidden(unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) return image_view def create(self, response, image): diff --git a/glance/api/v2/tasks.py b/glance/api/v2/tasks.py index 1fec2ac9b9..2f41033953 100644 --- a/glance/api/v2/tasks.py +++ b/glance/api/v2/tasks.py @@ -66,7 +66,7 @@ class TasksController(object): msg = (_("Forbidden to create task. Reason: %(reason)s") % {'reason': unicode(e)}) LOG.info(msg) - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) result = {'task': new_task, 'task_details': new_task_details} return result @@ -94,10 +94,10 @@ class TasksController(object): except (exception.NotFound, exception.InvalidSortKey, exception.InvalidFilterRangeValue) as e: LOG.info(unicode(e)) - raise webob.exc.HTTPBadRequest(explanation=unicode(e)) + raise webob.exc.HTTPBadRequest(explanation=e.msg) except exception.Forbidden as e: LOG.info(unicode(e)) - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) result['tasks'] = tasks return result @@ -109,12 +109,12 @@ class TasksController(object): msg = (_("Failed to find task %(task_id)s. Reason: %(reason)s") % {'task_id': task_id, 'reason': unicode(e)}) LOG.info(msg) - raise webob.exc.HTTPNotFound(explanation=unicode(e)) + raise webob.exc.HTTPNotFound(explanation=e.msg) except exception.Forbidden as e: msg = (_("Forbidden to get task %(task_id)s. Reason: %(reason)s") % {'task_id': task_id, 'reason': unicode(e)}) LOG.info(msg) - raise webob.exc.HTTPForbidden(explanation=unicode(e)) + raise webob.exc.HTTPForbidden(explanation=e.msg) result = {'task': task, 'task_details': task_details} return result @@ -197,7 +197,7 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer): try: self.schema.validate(body) except exception.InvalidObject as e: - raise webob.exc.HTTPBadRequest(explanation=unicode(e)) + raise webob.exc.HTTPBadRequest(explanation=e.msg) task = {} properties = body for key in self._required_properties: diff --git a/glance/cmd/__init__.py b/glance/cmd/__init__.py index b460b6d12f..c74be858f1 100644 --- a/glance/cmd/__init__.py +++ b/glance/cmd/__init__.py @@ -14,4 +14,4 @@ # under the License. from glance.openstack.common import gettextutils -gettextutils.install('glance') +gettextutils.install('glance', lazy=True) diff --git a/glance/cmd/api.py b/glance/cmd/api.py index 94753350f6..37ab59a5b5 100755 --- a/glance/cmd/api.py +++ b/glance/cmd/api.py @@ -25,6 +25,8 @@ import eventlet import os import sys +import six + # Monkey patch socket, time, select, threads eventlet.patcher.monkey_patch(all=False, socket=True, time=True, select=True, thread=True) @@ -45,7 +47,7 @@ import glance.store def fail(returncode, e): - sys.stderr.write("ERROR: %s\n" % e) + sys.stderr.write("ERROR: %s\n" % six.text_type(e)) sys.exit(returncode) diff --git a/glance/common/exception.py b/glance/common/exception.py index 5fd535d0cf..433ce61fa4 100644 --- a/glance/common/exception.py +++ b/glance/common/exception.py @@ -16,6 +16,7 @@ """Glance exception subclasses""" +import six import six.moves.urllib.parse as urlparse _FATAL_EXCEPTION_FORMAT_ERRORS = False @@ -40,16 +41,23 @@ class GlanceException(Exception): if not message: message = self.message try: - message = message % kwargs + if kwargs: + message = message % kwargs except Exception: if _FATAL_EXCEPTION_FORMAT_ERRORS: raise else: # at least get the core message out if something happened pass - + self.msg = message super(GlanceException, self).__init__(message) + def __unicode__(self): + # NOTE(flwang): By default, self.msg is an instance of Message, which + # can't be converted by str(). Based on the definition of + # __unicode__, it should return unicode always. + return six.text_type(self.msg) + class MissingCredentialError(GlanceException): message = _("Missing required credential: %(required)s") diff --git a/glance/common/rpc.py b/glance/common/rpc.py index e61031212c..560260c677 100644 --- a/glance/common/rpc.py +++ b/glance/common/rpc.py @@ -178,7 +178,7 @@ class Controller(object): if self.raise_exc: raise - cls, val = e.__class__, str(e) + cls, val = e.__class__, six.text_type(e) msg = (_("RPC Call Error: %(val)s\n%(tb)s") % dict(val=val, tb=traceback.format_exc())) LOG.error(msg) @@ -188,7 +188,7 @@ class Controller(object): module = cls.__module__ if module not in CONF.allowed_rpc_exception_modules: cls = exception.RPCError - val = str(exception.RPCError(cls=cls, val=val)) + val = six.text_type(exception.RPCError(cls=cls, val=val)) cls_path = "%s.%s" % (cls.__module__, cls.__name__) result = {"_error": {"cls": cls_path, "val": val}} diff --git a/glance/common/wsgi.py b/glance/common/wsgi.py index 48451f5174..32dbe9faf4 100644 --- a/glance/common/wsgi.py +++ b/glance/common/wsgi.py @@ -1,6 +1,7 @@ # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2010 OpenStack Foundation +# Copyright 2014 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -41,6 +42,7 @@ import webob.exc from glance.common import exception from glance.common import utils +from glance.openstack.common import gettextutils from glance.openstack.common import jsonutils import glance.openstack.common.log as logging @@ -512,6 +514,17 @@ class Request(webob.Request): else: return content_type + def best_match_language(self): + """Determines best available locale from the Accept-Language header. + + :returns: the best language match or None if the 'Accept-Language' + header was not available in the request. + """ + if not self.accept_language: + return None + langs = gettextutils.get_available_languages('glance') + return self.accept_language.best_match(langs) + class JSONRequestDeserializer(object): def has_body(self, request): @@ -563,6 +576,23 @@ class JSONResponseSerializer(object): response.body = self.to_json(result) +def translate_exception(req, e): + """Translates all translatable elements of the given exception.""" + + # The RequestClass attribute in the webob.dec.wsgify decorator + # does not guarantee that the request object will be a particular + # type; this check is therefore necessary. + if not hasattr(req, "best_match_language"): + return e + + locale = req.best_match_language() + + if isinstance(e, webob.exc.HTTPError): + e.explanation = gettextutils.translate(e.explanation, locale) + e.detail = gettextutils.translate(e.detail, locale) + return e + + class Resource(object): """ WSGI app that handles (de)serialization and controller dispatch. @@ -599,17 +629,22 @@ class Resource(object): action_args = self.get_action_args(request.environ) action = action_args.pop('action', None) - deserialized_request = self.dispatch(self.deserializer, - action, request) - action_args.update(deserialized_request) + try: + deserialized_request = self.dispatch(self.deserializer, + action, request) + action_args.update(deserialized_request) + action_result = self.dispatch(self.controller, action, + request, **action_args) + except webob.exc.WSGIHTTPException as e: + exc_info = sys.exc_info() + raise translate_exception(request, e), None, exc_info[2] - action_result = self.dispatch(self.controller, action, - request, **action_args) try: response = webob.Response(request=request) self.dispatch(self.serializer, action, response, action_result) return response - + except webob.exc.WSGIHTTPException as e: + return translate_exception(request, e) except webob.exc.HTTPException as e: return e # return unserializable result (typically a webob exc) diff --git a/glance/image_cache/drivers/xattr.py b/glance/image_cache/drivers/xattr.py index 1e35efb5d1..4ea7248b7f 100644 --- a/glance/image_cache/drivers/xattr.py +++ b/glance/image_cache/drivers/xattr.py @@ -59,6 +59,7 @@ import stat import time from oslo.config import cfg +import six import xattr from glance.common import exception @@ -281,13 +282,14 @@ class Driver(base.Driver): os.unlink(self.get_image_filepath(image_id, 'queue')) def rollback(e): - set_attr('error', "%s" % e) + set_attr('error', six.text_type(e)) invalid_path = self.get_image_filepath(image_id, 'invalid') LOG.debug(_("Fetch of cache file failed (%(e)s), rolling back by " "moving '%(incomplete_path)s' to " "'%(invalid_path)s'"), - {'e': e, 'incomplete_path': incomplete_path, + {'e': six.text_type(e), + 'incomplete_path': incomplete_path, 'invalid_path': invalid_path}) os.rename(incomplete_path, invalid_path) diff --git a/glance/notifier.py b/glance/notifier.py index cd8e907c14..d14e49d66a 100644 --- a/glance/notifier.py +++ b/glance/notifier.py @@ -14,9 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. - -import warnings - from oslo.config import cfg from oslo import messaging import webob @@ -70,7 +67,7 @@ class Notifier(object): if CONF.notifier_strategy != 'default': msg = _("notifier_strategy was deprecated in " "favor of `notification_driver`") - warnings.warn(msg, DeprecationWarning) + LOG.warn(msg) # NOTE(flaper87): Use this to keep backwards # compatibility. We'll try to get an oslo.messaging diff --git a/glance/schema.py b/glance/schema.py index a203b3e57f..396b3dc358 100644 --- a/glance/schema.py +++ b/glance/schema.py @@ -14,6 +14,7 @@ # under the License. import jsonschema +import six from glance.common import exception @@ -31,7 +32,8 @@ class Schema(object): try: jsonschema.validate(obj, self.raw()) except jsonschema.ValidationError as e: - raise exception.InvalidObject(schema=self.name, reason=str(e)) + raise exception.InvalidObject(schema=self.name, + reason=six.text_type(e)) def filter(self, obj): filtered = {} diff --git a/glance/store/__init__.py b/glance/store/__init__.py index f901361653..04c6f8ee54 100644 --- a/glance/store/__init__.py +++ b/glance/store/__init__.py @@ -18,6 +18,7 @@ import copy import sys from oslo.config import cfg +import six from glance.common import exception from glance.common import utils @@ -305,7 +306,7 @@ def safe_delete_from_backend(context, uri, image_id, **kwargs): msg = _('Failed to delete image %s in store from URI') LOG.warn(msg % image_id) except exception.StoreDeleteNotSupported as e: - LOG.warn(str(e)) + LOG.warn(six.text_type(e)) except UnsupportedBackend: exc_type = sys.exc_info()[0].__name__ msg = (_('Failed to delete image %(image_id)s from store ' @@ -369,8 +370,8 @@ def store_add_to_backend(image_id, data, size, store): if not isinstance(metadata, dict): msg = (_("The storage driver %(store)s returned invalid metadata " "%(metadata)s. This must be a dictionary type") % - {'store': store, - 'metadata': metadata}) + {'store': six.text_type(store), + 'metadata': six.text_type(metadata)}) LOG.error(msg) raise BackendException(msg) try: @@ -378,9 +379,9 @@ def store_add_to_backend(image_id, data, size, store): except BackendException as e: e_msg = (_("A bad metadata structure was returned from the " "%(store)s storage driver: %(metadata)s. %(error)s.") % - {'store': store, - 'metadata': metadata, - 'error': e}) + {'store': six.text_type(store), + 'metadata': six.text_type(metadata), + 'error': six.text_type(e)}) LOG.error(e_msg) raise BackendException(e_msg) return (location, size, checksum, metadata) @@ -725,7 +726,7 @@ class ImageProxy(glance.domain.proxy.Image): except Exception as e: LOG.warn(_('Get image %(id)s data failed: ' '%(err)s.') % {'id': self.image.image_id, - 'err': e}) + 'err': six.text_type(e)}) err = e # tried all locations LOG.error(_('Glance tried all locations to get data for image %s ' diff --git a/glance/store/filesystem.py b/glance/store/filesystem.py index 75812eed3e..7e96cc6a4c 100644 --- a/glance/store/filesystem.py +++ b/glance/store/filesystem.py @@ -22,6 +22,7 @@ import hashlib import os from oslo.config import cfg +import six import six.moves.urllib.parse as urlparse from glance.common import exception @@ -279,19 +280,19 @@ class Store(glance.store.base.Store): 'used: %(error)s An empty dictionary will be ' 'returned to the client.') % {'file': CONF.filesystem_store_metadata_file, - 'error': str(bee)}) + 'error': six.text_type(bee)}) return {} except IOError as ioe: LOG.error(_('The path for the metadata file %(file)s could not be ' 'opened: %(error)s An empty dictionary will be ' 'returned to the client.') % {'file': CONF.filesystem_store_metadata_file, - 'error': ioe}) + 'error': six.text_type(ioe)}) return {} except Exception as ex: LOG.exception(_('An error occurred processing the storage systems ' 'meta data file: %s. An empty dictionary will be ' - 'returned to the client.') % str(ex)) + 'returned to the client.') % six.text_type(ex)) return {} def get(self, location): diff --git a/glance/store/location.py b/glance/store/location.py index e6fe7920a1..7629c6cf4b 100644 --- a/glance/store/location.py +++ b/glance/store/location.py @@ -82,7 +82,8 @@ def register_scheme_map(scheme_map): """ for (k, v) in scheme_map.items(): if k not in SCHEME_TO_CLS_MAP: - LOG.debug("Registering scheme %s with %s", k, v) + LOG.debug(_("Registering scheme %(k)s with %(v)s") % {'k': k, + 'v': v}) SCHEME_TO_CLS_MAP[k] = v diff --git a/glance/tests/unit/common/test_exception.py b/glance/tests/unit/common/test_exception.py index 2184394bcf..8ff0e46533 100644 --- a/glance/tests/unit/common/test_exception.py +++ b/glance/tests/unit/common/test_exception.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import six + from glance.common import exception from glance.tests import utils as test_utils @@ -40,3 +42,7 @@ class GlanceExceptionTestCase(test_utils.BaseTestCase): self.assertTrue('test: 500' in unicode(exception.GlanceException('test: %(code)s', code=500))) + + def test_non_unicode_error_msg(self): + exc = exception.GlanceException(str('test')) + self.assertIsInstance(six.text_type(exc), six.text_type) diff --git a/glance/tests/unit/common/test_wsgi.py b/glance/tests/unit/common/test_wsgi.py index af6f876f91..650eaef73a 100644 --- a/glance/tests/unit/common/test_wsgi.py +++ b/glance/tests/unit/common/test_wsgi.py @@ -1,4 +1,5 @@ # Copyright 2010-2011 OpenStack Foundation +# Copyright 2014 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,19 +17,42 @@ import datetime import socket +from babel import localedata import eventlet.patcher import fixtures +import gettext import mock import webob from glance.common import exception from glance.common import utils from glance.common import wsgi +from glance.openstack.common import gettextutils from glance.tests import utils as test_utils class RequestTest(test_utils.BaseTestCase): + def _set_expected_languages(self, all_locales=[], avail_locales=None): + # Override localedata.locale_identifiers to return some locales. + def returns_some_locales(*args, **kwargs): + return all_locales + + self.stubs.Set(localedata, 'locale_identifiers', returns_some_locales) + + # Override gettext.find to return other than None for some languages. + def fake_gettext_find(lang_id, *args, **kwargs): + found_ret = '/glance/%s/LC_MESSAGES/glance.mo' % lang_id + if avail_locales is None: + # All locales are available. + return found_ret + languages = kwargs['languages'] + if languages[0] in avail_locales: + return found_ret + return None + + self.stubs.Set(gettext, 'find', fake_gettext_find) + def test_content_type_missing(self): request = wsgi.Request.blank('/tests/123') self.assertRaises(exception.InvalidContentType, @@ -77,6 +101,50 @@ class RequestTest(test_utils.BaseTestCase): result = request.best_match_content_type() self.assertEqual(result, "application/json") + def test_language_accept_default(self): + request = wsgi.Request.blank('/tests/123') + request.headers["Accept-Language"] = "zz-ZZ,zz;q=0.8" + result = request.best_match_language() + self.assertIsNone(result) + + def test_language_accept_none(self): + request = wsgi.Request.blank('/tests/123') + result = request.best_match_language() + self.assertIsNone(result) + + def test_best_match_language_expected(self): + # If Accept-Language is a supported language, best_match_language() + # returns it. + self._set_expected_languages(all_locales=['it']) + + req = wsgi.Request.blank('/', headers={'Accept-Language': 'it'}) + self.assertEqual('it', req.best_match_language()) + + def test_request_match_language_unexpected(self): + # If Accept-Language is a language we do not support, + # best_match_language() returns None. + self._set_expected_languages(all_locales=['it']) + + req = wsgi.Request.blank('/', headers={'Accept-Language': 'zh'}) + self.assertIsNone(req.best_match_language()) + + @mock.patch.object(webob.acceptparse.AcceptLanguage, 'best_match') + def test_best_match_language_unknown(self, mock_best_match): + # Test that we are actually invoking language negotiation by webop + request = wsgi.Request.blank('/') + accepted = 'unknown-lang' + request.headers = {'Accept-Language': accepted} + + mock_best_match.return_value = None + + self.assertIsNone(request.best_match_language()) + + # If Accept-Language is missing or empty, match should be None + request.headers = {'Accept-Language': ''} + self.assertIsNone(request.best_match_language()) + request.headers.pop('Accept-Language') + self.assertIsNone(request.best_match_language()) + class ResourceTest(test_utils.BaseTestCase): @@ -171,6 +239,42 @@ class ResourceTest(test_utils.BaseTestCase): self.assertIsInstance(response, webob.exc.HTTPForbidden) self.assertEqual(response.status_code, 403) + @mock.patch.object(wsgi, 'translate_exception') + def test_resource_call_error_handle_localized(self, + mock_translate_exception): + class Controller(object): + def delete(self, req, identity): + raise webob.exc.HTTPBadRequest(explanation='Not Found') + + actions = {'action': 'delete', 'identity': 12} + env = {'wsgiorg.routing_args': [None, actions]} + request = wsgi.Request.blank('/tests/123', environ=env) + message_es = 'No Encontrado' + + resource = wsgi.Resource(Controller(), + wsgi.JSONRequestDeserializer(), + None) + translated_exc = webob.exc.HTTPBadRequest(message_es) + mock_translate_exception.return_value = translated_exc + + e = self.assertRaises(webob.exc.HTTPBadRequest, + resource, request) + self.assertEqual(message_es, str(e)) + + @mock.patch.object(webob.acceptparse.AcceptLanguage, 'best_match') + @mock.patch.object(gettextutils, 'translate') + def test_translate_exception(self, mock_translate, mock_best_match): + + mock_translate.return_value = 'No Encontrado' + mock_best_match.return_value = 'de' + + req = wsgi.Request.blank('/tests/123') + req.headers["Accept-Language"] = "de" + + e = webob.exc.HTTPNotFound(explanation='Not Found') + e = wsgi.translate_exception(req, e) + self.assertEqual('No Encontrado', e.explanation) + class JSONResponseSerializerTest(test_utils.BaseTestCase): diff --git a/glance/tests/unit/test_swift_store.py b/glance/tests/unit/test_swift_store.py index 0e7bda9eff..1bf876065b 100644 --- a/glance/tests/unit/test_swift_store.py +++ b/glance/tests/unit/test_swift_store.py @@ -424,7 +424,7 @@ class SwiftTests(object): except BackendException as e: exception_caught = True self.assertTrue("container noexist does not exist " - "in Swift" in str(e)) + "in Swift" in six.text_type(e)) self.assertTrue(exception_caught) self.assertEqual(SWIFT_PUT_OBJECT_CALLS, 0)