Replace unicode() for six.text_type
To support Python3, unicode() calls has been replaced by six.text_type. Added utils.exception_to_str(): is the proper way to convert an exception to string, since it manages logic related to encoding. Change-Id: I27101390e4f95e5c7690b1b445b7e75b8bcb9a08 Closes-Bug: #1284677
This commit is contained in:
parent
ed816a214b
commit
627d5fbc13
|
@ -96,7 +96,7 @@ class CacheFilter(wsgi.Middleware):
|
|||
try:
|
||||
self.policy.enforce(req.context, action, {})
|
||||
except exception.Forbidden as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=unicode(e), request=req)
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg, request=req)
|
||||
|
||||
def process_request(self, request):
|
||||
"""
|
||||
|
|
|
@ -949,7 +949,7 @@ class Controller(controller.BaseController):
|
|||
request=req,
|
||||
content_type="text/plain")
|
||||
except (exception.Conflict, exception.Duplicate) as e:
|
||||
LOG.info(unicode(e))
|
||||
LOG.info(utils.exception_to_str(e))
|
||||
raise HTTPConflict(body='Image operation conflicts',
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
|
@ -1071,7 +1071,7 @@ class ImageDeserializer(wsgi.JSONRequestDeserializer):
|
|||
try:
|
||||
result['image_meta'] = utils.get_image_meta_from_headers(request)
|
||||
except exception.InvalidParameterValue as e:
|
||||
msg = unicode(e)
|
||||
msg = utils.exception_to_str(e)
|
||||
LOG.warn(msg, exc_info=True)
|
||||
raise HTTPBadRequest(explanation=e.msg, request=request)
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
# under the License.
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
import webob.exc
|
||||
|
||||
from glance.api import policy
|
||||
|
@ -95,10 +94,10 @@ class Controller(controller.BaseController):
|
|||
registry.delete_member(req.context, image_id, id)
|
||||
self._update_store_acls(req, image_id)
|
||||
except exception.NotFound as e:
|
||||
LOG.debug(six.text_type(e))
|
||||
LOG.debug(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
except exception.Forbidden as e:
|
||||
LOG.debug(six.text_type(e))
|
||||
LOG.debug(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
|
||||
return webob.exc.HTTPNoContent()
|
||||
|
@ -151,13 +150,13 @@ 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:
|
||||
LOG.debug(six.text_type(e))
|
||||
LOG.debug(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except exception.NotFound as e:
|
||||
LOG.debug(six.text_type(e))
|
||||
LOG.debug(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
except exception.Forbidden as e:
|
||||
LOG.debug(six.text_type(e))
|
||||
LOG.debug(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
|
||||
return webob.exc.HTTPNoContent()
|
||||
|
@ -186,13 +185,13 @@ class Controller(controller.BaseController):
|
|||
registry.replace_members(req.context, image_id, body)
|
||||
self._update_store_acls(req, image_id)
|
||||
except exception.Invalid as e:
|
||||
LOG.debug(six.text_type(e))
|
||||
LOG.debug(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except exception.NotFound as e:
|
||||
LOG.debug(six.text_type(e))
|
||||
LOG.debug(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
except exception.Forbidden as e:
|
||||
LOG.debug(six.text_type(e))
|
||||
LOG.debug(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
|
||||
return webob.exc.HTTPNoContent()
|
||||
|
@ -213,10 +212,10 @@ class Controller(controller.BaseController):
|
|||
try:
|
||||
members = registry.get_member_images(req.context, id)
|
||||
except exception.NotFound as e:
|
||||
LOG.debug(six.text_type(e))
|
||||
LOG.debug(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
except exception.Forbidden as e:
|
||||
LOG.debug(six.text_type(e))
|
||||
LOG.debug(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
return dict(shared_images=members)
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import six
|
||||
import webob.exc
|
||||
|
||||
import glance.api.policy
|
||||
|
@ -56,7 +55,7 @@ class ImageDataController(object):
|
|||
image_repo.save(image)
|
||||
except Exception as e:
|
||||
msg = _("Unable to restore image %(image_id)s: %(e)s") % \
|
||||
{'image_id': image.image_id, 'e': unicode(e)}
|
||||
{'image_id': image.image_id, 'e': utils.exception_to_str(e)}
|
||||
LOG.exception(msg)
|
||||
|
||||
@utils.mutating
|
||||
|
@ -88,12 +87,13 @@ class ImageDataController(object):
|
|||
|
||||
except ValueError as e:
|
||||
LOG.debug(_("Cannot save data for image %(id)s: %(e)s"),
|
||||
{'id': image_id, 'e': six.text_type(e)})
|
||||
{'id': image_id, 'e': utils.exception_to_str(e)})
|
||||
self._restore(image_repo, image)
|
||||
raise webob.exc.HTTPBadRequest(explanation=unicode(e))
|
||||
raise webob.exc.HTTPBadRequest(explanation=
|
||||
utils.exception_to_str(e))
|
||||
|
||||
except exception.InvalidImageStatusTransition as e:
|
||||
msg = unicode(e)
|
||||
msg = utils.exception_to_str(e)
|
||||
LOG.debug(msg)
|
||||
raise webob.exc.HTTPConflict(explanation=e.msg, request=req)
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# under the License.
|
||||
|
||||
import copy
|
||||
import six
|
||||
import webob
|
||||
|
||||
from glance.api import policy
|
||||
|
@ -104,7 +105,8 @@ class ImageMembersController(object):
|
|||
except exception.Forbidden as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
except ValueError as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=unicode(e))
|
||||
raise webob.exc.HTTPBadRequest(explanation=
|
||||
utils.exception_to_str(e))
|
||||
|
||||
def index(self, req, image_id):
|
||||
"""
|
||||
|
@ -233,13 +235,13 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
|||
def create(self, response, image_member):
|
||||
image_member_view = self._format_image_member(image_member)
|
||||
body = jsonutils.dumps(image_member_view, ensure_ascii=False)
|
||||
response.unicode_body = unicode(body)
|
||||
response.unicode_body = six.text_type(body)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
def update(self, response, image_member):
|
||||
image_member_view = self._format_image_member(image_member)
|
||||
body = jsonutils.dumps(image_member_view, ensure_ascii=False)
|
||||
response.unicode_body = unicode(body)
|
||||
response.unicode_body = six.text_type(body)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
def index(self, response, image_members):
|
||||
|
@ -251,13 +253,13 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
|||
totalview = dict(members=image_members_view)
|
||||
totalview['schema'] = '/v2/schemas/members'
|
||||
body = jsonutils.dumps(totalview, ensure_ascii=False)
|
||||
response.unicode_body = unicode(body)
|
||||
response.unicode_body = six.text_type(body)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
def show(self, response, image_member):
|
||||
image_member_view = self._format_image_member(image_member)
|
||||
body = jsonutils.dumps(image_member_view, ensure_ascii=False)
|
||||
response.unicode_body = unicode(body)
|
||||
response.unicode_body = six.text_type(body)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import re
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
import webob.exc
|
||||
|
||||
|
@ -66,7 +67,7 @@ class ImagesController(object):
|
|||
except exception.InvalidParameterValue as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except exception.LimitExceeded as e:
|
||||
LOG.info(unicode(e))
|
||||
LOG.info(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(
|
||||
explanation=e.msg, request=req, content_type='text/plain')
|
||||
|
||||
|
@ -135,7 +136,7 @@ class ImagesController(object):
|
|||
raise webob.exc.HTTPRequestEntityTooLarge(
|
||||
explanation=msg, request=req, content_type='text/plain')
|
||||
except exception.LimitExceeded as e:
|
||||
LOG.info(unicode(e))
|
||||
LOG.info(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(
|
||||
explanation=e.msg, request=req, content_type='text/plain')
|
||||
|
||||
|
@ -230,7 +231,8 @@ class ImagesController(object):
|
|||
except (exception.BadStoreUri, exception.DuplicateLocation) as bse:
|
||||
raise webob.exc.HTTPBadRequest(explanation=bse.msg)
|
||||
except ValueError as ve: # update image status failed.
|
||||
raise webob.exc.HTTPBadRequest(explanation=unicode(ve))
|
||||
raise webob.exc.HTTPBadRequest(explanation=
|
||||
utils.exception_to_str(ve))
|
||||
|
||||
def _do_add_locations(self, image, path_pos, value):
|
||||
pos = self._get_locations_op_pos(path_pos,
|
||||
|
@ -245,7 +247,8 @@ class ImagesController(object):
|
|||
except (exception.BadStoreUri, exception.DuplicateLocation) as bse:
|
||||
raise webob.exc.HTTPBadRequest(explanation=bse.msg)
|
||||
except ValueError as ve: # update image status failed.
|
||||
raise webob.exc.HTTPBadRequest(explanation=unicode(ve))
|
||||
raise webob.exc.HTTPBadRequest(explanation=
|
||||
utils.exception_to_str(ve))
|
||||
|
||||
def _do_remove_locations(self, image, path_pos):
|
||||
pos = self._get_locations_op_pos(path_pos,
|
||||
|
@ -258,7 +261,8 @@ class ImagesController(object):
|
|||
# from the backend store.
|
||||
image.locations.pop(pos)
|
||||
except Exception as e:
|
||||
raise webob.exc.HTTPInternalServerError(explanation=unicode(e))
|
||||
raise webob.exc.HTTPInternalServerError(explanation=
|
||||
utils.exception_to_str(e))
|
||||
if (len(image.locations) == 0) and (image.status == 'active'):
|
||||
image.status = 'queued'
|
||||
|
||||
|
@ -293,7 +297,8 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
|||
for key in cls._disallowed_properties:
|
||||
if key in image:
|
||||
msg = _("Attribute '%s' is read-only.") % key
|
||||
raise webob.exc.HTTPForbidden(explanation=unicode(msg))
|
||||
raise webob.exc.HTTPForbidden(explanation=
|
||||
utils.exception_to_str(msg))
|
||||
|
||||
def create(self, request):
|
||||
body = self._get_request_body(request)
|
||||
|
@ -392,10 +397,10 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
|||
path_root = change['path'][0]
|
||||
if path_root in self._readonly_properties:
|
||||
msg = _("Attribute '%s' is read-only.") % path_root
|
||||
raise webob.exc.HTTPForbidden(explanation=unicode(msg))
|
||||
raise webob.exc.HTTPForbidden(explanation=six.text_type(msg))
|
||||
if path_root in self._reserved_properties:
|
||||
msg = _("Attribute '%s' is reserved.") % path_root
|
||||
raise webob.exc.HTTPForbidden(explanation=unicode(msg))
|
||||
raise webob.exc.HTTPForbidden(explanation=six.text_type(msg))
|
||||
|
||||
if change['op'] == 'delete':
|
||||
return
|
||||
|
@ -426,7 +431,7 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
|||
if len(path) != limits.get(op, 1):
|
||||
msg = _("Invalid JSON pointer for this resource: "
|
||||
"'/%s'") % '/'.join(path)
|
||||
raise webob.exc.HTTPBadRequest(explanation=unicode(msg))
|
||||
raise webob.exc.HTTPBadRequest(explanation=six.text_type(msg))
|
||||
|
||||
def _parse_json_schema_change(self, raw_change, draft_version):
|
||||
if draft_version == 10:
|
||||
|
@ -606,13 +611,13 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
|||
def show(self, response, image):
|
||||
image_view = self._format_image(image)
|
||||
body = json.dumps(image_view, ensure_ascii=False)
|
||||
response.unicode_body = unicode(body)
|
||||
response.unicode_body = six.text_type(body)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
def update(self, response, image):
|
||||
image_view = self._format_image(image)
|
||||
body = json.dumps(image_view, ensure_ascii=False)
|
||||
response.unicode_body = unicode(body)
|
||||
response.unicode_body = six.text_type(body)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
def index(self, response, result):
|
||||
|
@ -630,7 +635,8 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
|||
params['marker'] = result['next_marker']
|
||||
next_query = urlparse.urlencode(params)
|
||||
body['next'] = '/v2/images?%s' % next_query
|
||||
response.unicode_body = unicode(json.dumps(body, ensure_ascii=False))
|
||||
response.unicode_body = six.text_type(json.dumps(body,
|
||||
ensure_ascii=False))
|
||||
response.content_type = 'application/json'
|
||||
|
||||
def delete(self, response, result):
|
||||
|
|
|
@ -18,6 +18,7 @@ import copy
|
|||
import webob.exc
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from glance.api import policy
|
||||
|
@ -63,7 +64,7 @@ class TasksController(object):
|
|||
task_repo.add(new_task)
|
||||
except exception.Forbidden as e:
|
||||
msg = (_("Forbidden to create task. Reason: %(reason)s")
|
||||
% {'reason': unicode(e)})
|
||||
% {'reason': utils.exception_to_str(e)})
|
||||
LOG.info(msg)
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
return new_task
|
||||
|
@ -87,10 +88,10 @@ class TasksController(object):
|
|||
result['next_marker'] = tasks[-1].task_id
|
||||
except (exception.NotFound, exception.InvalidSortKey,
|
||||
exception.InvalidFilterRangeValue) as e:
|
||||
LOG.info(unicode(e))
|
||||
LOG.info(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except exception.Forbidden as e:
|
||||
LOG.info(unicode(e))
|
||||
LOG.info(utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
result['tasks'] = tasks
|
||||
return result
|
||||
|
@ -101,12 +102,12 @@ class TasksController(object):
|
|||
task = task_repo.get(task_id)
|
||||
except exception.NotFound as e:
|
||||
msg = (_("Failed to find task %(task_id)s. Reason: %(reason)s") %
|
||||
{'task_id': task_id, 'reason': unicode(e)})
|
||||
{'task_id': task_id, 'reason': utils.exception_to_str(e)})
|
||||
LOG.info(msg)
|
||||
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)})
|
||||
{'task_id': task_id, 'reason': utils.exception_to_str(e)})
|
||||
LOG.info(msg)
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
return task
|
||||
|
@ -176,7 +177,7 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
|||
for param in self._required_properties:
|
||||
if param not in body:
|
||||
msg = _("Task '%s' is required") % param
|
||||
raise webob.exc.HTTPBadRequest(explanation=unicode(msg))
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def __init__(self, schema=None):
|
||||
super(RequestDeserializer, self).__init__()
|
||||
|
@ -272,7 +273,7 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
|||
def get(self, response, task):
|
||||
task_view = self._format_task(self.task_schema, task)
|
||||
body = json.dumps(task_view, ensure_ascii=False)
|
||||
response.unicode_body = unicode(body)
|
||||
response.unicode_body = six.text_type(body)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
def index(self, response, result):
|
||||
|
@ -291,7 +292,8 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
|||
params['marker'] = result['next_marker']
|
||||
next_query = urlparse.urlencode(params)
|
||||
body['next'] = '/v2/tasks?%s' % next_query
|
||||
response.unicode_body = unicode(json.dumps(body, ensure_ascii=False))
|
||||
response.unicode_body = six.text_type(json.dumps(body,
|
||||
ensure_ascii=False))
|
||||
response.content_type = 'application/json'
|
||||
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import eventlet
|
|||
import os
|
||||
import sys
|
||||
|
||||
import six
|
||||
from glance.common import utils
|
||||
|
||||
# Monkey patch socket, time, select, threads
|
||||
eventlet.patcher.monkey_patch(all=False, socket=True, time=True,
|
||||
|
@ -47,7 +47,7 @@ import glance.store
|
|||
|
||||
|
||||
def fail(returncode, e):
|
||||
sys.stderr.write("ERROR: %s\n" % six.text_type(e))
|
||||
sys.stderr.write("ERROR: %s\n" % utils.exception_to_str(e))
|
||||
sys.exit(returncode)
|
||||
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ import os
|
|||
import sys
|
||||
import time
|
||||
|
||||
from glance.common import utils
|
||||
|
||||
# If ../glance/__init__.py exists, add ../ to Python search path, so that
|
||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
|
@ -35,7 +37,6 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
|
|||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
import glance.image_cache.client
|
||||
from glance.openstack.common import timeutils
|
||||
from glance.version import version_info as version
|
||||
|
@ -66,7 +67,7 @@ def catch_error(action):
|
|||
if options.debug:
|
||||
raise
|
||||
print("Failed to %s. Got error:" % action)
|
||||
pieces = unicode(e).split('\n')
|
||||
pieces = utils.exception_to_str(e).split('\n')
|
||||
for piece in pieces:
|
||||
print(piece)
|
||||
return FAILURE
|
||||
|
|
|
@ -25,6 +25,7 @@ from webob import exc
|
|||
|
||||
from glance.common import client
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance.common import wsgi
|
||||
import glance.openstack.common.importutils as imp
|
||||
import glance.openstack.common.log as logging
|
||||
|
@ -178,7 +179,7 @@ class Controller(object):
|
|||
if self.raise_exc:
|
||||
raise
|
||||
|
||||
cls, val = e.__class__, six.text_type(e)
|
||||
cls, val = e.__class__, utils.exception_to_str(e)
|
||||
msg = (_("RPC Call Error: %(val)s\n%(tb)s") %
|
||||
dict(val=val, tb=traceback.format_exc()))
|
||||
LOG.error(msg)
|
||||
|
|
|
@ -40,6 +40,8 @@ from OpenSSL import crypto
|
|||
from oslo.config import cfg
|
||||
from webob import exc
|
||||
|
||||
import six
|
||||
|
||||
from glance.common import exception
|
||||
from glance.openstack.common import excutils
|
||||
import glance.openstack.common.log as logging
|
||||
|
@ -633,3 +635,15 @@ def parse_valid_host_port(host_port):
|
|||
'"[fe80::a:b:c]:9876").') % ex)
|
||||
|
||||
return (host, int(port))
|
||||
|
||||
|
||||
def exception_to_str(exc):
|
||||
try:
|
||||
error = six.text_type(exc)
|
||||
except UnicodeError:
|
||||
try:
|
||||
error = str(exc)
|
||||
except UnicodeError:
|
||||
error = ("Caught '%(exception)s' exception." %
|
||||
{"exception": exc.__class__.__name__})
|
||||
return strutils.safe_encode(error, errors='ignore')
|
||||
|
|
|
@ -17,6 +17,7 @@ import six.moves.urllib.parse as urlparse
|
|||
import sqlalchemy
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
import glance.openstack.common.log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -55,9 +56,10 @@ def migrate_location_credentials(migrate_engine, to_quoted):
|
|||
.where(images_table.c.id == image['id'])\
|
||||
.values(location=fixed_uri).execute()
|
||||
except exception.BadStoreUri as e:
|
||||
reason = utils.exception_to_str(e)
|
||||
err_msg = _("Invalid store uri for image: %(image_id)s. "
|
||||
"Details: %(reason)s") % {'image_id': image.id,
|
||||
'reason': unicode(e)}
|
||||
'reason': reason}
|
||||
LOG.exception(err_msg)
|
||||
raise
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import sqlalchemy
|
|||
|
||||
from glance.common import crypt
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
import glance.openstack.common.log as logging
|
||||
import glance.store.swift # noqa
|
||||
|
||||
|
@ -85,9 +86,10 @@ def migrate_location_credentials(migrate_engine, to_quoted):
|
|||
msg = _("Failed to decrypt location value for image %(image_id)s")
|
||||
LOG.warn(msg % {'image_id': image['id']})
|
||||
except exception.BadStoreUri as e:
|
||||
reason = utils.exception_to_str(e)
|
||||
err_msg = _("Invalid store uri for image: %(image_id)s. "
|
||||
"Details: %(reason)s") % {'image_id': image.id,
|
||||
'reason': unicode(e)}
|
||||
'reason': reason}
|
||||
LOG.exception(err_msg)
|
||||
raise
|
||||
|
||||
|
|
|
@ -59,10 +59,10 @@ import stat
|
|||
import time
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
import xattr
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance.image_cache.drivers import base
|
||||
from glance.openstack.common import excutils
|
||||
import glance.openstack.common.log as logging
|
||||
|
@ -283,13 +283,13 @@ class Driver(base.Driver):
|
|||
os.unlink(self.get_image_filepath(image_id, 'queue'))
|
||||
|
||||
def rollback(e):
|
||||
set_attr('error', six.text_type(e))
|
||||
set_attr('error', utils.exception_to_str(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': six.text_type(e),
|
||||
{'e': utils.exception_to_str(e),
|
||||
'incomplete_path': incomplete_path,
|
||||
'invalid_path': invalid_path})
|
||||
os.rename(incomplete_path, invalid_path)
|
||||
|
|
|
@ -19,6 +19,7 @@ from oslo import messaging
|
|||
import webob
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
import glance.domain.proxy
|
||||
from glance.openstack.common import excutils
|
||||
import glance.openstack.common.log as logging
|
||||
|
@ -260,7 +261,8 @@ class ImageProxy(glance.domain.proxy.Image):
|
|||
{'image_id': self.image.image_id,
|
||||
'error': e})
|
||||
self.notifier.error('image.upload', msg)
|
||||
raise webob.exc.HTTPBadRequest(explanation=unicode(e))
|
||||
raise webob.exc.HTTPBadRequest(explanation=
|
||||
utils.exception_to_str(e))
|
||||
except exception.Duplicate as e:
|
||||
msg = (_("Unable to upload duplicate image data for image"
|
||||
"%(image_id)s: %(error)s") %
|
||||
|
@ -280,7 +282,7 @@ class ImageProxy(glance.domain.proxy.Image):
|
|||
" %(error)s") % {'image_id': self.image.image_id,
|
||||
'error': e})
|
||||
self.notifier.error('image.upload', msg)
|
||||
raise webob.exc.HTTPNotFound(explanation=unicode(e))
|
||||
raise webob.exc.HTTPNotFound(explanation=utils.exception_to_str(e))
|
||||
except webob.exc.HTTPError as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
msg = (_("Failed to upload image data for image %(image_id)s"
|
||||
|
|
|
@ -18,6 +18,7 @@ Reference implementation registry server WSGI controller
|
|||
"""
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
from webob import exc
|
||||
|
||||
from glance.common import exception
|
||||
|
@ -481,7 +482,7 @@ class Controller(object):
|
|||
request=req,
|
||||
content_type='text/plain')
|
||||
except exception.Conflict as e:
|
||||
LOG.info(unicode(e))
|
||||
LOG.info(six.text_type(e))
|
||||
raise exc.HTTPConflict(body='Image operation conflicts',
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
# under the License.
|
||||
|
||||
import jsonschema
|
||||
import six
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
|
||||
|
||||
class Schema(object):
|
||||
|
@ -33,7 +33,7 @@ class Schema(object):
|
|||
jsonschema.validate(obj, self.raw())
|
||||
except jsonschema.ValidationError as e:
|
||||
raise exception.InvalidObject(schema=self.name,
|
||||
reason=six.text_type(e))
|
||||
reason=utils.exception_to_str(e))
|
||||
|
||||
def filter(self, obj):
|
||||
filtered = {}
|
||||
|
|
|
@ -20,6 +20,7 @@ import os
|
|||
import time
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
|
||||
from glance.common import crypt
|
||||
from glance.common import exception
|
||||
|
@ -386,11 +387,11 @@ class Daemon(object):
|
|||
class Scrubber(object):
|
||||
def __init__(self, store_api):
|
||||
LOG.info(_("Initializing scrubber with configuration: %s") %
|
||||
unicode({'scrubber_datadir': CONF.scrubber_datadir,
|
||||
'cleanup': CONF.cleanup_scrubber,
|
||||
'cleanup_time': CONF.cleanup_scrubber_time,
|
||||
'registry_host': CONF.registry_host,
|
||||
'registry_port': CONF.registry_port}))
|
||||
six.text_type({'scrubber_datadir': CONF.scrubber_datadir,
|
||||
'cleanup': CONF.cleanup_scrubber,
|
||||
'cleanup_time': CONF.cleanup_scrubber_time,
|
||||
'registry_host': CONF.registry_host,
|
||||
'registry_port': CONF.registry_port}))
|
||||
|
||||
utils.safe_mkdirs(CONF.scrubber_datadir)
|
||||
|
||||
|
@ -473,13 +474,13 @@ class Scrubber(object):
|
|||
"""
|
||||
try:
|
||||
if not os.path.exists(file_path):
|
||||
msg = _("%s file is not exists.") % unicode(file_path)
|
||||
msg = _("%s file is not exists.") % six.text_type(file_path)
|
||||
raise Exception(msg)
|
||||
atime = int(os.path.getatime(file_path))
|
||||
mtime = int(os.path.getmtime(file_path))
|
||||
if atime != mtime:
|
||||
msg = _("%s file contains conflicting cleanup "
|
||||
"timestamp.") % unicode(file_path)
|
||||
"timestamp.") % six.text_type(file_path)
|
||||
raise Exception(msg)
|
||||
return atime
|
||||
except Exception as e:
|
||||
|
@ -497,7 +498,8 @@ class Scrubber(object):
|
|||
os.chmod(file_path, 0o600)
|
||||
os.utime(file_path, (cleanup_time, cleanup_time))
|
||||
except Exception:
|
||||
LOG.error(_("%s file can not be created.") % unicode(file_path))
|
||||
LOG.error(_("%s file can not be created.") %
|
||||
six.text_type(file_path))
|
||||
|
||||
def _cleanup(self, pool):
|
||||
now = time.time()
|
||||
|
|
|
@ -191,7 +191,8 @@ def create_stores():
|
|||
store_instance = store_cls()
|
||||
except exception.BadStoreConfiguration as e:
|
||||
if store_entry in CONF.known_stores:
|
||||
LOG.warn(_("%s Skipping store driver.") % unicode(e))
|
||||
LOG.warn(_("%s Skipping store driver.") %
|
||||
utils.exception_to_str(e))
|
||||
continue
|
||||
finally:
|
||||
# NOTE(flaper87): To be removed in Juno
|
||||
|
@ -322,7 +323,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(six.text_type(e))
|
||||
LOG.warn(utils.exception_to_str(e))
|
||||
except UnsupportedBackend:
|
||||
exc_type = sys.exc_info()[0].__name__
|
||||
msg = (_('Failed to delete image %(image_id)s from store '
|
||||
|
@ -359,7 +360,7 @@ def check_location_metadata(val, key=''):
|
|||
for v in val:
|
||||
check_location_metadata(v, key='%s[%d]' % (key, ndx))
|
||||
ndx = ndx + 1
|
||||
elif not isinstance(val, unicode):
|
||||
elif not isinstance(val, six.text_type):
|
||||
raise BackendException(_("The image metadata key %(key)s has an "
|
||||
"invalid type of %(val)s. Only dict, list, "
|
||||
"and unicode are supported.") %
|
||||
|
@ -397,7 +398,7 @@ def store_add_to_backend(image_id, data, size, store):
|
|||
"%(store)s storage driver: %(metadata)s. %(error)s.") %
|
||||
{'store': six.text_type(store),
|
||||
'metadata': six.text_type(metadata),
|
||||
'error': six.text_type(e)})
|
||||
'error': utils.exception_to_str(e)})
|
||||
LOG.error(e_msg)
|
||||
raise BackendException(e_msg)
|
||||
return (location, size, checksum, metadata)
|
||||
|
@ -747,7 +748,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': six.text_type(e)})
|
||||
'err': utils.exception_to_str(e)})
|
||||
err = e
|
||||
# tried all locations
|
||||
LOG.error(_('Glance tried all locations to get data for image %s '
|
||||
|
|
|
@ -17,26 +17,14 @@
|
|||
"""Base class for all storage backends"""
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance.openstack.common import importutils
|
||||
import glance.openstack.common.log as logging
|
||||
from glance.openstack.common import strutils
|
||||
from glance.openstack.common import units
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _exception_to_unicode(exc):
|
||||
try:
|
||||
return unicode(exc)
|
||||
except UnicodeError:
|
||||
try:
|
||||
return strutils.safe_decode(str(exc), errors='ignore')
|
||||
except UnicodeError:
|
||||
msg = (_("Caught '%(exception)s' exception.") %
|
||||
{"exception": exc.__class__.__name__})
|
||||
return strutils.safe_decode(msg, errors='ignore')
|
||||
|
||||
|
||||
class Store(object):
|
||||
|
||||
CHUNKSIZE = 16 * units.Mi # 16M
|
||||
|
@ -54,7 +42,7 @@ class Store(object):
|
|||
except exception.BadStoreConfiguration as e:
|
||||
self.add = self.add_disabled
|
||||
msg = (_(u"Failed to configure store correctly: %s "
|
||||
"Disabling add method.") % _exception_to_unicode(e))
|
||||
"Disabling add method.") % utils.exception_to_str(e))
|
||||
LOG.warn(msg)
|
||||
|
||||
def configure(self):
|
||||
|
|
|
@ -22,7 +22,6 @@ import hashlib
|
|||
import os
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from glance.common import exception
|
||||
|
@ -281,19 +280,20 @@ 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': six.text_type(bee)})
|
||||
'error': utils.exception_to_str(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': six.text_type(ioe)})
|
||||
'error': utils.exception_to_str(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.') % six.text_type(ex))
|
||||
'returned to the client.') %
|
||||
utils.exception_to_str(ex))
|
||||
return {}
|
||||
|
||||
def get(self, location):
|
||||
|
|
|
@ -133,7 +133,7 @@ class TestRBDStore(store_tests.BaseTestCase, testtools.TestCase):
|
|||
# and uri to ascii before passing it to librbd.
|
||||
store = self.get_store()
|
||||
|
||||
image_id = unicode(str(uuid.uuid4()))
|
||||
image_id = six.text_type(str(uuid.uuid4()))
|
||||
image_size = 300
|
||||
image_data = six.StringIO('X' * image_size)
|
||||
image_checksum = '41757066eaff7c4c6c965556b4d3c6c5'
|
||||
|
@ -141,7 +141,7 @@ class TestRBDStore(store_tests.BaseTestCase, testtools.TestCase):
|
|||
uri, add_size, add_checksum = store.add(image_id,
|
||||
image_data,
|
||||
image_size)
|
||||
uri = unicode(uri)
|
||||
uri = six.text_type(uri)
|
||||
|
||||
self.assertEqual(image_size, add_size)
|
||||
self.assertEqual(image_checksum, add_checksum)
|
||||
|
|
|
@ -13,9 +13,8 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import six
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance.tests import utils as test_utils
|
||||
|
||||
|
||||
|
@ -26,23 +25,23 @@ class GlanceExceptionTestCase(test_utils.BaseTestCase):
|
|||
message = "default message"
|
||||
|
||||
exc = FakeGlanceException()
|
||||
self.assertEqual(unicode(exc), 'default message')
|
||||
self.assertEqual(utils.exception_to_str(exc), 'default message')
|
||||
|
||||
def test_specified_error_msg(self):
|
||||
self.assertIn('test', unicode(exception.GlanceException('test')))
|
||||
msg = exception.GlanceException('test')
|
||||
self.assertIn('test', utils.exception_to_str(msg))
|
||||
|
||||
def test_default_error_msg_with_kwargs(self):
|
||||
class FakeGlanceException(exception.GlanceException):
|
||||
message = "default message: %(code)s"
|
||||
|
||||
exc = FakeGlanceException(code=500)
|
||||
self.assertEqual(unicode(exc), "default message: 500")
|
||||
self.assertEqual(utils.exception_to_str(exc), "default message: 500")
|
||||
|
||||
def test_specified_error_msg_with_kwargs(self):
|
||||
self.assertIn('test: 500',
|
||||
unicode(exception.GlanceException('test: %(code)s',
|
||||
code=500)))
|
||||
msg = exception.GlanceException('test: %(code)s', code=500)
|
||||
self.assertIn('test: 500', utils.exception_to_str(msg))
|
||||
|
||||
def test_non_unicode_error_msg(self):
|
||||
exc = exception.GlanceException(str('test'))
|
||||
self.assertIsInstance(six.text_type(exc), six.text_type)
|
||||
self.assertIsInstance(utils.exception_to_str(exc), str)
|
||||
|
|
|
@ -363,6 +363,21 @@ class TestUtils(test_utils.BaseTestCase):
|
|||
utils.parse_valid_host_port,
|
||||
pair)
|
||||
|
||||
def test_exception_to_str(self):
|
||||
class FakeException(Exception):
|
||||
def __str__(self):
|
||||
raise UnicodeError()
|
||||
|
||||
ret = utils.exception_to_str(Exception('error message'))
|
||||
self.assertEqual(ret, 'error message')
|
||||
|
||||
ret = utils.exception_to_str(Exception('\xa5 error message'))
|
||||
self.assertEqual(ret, ' error message')
|
||||
|
||||
ret = utils.exception_to_str(FakeException('\xa5 error message'))
|
||||
self.assertEqual(ret, "Caught '%(exception)s' exception." %
|
||||
{'exception': 'FakeException'})
|
||||
|
||||
|
||||
class UUIDTestCase(test_utils.BaseTestCase):
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ from oslo.config import cfg
|
|||
|
||||
from glance.common import crypt
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
import glance.context
|
||||
import glance.db
|
||||
import glance.tests.unit.utils as unit_test_utils
|
||||
|
@ -188,7 +189,7 @@ class TestImageRepo(test_utils.BaseTestCase):
|
|||
fake_uuid = str(uuid.uuid4())
|
||||
exc = self.assertRaises(exception.NotFound, self.image_repo.get,
|
||||
fake_uuid)
|
||||
self.assertIn(fake_uuid, unicode(exc))
|
||||
self.assertIn(fake_uuid, utils.exception_to_str(exc))
|
||||
|
||||
def test_get_forbidden(self):
|
||||
self.assertRaises(exception.NotFound, self.image_repo.get, UUID4)
|
||||
|
@ -329,7 +330,7 @@ class TestImageRepo(test_utils.BaseTestCase):
|
|||
image.image_id = fake_uuid
|
||||
exc = self.assertRaises(exception.NotFound, self.image_repo.save,
|
||||
image)
|
||||
self.assertIn(fake_uuid, unicode(exc))
|
||||
self.assertIn(fake_uuid, utils.exception_to_str(exc))
|
||||
|
||||
def test_remove_image(self):
|
||||
image = self.image_repo.get(UUID1)
|
||||
|
@ -344,7 +345,7 @@ class TestImageRepo(test_utils.BaseTestCase):
|
|||
image.image_id = fake_uuid
|
||||
exc = self.assertRaises(exception.NotFound, self.image_repo.remove,
|
||||
image)
|
||||
self.assertIn(fake_uuid, unicode(exc))
|
||||
self.assertIn(fake_uuid, utils.exception_to_str(exc))
|
||||
|
||||
|
||||
class TestEncryptedLocations(test_utils.BaseTestCase):
|
||||
|
@ -536,7 +537,7 @@ class TestImageMemberRepo(test_utils.BaseTestCase):
|
|||
exc = self.assertRaises(exception.NotFound,
|
||||
self.image_member_repo.remove,
|
||||
fake_member)
|
||||
self.assertIn(fake_uuid, unicode(exc))
|
||||
self.assertIn(fake_uuid, utils.exception_to_str(exc))
|
||||
|
||||
|
||||
class TestTaskRepo(test_utils.BaseTestCase):
|
||||
|
|
|
@ -30,27 +30,6 @@ class TestStoreBase(test_base.StoreClearingUnitTest):
|
|||
self.config(default_store='file')
|
||||
super(TestStoreBase, self).setUp()
|
||||
|
||||
def test_exception_to_unicode(self):
|
||||
class FakeException(Exception):
|
||||
def __str__(self):
|
||||
raise UnicodeError()
|
||||
|
||||
exc = Exception('error message')
|
||||
ret = store_base._exception_to_unicode(exc)
|
||||
self.assertIsInstance(ret, unicode)
|
||||
self.assertEqual(ret, 'error message')
|
||||
|
||||
exc = Exception('\xa5 error message')
|
||||
ret = store_base._exception_to_unicode(exc)
|
||||
self.assertIsInstance(ret, unicode)
|
||||
self.assertEqual(ret, ' error message')
|
||||
|
||||
exc = FakeException('\xa5 error message')
|
||||
ret = store_base._exception_to_unicode(exc)
|
||||
self.assertIsInstance(ret, unicode)
|
||||
self.assertEqual(ret, _("Caught '%(exception)s' exception.") %
|
||||
{'exception': 'FakeException'})
|
||||
|
||||
def test_create_store_exclude_unconfigurable_drivers(self):
|
||||
self.config(known_stores=[
|
||||
"glance.tests.unit.test_store_base.FakeUnconfigurableStoreDriver",
|
||||
|
|
|
@ -29,6 +29,7 @@ import swiftclient
|
|||
|
||||
import glance.common.auth
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance.openstack.common import units
|
||||
|
||||
from glance.store import BackendException
|
||||
|
@ -424,7 +425,7 @@ class SwiftTests(object):
|
|||
except BackendException as e:
|
||||
exception_caught = True
|
||||
self.assertIn("container noexist does not exist "
|
||||
"in Swift", six.text_type(e))
|
||||
"in Swift", utils.exception_to_str(e))
|
||||
self.assertTrue(exception_caught)
|
||||
self.assertEqual(SWIFT_PUT_OBJECT_CALLS, 0)
|
||||
|
||||
|
|
|
@ -3054,7 +3054,7 @@ class TestImageSerializer(base.IsolatedUnitTest):
|
|||
# want to output utf-8.
|
||||
FIXTURE = {
|
||||
'image_meta': {
|
||||
'id': unicode(UUID2),
|
||||
'id': six.text_type(UUID2),
|
||||
'name': u'fake image #2 with utf-8 éàè',
|
||||
'status': u'active',
|
||||
'disk_format': u'vhd',
|
||||
|
@ -3066,7 +3066,7 @@ class TestImageSerializer(base.IsolatedUnitTest):
|
|||
'deleted': False,
|
||||
'checksum': u'06ff575a2856444fbe93100157ed74ab92eb7eff',
|
||||
'size': 19,
|
||||
'owner': unicode(_gen_uuid()),
|
||||
'owner': six.text_type(_gen_uuid()),
|
||||
'location': u"file:///tmp/glance-tests/2",
|
||||
'properties': {
|
||||
u'prop_éé': u'ça marche',
|
||||
|
|
|
@ -17,6 +17,7 @@ import datetime
|
|||
import uuid
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
import testtools
|
||||
import webob
|
||||
|
||||
|
@ -2805,8 +2806,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
|
|||
u'disk_format': u'ami',
|
||||
u'min_ram': 128,
|
||||
u'min_disk': 10,
|
||||
u'created_at': unicode(ISOTIME),
|
||||
u'updated_at': unicode(ISOTIME),
|
||||
u'created_at': six.text_type(ISOTIME),
|
||||
u'updated_at': six.text_type(ISOTIME),
|
||||
u'self': u'/v2/images/%s' % UUID1,
|
||||
u'file': u'/v2/images/%s/file' % UUID1,
|
||||
u'schema': u'/v2/schemas/image',
|
||||
|
@ -2840,8 +2841,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
|
|||
u'disk_format': u'ami',
|
||||
u'min_ram': 128,
|
||||
u'min_disk': 10,
|
||||
u'created_at': unicode(ISOTIME),
|
||||
u'updated_at': unicode(ISOTIME),
|
||||
u'created_at': six.text_type(ISOTIME),
|
||||
u'updated_at': six.text_type(ISOTIME),
|
||||
u'self': u'/v2/images/%s' % UUID1,
|
||||
u'file': u'/v2/images/%s/file' % UUID1,
|
||||
u'schema': u'/v2/schemas/image',
|
||||
|
@ -2871,8 +2872,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
|
|||
u'disk_format': u'ami',
|
||||
u'min_ram': 128,
|
||||
u'min_disk': 10,
|
||||
u'created_at': unicode(ISOTIME),
|
||||
u'updated_at': unicode(ISOTIME),
|
||||
u'created_at': six.text_type(ISOTIME),
|
||||
u'updated_at': six.text_type(ISOTIME),
|
||||
u'self': u'/v2/images/%s' % UUID1,
|
||||
u'file': u'/v2/images/%s/file' % UUID1,
|
||||
u'schema': u'/v2/schemas/image',
|
||||
|
@ -2902,8 +2903,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
|
|||
u'disk_format': u'ami',
|
||||
u'min_ram': 128,
|
||||
u'min_disk': 10,
|
||||
u'created_at': unicode(ISOTIME),
|
||||
u'updated_at': unicode(ISOTIME),
|
||||
u'created_at': six.text_type(ISOTIME),
|
||||
u'updated_at': six.text_type(ISOTIME),
|
||||
u'self': u'/v2/images/%s' % UUID1,
|
||||
u'file': u'/v2/images/%s/file' % UUID1,
|
||||
u'schema': u'/v2/schemas/image',
|
||||
|
|
Loading…
Reference in New Issue