160 lines
6.2 KiB
Python
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)
|