"Fossies" - the Fresh Open Source Software Archive

Member "glance-20.0.1/glance/api/v2/images.py" (12 Aug 2020, 64548 Bytes) of package /linux/misc/openstack/glance-20.0.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "images.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 20.0.0_vs_20.0.1.

    1 # Copyright 2012 OpenStack Foundation.
    2 # All Rights Reserved.
    3 #
    4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    5 #    not use this file except in compliance with the License. You may obtain
    6 #    a copy of the License at
    7 #
    8 #         http://www.apache.org/licenses/LICENSE-2.0
    9 #
   10 #    Unless required by applicable law or agreed to in writing, software
   11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   13 #    License for the specific language governing permissions and limitations
   14 #    under the License.
   15 
   16 import hashlib
   17 import os
   18 import re
   19 
   20 from castellan.common import exception as castellan_exception
   21 from castellan import key_manager
   22 import glance_store
   23 from glance_store import location
   24 from oslo_config import cfg
   25 from oslo_log import log as logging
   26 from oslo_serialization import jsonutils as json
   27 from oslo_utils import encodeutils
   28 import six
   29 from six.moves import http_client as http
   30 import six.moves.urllib.parse as urlparse
   31 import webob.exc
   32 
   33 from glance.api import authorization
   34 from glance.api import common
   35 from glance.api import policy
   36 from glance.common import exception
   37 from glance.common import location_strategy
   38 from glance.common import store_utils
   39 from glance.common import timeutils
   40 from glance.common import utils
   41 from glance.common import wsgi
   42 import glance.db
   43 import glance.gateway
   44 from glance.i18n import _, _LI, _LW
   45 import glance.notifier
   46 import glance.schema
   47 
   48 LOG = logging.getLogger(__name__)
   49 
   50 CONF = cfg.CONF
   51 CONF.import_opt('disk_formats', 'glance.common.config', group='image_format')
   52 CONF.import_opt('container_formats', 'glance.common.config',
   53                 group='image_format')
   54 CONF.import_opt('show_multiple_locations', 'glance.common.config')
   55 CONF.import_opt('hashing_algorithm', 'glance.common.config')
   56 
   57 
   58 class ImagesController(object):
   59     def __init__(self, db_api=None, policy_enforcer=None, notifier=None,
   60                  store_api=None):
   61         self.db_api = db_api or glance.db.get_api()
   62         self.policy = policy_enforcer or policy.Enforcer()
   63         self.notifier = notifier or glance.notifier.Notifier()
   64         self.store_api = store_api or glance_store
   65         self.gateway = glance.gateway.Gateway(self.db_api, self.store_api,
   66                                               self.notifier, self.policy)
   67 
   68         self._key_manager = key_manager.API(CONF)
   69 
   70     @utils.mutating
   71     def create(self, req, image, extra_properties, tags):
   72         image_factory = self.gateway.get_image_factory(req.context)
   73         image_repo = self.gateway.get_repo(req.context)
   74         try:
   75             image = image_factory.new_image(extra_properties=extra_properties,
   76                                             tags=tags, **image)
   77             image_repo.add(image)
   78         except (exception.DuplicateLocation,
   79                 exception.Invalid) as e:
   80             raise webob.exc.HTTPBadRequest(explanation=e.msg)
   81         except (exception.ReservedProperty,
   82                 exception.ReadonlyProperty) as e:
   83             raise webob.exc.HTTPForbidden(explanation=e.msg)
   84         except exception.Forbidden as e:
   85             LOG.debug("User not permitted to create image")
   86             raise webob.exc.HTTPForbidden(explanation=e.msg)
   87         except exception.LimitExceeded as e:
   88             LOG.warn(encodeutils.exception_to_unicode(e))
   89             raise webob.exc.HTTPRequestEntityTooLarge(
   90                 explanation=e.msg, request=req, content_type='text/plain')
   91         except exception.Duplicate as e:
   92             raise webob.exc.HTTPConflict(explanation=e.msg)
   93         except exception.NotAuthenticated as e:
   94             raise webob.exc.HTTPUnauthorized(explanation=e.msg)
   95         except TypeError as e:
   96             LOG.debug(encodeutils.exception_to_unicode(e))
   97             raise webob.exc.HTTPBadRequest(explanation=e)
   98 
   99         return image
  100 
  101     @utils.mutating
  102     def import_image(self, req, image_id, body):
  103         image_repo = self.gateway.get_repo(req.context)
  104         task_factory = self.gateway.get_task_factory(req.context)
  105         executor_factory = self.gateway.get_task_executor_factory(req.context)
  106         task_repo = self.gateway.get_task_repo(req.context)
  107         import_method = body.get('method').get('name')
  108         uri = body.get('method').get('uri')
  109         all_stores_must_succeed = body.get('all_stores_must_succeed', True)
  110 
  111         try:
  112             image = image_repo.get(image_id)
  113             if image.status == 'active' and import_method != "copy-image":
  114                 msg = _("Image with status active cannot be target for import")
  115                 raise exception.Conflict(msg)
  116             if image.status != 'active' and import_method == "copy-image":
  117                 msg = _("Only images with status active can be targeted for "
  118                         "copying")
  119                 raise exception.Conflict(msg)
  120             if image.status != 'queued' and import_method == 'web-download':
  121                 msg = _("Image needs to be in 'queued' state to use "
  122                         "'web-download' method")
  123                 raise exception.Conflict(msg)
  124             if (image.status != 'uploading' and
  125                     import_method == 'glance-direct'):
  126                 msg = _("Image needs to be staged before 'glance-direct' "
  127                         "method can be used")
  128                 raise exception.Conflict(msg)
  129             if not getattr(image, 'container_format', None):
  130                 msg = _("'container_format' needs to be set before import")
  131                 raise exception.Conflict(msg)
  132             if not getattr(image, 'disk_format', None):
  133                 msg = _("'disk_format' needs to be set before import")
  134                 raise exception.Conflict(msg)
  135             if not authorization.is_image_mutable(req.context, image):
  136                 raise webob.exc.HTTPForbidden(
  137                     explanation=_("Operation not permitted"))
  138 
  139             stores = [None]
  140             if CONF.enabled_backends:
  141                 try:
  142                     stores = utils.get_stores_from_request(req, body)
  143                 except glance_store.UnknownScheme as exc:
  144                     LOG.warn(exc.msg)
  145                     raise exception.Conflict(exc.msg)
  146 
  147             # NOTE(abhishekk): If all_stores is specified and import_method is
  148             # copy_image, then remove those stores where image is already
  149             # present.
  150             all_stores = body.get('all_stores', False)
  151             if import_method == 'copy-image' and all_stores:
  152                 for loc in image.locations:
  153                     existing_store = loc['metadata']['store']
  154                     if existing_store in stores:
  155                         LOG.debug("Removing store '%s' from all stores as "
  156                                   "image is already available in that "
  157                                   "store." % existing_store)
  158                         stores.remove(existing_store)
  159 
  160                 if len(stores) == 0:
  161                     LOG.info(_LI("Exiting copying workflow as image is "
  162                                  "available in all configured stores."))
  163                     return image_id
  164 
  165             # validate if image is already existing in given stores when
  166             # all_stores is False
  167             if import_method == 'copy-image' and not all_stores:
  168                 for loc in image.locations:
  169                     existing_store = loc['metadata']['store']
  170                     if existing_store in stores:
  171                         msg = _("Image is already present at store "
  172                                 "'%s'") % existing_store
  173                         raise webob.exc.HTTPBadRequest(explanation=msg)
  174         except exception.Conflict as e:
  175             raise webob.exc.HTTPConflict(explanation=e.msg)
  176         except exception.NotFound as e:
  177             raise webob.exc.HTTPNotFound(explanation=e.msg)
  178 
  179         if (not all_stores_must_succeed) and (not CONF.enabled_backends):
  180             msg = (_("All_stores_must_succeed can only be set with "
  181                      "enabled_backends %s") % uri)
  182             raise webob.exc.HTTPBadRequest(explanation=msg)
  183 
  184         task_input = {'image_id': image_id,
  185                       'import_req': body,
  186                       'backend': stores}
  187 
  188         if (import_method == 'web-download' and
  189                 not utils.validate_import_uri(uri)):
  190             LOG.debug("URI for web-download does not pass filtering: %s", uri)
  191             msg = (_("URI for web-download does not pass filtering: %s") % uri)
  192             raise webob.exc.HTTPBadRequest(explanation=msg)
  193 
  194         try:
  195             import_task = task_factory.new_task(task_type='api_image_import',
  196                                                 owner=req.context.owner,
  197                                                 task_input=task_input)
  198             task_repo.add(import_task)
  199             task_executor = executor_factory.new_task_executor(req.context)
  200             pool = common.get_thread_pool("tasks_eventlet_pool")
  201             pool.spawn_n(import_task.run, task_executor)
  202         except exception.Forbidden as e:
  203             LOG.debug("User not permitted to create image import task.")
  204             raise webob.exc.HTTPForbidden(explanation=e.msg)
  205         except exception.Conflict as e:
  206             raise webob.exc.HTTPConflict(explanation=e.msg)
  207         except exception.InvalidImageStatusTransition as e:
  208             raise webob.exc.HTTPConflict(explanation=e.msg)
  209         except ValueError as e:
  210             LOG.debug("Cannot import data for image %(id)s: %(e)s",
  211                       {'id': image_id,
  212                        'e': encodeutils.exception_to_unicode(e)})
  213             raise webob.exc.HTTPBadRequest(
  214                 explanation=encodeutils.exception_to_unicode(e))
  215 
  216         return image_id
  217 
  218     def index(self, req, marker=None, limit=None, sort_key=None,
  219               sort_dir=None, filters=None, member_status='accepted'):
  220         sort_key = ['created_at'] if not sort_key else sort_key
  221 
  222         sort_dir = ['desc'] if not sort_dir else sort_dir
  223 
  224         result = {}
  225         if filters is None:
  226             filters = {}
  227         filters['deleted'] = False
  228 
  229         os_hidden = filters.get('os_hidden', 'false').lower()
  230         if os_hidden not in ['true', 'false']:
  231             message = _("Invalid value '%s' for 'os_hidden' filter."
  232                         " Valid values are 'true' or 'false'.") % os_hidden
  233             raise webob.exc.HTTPBadRequest(explanation=message)
  234         # ensure the type of os_hidden is boolean
  235         filters['os_hidden'] = os_hidden == 'true'
  236 
  237         protected = filters.get('protected')
  238         if protected is not None:
  239             if protected not in ['true', 'false']:
  240                 message = _("Invalid value '%s' for 'protected' filter."
  241                             " Valid values are 'true' or 'false'.") % protected
  242                 raise webob.exc.HTTPBadRequest(explanation=message)
  243             # ensure the type of protected is boolean
  244             filters['protected'] = protected == 'true'
  245 
  246         if limit is None:
  247             limit = CONF.limit_param_default
  248         limit = min(CONF.api_limit_max, limit)
  249 
  250         image_repo = self.gateway.get_repo(req.context)
  251         try:
  252             images = image_repo.list(marker=marker, limit=limit,
  253                                      sort_key=sort_key,
  254                                      sort_dir=sort_dir,
  255                                      filters=filters,
  256                                      member_status=member_status)
  257             if len(images) != 0 and len(images) == limit:
  258                 result['next_marker'] = images[-1].image_id
  259         except (exception.NotFound, exception.InvalidSortKey,
  260                 exception.InvalidFilterRangeValue,
  261                 exception.InvalidParameterValue,
  262                 exception.InvalidFilterOperatorValue) as e:
  263             raise webob.exc.HTTPBadRequest(explanation=e.msg)
  264         except exception.Forbidden as e:
  265             LOG.debug("User not permitted to retrieve images index")
  266             raise webob.exc.HTTPForbidden(explanation=e.msg)
  267         except exception.NotAuthenticated as e:
  268             raise webob.exc.HTTPUnauthorized(explanation=e.msg)
  269         result['images'] = images
  270         return result
  271 
  272     def show(self, req, image_id):
  273         image_repo = self.gateway.get_repo(req.context)
  274         try:
  275             return image_repo.get(image_id)
  276         except exception.Forbidden as e:
  277             LOG.debug("User not permitted to show image '%s'", image_id)
  278             raise webob.exc.HTTPForbidden(explanation=e.msg)
  279         except exception.NotFound as e:
  280             raise webob.exc.HTTPNotFound(explanation=e.msg)
  281         except exception.NotAuthenticated as e:
  282             raise webob.exc.HTTPUnauthorized(explanation=e.msg)
  283 
  284     @utils.mutating
  285     def update(self, req, image_id, changes):
  286         image_repo = self.gateway.get_repo(req.context)
  287         try:
  288             image = image_repo.get(image_id)
  289 
  290             for change in changes:
  291                 change_method_name = '_do_%s' % change['op']
  292                 change_method = getattr(self, change_method_name)
  293                 change_method(req, image, change)
  294 
  295             if changes:
  296                 image_repo.save(image)
  297         except exception.NotFound as e:
  298             raise webob.exc.HTTPNotFound(explanation=e.msg)
  299         except (exception.Invalid, exception.BadStoreUri) as e:
  300             raise webob.exc.HTTPBadRequest(explanation=e.msg)
  301         except exception.Forbidden as e:
  302             LOG.debug("User not permitted to update image '%s'", image_id)
  303             raise webob.exc.HTTPForbidden(explanation=e.msg)
  304         except exception.StorageQuotaFull as e:
  305             msg = (_("Denying attempt to upload image because it exceeds the"
  306                      " quota: %s") % encodeutils.exception_to_unicode(e))
  307             LOG.warn(msg)
  308             raise webob.exc.HTTPRequestEntityTooLarge(
  309                 explanation=msg, request=req, content_type='text/plain')
  310         except exception.LimitExceeded as e:
  311             LOG.exception(encodeutils.exception_to_unicode(e))
  312             raise webob.exc.HTTPRequestEntityTooLarge(
  313                 explanation=e.msg, request=req, content_type='text/plain')
  314         except exception.NotAuthenticated as e:
  315             raise webob.exc.HTTPUnauthorized(explanation=e.msg)
  316 
  317         return image
  318 
  319     def _do_replace(self, req, image, change):
  320         path = change['path']
  321         path_root = path[0]
  322         value = change['value']
  323         if path_root == 'locations' and not value:
  324             msg = _("Cannot set locations to empty list.")
  325             raise webob.exc.HTTPForbidden(msg)
  326         elif path_root == 'locations' and value:
  327             self._do_replace_locations(image, value)
  328         elif path_root == 'owner' and req.context.is_admin == False:
  329             msg = _("Owner can't be updated by non admin.")
  330             raise webob.exc.HTTPForbidden(msg)
  331         else:
  332             if hasattr(image, path_root):
  333                 setattr(image, path_root, value)
  334             elif path_root in image.extra_properties:
  335                 image.extra_properties[path_root] = value
  336             else:
  337                 msg = _("Property %s does not exist.")
  338                 raise webob.exc.HTTPConflict(msg % path_root)
  339 
  340     def _do_add(self, req, image, change):
  341         path = change['path']
  342         path_root = path[0]
  343         value = change['value']
  344         json_schema_version = change.get('json_schema_version', 10)
  345         if path_root == 'locations':
  346             self._do_add_locations(image, path[1], value)
  347         else:
  348             if ((hasattr(image, path_root) or
  349                     path_root in image.extra_properties)
  350                     and json_schema_version == 4):
  351                 msg = _("Property %s already present.")
  352                 raise webob.exc.HTTPConflict(msg % path_root)
  353             if hasattr(image, path_root):
  354                 setattr(image, path_root, value)
  355             else:
  356                 image.extra_properties[path_root] = value
  357 
  358     def _do_remove(self, req, image, change):
  359         path = change['path']
  360         path_root = path[0]
  361         if path_root == 'locations':
  362             try:
  363                 self._do_remove_locations(image, path[1])
  364             except exception.Forbidden as e:
  365                 raise webob.exc.HTTPForbidden(e.msg)
  366         else:
  367             if hasattr(image, path_root):
  368                 msg = _("Property %s may not be removed.")
  369                 raise webob.exc.HTTPForbidden(msg % path_root)
  370             elif path_root in image.extra_properties:
  371                 del image.extra_properties[path_root]
  372             else:
  373                 msg = _("Property %s does not exist.")
  374                 raise webob.exc.HTTPConflict(msg % path_root)
  375 
  376     def _delete_encryption_key(self, context, image):
  377         props = image.extra_properties
  378 
  379         cinder_encryption_key_id = props.get('cinder_encryption_key_id')
  380         if cinder_encryption_key_id is None:
  381             return
  382 
  383         deletion_policy = props.get('cinder_encryption_key_deletion_policy',
  384                                     '')
  385         if deletion_policy != 'on_image_deletion':
  386             return
  387 
  388         try:
  389             self._key_manager.delete(context, cinder_encryption_key_id)
  390         except castellan_exception.Forbidden:
  391             msg = ('Not allowed to delete encryption key %s' %
  392                    cinder_encryption_key_id)
  393             LOG.warn(msg)
  394         except (castellan_exception.ManagedObjectNotFoundError, KeyError):
  395             msg = 'Could not find encryption key %s' % cinder_encryption_key_id
  396             LOG.warn(msg)
  397         except castellan_exception.KeyManagerError:
  398             msg = ('Failed to delete cinder encryption key %s' %
  399                    cinder_encryption_key_id)
  400             LOG.warn(msg)
  401 
  402     @utils.mutating
  403     def delete_from_store(self, req, store_id, image_id):
  404         if not CONF.enabled_backends:
  405             raise webob.exc.HTTPNotFound()
  406         if store_id not in CONF.enabled_backends:
  407             msg = (_("The selected store %s is not available on this node.") %
  408                    store_id)
  409             raise webob.exc.HTTPConflict(explanation=msg)
  410 
  411         image_repo = self.gateway.get_repo(req.context)
  412         try:
  413             image = image_repo.get(image_id)
  414         except exception.NotAuthenticated as e:
  415             raise webob.exc.HTTPUnauthorized(explanation=e.msg)
  416         except exception.NotFound:
  417             msg = (_("Failed to find image %(image_id)s") %
  418                    {'image_id': image_id})
  419             raise webob.exc.HTTPNotFound(explanation=msg)
  420 
  421         if image.status != 'active':
  422             msg = _("It's not allowed to remove image data from store if "
  423                     "image status is not 'active'")
  424             raise webob.exc.HTTPConflict(explanation=msg)
  425 
  426         if len(image.locations) == 1:
  427             LOG.debug("User forbidden to remove last location of image %s",
  428                       image_id)
  429             msg = _("Cannot delete image data from the only store containing "
  430                     "it. Consider deleting the image instead.")
  431             raise webob.exc.HTTPForbidden(explanation=msg)
  432 
  433         try:
  434             # NOTE(jokke): Here we go through the locations list and act on
  435             # the first hit. image.locations.pop() will actually remove the
  436             # data from the backend as well as remove the location object
  437             # from the list.
  438             for pos, loc in enumerate(image.locations):
  439                 if loc['metadata'].get('store') == store_id:
  440                     image.locations.pop(pos)
  441                     break
  442             else:
  443                 msg = (_("Image %(iid)s is not stored in store %(sid)s.") %
  444                        {'iid': image_id, 'sid': store_id})
  445                 raise exception.Invalid(msg)
  446         except exception.Forbidden as e:
  447             raise webob.exc.HTTPForbidden(explanation=e.msg)
  448         except exception.Invalid as e:
  449             raise webob.exc.HTTPNotFound(explanation=e.msg)
  450         except glance_store.exceptions.HasSnapshot as e:
  451             raise webob.exc.HTTPConflict(explanation=e.msg)
  452         except glance_store.exceptions.InUseByStore as e:
  453             msg = ("The data for Image %(id)s could not be deleted "
  454                    "because it is in use: %(exc)s" % {"id": image_id,
  455                                                       "exc": e.msg})
  456             LOG.warning(msg)
  457             raise webob.exc.HTTPConflict(explanation=msg)
  458         except Exception as e:
  459             raise webob.exc.HTTPInternalServerError(
  460                 explanation=encodeutils.exception_to_unicode(e))
  461 
  462         image_repo.save(image)
  463 
  464     @utils.mutating
  465     def delete(self, req, image_id):
  466         image_repo = self.gateway.get_repo(req.context)
  467         try:
  468             image = image_repo.get(image_id)
  469             # NOTE(abhishekk): Delete the data from staging area
  470             if CONF.enabled_backends:
  471                 separator, staging_dir = store_utils.get_dir_separator()
  472                 file_path = "%s%s%s" % (staging_dir,
  473                                         separator,
  474                                         image_id)
  475                 try:
  476                     fn_call = glance_store.get_store_from_store_identifier
  477                     staging_store = fn_call('os_glance_staging_store')
  478                     loc = location.get_location_from_uri_and_backend(
  479                         file_path, 'os_glance_staging_store')
  480                     staging_store.delete(loc)
  481                 except (glance_store.exceptions.NotFound,
  482                         glance_store.exceptions.UnknownScheme):
  483                     pass
  484             else:
  485                 file_path = str(
  486                     CONF.node_staging_uri + '/' + image_id)[7:]
  487                 if os.path.exists(file_path):
  488                     try:
  489                         LOG.debug(
  490                             "After upload to the backend, deleting staged "
  491                             "image data from %(fn)s", {'fn': file_path})
  492                         os.unlink(file_path)
  493                     except OSError as e:
  494                         LOG.error(
  495                             "After upload to backend, deletion of staged "
  496                             "image data from %(fn)s has failed because "
  497                             "[Errno %(en)d]", {'fn': file_path,
  498                                                'en': e.errno})
  499                 else:
  500                     LOG.warning(_(
  501                         "After upload to backend, deletion of staged "
  502                         "image data has failed because "
  503                         "it cannot be found at %(fn)s"), {'fn': file_path})
  504 
  505             image.delete()
  506             self._delete_encryption_key(req.context, image)
  507             image_repo.remove(image)
  508         except (glance_store.Forbidden, exception.Forbidden) as e:
  509             LOG.debug("User not permitted to delete image '%s'", image_id)
  510             raise webob.exc.HTTPForbidden(explanation=e.msg)
  511         except (glance_store.NotFound, exception.NotFound):
  512             msg = (_("Failed to find image %(image_id)s to delete") %
  513                    {'image_id': image_id})
  514             LOG.warn(msg)
  515             raise webob.exc.HTTPNotFound(explanation=msg)
  516         except glance_store.exceptions.InUseByStore as e:
  517             msg = (_("Image %(id)s could not be deleted "
  518                      "because it is in use: %(exc)s") %
  519                    {"id": image_id,
  520                     "exc": e.msg})
  521             LOG.warn(msg)
  522             raise webob.exc.HTTPConflict(explanation=msg)
  523         except glance_store.exceptions.HasSnapshot as e:
  524             raise webob.exc.HTTPConflict(explanation=e.msg)
  525         except exception.InvalidImageStatusTransition as e:
  526             raise webob.exc.HTTPBadRequest(explanation=e.msg)
  527         except exception.NotAuthenticated as e:
  528             raise webob.exc.HTTPUnauthorized(explanation=e.msg)
  529 
  530     def _validate_validation_data(self, image, locations):
  531         val_data = {}
  532         for loc in locations:
  533             if 'validation_data' not in loc:
  534                 continue
  535             for k, v in loc['validation_data'].items():
  536                 if val_data.get(k, v) != v:
  537                     msg = _("Conflicting values for %s") % k
  538                     raise webob.exc.HTTPConflict(explanation=msg)
  539                 val_data[k] = v
  540 
  541         # NOTE(imacdonn): values may be provided for items which are
  542         # already set, so long as the values exactly match. In this
  543         # case, nothing actually needs to be updated, but we should
  544         # reject the request if there's an apparent attempt to supply
  545         # a different value.
  546         new_val_data = {}
  547         for k, v in val_data.items():
  548             current = getattr(image, k)
  549             if v == current:
  550                 continue
  551             if current:
  552                 msg = _("%s is already set with a different value") % k
  553                 raise webob.exc.HTTPConflict(explanation=msg)
  554             new_val_data[k] = v
  555 
  556         if not new_val_data:
  557             return {}
  558 
  559         if image.status != 'queued':
  560             msg = _("New value(s) for %s may only be provided when image "
  561                     "status is 'queued'") % ', '.join(new_val_data.keys())
  562             raise webob.exc.HTTPConflict(explanation=msg)
  563 
  564         if 'checksum' in new_val_data:
  565             try:
  566                 checksum_bytes = bytearray.fromhex(new_val_data['checksum'])
  567             except ValueError:
  568                 msg = (_("checksum (%s) is not a valid hexadecimal value") %
  569                        new_val_data['checksum'])
  570                 raise webob.exc.HTTPConflict(explanation=msg)
  571             if len(checksum_bytes) != 16:
  572                 msg = (_("checksum (%s) is not the correct size for md5 "
  573                          "(should be 16 bytes)") %
  574                        new_val_data['checksum'])
  575                 raise webob.exc.HTTPConflict(explanation=msg)
  576 
  577         hash_algo = new_val_data.get('os_hash_algo')
  578         if hash_algo != CONF['hashing_algorithm']:
  579             msg = (_("os_hash_algo must be %(want)s, not %(got)s") %
  580                    {'want': CONF['hashing_algorithm'], 'got': hash_algo})
  581             raise webob.exc.HTTPConflict(explanation=msg)
  582 
  583         try:
  584             hash_bytes = bytearray.fromhex(new_val_data['os_hash_value'])
  585         except ValueError:
  586             msg = (_("os_hash_value (%s) is not a valid hexadecimal value") %
  587                    new_val_data['os_hash_value'])
  588             raise webob.exc.HTTPConflict(explanation=msg)
  589         want_size = hashlib.new(hash_algo).digest_size
  590         if len(hash_bytes) != want_size:
  591             msg = (_("os_hash_value (%(value)s) is not the correct size for "
  592                      "%(algo)s (should be %(want)d bytes)") %
  593                    {'value': new_val_data['os_hash_value'],
  594                     'algo': hash_algo,
  595                     'want': want_size})
  596             raise webob.exc.HTTPConflict(explanation=msg)
  597 
  598         return new_val_data
  599 
  600     def _get_locations_op_pos(self, path_pos, max_pos, allow_max):
  601         if path_pos is None or max_pos is None:
  602             return None
  603         pos = max_pos if allow_max else max_pos - 1
  604         if path_pos.isdigit():
  605             pos = int(path_pos)
  606         elif path_pos != '-':
  607             return None
  608         if not (allow_max or 0 <= pos < max_pos):
  609             return None
  610         return pos
  611 
  612     def _do_replace_locations(self, image, value):
  613         if CONF.show_multiple_locations == False:
  614             msg = _("It's not allowed to update locations if locations are "
  615                     "invisible.")
  616             raise webob.exc.HTTPForbidden(explanation=msg)
  617 
  618         if image.status not in ('active', 'queued'):
  619             msg = _("It's not allowed to replace locations if image status is "
  620                     "%s.") % image.status
  621             raise webob.exc.HTTPConflict(explanation=msg)
  622 
  623         val_data = self._validate_validation_data(image, value)
  624         # NOTE(abhishekk): get glance store based on location uri
  625         updated_location = value
  626         if CONF.enabled_backends:
  627             updated_location = store_utils.get_updated_store_location(
  628                 value)
  629 
  630         try:
  631             # NOTE(flwang): _locations_proxy's setattr method will check if
  632             # the update is acceptable.
  633             image.locations = updated_location
  634             if image.status == 'queued':
  635                 for k, v in val_data.items():
  636                     setattr(image, k, v)
  637                 image.status = 'active'
  638         except (exception.BadStoreUri, exception.DuplicateLocation) as e:
  639             raise webob.exc.HTTPBadRequest(explanation=e.msg)
  640         except ValueError as ve:    # update image status failed.
  641             raise webob.exc.HTTPBadRequest(
  642                 explanation=encodeutils.exception_to_unicode(ve))
  643 
  644     def _do_add_locations(self, image, path_pos, value):
  645         if CONF.show_multiple_locations == False:
  646             msg = _("It's not allowed to add locations if locations are "
  647                     "invisible.")
  648             raise webob.exc.HTTPForbidden(explanation=msg)
  649 
  650         if image.status not in ('active', 'queued'):
  651             msg = _("It's not allowed to add locations if image status is "
  652                     "%s.") % image.status
  653             raise webob.exc.HTTPConflict(explanation=msg)
  654 
  655         val_data = self._validate_validation_data(image, [value])
  656         # NOTE(abhishekk): get glance store based on location uri
  657         updated_location = value
  658         if CONF.enabled_backends:
  659             updated_location = store_utils.get_updated_store_location(
  660                 [value])[0]
  661 
  662         pos = self._get_locations_op_pos(path_pos,
  663                                          len(image.locations), True)
  664         if pos is None:
  665             msg = _("Invalid position for adding a location.")
  666             raise webob.exc.HTTPBadRequest(explanation=msg)
  667         try:
  668             image.locations.insert(pos, updated_location)
  669             if image.status == 'queued':
  670                 for k, v in val_data.items():
  671                     setattr(image, k, v)
  672                 image.status = 'active'
  673         except (exception.BadStoreUri, exception.DuplicateLocation) as e:
  674             raise webob.exc.HTTPBadRequest(explanation=e.msg)
  675         except ValueError as e:    # update image status failed.
  676             raise webob.exc.HTTPBadRequest(
  677                 explanation=encodeutils.exception_to_unicode(e))
  678 
  679     def _do_remove_locations(self, image, path_pos):
  680         if CONF.show_multiple_locations == False:
  681             msg = _("It's not allowed to remove locations if locations are "
  682                     "invisible.")
  683             raise webob.exc.HTTPForbidden(explanation=msg)
  684 
  685         if image.status not in ('active'):
  686             msg = _("It's not allowed to remove locations if image status is "
  687                     "%s.") % image.status
  688             raise webob.exc.HTTPConflict(explanation=msg)
  689 
  690         if len(image.locations) == 1:
  691             LOG.debug("User forbidden to remove last location of image %s",
  692                       image.image_id)
  693             msg = _("Cannot remove last location in the image.")
  694             raise exception.Forbidden(msg)
  695         pos = self._get_locations_op_pos(path_pos,
  696                                          len(image.locations), False)
  697         if pos is None:
  698             msg = _("Invalid position for removing a location.")
  699             raise webob.exc.HTTPBadRequest(explanation=msg)
  700         try:
  701             # NOTE(zhiyan): this actually deletes the location
  702             # from the backend store.
  703             image.locations.pop(pos)
  704         # TODO(jokke): Fix this, we should catch what store throws and
  705         # provide definitely something else than IternalServerError to user.
  706         except Exception as e:
  707             raise webob.exc.HTTPInternalServerError(
  708                 explanation=encodeutils.exception_to_unicode(e))
  709 
  710 
  711 class RequestDeserializer(wsgi.JSONRequestDeserializer):
  712 
  713     _disallowed_properties = ('direct_url', 'self', 'file', 'schema', 'stores')
  714     _readonly_properties = ('created_at', 'updated_at', 'status', 'checksum',
  715                             'size', 'virtual_size', 'direct_url', 'self',
  716                             'file', 'schema', 'id', 'os_hash_algo',
  717                             'os_hash_value')
  718     _reserved_properties = ('location', 'deleted', 'deleted_at')
  719     _base_properties = ('checksum', 'created_at', 'container_format',
  720                         'disk_format', 'id', 'min_disk', 'min_ram', 'name',
  721                         'size', 'virtual_size', 'status', 'tags', 'owner',
  722                         'updated_at', 'visibility', 'protected', 'os_hidden')
  723     _available_sort_keys = ('name', 'status', 'container_format',
  724                             'disk_format', 'size', 'id', 'created_at',
  725                             'updated_at')
  726 
  727     _default_sort_key = 'created_at'
  728 
  729     _default_sort_dir = 'desc'
  730 
  731     _path_depth_limits = {'locations': {'add': 2, 'remove': 2, 'replace': 1}}
  732 
  733     _supported_operations = ('add', 'remove', 'replace')
  734 
  735     def __init__(self, schema=None):
  736         super(RequestDeserializer, self).__init__()
  737         self.schema = schema or get_schema()
  738 
  739     def _get_request_body(self, request):
  740         output = super(RequestDeserializer, self).default(request)
  741         if 'body' not in output:
  742             msg = _('Body expected in request.')
  743             raise webob.exc.HTTPBadRequest(explanation=msg)
  744         return output['body']
  745 
  746     @classmethod
  747     def _check_allowed(cls, image):
  748         for key in cls._disallowed_properties:
  749             if key in image:
  750                 msg = _("Attribute '%s' is read-only.") % key
  751                 raise webob.exc.HTTPForbidden(
  752                     explanation=six.text_type(msg))
  753 
  754     def create(self, request):
  755         body = self._get_request_body(request)
  756         self._check_allowed(body)
  757         try:
  758             self.schema.validate(body)
  759         except exception.InvalidObject as e:
  760             raise webob.exc.HTTPBadRequest(explanation=e.msg)
  761         image = {}
  762         properties = body
  763         tags = properties.pop('tags', [])
  764         for key in self._base_properties:
  765             try:
  766                 # NOTE(flwang): Instead of changing the _check_unexpected
  767                 # of ImageFactory. It would be better to do the mapping
  768                 # at here.
  769                 if key == 'id':
  770                     image['image_id'] = properties.pop(key)
  771                 else:
  772                     image[key] = properties.pop(key)
  773             except KeyError:
  774                 pass
  775 
  776         # NOTE(abhishekk): Check if custom property key name is less than 255
  777         # characters. Reference LP #1737952
  778         for key in properties:
  779             if len(key) > 255:
  780                 msg = (_("Custom property should not be greater than 255 "
  781                          "characters."))
  782                 raise webob.exc.HTTPBadRequest(explanation=msg)
  783 
  784         return dict(image=image, extra_properties=properties, tags=tags)
  785 
  786     def _get_change_operation_d10(self, raw_change):
  787         op = raw_change.get('op')
  788         if op is None:
  789             msg = (_('Unable to find `op` in JSON Schema change. '
  790                      'It must be one of the following: %(available)s.') %
  791                    {'available': ', '.join(self._supported_operations)})
  792             raise webob.exc.HTTPBadRequest(explanation=msg)
  793         if op not in self._supported_operations:
  794             msg = (_('Invalid operation: `%(op)s`. '
  795                      'It must be one of the following: %(available)s.') %
  796                    {'op': op,
  797                     'available': ', '.join(self._supported_operations)})
  798             raise webob.exc.HTTPBadRequest(explanation=msg)
  799         return op
  800 
  801     def _get_change_operation_d4(self, raw_change):
  802         op = None
  803         for key in self._supported_operations:
  804             if key in raw_change:
  805                 if op is not None:
  806                     msg = _('Operation objects must contain only one member'
  807                             ' named "add", "remove", or "replace".')
  808                     raise webob.exc.HTTPBadRequest(explanation=msg)
  809                 op = key
  810         if op is None:
  811             msg = _('Operation objects must contain exactly one member'
  812                     ' named "add", "remove", or "replace".')
  813             raise webob.exc.HTTPBadRequest(explanation=msg)
  814         return op
  815 
  816     def _get_change_path_d10(self, raw_change):
  817         try:
  818             return raw_change['path']
  819         except KeyError:
  820             msg = _("Unable to find '%s' in JSON Schema change") % 'path'
  821             raise webob.exc.HTTPBadRequest(explanation=msg)
  822 
  823     def _get_change_path_d4(self, raw_change, op):
  824         return raw_change[op]
  825 
  826     def _decode_json_pointer(self, pointer):
  827         """Parse a json pointer.
  828 
  829         Json Pointers are defined in
  830         http://tools.ietf.org/html/draft-pbryan-zyp-json-pointer .
  831         The pointers use '/' for separation between object attributes, such
  832         that '/A/B' would evaluate to C in {"A": {"B": "C"}}. A '/' character
  833         in an attribute name is encoded as "~1" and a '~' character is encoded
  834         as "~0".
  835         """
  836         self._validate_json_pointer(pointer)
  837         ret = []
  838         for part in pointer.lstrip('/').split('/'):
  839             ret.append(part.replace('~1', '/').replace('~0', '~').strip())
  840         return ret
  841 
  842     def _validate_json_pointer(self, pointer):
  843         """Validate a json pointer.
  844 
  845         We only accept a limited form of json pointers.
  846         """
  847         if not pointer.startswith('/'):
  848             msg = _('Pointer `%s` does not start with "/".') % pointer
  849             raise webob.exc.HTTPBadRequest(explanation=msg)
  850         if re.search(r'/\s*?/', pointer[1:]):
  851             msg = _('Pointer `%s` contains adjacent "/".') % pointer
  852             raise webob.exc.HTTPBadRequest(explanation=msg)
  853         if len(pointer) > 1 and pointer.endswith('/'):
  854             msg = _('Pointer `%s` end with "/".') % pointer
  855             raise webob.exc.HTTPBadRequest(explanation=msg)
  856         if pointer[1:].strip() == '/':
  857             msg = _('Pointer `%s` does not contains valid token.') % pointer
  858             raise webob.exc.HTTPBadRequest(explanation=msg)
  859         if re.search('~[^01]', pointer) or pointer.endswith('~'):
  860             msg = _('Pointer `%s` contains "~" not part of'
  861                     ' a recognized escape sequence.') % pointer
  862             raise webob.exc.HTTPBadRequest(explanation=msg)
  863 
  864     def _get_change_value(self, raw_change, op):
  865         if 'value' not in raw_change:
  866             msg = _('Operation "%s" requires a member named "value".')
  867             raise webob.exc.HTTPBadRequest(explanation=msg % op)
  868         return raw_change['value']
  869 
  870     def _validate_change(self, change):
  871         path_root = change['path'][0]
  872         if path_root in self._readonly_properties:
  873             msg = _("Attribute '%s' is read-only.") % path_root
  874             raise webob.exc.HTTPForbidden(explanation=six.text_type(msg))
  875         if path_root in self._reserved_properties:
  876             msg = _("Attribute '%s' is reserved.") % path_root
  877             raise webob.exc.HTTPForbidden(explanation=six.text_type(msg))
  878 
  879         if change['op'] == 'remove':
  880             return
  881 
  882         partial_image = None
  883         if len(change['path']) == 1:
  884             partial_image = {path_root: change['value']}
  885         elif ((path_root in get_base_properties().keys()) and
  886               (get_base_properties()[path_root].get('type', '') == 'array')):
  887             # NOTE(zhiyan): client can use the PATCH API to add an element
  888             # directly to an existing property
  889             # Such as: 1. using '/locations/N' path to add a location
  890             #             to the image's 'locations' list at position N.
  891             #             (implemented)
  892             #          2. using '/tags/-' path to append a tag to the
  893             #             image's 'tags' list at the end. (Not implemented)
  894             partial_image = {path_root: [change['value']]}
  895 
  896         if partial_image:
  897             try:
  898                 self.schema.validate(partial_image)
  899             except exception.InvalidObject as e:
  900                 raise webob.exc.HTTPBadRequest(explanation=e.msg)
  901 
  902     def _validate_path(self, op, path):
  903         path_root = path[0]
  904         limits = self._path_depth_limits.get(path_root, {})
  905         if len(path) != limits.get(op, 1):
  906             msg = _("Invalid JSON pointer for this resource: "
  907                     "'/%s'") % '/'.join(path)
  908             raise webob.exc.HTTPBadRequest(explanation=six.text_type(msg))
  909 
  910     def _parse_json_schema_change(self, raw_change, draft_version):
  911         if draft_version == 10:
  912             op = self._get_change_operation_d10(raw_change)
  913             path = self._get_change_path_d10(raw_change)
  914         elif draft_version == 4:
  915             op = self._get_change_operation_d4(raw_change)
  916             path = self._get_change_path_d4(raw_change, op)
  917         else:
  918             msg = _('Unrecognized JSON Schema draft version')
  919             raise webob.exc.HTTPBadRequest(explanation=msg)
  920 
  921         path_list = self._decode_json_pointer(path)
  922         return op, path_list
  923 
  924     def update(self, request):
  925         changes = []
  926         content_types = {
  927             'application/openstack-images-v2.0-json-patch': 4,
  928             'application/openstack-images-v2.1-json-patch': 10,
  929         }
  930         if request.content_type not in content_types:
  931             headers = {'Accept-Patch':
  932                        ', '.join(sorted(content_types.keys()))}
  933             raise webob.exc.HTTPUnsupportedMediaType(headers=headers)
  934 
  935         json_schema_version = content_types[request.content_type]
  936 
  937         body = self._get_request_body(request)
  938 
  939         if not isinstance(body, list):
  940             msg = _('Request body must be a JSON array of operation objects.')
  941             raise webob.exc.HTTPBadRequest(explanation=msg)
  942 
  943         for raw_change in body:
  944             if not isinstance(raw_change, dict):
  945                 msg = _('Operations must be JSON objects.')
  946                 raise webob.exc.HTTPBadRequest(explanation=msg)
  947 
  948             (op, path) = self._parse_json_schema_change(raw_change,
  949                                                         json_schema_version)
  950 
  951             # NOTE(zhiyan): the 'path' is a list.
  952             self._validate_path(op, path)
  953             change = {'op': op, 'path': path,
  954                       'json_schema_version': json_schema_version}
  955 
  956             if not op == 'remove':
  957                 change['value'] = self._get_change_value(raw_change, op)
  958 
  959             self._validate_change(change)
  960 
  961             changes.append(change)
  962 
  963         return {'changes': changes}
  964 
  965     def _validate_limit(self, limit):
  966         try:
  967             limit = int(limit)
  968         except ValueError:
  969             msg = _("limit param must be an integer")
  970             raise webob.exc.HTTPBadRequest(explanation=msg)
  971 
  972         if limit < 0:
  973             msg = _("limit param must be positive")
  974             raise webob.exc.HTTPBadRequest(explanation=msg)
  975 
  976         return limit
  977 
  978     def _validate_sort_key(self, sort_key):
  979         if sort_key not in self._available_sort_keys:
  980             msg = _('Invalid sort key: %(sort_key)s. '
  981                     'It must be one of the following: %(available)s.') % (
  982                 {'sort_key': sort_key,
  983                  'available': ', '.join(self._available_sort_keys)})
  984             raise webob.exc.HTTPBadRequest(explanation=msg)
  985 
  986         return sort_key
  987 
  988     def _validate_sort_dir(self, sort_dir):
  989         if sort_dir not in ['asc', 'desc']:
  990             msg = _('Invalid sort direction: %s') % sort_dir
  991             raise webob.exc.HTTPBadRequest(explanation=msg)
  992 
  993         return sort_dir
  994 
  995     def _validate_member_status(self, member_status):
  996         if member_status not in ['pending', 'accepted', 'rejected', 'all']:
  997             msg = _('Invalid status: %s') % member_status
  998             raise webob.exc.HTTPBadRequest(explanation=msg)
  999 
 1000         return member_status
 1001 
 1002     def _get_filters(self, filters):
 1003         visibility = filters.get('visibility')
 1004         if visibility:
 1005             if visibility not in ['community', 'public', 'private', 'shared',
 1006                                   'all']:
 1007                 msg = _('Invalid visibility value: %s') % visibility
 1008                 raise webob.exc.HTTPBadRequest(explanation=msg)
 1009         changes_since = filters.get('changes-since')
 1010         if changes_since:
 1011             msg = _('The "changes-since" filter is no longer available on v2.')
 1012             raise webob.exc.HTTPBadRequest(explanation=msg)
 1013 
 1014         return filters
 1015 
 1016     def _get_sorting_params(self, params):
 1017         """
 1018         Process sorting params.
 1019         Currently glance supports two sorting syntax: classic and new one,
 1020         that is uniform for all OpenStack projects.
 1021         Classic syntax: sort_key=name&sort_dir=asc&sort_key=size&sort_dir=desc
 1022         New syntax: sort=name:asc,size:desc
 1023         """
 1024         sort_keys = []
 1025         sort_dirs = []
 1026 
 1027         if 'sort' in params:
 1028             # use new sorting syntax here
 1029             if 'sort_key' in params or 'sort_dir' in params:
 1030                 msg = _('Old and new sorting syntax cannot be combined')
 1031                 raise webob.exc.HTTPBadRequest(explanation=msg)
 1032             for sort_param in params.pop('sort').strip().split(','):
 1033                 key, _sep, dir = sort_param.partition(':')
 1034                 if not dir:
 1035                     dir = self._default_sort_dir
 1036                 sort_keys.append(self._validate_sort_key(key.strip()))
 1037                 sort_dirs.append(self._validate_sort_dir(dir.strip()))
 1038         else:
 1039             # continue with classic syntax
 1040             # NOTE(mfedosin): we have 3 options here:
 1041             # 1. sort_dir wasn't passed: we use default one - 'desc'.
 1042             # 2. Only one sort_dir was passed: use it for every sort_key
 1043             # in the list.
 1044             # 3. Multiple sort_dirs were passed: consistently apply each one to
 1045             # the corresponding sort_key.
 1046             # If number of sort_dirs and sort_keys doesn't match then raise an
 1047             # exception.
 1048             while 'sort_key' in params:
 1049                 sort_keys.append(self._validate_sort_key(
 1050                     params.pop('sort_key').strip()))
 1051 
 1052             while 'sort_dir' in params:
 1053                 sort_dirs.append(self._validate_sort_dir(
 1054                     params.pop('sort_dir').strip()))
 1055 
 1056             if sort_dirs:
 1057                 dir_len = len(sort_dirs)
 1058                 key_len = len(sort_keys)
 1059 
 1060                 if dir_len > 1 and dir_len != key_len:
 1061                     msg = _('Number of sort dirs does not match the number '
 1062                             'of sort keys')
 1063                     raise webob.exc.HTTPBadRequest(explanation=msg)
 1064 
 1065         if not sort_keys:
 1066             sort_keys = [self._default_sort_key]
 1067 
 1068         if not sort_dirs:
 1069             sort_dirs = [self._default_sort_dir]
 1070 
 1071         return sort_keys, sort_dirs
 1072 
 1073     def index(self, request):
 1074         params = request.params.copy()
 1075         limit = params.pop('limit', None)
 1076         marker = params.pop('marker', None)
 1077         member_status = params.pop('member_status', 'accepted')
 1078 
 1079         # NOTE (flwang) To avoid using comma or any predefined chars to split
 1080         # multiple tags, now we allow user specify multiple 'tag' parameters
 1081         # in URL, such as v2/images?tag=x86&tag=64bit.
 1082         tags = []
 1083         while 'tag' in params:
 1084             tags.append(params.pop('tag').strip())
 1085 
 1086         query_params = {
 1087             'filters': self._get_filters(params),
 1088             'member_status': self._validate_member_status(member_status),
 1089         }
 1090 
 1091         if marker is not None:
 1092             query_params['marker'] = marker
 1093 
 1094         if limit is not None:
 1095             query_params['limit'] = self._validate_limit(limit)
 1096 
 1097         if tags:
 1098             query_params['filters']['tags'] = tags
 1099 
 1100         # NOTE(mfedosin): param is still called sort_key and sort_dir,
 1101         # instead of sort_keys and sort_dirs respectively.
 1102         # It's done because in v1 it's still a single value.
 1103 
 1104         query_params['sort_key'], query_params['sort_dir'] = (
 1105             self._get_sorting_params(params))
 1106 
 1107         return query_params
 1108 
 1109     def _validate_import_body(self, body):
 1110         # TODO(rosmaita): do schema validation of body instead
 1111         # of this ad-hoc stuff
 1112         try:
 1113             method = body['method']
 1114         except KeyError:
 1115             msg = _("Import request requires a 'method' field.")
 1116             raise webob.exc.HTTPBadRequest(explanation=msg)
 1117         try:
 1118             method_name = method['name']
 1119         except KeyError:
 1120             msg = _("Import request requires a 'name' field.")
 1121             raise webob.exc.HTTPBadRequest(explanation=msg)
 1122         if method_name not in CONF.enabled_import_methods:
 1123             msg = _("Unknown import method name '%s'.") % method_name
 1124             raise webob.exc.HTTPBadRequest(explanation=msg)
 1125 
 1126         # Validate 'all_stores_must_succeed' and 'all_stores'
 1127         all_stores_must_succeed = body.get('all_stores_must_succeed', True)
 1128         if not isinstance(all_stores_must_succeed, bool):
 1129             msg = (_("'all_stores_must_succeed' must be boolean value only"))
 1130             raise webob.exc.HTTPBadRequest(explanation=msg)
 1131 
 1132         all_stores = body.get('all_stores', False)
 1133         if not isinstance(all_stores, bool):
 1134             msg = (_("'all_stores' must be boolean value only"))
 1135             raise webob.exc.HTTPBadRequest(explanation=msg)
 1136 
 1137     def import_image(self, request):
 1138         body = self._get_request_body(request)
 1139         self._validate_import_body(body)
 1140         return {'body': body}
 1141 
 1142 
 1143 class ResponseSerializer(wsgi.JSONResponseSerializer):
 1144     def __init__(self, schema=None):
 1145         super(ResponseSerializer, self).__init__()
 1146         self.schema = schema or get_schema()
 1147 
 1148     def _get_image_href(self, image, subcollection=''):
 1149         base_href = '/v2/images/%s' % image.image_id
 1150         if subcollection:
 1151             base_href = '%s/%s' % (base_href, subcollection)
 1152         return base_href
 1153 
 1154     def _format_image(self, image):
 1155 
 1156         def _get_image_locations(image):
 1157             try:
 1158                 return list(image.locations)
 1159             except exception.Forbidden:
 1160                 return []
 1161 
 1162         try:
 1163             image_view = dict(image.extra_properties)
 1164             attributes = ['name', 'disk_format', 'container_format',
 1165                           'visibility', 'size', 'virtual_size', 'status',
 1166                           'checksum', 'protected', 'min_ram', 'min_disk',
 1167                           'owner', 'os_hidden', 'os_hash_algo',
 1168                           'os_hash_value']
 1169             for key in attributes:
 1170                 image_view[key] = getattr(image, key)
 1171             image_view['id'] = image.image_id
 1172             image_view['created_at'] = timeutils.isotime(image.created_at)
 1173             image_view['updated_at'] = timeutils.isotime(image.updated_at)
 1174 
 1175             if CONF.show_multiple_locations:
 1176                 locations = _get_image_locations(image)
 1177                 if locations:
 1178                     image_view['locations'] = []
 1179                     for loc in locations:
 1180                         tmp = dict(loc)
 1181                         tmp.pop('id', None)
 1182                         tmp.pop('status', None)
 1183                         image_view['locations'].append(tmp)
 1184                 else:
 1185                     # NOTE (flwang): We will still show "locations": [] if
 1186                     # image.locations is None to indicate it's allowed to show
 1187                     # locations but it's just non-existent.
 1188                     image_view['locations'] = []
 1189                     LOG.debug("The 'locations' list of image %s is empty",
 1190                               image.image_id)
 1191 
 1192             if CONF.show_image_direct_url:
 1193                 locations = _get_image_locations(image)
 1194                 if locations:
 1195                     # Choose best location configured strategy
 1196                     loc = location_strategy.choose_best_location(locations)
 1197                     image_view['direct_url'] = loc['url']
 1198                 else:
 1199                     LOG.debug("The 'locations' list of image %s is empty, "
 1200                               "not including 'direct_url' in response",
 1201                               image.image_id)
 1202 
 1203             image_view['tags'] = list(image.tags)
 1204             image_view['self'] = self._get_image_href(image)
 1205             image_view['file'] = self._get_image_href(image, 'file')
 1206             image_view['schema'] = '/v2/schemas/image'
 1207             image_view = self.schema.filter(image_view)  # domain
 1208 
 1209             # add store information to image
 1210             if CONF.enabled_backends:
 1211                 locations = _get_image_locations(image)
 1212                 if locations:
 1213                     stores = []
 1214                     for loc in locations:
 1215                         backend = loc['metadata'].get('store')
 1216                         if backend:
 1217                             stores.append(backend)
 1218 
 1219                     if stores:
 1220                         image_view['stores'] = ",".join(stores)
 1221 
 1222             return image_view
 1223         except exception.Forbidden as e:
 1224             raise webob.exc.HTTPForbidden(explanation=e.msg)
 1225 
 1226     def create(self, response, image):
 1227         response.status_int = http.CREATED
 1228         self.show(response, image)
 1229         response.location = self._get_image_href(image)
 1230         # according to RFC7230, headers should not have empty fields
 1231         # see http://httpwg.org/specs/rfc7230.html#field.components
 1232         if CONF.enabled_import_methods:
 1233             import_methods = ("OpenStack-image-import-methods",
 1234                               ','.join(CONF.enabled_import_methods))
 1235             response.headerlist.append(import_methods)
 1236 
 1237         if CONF.enabled_backends:
 1238             enabled_backends = ("OpenStack-image-store-ids",
 1239                                 ','.join(CONF.enabled_backends.keys()))
 1240             response.headerlist.append(enabled_backends)
 1241 
 1242     def show(self, response, image):
 1243         image_view = self._format_image(image)
 1244         body = json.dumps(image_view, ensure_ascii=False)
 1245         response.unicode_body = six.text_type(body)
 1246         response.content_type = 'application/json'
 1247 
 1248     def update(self, response, image):
 1249         image_view = self._format_image(image)
 1250         body = json.dumps(image_view, ensure_ascii=False)
 1251         response.unicode_body = six.text_type(body)
 1252         response.content_type = 'application/json'
 1253 
 1254     def index(self, response, result):
 1255         params = dict(response.request.params)
 1256         params.pop('marker', None)
 1257         query = urlparse.urlencode(params)
 1258         body = {
 1259             'images': [self._format_image(i) for i in result['images']],
 1260             'first': '/v2/images',
 1261             'schema': '/v2/schemas/images',
 1262         }
 1263         if query:
 1264             body['first'] = '%s?%s' % (body['first'], query)
 1265         if 'next_marker' in result:
 1266             params['marker'] = result['next_marker']
 1267             next_query = urlparse.urlencode(params)
 1268             body['next'] = '/v2/images?%s' % next_query
 1269         response.unicode_body = six.text_type(json.dumps(body,
 1270                                                          ensure_ascii=False))
 1271         response.content_type = 'application/json'
 1272 
 1273     def delete_from_store(self, response, result):
 1274         response.status_int = http.NO_CONTENT
 1275 
 1276     def delete(self, response, result):
 1277         response.status_int = http.NO_CONTENT
 1278 
 1279     def import_image(self, response, result):
 1280         response.status_int = http.ACCEPTED
 1281 
 1282 
 1283 def get_base_properties():
 1284     return {
 1285         'id': {
 1286             'type': 'string',
 1287             'description': _('An identifier for the image'),
 1288             'pattern': ('^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}'
 1289                         '-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$'),
 1290         },
 1291         'name': {
 1292             'type': ['null', 'string'],
 1293             'description': _('Descriptive name for the image'),
 1294             'maxLength': 255,
 1295         },
 1296         'status': {
 1297             'type': 'string',
 1298             'readOnly': True,
 1299             'description': _('Status of the image'),
 1300             'enum': ['queued', 'saving', 'active', 'killed',
 1301                      'deleted', 'uploading', 'importing',
 1302                      'pending_delete', 'deactivated'],
 1303         },
 1304         'visibility': {
 1305             'type': 'string',
 1306             'description': _('Scope of image accessibility'),
 1307             'enum': ['community', 'public', 'private', 'shared'],
 1308         },
 1309         'protected': {
 1310             'type': 'boolean',
 1311             'description': _('If true, image will not be deletable.'),
 1312         },
 1313         'os_hidden': {
 1314             'type': 'boolean',
 1315             'description': _('If true, image will not appear in default '
 1316                              'image list response.'),
 1317         },
 1318         'checksum': {
 1319             'type': ['null', 'string'],
 1320             'readOnly': True,
 1321             'description': _('md5 hash of image contents.'),
 1322             'maxLength': 32,
 1323         },
 1324         'os_hash_algo': {
 1325             'type': ['null', 'string'],
 1326             'readOnly': True,
 1327             'description': _('Algorithm to calculate the os_hash_value'),
 1328             'maxLength': 64,
 1329         },
 1330         'os_hash_value': {
 1331             'type': ['null', 'string'],
 1332             'readOnly': True,
 1333             'description': _('Hexdigest of the image contents using the '
 1334                              'algorithm specified by the os_hash_algo'),
 1335             'maxLength': 128,
 1336         },
 1337         'owner': {
 1338             'type': ['null', 'string'],
 1339             'description': _('Owner of the image'),
 1340             'maxLength': 255,
 1341         },
 1342         'size': {
 1343             'type': ['null', 'integer'],
 1344             'readOnly': True,
 1345             'description': _('Size of image file in bytes'),
 1346         },
 1347         'virtual_size': {
 1348             'type': ['null', 'integer'],
 1349             'readOnly': True,
 1350             'description': _('Virtual size of image in bytes'),
 1351         },
 1352         'container_format': {
 1353             'type': ['null', 'string'],
 1354             'description': _('Format of the container'),
 1355             'enum': [None] + CONF.image_format.container_formats,
 1356         },
 1357         'disk_format': {
 1358             'type': ['null', 'string'],
 1359             'description': _('Format of the disk'),
 1360             'enum': [None] + CONF.image_format.disk_formats,
 1361         },
 1362         'created_at': {
 1363             'type': 'string',
 1364             'readOnly': True,
 1365             'description': _('Date and time of image registration'
 1366                              ),
 1367             # TODO(bcwaldon): our jsonschema library doesn't seem to like the
 1368             # format attribute, figure out why!
 1369             # 'format': 'date-time',
 1370         },
 1371         'updated_at': {
 1372             'type': 'string',
 1373             'readOnly': True,
 1374             'description': _('Date and time of the last image modification'
 1375                              ),
 1376             # 'format': 'date-time',
 1377         },
 1378         'tags': {
 1379             'type': 'array',
 1380             'description': _('List of strings related to the image'),
 1381             'items': {
 1382                 'type': 'string',
 1383                 'maxLength': 255,
 1384             },
 1385         },
 1386         'direct_url': {
 1387             'type': 'string',
 1388             'readOnly': True,
 1389             'description': _('URL to access the image file kept in external '
 1390                              'store'),
 1391         },
 1392         'min_ram': {
 1393             'type': 'integer',
 1394             'description': _('Amount of ram (in MB) required to boot image.'),
 1395         },
 1396         'min_disk': {
 1397             'type': 'integer',
 1398             'description': _('Amount of disk space (in GB) required to boot '
 1399                              'image.'),
 1400         },
 1401         'self': {
 1402             'type': 'string',
 1403             'readOnly': True,
 1404             'description': _('An image self url'),
 1405         },
 1406         'file': {
 1407             'type': 'string',
 1408             'readOnly': True,
 1409             'description': _('An image file url'),
 1410         },
 1411         'stores': {
 1412             'type': 'string',
 1413             'readOnly': True,
 1414             'description': _('Store in which image data resides.  Only '
 1415                              'present when the operator has enabled multiple '
 1416                              'stores.  May be a comma-separated list of store '
 1417                              'identifiers.'),
 1418         },
 1419         'schema': {
 1420             'type': 'string',
 1421             'readOnly': True,
 1422             'description': _('An image schema url'),
 1423         },
 1424         'locations': {
 1425             'type': 'array',
 1426             'items': {
 1427                 'type': 'object',
 1428                 'properties': {
 1429                     'url': {
 1430                         'type': 'string',
 1431                         'maxLength': 255,
 1432                     },
 1433                     'metadata': {
 1434                         'type': 'object',
 1435                     },
 1436                     'validation_data': {
 1437                         'description': _(
 1438                             'Values to be used to populate the corresponding '
 1439                             'image properties. If the image status is not '
 1440                             '\'queued\', values must exactly match those '
 1441                             'already contained in the image properties.'
 1442                         ),
 1443                         'type': 'object',
 1444                         'writeOnly': True,
 1445                         'additionalProperties': False,
 1446                         'properties': {
 1447                             'checksum': {
 1448                                 'type': 'string',
 1449                                 'minLength': 32,
 1450                                 'maxLength': 32,
 1451                             },
 1452                             'os_hash_algo': {
 1453                                 'type': 'string',
 1454                                 'maxLength': 64,
 1455                             },
 1456                             'os_hash_value': {
 1457                                 'type': 'string',
 1458                                 'maxLength': 128,
 1459                             },
 1460                         },
 1461                         'required': [
 1462                             'os_hash_algo',
 1463                             'os_hash_value',
 1464                         ],
 1465                     },
 1466                 },
 1467                 'required': ['url', 'metadata'],
 1468             },
 1469             'description': _('A set of URLs to access the image file kept in '
 1470                              'external store'),
 1471         },
 1472     }
 1473 
 1474 
 1475 def _get_base_links():
 1476     return [
 1477         {'rel': 'self', 'href': '{self}'},
 1478         {'rel': 'enclosure', 'href': '{file}'},
 1479         {'rel': 'describedby', 'href': '{schema}'},
 1480     ]
 1481 
 1482 
 1483 def get_schema(custom_properties=None):
 1484     properties = get_base_properties()
 1485     links = _get_base_links()
 1486     if CONF.allow_additional_image_properties:
 1487         schema = glance.schema.PermissiveSchema('image', properties, links)
 1488     else:
 1489         schema = glance.schema.Schema('image', properties)
 1490 
 1491     if custom_properties:
 1492         for property_value in custom_properties.values():
 1493             property_value['is_base'] = False
 1494         schema.merge_properties(custom_properties)
 1495     return schema
 1496 
 1497 
 1498 def get_collection_schema(custom_properties=None):
 1499     image_schema = get_schema(custom_properties)
 1500     return glance.schema.CollectionSchema('images', image_schema)
 1501 
 1502 
 1503 def load_custom_properties():
 1504     """Find the schema properties files and load them into a dict."""
 1505     filename = 'schema-image.json'
 1506     match = CONF.find_file(filename)
 1507     if match:
 1508         with open(match, 'r') as schema_file:
 1509             schema_data = schema_file.read()
 1510         return json.loads(schema_data)
 1511     else:
 1512         msg = (_LW('Could not find schema properties file %s. Continuing '
 1513                    'without custom properties') % filename)
 1514         LOG.warn(msg)
 1515         return {}
 1516 
 1517 
 1518 def create_resource(custom_properties=None):
 1519     """Images resource factory method"""
 1520     schema = get_schema(custom_properties)
 1521     deserializer = RequestDeserializer(schema)
 1522     serializer = ResponseSerializer(schema)
 1523     controller = ImagesController()
 1524     return wsgi.Resource(controller, deserializer, serializer)