# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010-2011 OpenStack LLC. # 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 logging import webob.exc from glance.common import exception from glance.common import utils from glance.common import wsgi from glance.db.sqlalchemy import api as db_api logger = logging.getLogger('glance.registry.api.v1.members') class Controller(object): def __init__(self, conf): self.conf = conf db_api.configure_db(conf) def index(self, req, image_id): """ Get the members of an image. """ try: image = db_api.image_get(req.context, image_id) except exception.NotFound: raise webob.exc.HTTPNotFound() except exception.Forbidden: # If it's private and doesn't belong to them, don't let on # that it exists msg = _("Access by %(user)s to image %(id)s " "denied") % ({'user': req.context.user, 'id': image_id}) logger.info(msg) raise webob.exc.HTTPNotFound() return dict(members=make_member_list(image['members'], member_id='member', can_share='can_share')) @utils.mutating def update_all(self, req, image_id, body): """ Replaces the members of the image with those specified in the body. The body is a dict with the following format:: {"memberships": [ {"member_id": , ["can_share": [True|False]]}, ... ]} """ if req.context.owner is None: raise webob.exc.HTTPUnauthorized(_("No authenticated user")) # Make sure the image exists session = db_api.get_session() try: image = db_api.image_get(req.context, image_id, session=session) except exception.NotFound: raise webob.exc.HTTPNotFound() except exception.Forbidden: # If it's private and doesn't belong to them, don't let on # that it exists msg = _("Access by %(user)s to image %(id)s " "denied") % ({'user': req.context.user, 'id': image_id}) logger.info(msg) raise webob.exc.HTTPNotFound() # Can they manipulate the membership? if not req.context.is_image_sharable(image): msg = _("No permission to share that image") raise webob.exc.HTTPForbidden(msg) # Get the membership list try: memb_list = body['memberships'] except Exception, e: # Malformed entity... msg = _("Invalid membership association: %s") % e raise webob.exc.HTTPBadRequest(explanation=msg) add = [] existing = {} # Walk through the incoming memberships for memb in memb_list: try: datum = dict(image_id=image['id'], member=memb['member_id'], can_share=None) except Exception, e: # Malformed entity... msg = _("Invalid membership association: %s") % e raise webob.exc.HTTPBadRequest(explanation=msg) # Figure out what can_share should be if 'can_share' in memb: datum['can_share'] = bool(memb['can_share']) # Try to find the corresponding membership try: membership = db_api.image_member_find(req.context, datum['image_id'], datum['member'], session=session) # Are we overriding can_share? if datum['can_share'] is None: datum['can_share'] = membership['can_share'] existing[membership['id']] = { 'values': datum, 'membership': membership, } except exception.NotFound: # Default can_share datum['can_share'] = bool(datum['can_share']) add.append(datum) # We now have a filtered list of memberships to add and # memberships to modify. Let's start by walking through all # the existing image memberships... for memb in image['members']: if memb['id'] in existing: # Just update the membership in place update = existing[memb['id']]['values'] db_api.image_member_update(req.context, memb, update, session=session) else: # Outdated one; needs to be deleted db_api.image_member_delete(req.context, memb, session=session) # Now add the non-existant ones for memb in add: db_api.image_member_create(req.context, memb, session=session) # Make an appropriate result return webob.exc.HTTPNoContent() @utils.mutating def update(self, req, image_id, id, body=None): """ Adds a membership to the image, or updates an existing one. If a body is present, it is a dict with the following format:: {"member": { "can_share": [True|False] }} If "can_share" is provided, the member's ability to share is set accordingly. If it is not provided, existing memberships remain unchanged and new memberships default to False. """ if req.context.owner is None: raise webob.exc.HTTPUnauthorized(_("No authenticated user")) # Make sure the image exists try: image = db_api.image_get(req.context, image_id) except exception.NotFound: raise webob.exc.HTTPNotFound() except exception.Forbidden: # If it's private and doesn't belong to them, don't let on # that it exists msg = _("Access by %(user)s to image %(id)s " "denied") % ({'user': req.context.user, 'id': image_id}) logger.info(msg) raise webob.exc.HTTPNotFound() # Can they manipulate the membership? if not req.context.is_image_sharable(image): msg = _("No permission to share that image") raise webob.exc.HTTPForbidden(msg) # Determine the applicable can_share value can_share = None if body: try: can_share = bool(body['member']['can_share']) except Exception, e: # Malformed entity... msg = _("Invalid membership association: %s") % e raise webob.exc.HTTPBadRequest(explanation=msg) # Look up an existing membership... try: session = db_api.get_session() membership = db_api.image_member_find(req.context, image_id, id, session=session) if can_share is not None: values = dict(can_share=can_share) db_api.image_member_update(req.context, membership, values, session=session) except exception.NotFound: values = dict(image_id=image['id'], member=id, can_share=bool(can_share)) db_api.image_member_create(req.context, values, session=session) return webob.exc.HTTPNoContent() @utils.mutating def delete(self, req, image_id, id): """ Removes a membership from the image. """ if req.context.owner is None: raise webob.exc.HTTPUnauthorized(_("No authenticated user")) # Make sure the image exists try: image = db_api.image_get(req.context, image_id) except exception.NotFound: raise webob.exc.HTTPNotFound() except exception.Forbidden: # If it's private and doesn't belong to them, don't let on # that it exists msg = _("Access by %(user)s to image %(id)s " "denied") % ({'user': req.context.user, 'id': image_id}) logger.info(msg) raise webob.exc.HTTPNotFound() # Can they manipulate the membership? if not req.context.is_image_sharable(image): msg = _("No permission to share that image") raise webob.exc.HTTPForbidden(msg) # Look up an existing membership try: session = db_api.get_session() member_ref = db_api.image_member_find(req.context, image_id, id, session=session) db_api.image_member_delete(req.context, member_ref, session=session) except exception.NotFound: pass # Make an appropriate result return webob.exc.HTTPNoContent() def index_shared_images(self, req, id): """ Retrieves images shared with the given member. """ params = {} try: memberships = db_api.image_member_get_memberships(req.context, id, **params) except exception.NotFound, e: msg = _("Invalid marker. Membership could not be found.") raise webob.exc.HTTPBadRequest(explanation=msg) return dict(shared_images=make_member_list(memberships, image_id='image_id', can_share='can_share')) def make_member_list(members, **attr_map): """ Create a dict representation of a list of members which we can use to serialize the members list. Keyword arguments map the names of optional attributes to include to the database attribute. """ def _fetch_memb(memb, attr_map): return dict([(k, memb[v]) for k, v in attr_map.items() if v in memb.keys()]) # Return the list of members with the given attribute mapping return [_fetch_memb(memb, attr_map) for memb in members if not memb.deleted] def create_resource(conf): """Image members resource factory method.""" deserializer = wsgi.JSONRequestDeserializer() serializer = wsgi.JSONResponseSerializer() return wsgi.Resource(Controller(conf), deserializer, serializer)