glance/glance/api/v2/image_data.py

160 lines
6.2 KiB
Python

# 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 webob.exc
from glance.api import common
from glance.api import policy
import glance.api.v2 as v2
from glance.common import exception
from glance.common import utils
from glance.common import wsgi
import glance.db
import glance.notifier
import glance.openstack.common.log as logging
import glance.store
LOG = logging.getLogger(__name__)
class ImageDataController(object):
def __init__(self, db_api=None, store_api=None,
policy_enforcer=None, notifier=None):
self.db_api = db_api or glance.db.get_api()
self.db_api.setup_db_env()
self.store_api = store_api or glance.store
self.policy = policy_enforcer or policy.Enforcer()
self.notifier = notifier or glance.notifier.Notifier()
def _get_image(self, context, image_id):
try:
return self.db_api.image_get(context, image_id)
except exception.NotFound:
raise webob.exc.HTTPNotFound(_("Image does not exist"))
def _enforce(self, req, action):
"""Authorize an action against our policies"""
try:
self.policy.enforce(req.context, action, {})
except exception.Forbidden:
raise webob.exc.HTTPForbidden()
@utils.mutating
def upload(self, req, image_id, data, size):
try:
image = self._get_image(req.context, image_id)
location, size, checksum = self.store_api.add_to_backend(
req.context, 'file', image_id, data, size)
except exception.Duplicate, e:
msg = _("Unable to upload duplicate image data for image: %s")
LOG.debug(msg % image_id)
raise webob.exc.HTTPConflict(explanation=msg, request=req)
except exception.Forbidden, e:
msg = _("Not allowed to upload image data for image %s")
LOG.debug(msg % image_id)
raise webob.exc.HTTPForbidden(explanation=msg, request=req)
except exception.StorageFull, e:
msg = _("Image storage media is full: %s") % e
LOG.error(msg)
raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg,
request=req)
except exception.StorageWriteDenied, e:
msg = _("Insufficient permissions on image storage media: %s") % e
LOG.error(msg)
raise webob.exc.HTTPServiceUnavailable(explanation=msg,
request=req)
except webob.exc.HTTPError, e:
LOG.error(_("Failed to upload image data due to HTTP error"))
raise
except Exception, e:
LOG.exception(_("Failed to upload image data due to "
"internal error"))
raise
else:
v2.update_image_read_acl(req, self.store_api, self.db_api, image)
values = {'location': location, 'size': size, 'checksum': checksum,
'status': 'active'}
self.db_api.image_update(req.context, image_id, values)
updated_image = self._get_image(req.context, image_id)
self.notifier.info('image.upload', updated_image)
def download(self, req, image_id):
self._enforce(req, 'download_image')
ctx = req.context
image = self._get_image(ctx, image_id)
location = image['location']
if location:
image_data, image_size = self.store_api.get_from_backend(ctx,
location)
#NOTE(bcwaldon): This is done to match the behavior of the v1 API.
# The store should always return a size that matches what we have
# in the database. If the store says otherwise, that's a security
# risk.
if image_size is not None:
image['size'] = int(image_size)
return {'data': image_data, 'meta': image}
else:
raise webob.exc.HTTPNotFound(_("No image data could be found"))
class RequestDeserializer(wsgi.JSONRequestDeserializer):
def upload(self, request):
try:
request.get_content_type('application/octet-stream')
except exception.InvalidContentType:
raise webob.exc.HTTPUnsupportedMediaType()
image_size = request.content_length or None
return {'size': image_size, 'data': request.body_file}
class ResponseSerializer(wsgi.JSONResponseSerializer):
def __init__(self, notifier=None):
super(ResponseSerializer, self).__init__()
self.notifier = notifier or glance.notifier.Notifier()
def download(self, response, result):
size = result['meta']['size']
checksum = result['meta']['checksum']
response.headers['Content-Type'] = 'application/octet-stream'
response.app_iter = common.size_checked_iter(
response, result['meta'], size, result['data'], self.notifier)
#NOTE(saschpe): "response.app_iter = ..." currently resets Content-MD5
# (https://github.com/Pylons/webob/issues/86), so it should be set
# afterwards for the time being.
if checksum:
response.headers['Content-MD5'] = checksum
#NOTE(markwash): "response.app_iter = ..." also erroneously resets the
# content-length
response.headers['Content-Length'] = str(size)
def upload(self, response, result):
response.status_int = 201
def create_resource():
"""Image data resource factory method"""
deserializer = RequestDeserializer()
serializer = ResponseSerializer()
controller = ImageDataController()
return wsgi.Resource(controller, deserializer, serializer)