# Copyright 2012 OpenStack Foundation.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import hashlib
import os
import re

from castellan.common import exception as castellan_exception
from castellan import key_manager
import glance_store
from glance_store import location
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils as json
from oslo_utils import encodeutils
import six
from six.moves import http_client as http
import six.moves.urllib.parse as urlparse
import webob.exc

from glance.api import common
from glance.api import policy
from glance.common import exception
from glance.common import location_strategy
from glance.common import store_utils
from glance.common import timeutils
from glance.common import utils
from glance.common import wsgi
import glance.db
import glance.gateway
from glance.i18n import _, _LI, _LW
import glance.notifier
import glance.schema

LOG = logging.getLogger(__name__)

CONF = cfg.CONF
CONF.import_opt('disk_formats', 'glance.common.config', group='image_format')
CONF.import_opt('container_formats', 'glance.common.config',
                group='image_format')
CONF.import_opt('show_multiple_locations', 'glance.common.config')
CONF.import_opt('hashing_algorithm', 'glance.common.config')


class ImagesController(object):
    def __init__(self, db_api=None, policy_enforcer=None, notifier=None,
                 store_api=None):
        self.db_api = db_api or glance.db.get_api()
        self.policy = policy_enforcer or policy.Enforcer()
        self.notifier = notifier or glance.notifier.Notifier()
        self.store_api = store_api or glance_store
        self.gateway = glance.gateway.Gateway(self.db_api, self.store_api,
                                              self.notifier, self.policy)

        self._key_manager = key_manager.API(CONF)

    @utils.mutating
    def create(self, req, image, extra_properties, tags):
        image_factory = self.gateway.get_image_factory(req.context)
        image_repo = self.gateway.get_repo(req.context)
        try:
            image = image_factory.new_image(extra_properties=extra_properties,
                                            tags=tags, **image)
            image_repo.add(image)
        except (exception.DuplicateLocation,
                exception.Invalid) as e:
            raise webob.exc.HTTPBadRequest(explanation=e.msg)
        except (exception.ReservedProperty,
                exception.ReadonlyProperty) as e:
            raise webob.exc.HTTPForbidden(explanation=e.msg)
        except exception.Forbidden as e:
            LOG.debug("User not permitted to create image")
            raise webob.exc.HTTPForbidden(explanation=e.msg)
        except exception.LimitExceeded as e:
            LOG.warn(encodeutils.exception_to_unicode(e))
            raise webob.exc.HTTPRequestEntityTooLarge(
                explanation=e.msg, request=req, content_type='text/plain')
        except exception.Duplicate as e:
            raise webob.exc.HTTPConflict(explanation=e.msg)
        except exception.NotAuthenticated as e:
            raise webob.exc.HTTPUnauthorized(explanation=e.msg)
        except TypeError as e:
            LOG.debug(encodeutils.exception_to_unicode(e))
            raise webob.exc.HTTPBadRequest(explanation=e)

        return image

    @utils.mutating
    def import_image(self, req, image_id, body):
        image_repo = self.gateway.get_repo(req.context)
        task_factory = self.gateway.get_task_factory(req.context)
        executor_factory = self.gateway.get_task_executor_factory(req.context)
        task_repo = self.gateway.get_task_repo(req.context)
        import_method = body.get('method').get('name')
        uri = body.get('method').get('uri')
        all_stores_must_succeed = body.get('all_stores_must_succeed', True)

        try:
            image = image_repo.get(image_id)
            if image.status == 'active' and import_method != "copy-image":
                msg = _("Image with status active cannot be target for import")
                raise exception.Conflict(msg)
            if image.status != 'active' and import_method == "copy-image":
                msg = _("Only images with status active can be targeted for "
                        "copying")
                raise exception.Conflict(msg)
            if image.status != 'queued' and import_method == 'web-download':
                msg = _("Image needs to be in 'queued' state to use "
                        "'web-download' method")
                raise exception.Conflict(msg)
            if (image.status != 'uploading' and
                    import_method == 'glance-direct'):
                msg = _("Image needs to be staged before 'glance-direct' "
                        "method can be used")
                raise exception.Conflict(msg)
            if not getattr(image, 'container_format', None):
                msg = _("'container_format' needs to be set before import")
                raise exception.Conflict(msg)
            if not getattr(image, 'disk_format', None):
                msg = _("'disk_format' needs to be set before import")
                raise exception.Conflict(msg)

            stores = [None]
            if CONF.enabled_backends:
                try:
                    stores = utils.get_stores_from_request(req, body)
                except glance_store.UnknownScheme as exc:
                    LOG.warn(exc.msg)
                    raise exception.Conflict(exc.msg)

            # NOTE(abhishekk): If all_stores is specified and import_method is
            # copy_image, then remove those stores where image is already
            # present.
            all_stores = body.get('all_stores', False)
            if import_method == 'copy-image' and all_stores:
                for loc in image.locations:
                    existing_store = loc['metadata']['store']
                    if existing_store in stores:
                        LOG.debug("Removing store '%s' from all stores as "
                                  "image is already available in that "
                                  "store." % existing_store)
                        stores.remove(existing_store)

                if len(stores) == 0:
                    LOG.info(_LI("Exiting copying workflow as image is "
                                 "available in all configured stores."))
                    return image_id

            # validate if image is already existing in given stores when
            # all_stores is False
            if import_method == 'copy-image' and not all_stores:
                for loc in image.locations:
                    existing_store = loc['metadata']['store']
                    if existing_store in stores:
                        msg = _("Image is already present at store "
                                "'%s'") % existing_store
                        raise webob.exc.HTTPBadRequest(explanation=msg)
        except exception.Conflict as e:
            raise webob.exc.HTTPConflict(explanation=e.msg)
        except exception.NotFound as e:
            raise webob.exc.HTTPNotFound(explanation=e.msg)

        if (not all_stores_must_succeed) and (not CONF.enabled_backends):
            msg = (_("All_stores_must_succeed can only be set with "
                     "enabled_backends %s") % uri)
            raise webob.exc.HTTPBadRequest(explanation=msg)

        task_input = {'image_id': image_id,
                      'import_req': body,
                      'backend': stores}

        if (import_method == 'web-download' and
                not utils.validate_import_uri(uri)):
            LOG.debug("URI for web-download does not pass filtering: %s", uri)
            msg = (_("URI for web-download does not pass filtering: %s") % uri)
            raise webob.exc.HTTPBadRequest(explanation=msg)

        try:
            import_task = task_factory.new_task(task_type='api_image_import',
                                                owner=req.context.owner,
                                                task_input=task_input)
            task_repo.add(import_task)
            task_executor = executor_factory.new_task_executor(req.context)
            pool = common.get_thread_pool("tasks_eventlet_pool")
            pool.spawn_n(import_task.run, task_executor)
        except exception.Forbidden as e:
            LOG.debug("User not permitted to create image import task.")
            raise webob.exc.HTTPForbidden(explanation=e.msg)
        except exception.Conflict as e:
            raise webob.exc.HTTPConflict(explanation=e.msg)
        except exception.InvalidImageStatusTransition as e:
            raise webob.exc.HTTPConflict(explanation=e.msg)
        except ValueError as e:
            LOG.debug("Cannot import data for image %(id)s: %(e)s",
                      {'id': image_id,
                       'e': encodeutils.exception_to_unicode(e)})
            raise webob.exc.HTTPBadRequest(
                explanation=encodeutils.exception_to_unicode(e))

        return image_id

    def index(self, req, marker=None, limit=None, sort_key=None,
              sort_dir=None, filters=None, member_status='accepted'):
        sort_key = ['created_at'] if not sort_key else sort_key

        sort_dir = ['desc'] if not sort_dir else sort_dir

        result = {}
        if filters is None:
            filters = {}
        filters['deleted'] = False

        os_hidden = filters.get('os_hidden', 'false').lower()
        if os_hidden not in ['true', 'false']:
            message = _("Invalid value '%s' for 'os_hidden' filter."
                        " Valid values are 'true' or 'false'.") % os_hidden
            raise webob.exc.HTTPBadRequest(explanation=message)
        # ensure the type of os_hidden is boolean
        filters['os_hidden'] = os_hidden == 'true'

        protected = filters.get('protected')
        if protected is not None:
            if protected not in ['true', 'false']:
                message = _("Invalid value '%s' for 'protected' filter."
                            " Valid values are 'true' or 'false'.") % protected
                raise webob.exc.HTTPBadRequest(explanation=message)
            # ensure the type of protected is boolean
            filters['protected'] = protected == 'true'

        if limit is None:
            limit = CONF.limit_param_default
        limit = min(CONF.api_limit_max, limit)

        image_repo = self.gateway.get_repo(req.context)
        try:
            images = image_repo.list(marker=marker, limit=limit,
                                     sort_key=sort_key,
                                     sort_dir=sort_dir,
                                     filters=filters,
                                     member_status=member_status)
            if len(images) != 0 and len(images) == limit:
                result['next_marker'] = images[-1].image_id
        except (exception.NotFound, exception.InvalidSortKey,
                exception.InvalidFilterRangeValue,
                exception.InvalidParameterValue,
                exception.InvalidFilterOperatorValue) as e:
            raise webob.exc.HTTPBadRequest(explanation=e.msg)
        except exception.Forbidden as e:
            LOG.debug("User not permitted to retrieve images index")
            raise webob.exc.HTTPForbidden(explanation=e.msg)
        except exception.NotAuthenticated as e:
            raise webob.exc.HTTPUnauthorized(explanation=e.msg)
        result['images'] = images
        return result

    def show(self, req, image_id):
        image_repo = self.gateway.get_repo(req.context)
        try:
            return image_repo.get(image_id)
        except exception.Forbidden as e:
            LOG.debug("User not permitted to show image '%s'", image_id)
            raise webob.exc.HTTPForbidden(explanation=e.msg)
        except exception.NotFound as e:
            raise webob.exc.HTTPNotFound(explanation=e.msg)
        except exception.NotAuthenticated as e:
            raise webob.exc.HTTPUnauthorized(explanation=e.msg)

    @utils.mutating
    def update(self, req, image_id, changes):
        image_repo = self.gateway.get_repo(req.context)
        try:
            image = image_repo.get(image_id)

            for change in changes:
                change_method_name = '_do_%s' % change['op']
                change_method = getattr(self, change_method_name)
                change_method(req, image, change)

            if changes:
                image_repo.save(image)
        except exception.NotFound as e:
            raise webob.exc.HTTPNotFound(explanation=e.msg)
        except (exception.Invalid, exception.BadStoreUri) as e:
            raise webob.exc.HTTPBadRequest(explanation=e.msg)
        except exception.Forbidden as e:
            LOG.debug("User not permitted to update image '%s'", image_id)
            raise webob.exc.HTTPForbidden(explanation=e.msg)
        except exception.StorageQuotaFull as e:
            msg = (_("Denying attempt to upload image because it exceeds the"
                     " quota: %s") % encodeutils.exception_to_unicode(e))
            LOG.warn(msg)
            raise webob.exc.HTTPRequestEntityTooLarge(
                explanation=msg, request=req, content_type='text/plain')
        except exception.LimitExceeded as e:
            LOG.exception(encodeutils.exception_to_unicode(e))
            raise webob.exc.HTTPRequestEntityTooLarge(
                explanation=e.msg, request=req, content_type='text/plain')
        except exception.NotAuthenticated as e:
            raise webob.exc.HTTPUnauthorized(explanation=e.msg)

        return image

    def _do_replace(self, req, image, change):
        path = change['path']
        path_root = path[0]
        value = change['value']
        if path_root == 'locations' and not value:
            msg = _("Cannot set locations to empty list.")
            raise webob.exc.HTTPForbidden(msg)
        elif path_root == 'locations' and value:
            self._do_replace_locations(image, value)
        elif path_root == 'owner' and req.context.is_admin == False:
            msg = _("Owner can't be updated by non admin.")
            raise webob.exc.HTTPForbidden(msg)
        else:
            if hasattr(image, path_root):
                setattr(image, path_root, value)
            elif path_root in image.extra_properties:
                image.extra_properties[path_root] = value
            else:
                msg = _("Property %s does not exist.")
                raise webob.exc.HTTPConflict(msg % path_root)

    def _do_add(self, req, image, change):
        path = change['path']
        path_root = path[0]
        value = change['value']
        json_schema_version = change.get('json_schema_version', 10)
        if path_root == 'locations':
            self._do_add_locations(image, path[1], value)
        else:
            if ((hasattr(image, path_root) or
                    path_root in image.extra_properties)
                    and json_schema_version == 4):
                msg = _("Property %s already present.")
                raise webob.exc.HTTPConflict(msg % path_root)
            if hasattr(image, path_root):
                setattr(image, path_root, value)
            else:
                image.extra_properties[path_root] = value

    def _do_remove(self, req, image, change):
        path = change['path']
        path_root = path[0]
        if path_root == 'locations':
            try:
                self._do_remove_locations(image, path[1])
            except exception.Forbidden as e:
                raise webob.exc.HTTPForbidden(e.msg)
        else:
            if hasattr(image, path_root):
                msg = _("Property %s may not be removed.")
                raise webob.exc.HTTPForbidden(msg % path_root)
            elif path_root in image.extra_properties:
                del image.extra_properties[path_root]
            else:
                msg = _("Property %s does not exist.")
                raise webob.exc.HTTPConflict(msg % path_root)

    def _delete_encryption_key(self, context, image):
        props = image.extra_properties

        cinder_encryption_key_id = props.get('cinder_encryption_key_id')
        if cinder_encryption_key_id is None:
            return

        deletion_policy = props.get('cinder_encryption_key_deletion_policy',
                                    '')
        if deletion_policy != 'on_image_deletion':
            return

        try:
            self._key_manager.delete(context, cinder_encryption_key_id)
        except castellan_exception.Forbidden:
            msg = ('Not allowed to delete encryption key %s' %
                   cinder_encryption_key_id)
            LOG.warn(msg)
        except (castellan_exception.ManagedObjectNotFoundError, KeyError):
            msg = 'Could not find encryption key %s' % cinder_encryption_key_id
            LOG.warn(msg)
        except castellan_exception.KeyManagerError:
            msg = ('Failed to delete cinder encryption key %s' %
                   cinder_encryption_key_id)
            LOG.warn(msg)

    @utils.mutating
    def delete_from_store(self, req, store_id, image_id):
        if not CONF.enabled_backends:
            raise webob.exc.HTTPNotFound()
        if store_id not in CONF.enabled_backends:
            msg = (_("The selected store %s is not available on this node.") %
                   store_id)
            raise webob.exc.HTTPConflict(explanation=msg)

        image_repo = self.gateway.get_repo(req.context)
        try:
            image = image_repo.get(image_id)
        except exception.NotAuthenticated as e:
            raise webob.exc.HTTPUnauthorized(explanation=e.msg)
        except exception.NotFound:
            msg = (_("Failed to find image %(image_id)s") %
                   {'image_id': image_id})
            raise webob.exc.HTTPNotFound(explanation=msg)

        if image.status != 'active':
            msg = _("It's not allowed to remove image data from store if "
                    "image status is not 'active'")
            raise webob.exc.HTTPConflict(explanation=msg)

        if len(image.locations) == 1:
            LOG.debug("User forbidden to remove last location of image %s",
                      image_id)
            msg = _("Cannot delete image data from the only store containing "
                    "it. Consider deleting the image instead.")
            raise webob.exc.HTTPForbidden(explanation=msg)

        try:
            # NOTE(jokke): Here we go through the locations list and act on
            # the first hit. image.locations.pop() will actually remove the
            # data from the backend as well as remove the location object
            # from the list.
            for pos, loc in enumerate(image.locations):
                if loc['metadata'].get('store') == store_id:
                    image.locations.pop(pos)
                    break
            else:
                msg = (_("Image %(iid)s is not stored in store %(sid)s.") %
                       {'iid': image_id, 'sid': store_id})
                raise exception.Invalid(msg)
        except exception.Forbidden as e:
            raise webob.exc.HTTPForbidden(explanation=e.msg)
        except exception.Invalid as e:
            raise webob.exc.HTTPNotFound(explanation=e.msg)
        except glance_store.exceptions.HasSnapshot as e:
            raise webob.exc.HTTPConflict(explanation=e.msg)
        except glance_store.exceptions.InUseByStore as e:
            msg = ("The data for Image %(id)s could not be deleted "
                   "because it is in use: %(exc)s" % {"id": image_id,
                                                      "exc": e.msg})
            LOG.warning(msg)
            raise webob.exc.HTTPConflict(explanation=msg)
        except Exception as e:
            raise webob.exc.HTTPInternalServerError(
                explanation=encodeutils.exception_to_unicode(e))

        image_repo.save(image)

    @utils.mutating
    def delete(self, req, image_id):
        image_repo = self.gateway.get_repo(req.context)
        try:
            image = image_repo.get(image_id)
            # NOTE(abhishekk): Delete the data from staging area
            if CONF.enabled_backends:
                separator, staging_dir = store_utils.get_dir_separator()
                file_path = "%s%s%s" % (staging_dir,
                                        separator,
                                        image_id)
                try:
                    fn_call = glance_store.get_store_from_store_identifier
                    staging_store = fn_call('os_glance_staging_store')
                    loc = location.get_location_from_uri_and_backend(
                        file_path, 'os_glance_staging_store')
                    staging_store.delete(loc)
                except (glance_store.exceptions.NotFound,
                        glance_store.exceptions.UnknownScheme):
                    pass
            else:
                file_path = str(
                    CONF.node_staging_uri + '/' + image_id)[7:]
                if os.path.exists(file_path):
                    try:
                        LOG.debug(
                            "After upload to the backend, deleting staged "
                            "image data from %(fn)s", {'fn': file_path})
                        os.unlink(file_path)
                    except OSError as e:
                        LOG.error(
                            "After upload to backend, deletion of staged "
                            "image data from %(fn)s has failed because "
                            "[Errno %(en)d]", {'fn': file_path,
                                               'en': e.errno})
                else:
                    LOG.warning(_(
                        "After upload to backend, deletion of staged "
                        "image data has failed because "
                        "it cannot be found at %(fn)s"), {'fn': file_path})

            image.delete()
            self._delete_encryption_key(req.context, image)
            image_repo.remove(image)
        except (glance_store.Forbidden, exception.Forbidden) as e:
            LOG.debug("User not permitted to delete image '%s'", image_id)
            raise webob.exc.HTTPForbidden(explanation=e.msg)
        except (glance_store.NotFound, exception.NotFound):
            msg = (_("Failed to find image %(image_id)s to delete") %
                   {'image_id': image_id})
            LOG.warn(msg)
            raise webob.exc.HTTPNotFound(explanation=msg)
        except glance_store.exceptions.InUseByStore as e:
            msg = (_("Image %(id)s could not be deleted "
                     "because it is in use: %(exc)s") %
                   {"id": image_id,
                    "exc": e.msg})
            LOG.warn(msg)
            raise webob.exc.HTTPConflict(explanation=msg)
        except glance_store.exceptions.HasSnapshot as e:
            raise webob.exc.HTTPConflict(explanation=e.msg)
        except exception.InvalidImageStatusTransition as e:
            raise webob.exc.HTTPBadRequest(explanation=e.msg)
        except exception.NotAuthenticated as e:
            raise webob.exc.HTTPUnauthorized(explanation=e.msg)

    def _validate_validation_data(self, image, locations):
        val_data = {}
        for loc in locations:
            if 'validation_data' not in loc:
                continue
            for k, v in loc['validation_data'].items():
                if val_data.get(k, v) != v:
                    msg = _("Conflicting values for %s") % k
                    raise webob.exc.HTTPConflict(explanation=msg)
                val_data[k] = v

        # NOTE(imacdonn): values may be provided for items which are
        # already set, so long as the values exactly match. In this
        # case, nothing actually needs to be updated, but we should
        # reject the request if there's an apparent attempt to supply
        # a different value.
        new_val_data = {}
        for k, v in val_data.items():
            current = getattr(image, k)
            if v == current:
                continue
            if current:
                msg = _("%s is already set with a different value") % k
                raise webob.exc.HTTPConflict(explanation=msg)
            new_val_data[k] = v

        if not new_val_data:
            return {}

        if image.status != 'queued':
            msg = _("New value(s) for %s may only be provided when image "
                    "status is 'queued'") % ', '.join(new_val_data.keys())
            raise webob.exc.HTTPConflict(explanation=msg)

        if 'checksum' in new_val_data:
            try:
                checksum_bytes = bytearray.fromhex(new_val_data['checksum'])
            except ValueError:
                msg = (_("checksum (%s) is not a valid hexadecimal value") %
                       new_val_data['checksum'])
                raise webob.exc.HTTPConflict(explanation=msg)
            if len(checksum_bytes) != 16:
                msg = (_("checksum (%s) is not the correct size for md5 "
                         "(should be 16 bytes)") %
                       new_val_data['checksum'])
                raise webob.exc.HTTPConflict(explanation=msg)

        hash_algo = new_val_data.get('os_hash_algo')
        if hash_algo != CONF['hashing_algorithm']:
            msg = (_("os_hash_algo must be %(want)s, not %(got)s") %
                   {'want': CONF['hashing_algorithm'], 'got': hash_algo})
            raise webob.exc.HTTPConflict(explanation=msg)

        try:
            hash_bytes = bytearray.fromhex(new_val_data['os_hash_value'])
        except ValueError:
            msg = (_("os_hash_value (%s) is not a valid hexadecimal value") %
                   new_val_data['os_hash_value'])
            raise webob.exc.HTTPConflict(explanation=msg)
        want_size = hashlib.new(hash_algo).digest_size
        if len(hash_bytes) != want_size:
            msg = (_("os_hash_value (%(value)s) is not the correct size for "
                     "%(algo)s (should be %(want)d bytes)") %
                   {'value': new_val_data['os_hash_value'],
                    'algo': hash_algo,
                    'want': want_size})
            raise webob.exc.HTTPConflict(explanation=msg)

        return new_val_data

    def _get_locations_op_pos(self, path_pos, max_pos, allow_max):
        if path_pos is None or max_pos is None:
            return None
        pos = max_pos if allow_max else max_pos - 1
        if path_pos.isdigit():
            pos = int(path_pos)
        elif path_pos != '-':
            return None
        if not (allow_max or 0 <= pos < max_pos):
            return None
        return pos

    def _do_replace_locations(self, image, value):
        if CONF.show_multiple_locations == False:
            msg = _("It's not allowed to update locations if locations are "
                    "invisible.")
            raise webob.exc.HTTPForbidden(explanation=msg)

        if image.status not in ('active', 'queued'):
            msg = _("It's not allowed to replace locations if image status is "
                    "%s.") % image.status
            raise webob.exc.HTTPConflict(explanation=msg)

        val_data = self._validate_validation_data(image, value)
        # NOTE(abhishekk): get glance store based on location uri
        if CONF.enabled_backends:
            store_utils.update_store_in_locations(value, image.image_id)

        try:
            # NOTE(flwang): _locations_proxy's setattr method will check if
            # the update is acceptable.
            image.locations = value
            if image.status == 'queued':
                for k, v in val_data.items():
                    setattr(image, k, v)
                image.status = 'active'
        except (exception.BadStoreUri, exception.DuplicateLocation) as e:
            raise webob.exc.HTTPBadRequest(explanation=e.msg)
        except ValueError as ve:    # update image status failed.
            raise webob.exc.HTTPBadRequest(
                explanation=encodeutils.exception_to_unicode(ve))

    def _do_add_locations(self, image, path_pos, value):
        if CONF.show_multiple_locations == False:
            msg = _("It's not allowed to add locations if locations are "
                    "invisible.")
            raise webob.exc.HTTPForbidden(explanation=msg)

        if image.status not in ('active', 'queued'):
            msg = _("It's not allowed to add locations if image status is "
                    "%s.") % image.status
            raise webob.exc.HTTPConflict(explanation=msg)

        val_data = self._validate_validation_data(image, [value])
        # NOTE(abhishekk): get glance store based on location uri
        if CONF.enabled_backends:
            store_utils.update_store_in_locations([value], image.image_id)

        pos = self._get_locations_op_pos(path_pos,
                                         len(image.locations), True)
        if pos is None:
            msg = _("Invalid position for adding a location.")
            raise webob.exc.HTTPBadRequest(explanation=msg)
        try:
            image.locations.insert(pos, value)
            if image.status == 'queued':
                for k, v in val_data.items():
                    setattr(image, k, v)
                image.status = 'active'
        except (exception.BadStoreUri, exception.DuplicateLocation) as e:
            raise webob.exc.HTTPBadRequest(explanation=e.msg)
        except ValueError as e:    # update image status failed.
            raise webob.exc.HTTPBadRequest(
                explanation=encodeutils.exception_to_unicode(e))

    def _do_remove_locations(self, image, path_pos):
        if CONF.show_multiple_locations == False:
            msg = _("It's not allowed to remove locations if locations are "
                    "invisible.")
            raise webob.exc.HTTPForbidden(explanation=msg)

        if image.status not in ('active'):
            msg = _("It's not allowed to remove locations if image status is "
                    "%s.") % image.status
            raise webob.exc.HTTPConflict(explanation=msg)

        if len(image.locations) == 1:
            LOG.debug("User forbidden to remove last location of image %s",
                      image.image_id)
            msg = _("Cannot remove last location in the image.")
            raise exception.Forbidden(msg)
        pos = self._get_locations_op_pos(path_pos,
                                         len(image.locations), False)
        if pos is None:
            msg = _("Invalid position for removing a location.")
            raise webob.exc.HTTPBadRequest(explanation=msg)
        try:
            # NOTE(zhiyan): this actually deletes the location
            # from the backend store.
            image.locations.pop(pos)
        # TODO(jokke): Fix this, we should catch what store throws and
        # provide definitely something else than IternalServerError to user.
        except Exception as e:
            raise webob.exc.HTTPInternalServerError(
                explanation=encodeutils.exception_to_unicode(e))


class RequestDeserializer(wsgi.JSONRequestDeserializer):

    _disallowed_properties = ('direct_url', 'self', 'file', 'schema')
    _readonly_properties = ('created_at', 'updated_at', 'status', 'checksum',
                            'size', 'virtual_size', 'direct_url', 'self',
                            'file', 'schema', 'id', 'os_hash_algo',
                            'os_hash_value')
    _reserved_properties = ('location', 'deleted', 'deleted_at')
    _base_properties = ('checksum', 'created_at', 'container_format',
                        'disk_format', 'id', 'min_disk', 'min_ram', 'name',
                        'size', 'virtual_size', 'status', 'tags', 'owner',
                        'updated_at', 'visibility', 'protected', 'os_hidden')
    _available_sort_keys = ('name', 'status', 'container_format',
                            'disk_format', 'size', 'id', 'created_at',
                            'updated_at')

    _default_sort_key = 'created_at'

    _default_sort_dir = 'desc'

    _path_depth_limits = {'locations': {'add': 2, 'remove': 2, 'replace': 1}}

    _supported_operations = ('add', 'remove', 'replace')

    def __init__(self, schema=None):
        super(RequestDeserializer, self).__init__()
        self.schema = schema or get_schema()

    def _get_request_body(self, request):
        output = super(RequestDeserializer, self).default(request)
        if 'body' not in output:
            msg = _('Body expected in request.')
            raise webob.exc.HTTPBadRequest(explanation=msg)
        return output['body']

    @classmethod
    def _check_allowed(cls, image):
        for key in cls._disallowed_properties:
            if key in image:
                msg = _("Attribute '%s' is read-only.") % key
                raise webob.exc.HTTPForbidden(
                    explanation=six.text_type(msg))

    def create(self, request):
        body = self._get_request_body(request)
        self._check_allowed(body)
        try:
            self.schema.validate(body)
        except exception.InvalidObject as e:
            raise webob.exc.HTTPBadRequest(explanation=e.msg)
        image = {}
        properties = body
        tags = properties.pop('tags', [])
        for key in self._base_properties:
            try:
                # NOTE(flwang): Instead of changing the _check_unexpected
                # of ImageFactory. It would be better to do the mapping
                # at here.
                if key == 'id':
                    image['image_id'] = properties.pop(key)
                else:
                    image[key] = properties.pop(key)
            except KeyError:
                pass

        # NOTE(abhishekk): Check if custom property key name is less than 255
        # characters. Reference LP #1737952
        for key in properties:
            if len(key) > 255:
                msg = (_("Custom property should not be greater than 255 "
                         "characters."))
                raise webob.exc.HTTPBadRequest(explanation=msg)

        return dict(image=image, extra_properties=properties, tags=tags)

    def _get_change_operation_d10(self, raw_change):
        op = raw_change.get('op')
        if op is None:
            msg = (_('Unable to find `op` in JSON Schema change. '
                     'It must be one of the following: %(available)s.') %
                   {'available': ', '.join(self._supported_operations)})
            raise webob.exc.HTTPBadRequest(explanation=msg)
        if op not in self._supported_operations:
            msg = (_('Invalid operation: `%(op)s`. '
                     'It must be one of the following: %(available)s.') %
                   {'op': op,
                    'available': ', '.join(self._supported_operations)})
            raise webob.exc.HTTPBadRequest(explanation=msg)
        return op

    def _get_change_operation_d4(self, raw_change):
        op = None
        for key in self._supported_operations:
            if key in raw_change:
                if op is not None:
                    msg = _('Operation objects must contain only one member'
                            ' named "add", "remove", or "replace".')
                    raise webob.exc.HTTPBadRequest(explanation=msg)
                op = key
        if op is None:
            msg = _('Operation objects must contain exactly one member'
                    ' named "add", "remove", or "replace".')
            raise webob.exc.HTTPBadRequest(explanation=msg)
        return op

    def _get_change_path_d10(self, raw_change):
        try:
            return raw_change['path']
        except KeyError:
            msg = _("Unable to find '%s' in JSON Schema change") % 'path'
            raise webob.exc.HTTPBadRequest(explanation=msg)

    def _get_change_path_d4(self, raw_change, op):
        return raw_change[op]

    def _decode_json_pointer(self, pointer):
        """Parse a json pointer.

        Json Pointers are defined in
        http://tools.ietf.org/html/draft-pbryan-zyp-json-pointer .
        The pointers use '/' for separation between object attributes, such
        that '/A/B' would evaluate to C in {"A": {"B": "C"}}. A '/' character
        in an attribute name is encoded as "~1" and a '~' character is encoded
        as "~0".
        """
        self._validate_json_pointer(pointer)
        ret = []
        for part in pointer.lstrip('/').split('/'):
            ret.append(part.replace('~1', '/').replace('~0', '~').strip())
        return ret

    def _validate_json_pointer(self, pointer):
        """Validate a json pointer.

        We only accept a limited form of json pointers.
        """
        if not pointer.startswith('/'):
            msg = _('Pointer `%s` does not start with "/".') % pointer
            raise webob.exc.HTTPBadRequest(explanation=msg)
        if re.search(r'/\s*?/', pointer[1:]):
            msg = _('Pointer `%s` contains adjacent "/".') % pointer
            raise webob.exc.HTTPBadRequest(explanation=msg)
        if len(pointer) > 1 and pointer.endswith('/'):
            msg = _('Pointer `%s` end with "/".') % pointer
            raise webob.exc.HTTPBadRequest(explanation=msg)
        if pointer[1:].strip() == '/':
            msg = _('Pointer `%s` does not contains valid token.') % pointer
            raise webob.exc.HTTPBadRequest(explanation=msg)
        if re.search('~[^01]', pointer) or pointer.endswith('~'):
            msg = _('Pointer `%s` contains "~" not part of'
                    ' a recognized escape sequence.') % pointer
            raise webob.exc.HTTPBadRequest(explanation=msg)

    def _get_change_value(self, raw_change, op):
        if 'value' not in raw_change:
            msg = _('Operation "%s" requires a member named "value".')
            raise webob.exc.HTTPBadRequest(explanation=msg % op)
        return raw_change['value']

    def _validate_change(self, change):
        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=six.text_type(msg))
        if path_root in self._reserved_properties:
            msg = _("Attribute '%s' is reserved.") % path_root
            raise webob.exc.HTTPForbidden(explanation=six.text_type(msg))

        if change['op'] == 'remove':
            return

        partial_image = None
        if len(change['path']) == 1:
            partial_image = {path_root: change['value']}
        elif ((path_root in get_base_properties().keys()) and
              (get_base_properties()[path_root].get('type', '') == 'array')):
            # NOTE(zhiyan): client can use the PATCH API to add an element
            # directly to an existing property
            # Such as: 1. using '/locations/N' path to add a location
            #             to the image's 'locations' list at position N.
            #             (implemented)
            #          2. using '/tags/-' path to append a tag to the
            #             image's 'tags' list at the end. (Not implemented)
            partial_image = {path_root: [change['value']]}

        if partial_image:
            try:
                self.schema.validate(partial_image)
            except exception.InvalidObject as e:
                raise webob.exc.HTTPBadRequest(explanation=e.msg)

    def _validate_path(self, op, path):
        path_root = path[0]
        limits = self._path_depth_limits.get(path_root, {})
        if len(path) != limits.get(op, 1):
            msg = _("Invalid JSON pointer for this resource: "
                    "'/%s'") % '/'.join(path)
            raise webob.exc.HTTPBadRequest(explanation=six.text_type(msg))

    def _parse_json_schema_change(self, raw_change, draft_version):
        if draft_version == 10:
            op = self._get_change_operation_d10(raw_change)
            path = self._get_change_path_d10(raw_change)
        elif draft_version == 4:
            op = self._get_change_operation_d4(raw_change)
            path = self._get_change_path_d4(raw_change, op)
        else:
            msg = _('Unrecognized JSON Schema draft version')
            raise webob.exc.HTTPBadRequest(explanation=msg)

        path_list = self._decode_json_pointer(path)
        return op, path_list

    def update(self, request):
        changes = []
        content_types = {
            'application/openstack-images-v2.0-json-patch': 4,
            'application/openstack-images-v2.1-json-patch': 10,
        }
        if request.content_type not in content_types:
            headers = {'Accept-Patch':
                       ', '.join(sorted(content_types.keys()))}
            raise webob.exc.HTTPUnsupportedMediaType(headers=headers)

        json_schema_version = content_types[request.content_type]

        body = self._get_request_body(request)

        if not isinstance(body, list):
            msg = _('Request body must be a JSON array of operation objects.')
            raise webob.exc.HTTPBadRequest(explanation=msg)

        for raw_change in body:
            if not isinstance(raw_change, dict):
                msg = _('Operations must be JSON objects.')
                raise webob.exc.HTTPBadRequest(explanation=msg)

            (op, path) = self._parse_json_schema_change(raw_change,
                                                        json_schema_version)

            # NOTE(zhiyan): the 'path' is a list.
            self._validate_path(op, path)
            change = {'op': op, 'path': path,
                      'json_schema_version': json_schema_version}

            if not op == 'remove':
                change['value'] = self._get_change_value(raw_change, op)

            self._validate_change(change)

            changes.append(change)

        return {'changes': changes}

    def _validate_limit(self, limit):
        try:
            limit = int(limit)
        except ValueError:
            msg = _("limit param must be an integer")
            raise webob.exc.HTTPBadRequest(explanation=msg)

        if limit < 0:
            msg = _("limit param must be positive")
            raise webob.exc.HTTPBadRequest(explanation=msg)

        return limit

    def _validate_sort_key(self, sort_key):
        if sort_key not in self._available_sort_keys:
            msg = _('Invalid sort key: %(sort_key)s. '
                    'It must be one of the following: %(available)s.') % (
                {'sort_key': sort_key,
                 'available': ', '.join(self._available_sort_keys)})
            raise webob.exc.HTTPBadRequest(explanation=msg)

        return sort_key

    def _validate_sort_dir(self, sort_dir):
        if sort_dir not in ['asc', 'desc']:
            msg = _('Invalid sort direction: %s') % sort_dir
            raise webob.exc.HTTPBadRequest(explanation=msg)

        return sort_dir

    def _validate_member_status(self, member_status):
        if member_status not in ['pending', 'accepted', 'rejected', 'all']:
            msg = _('Invalid status: %s') % member_status
            raise webob.exc.HTTPBadRequest(explanation=msg)

        return member_status

    def _get_filters(self, filters):
        visibility = filters.get('visibility')
        if visibility:
            if visibility not in ['community', 'public', 'private', 'shared',
                                  'all']:
                msg = _('Invalid visibility value: %s') % visibility
                raise webob.exc.HTTPBadRequest(explanation=msg)
        changes_since = filters.get('changes-since')
        if changes_since:
            msg = _('The "changes-since" filter is no longer available on v2.')
            raise webob.exc.HTTPBadRequest(explanation=msg)

        return filters

    def _get_sorting_params(self, params):
        """
        Process sorting params.
        Currently glance supports two sorting syntax: classic and new one,
        that is uniform for all OpenStack projects.
        Classic syntax: sort_key=name&sort_dir=asc&sort_key=size&sort_dir=desc
        New syntax: sort=name:asc,size:desc
        """
        sort_keys = []
        sort_dirs = []

        if 'sort' in params:
            # use new sorting syntax here
            if 'sort_key' in params or 'sort_dir' in params:
                msg = _('Old and new sorting syntax cannot be combined')
                raise webob.exc.HTTPBadRequest(explanation=msg)
            for sort_param in params.pop('sort').strip().split(','):
                key, _sep, dir = sort_param.partition(':')
                if not dir:
                    dir = self._default_sort_dir
                sort_keys.append(self._validate_sort_key(key.strip()))
                sort_dirs.append(self._validate_sort_dir(dir.strip()))
        else:
            # continue with classic syntax
            # NOTE(mfedosin): we have 3 options here:
            # 1. sort_dir wasn't passed: we use default one - 'desc'.
            # 2. Only one sort_dir was passed: use it for every sort_key
            # in the list.
            # 3. Multiple sort_dirs were passed: consistently apply each one to
            # the corresponding sort_key.
            # If number of sort_dirs and sort_keys doesn't match then raise an
            # exception.
            while 'sort_key' in params:
                sort_keys.append(self._validate_sort_key(
                    params.pop('sort_key').strip()))

            while 'sort_dir' in params:
                sort_dirs.append(self._validate_sort_dir(
                    params.pop('sort_dir').strip()))

            if sort_dirs:
                dir_len = len(sort_dirs)
                key_len = len(sort_keys)

                if dir_len > 1 and dir_len != key_len:
                    msg = _('Number of sort dirs does not match the number '
                            'of sort keys')
                    raise webob.exc.HTTPBadRequest(explanation=msg)

        if not sort_keys:
            sort_keys = [self._default_sort_key]

        if not sort_dirs:
            sort_dirs = [self._default_sort_dir]

        return sort_keys, sort_dirs

    def index(self, request):
        params = request.params.copy()
        limit = params.pop('limit', None)
        marker = params.pop('marker', None)
        member_status = params.pop('member_status', 'accepted')

        # NOTE (flwang) To avoid using comma or any predefined chars to split
        # multiple tags, now we allow user specify multiple 'tag' parameters
        # in URL, such as v2/images?tag=x86&tag=64bit.
        tags = []
        while 'tag' in params:
            tags.append(params.pop('tag').strip())

        query_params = {
            'filters': self._get_filters(params),
            'member_status': self._validate_member_status(member_status),
        }

        if marker is not None:
            query_params['marker'] = marker

        if limit is not None:
            query_params['limit'] = self._validate_limit(limit)

        if tags:
            query_params['filters']['tags'] = tags

        # NOTE(mfedosin): param is still called sort_key and sort_dir,
        # instead of sort_keys and sort_dirs respectively.
        # It's done because in v1 it's still a single value.

        query_params['sort_key'], query_params['sort_dir'] = (
            self._get_sorting_params(params))

        return query_params

    def _validate_import_body(self, body):
        # TODO(rosmaita): do schema validation of body instead
        # of this ad-hoc stuff
        try:
            method = body['method']
        except KeyError:
            msg = _("Import request requires a 'method' field.")
            raise webob.exc.HTTPBadRequest(explanation=msg)
        try:
            method_name = method['name']
        except KeyError:
            msg = _("Import request requires a 'name' field.")
            raise webob.exc.HTTPBadRequest(explanation=msg)
        if method_name not in CONF.enabled_import_methods:
            msg = _("Unknown import method name '%s'.") % method_name
            raise webob.exc.HTTPBadRequest(explanation=msg)

        # Validate 'all_stores_must_succeed' and 'all_stores'
        all_stores_must_succeed = body.get('all_stores_must_succeed', True)
        if not isinstance(all_stores_must_succeed, bool):
            msg = (_("'all_stores_must_succeed' must be boolean value only"))
            raise webob.exc.HTTPBadRequest(explanation=msg)

        all_stores = body.get('all_stores', False)
        if not isinstance(all_stores, bool):
            msg = (_("'all_stores' must be boolean value only"))
            raise webob.exc.HTTPBadRequest(explanation=msg)

    def import_image(self, request):
        body = self._get_request_body(request)
        self._validate_import_body(body)
        return {'body': body}


class ResponseSerializer(wsgi.JSONResponseSerializer):
    def __init__(self, schema=None):
        super(ResponseSerializer, self).__init__()
        self.schema = schema or get_schema()

    def _get_image_href(self, image, subcollection=''):
        base_href = '/v2/images/%s' % image.image_id
        if subcollection:
            base_href = '%s/%s' % (base_href, subcollection)
        return base_href

    def _format_image(self, image):

        def _get_image_locations(image):
            try:
                return list(image.locations)
            except exception.Forbidden:
                return []

        try:
            image_view = dict(image.extra_properties)
            attributes = ['name', 'disk_format', 'container_format',
                          'visibility', 'size', 'virtual_size', 'status',
                          'checksum', 'protected', 'min_ram', 'min_disk',
                          'owner', 'os_hidden', 'os_hash_algo',
                          'os_hash_value']
            for key in attributes:
                image_view[key] = getattr(image, key)
            image_view['id'] = image.image_id
            image_view['created_at'] = timeutils.isotime(image.created_at)
            image_view['updated_at'] = timeutils.isotime(image.updated_at)

            if CONF.show_multiple_locations:
                locations = _get_image_locations(image)
                if locations:
                    image_view['locations'] = []
                    for loc in locations:
                        tmp = dict(loc)
                        tmp.pop('id', None)
                        tmp.pop('status', None)
                        image_view['locations'].append(tmp)
                else:
                    # NOTE (flwang): We will still show "locations": [] if
                    # image.locations is None to indicate it's allowed to show
                    # locations but it's just non-existent.
                    image_view['locations'] = []
                    LOG.debug("The 'locations' list of image %s is empty",
                              image.image_id)

            if CONF.show_image_direct_url:
                locations = _get_image_locations(image)
                if locations:
                    # Choose best location configured strategy
                    loc = location_strategy.choose_best_location(locations)
                    image_view['direct_url'] = loc['url']
                else:
                    LOG.debug("The 'locations' list of image %s is empty, "
                              "not including 'direct_url' in response",
                              image.image_id)

            image_view['tags'] = list(image.tags)
            image_view['self'] = self._get_image_href(image)
            image_view['file'] = self._get_image_href(image, 'file')
            image_view['schema'] = '/v2/schemas/image'
            image_view = self.schema.filter(image_view)  # domain

            # add store information to image
            if CONF.enabled_backends:
                locations = _get_image_locations(image)
                if locations:
                    stores = []
                    for loc in locations:
                        backend = loc['metadata'].get('store')
                        if backend:
                            stores.append(backend)

                    if stores:
                        image_view['stores'] = ",".join(stores)

            return image_view
        except exception.Forbidden as e:
            raise webob.exc.HTTPForbidden(explanation=e.msg)

    def create(self, response, image):
        response.status_int = http.CREATED
        self.show(response, image)
        response.location = self._get_image_href(image)
        # according to RFC7230, headers should not have empty fields
        # see http://httpwg.org/specs/rfc7230.html#field.components
        if CONF.enabled_import_methods:
            import_methods = ("OpenStack-image-import-methods",
                              ','.join(CONF.enabled_import_methods))
            response.headerlist.append(import_methods)

        if CONF.enabled_backends:
            enabled_backends = ("OpenStack-image-store-ids",
                                ','.join(CONF.enabled_backends.keys()))
            response.headerlist.append(enabled_backends)

    def show(self, response, image):
        image_view = self._format_image(image)
        body = json.dumps(image_view, ensure_ascii=False)
        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 = six.text_type(body)
        response.content_type = 'application/json'

    def index(self, response, result):
        params = dict(response.request.params)
        params.pop('marker', None)
        query = urlparse.urlencode(params)
        body = {
            'images': [self._format_image(i) for i in result['images']],
            'first': '/v2/images',
            'schema': '/v2/schemas/images',
        }
        if query:
            body['first'] = '%s?%s' % (body['first'], query)
        if 'next_marker' in result:
            params['marker'] = result['next_marker']
            next_query = urlparse.urlencode(params)
            body['next'] = '/v2/images?%s' % next_query
        response.unicode_body = six.text_type(json.dumps(body,
                                                         ensure_ascii=False))
        response.content_type = 'application/json'

    def delete_from_store(self, response, result):
        response.status_int = http.NO_CONTENT

    def delete(self, response, result):
        response.status_int = http.NO_CONTENT

    def import_image(self, response, result):
        response.status_int = http.ACCEPTED


def get_base_properties():
    return {
        'id': {
            'type': 'string',
            'description': _('An identifier for the image'),
            'pattern': ('^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}'
                        '-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$'),
        },
        'name': {
            'type': ['null', 'string'],
            'description': _('Descriptive name for the image'),
            'maxLength': 255,
        },
        'status': {
            'type': 'string',
            'readOnly': True,
            'description': _('Status of the image'),
            'enum': ['queued', 'saving', 'active', 'killed',
                     'deleted', 'uploading', 'importing',
                     'pending_delete', 'deactivated'],
        },
        'visibility': {
            'type': 'string',
            'description': _('Scope of image accessibility'),
            'enum': ['community', 'public', 'private', 'shared'],
        },
        'protected': {
            'type': 'boolean',
            'description': _('If true, image will not be deletable.'),
        },
        'os_hidden': {
            'type': 'boolean',
            'description': _('If true, image will not appear in default '
                             'image list response.'),
        },
        'checksum': {
            'type': ['null', 'string'],
            'readOnly': True,
            'description': _('md5 hash of image contents.'),
            'maxLength': 32,
        },
        'os_hash_algo': {
            'type': ['null', 'string'],
            'readOnly': True,
            'description': _('Algorithm to calculate the os_hash_value'),
            'maxLength': 64,
        },
        'os_hash_value': {
            'type': ['null', 'string'],
            'readOnly': True,
            'description': _('Hexdigest of the image contents using the '
                             'algorithm specified by the os_hash_algo'),
            'maxLength': 128,
        },
        'owner': {
            'type': ['null', 'string'],
            'description': _('Owner of the image'),
            'maxLength': 255,
        },
        'size': {
            'type': ['null', 'integer'],
            'readOnly': True,
            'description': _('Size of image file in bytes'),
        },
        'virtual_size': {
            'type': ['null', 'integer'],
            'readOnly': True,
            'description': _('Virtual size of image in bytes'),
        },
        'container_format': {
            'type': ['null', 'string'],
            'description': _('Format of the container'),
            'enum': [None] + CONF.image_format.container_formats,
        },
        'disk_format': {
            'type': ['null', 'string'],
            'description': _('Format of the disk'),
            'enum': [None] + CONF.image_format.disk_formats,
        },
        'created_at': {
            'type': 'string',
            'readOnly': True,
            'description': _('Date and time of image registration'
                             ),
            # TODO(bcwaldon): our jsonschema library doesn't seem to like the
            # format attribute, figure out why!
            # 'format': 'date-time',
        },
        'updated_at': {
            'type': 'string',
            'readOnly': True,
            'description': _('Date and time of the last image modification'
                             ),
            # 'format': 'date-time',
        },
        'tags': {
            'type': 'array',
            'description': _('List of strings related to the image'),
            'items': {
                'type': 'string',
                'maxLength': 255,
            },
        },
        'direct_url': {
            'type': 'string',
            'readOnly': True,
            'description': _('URL to access the image file kept in external '
                             'store'),
        },
        'min_ram': {
            'type': 'integer',
            'description': _('Amount of ram (in MB) required to boot image.'),
        },
        'min_disk': {
            'type': 'integer',
            'description': _('Amount of disk space (in GB) required to boot '
                             'image.'),
        },
        'self': {
            'type': 'string',
            'readOnly': True,
            'description': _('An image self url'),
        },
        'file': {
            'type': 'string',
            'readOnly': True,
            'description': _('An image file url'),
        },
        'stores': {
            'type': 'string',
            'readOnly': True,
            'description': _('Store in which image data resides.  Only '
                             'present when the operator has enabled multiple '
                             'stores.  May be a comma-separated list of store '
                             'identifiers.'),
        },
        'schema': {
            'type': 'string',
            'readOnly': True,
            'description': _('An image schema url'),
        },
        'locations': {
            'type': 'array',
            'items': {
                'type': 'object',
                'properties': {
                    'url': {
                        'type': 'string',
                        'maxLength': 255,
                    },
                    'metadata': {
                        'type': 'object',
                    },
                    'validation_data': {
                        'description': _(
                            'Values to be used to populate the corresponding '
                            'image properties. If the image status is not '
                            '\'queued\', values must exactly match those '
                            'already contained in the image properties.'
                        ),
                        'type': 'object',
                        'writeOnly': True,
                        'additionalProperties': False,
                        'properties': {
                            'checksum': {
                                'type': 'string',
                                'minLength': 32,
                                'maxLength': 32,
                            },
                            'os_hash_algo': {
                                'type': 'string',
                                'maxLength': 64,
                            },
                            'os_hash_value': {
                                'type': 'string',
                                'maxLength': 128,
                            },
                        },
                        'required': [
                            'os_hash_algo',
                            'os_hash_value',
                        ],
                    },
                },
                'required': ['url', 'metadata'],
            },
            'description': _('A set of URLs to access the image file kept in '
                             'external store'),
        },
    }


def _get_base_links():
    return [
        {'rel': 'self', 'href': '{self}'},
        {'rel': 'enclosure', 'href': '{file}'},
        {'rel': 'describedby', 'href': '{schema}'},
    ]


def get_schema(custom_properties=None):
    properties = get_base_properties()
    links = _get_base_links()
    if CONF.allow_additional_image_properties:
        schema = glance.schema.PermissiveSchema('image', properties, links)
    else:
        schema = glance.schema.Schema('image', properties)

    if custom_properties:
        for property_value in custom_properties.values():
            property_value['is_base'] = False
        schema.merge_properties(custom_properties)
    return schema


def get_collection_schema(custom_properties=None):
    image_schema = get_schema(custom_properties)
    return glance.schema.CollectionSchema('images', image_schema)


def load_custom_properties():
    """Find the schema properties files and load them into a dict."""
    filename = 'schema-image.json'
    match = CONF.find_file(filename)
    if match:
        with open(match, 'r') as schema_file:
            schema_data = schema_file.read()
        return json.loads(schema_data)
    else:
        msg = (_LW('Could not find schema properties file %s. Continuing '
                   'without custom properties') % filename)
        LOG.warn(msg)
        return {}


def create_resource(custom_properties=None):
    """Images resource factory method"""
    schema = get_schema(custom_properties)
    deserializer = RequestDeserializer(schema)
    serializer = ResponseSerializer(schema)
    controller = ImagesController()
    return wsgi.Resource(controller, deserializer, serializer)
