Merge "Implement image data upload/download for v2 API"

This commit is contained in:
Jenkins 2012-05-08 02:14:45 +00:00 committed by Gerrit Code Review
commit 5e85329ed1
8 changed files with 283 additions and 17 deletions

View File

@ -0,0 +1,78 @@
# Copyright 2012 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 webob.exc
from glance.api.v2 import base
from glance.common import exception
from glance.common import wsgi
import glance.registry.db.api
import glance.store
import glance.store.filesystem
import glance.store.http
import glance.store.rbd
import glance.store.s3
import glance.store.swift
class ImageDataController(base.Controller):
def __init__(self, conf, db_api=None, store_api=None):
super(ImageDataController, self).__init__(conf)
self.db_api = db_api or glance.registry.db.api
self.db_api.configure_db(conf)
self.store_api = store_api or glance.store
self.store_api.create_stores(conf)
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 upload(self, req, image_id, data):
self._get_image(req.context, image_id)
size = len(data)
location, size, checksum = self.store_api.add_to_backend(
'file', image_id, data, size)
self.db_api.image_update(req.context, image_id, {'location': location})
def download(self, req, image_id):
image = self._get_image(req.context, image_id)
location = image['location']
if location:
image_data, image_size = self.store_api.get_from_backend(location)
return {'data': image_data, 'size': image_size}
else:
raise webob.exc.HTTPNotFound(_("No image data could be found"))
class RequestDeserializer(wsgi.JSONRequestDeserializer):
def upload(self, request):
return {'data': request.body}
class ResponseSerializer(wsgi.JSONResponseSerializer):
def download(self, response, result):
response.headers['Content-Length'] = result['size']
response.headers['Content-Type'] = 'application/octet-stream'
response.app_iter = result['data']
def create_resource(conf):
"""Image data resource factory method"""
deserializer = RequestDeserializer()
serializer = ResponseSerializer()
controller = ImageDataController(conf)
return wsgi.Resource(controller, deserializer, serializer)

View File

@ -26,9 +26,9 @@ import glance.registry.db.api
class ImagesController(base.Controller):
def __init__(self, conf, db=None):
def __init__(self, conf, db_api=None):
super(ImagesController, self).__init__(conf)
self.db_api = db or glance.registry.db.api
self.db_api = db_api or glance.registry.db.api
self.db_api.configure_db(conf)
def create(self, req, image):
@ -86,17 +86,20 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
body = output.pop('body')
self._validate(request, body)
output['image'] = body
return output
class ResponseSerializer(wsgi.JSONResponseSerializer):
def _get_image_href(self, image):
return '/v2/images/%s' % image['id']
def _get_image_href(self, image, subcollection=''):
base_href = '/v2/images/%s' % image['id']
if subcollection:
base_href = '%s/%s' % (base_href, subcollection)
return base_href
def _get_image_links(self, image):
return [
{'rel': 'self', 'href': self._get_image_href(image)},
{'rel': 'file', 'href': self._get_image_href(image, 'file')},
{'rel': 'describedby', 'href': '/v2/schemas/image'},
]

View File

@ -19,6 +19,7 @@ import logging
import routes
from glance.api.v2 import image_data
from glance.api.v2 import images
from glance.api.v2 import root
from glance.api.v2 import schemas
@ -74,4 +75,14 @@ class API(wsgi.Router):
action='delete',
conditions={'method': ['DELETE']})
image_data_resource = image_data.create_resource(conf)
mapper.connect('/images/{image_id}/file',
controller=image_data_resource,
action='download',
conditions={'method': ['GET']})
mapper.connect('/images/{image_id}/file',
controller=image_data_resource,
action='upload',
conditions={'method': ['PUT']})
super(API, self).__init__(mapper)

View File

@ -279,3 +279,8 @@ def schedule_delete_from_backend(uri, conf, context, image_id, **kwargs):
registry.update_image_metadata(context, image_id,
{'status': 'pending_delete'})
def add_to_backend(scheme, image_id, data, size):
store = get_store_from_scheme(scheme)
return store.add(image_id, data, size)

View File

@ -96,6 +96,25 @@ class TestImages(functional.FunctionalTest):
self.assertEqual(image_id, image['id'])
self.assertEqual('image-2', image['name'])
# Try to download data before its uploaded
path = self._url('/v2/images/%s/file' % image_id)
headers = self._headers()
response = requests.get(path, headers=headers)
self.assertEqual(404, response.status_code)
# Upload some image data
path = self._url('/v2/images/%s/file' % image_id)
headers = self._headers()
response = requests.put(path, headers=headers, data='ZZZZZ')
self.assertEqual(200, response.status_code)
# Try to download the data that was just uploaded
path = self._url('/v2/images/%s/file' % image_id)
headers = self._headers()
response = requests.get(path, headers=headers)
self.assertEqual(200, response.status_code)
self.assertEqual(response.text, 'ZZZZZ')
# Deletion should work
path = self._url('/v2/images/%s' % image_id)
response = requests.delete(path, headers=self._headers())
@ -106,6 +125,12 @@ class TestImages(functional.FunctionalTest):
response = requests.get(path, headers=self._headers())
self.assertEqual(404, response.status_code)
# And neither should its data
path = self._url('/v2/images/%s/file' % image_id)
headers = self._headers()
response = requests.get(path, headers=headers)
self.assertEqual(404, response.status_code)
# Image list should now be empty
path = self._url('/v2/images')
response = requests.get(path, headers=self._headers())

View File

@ -20,6 +20,7 @@ import webob
import glance.common.context
from glance.common import exception
from glance.common import utils
LOG = logging.getLogger(__name__)
@ -51,7 +52,7 @@ class FakeDB(object):
def __init__(self):
self.images = {
UUID1: self._image_format(UUID1),
UUID1: self._image_format(UUID1, location='1'),
UUID2: self._image_format(UUID2),
}
self.members = {
@ -77,7 +78,13 @@ class FakeDB(object):
}
def _image_format(self, image_id, **values):
image = {'id': image_id, 'name': 'image-name'}
image = {
'id': image_id,
'name': 'image-name',
'owner': TENANT1,
'location': None,
'status': 'queued',
}
image.update(values)
return image
@ -137,3 +144,31 @@ class FakeDB(object):
self.images[image_id] = image
LOG.info('Image %s updated to %s' % (image_id, str(image)))
return image
class FakeStoreAPI(object):
def __init__(self):
self.data = {
'1': ('XXX', 3),
'2': ('FFFFF', 5),
}
def create_stores(self, conf):
pass
def get_from_backend(self, location):
try:
#NOTE(bcwaldon): This fake API is store-agnostic, so we only
# care about location being some unique string
return self.data[location]
except KeyError:
raise exception.NotFound()
def get_size_from_backend(self, location):
return self.get_from_backend(location)[1]
def add_to_backend(self, scheme, image_id, data, size):
location = utils.generate_uuid()
self.data[location] = (data, size)
checksum = 'Z'
return (location, size, checksum)

View File

@ -0,0 +1,89 @@
# Copyright 2012 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 unittest
import webob
import glance.api.v2.image_data
from glance.common import utils
import glance.tests.unit.utils as test_utils
import glance.tests.utils
class TestImagesController(unittest.TestCase):
def setUp(self):
super(TestImagesController, self).setUp()
conf = glance.tests.utils.TestConfigOpts({
'verbose': True,
'debug': True,
})
self.controller = glance.api.v2.image_data.ImageDataController(conf,
db_api=test_utils.FakeDB(),
store_api=test_utils.FakeStoreAPI())
def test_download(self):
request = test_utils.FakeRequest()
output = self.controller.download(request, test_utils.UUID1)
expected = {'data': 'XXX', 'size': 3}
self.assertEqual(expected, output)
def test_download_no_data(self):
request = test_utils.FakeRequest()
self.assertRaises(webob.exc.HTTPNotFound, self.controller.download,
request, test_utils.UUID2)
def test_download_non_existant_image(self):
request = test_utils.FakeRequest()
self.assertRaises(webob.exc.HTTPNotFound, self.controller.download,
request, utils.generate_uuid())
def test_upload_download(self):
request = test_utils.FakeRequest()
self.controller.upload(request, test_utils.UUID2, 'YYYY')
output = self.controller.download(request, test_utils.UUID2)
expected = {'data': 'YYYY', 'size': 4}
self.assertEqual(expected, output)
def test_upload_non_existant_image(self):
request = test_utils.FakeRequest()
self.assertRaises(webob.exc.HTTPNotFound, self.controller.upload,
request, utils.generate_uuid(), 'YYYY')
class TestImageDataDeserializer(unittest.TestCase):
def setUp(self):
self.deserializer = glance.api.v2.image_data.RequestDeserializer()
def test_upload(self):
request = test_utils.FakeRequest()
request.body = 'YYY'
output = self.deserializer.upload(request)
expected = {'data': 'YYY'}
self.assertEqual(expected, output)
class TestImageDataSerializer(unittest.TestCase):
def setUp(self):
self.serializer = glance.api.v2.image_data.ResponseSerializer()
def test_download(self):
response = webob.Response()
self.serializer.download(response, {'data': 'ZZZ', 'size': 3})
self.assertEqual('ZZZ', response.body)
self.assertEqual('3', response.headers['Content-Length'])
self.assertEqual('application/octet-stream',
response.headers['Content-Type'])

View File

@ -57,7 +57,13 @@ class TestImagesController(unittest.TestCase):
image = {'name': 'image-1'}
output = self.controller.create(request, image)
output.pop('id')
self.assertEqual(image, output)
expected = {
'name': 'image-1',
'owner': test_utils.TENANT1,
'location': None,
'status': 'queued',
}
self.assertEqual(expected, output)
def test_create_with_owner_forbidden(self):
request = test_utils.FakeRequest()
@ -70,7 +76,13 @@ class TestImagesController(unittest.TestCase):
image = {'name': 'image-2'}
output = self.controller.update(request, test_utils.UUID1, image)
output.pop('id')
self.assertEqual(image, output)
expected = {
'name': 'image-2',
'owner': test_utils.TENANT1,
'location': '1',
'status': 'queued',
}
self.assertEqual(expected, output)
def test_update_non_existant(self):
request = test_utils.FakeRequest()
@ -139,6 +151,10 @@ class TestImagesSerializer(unittest.TestCase):
'rel': 'self',
'href': '/v2/images/%s' % test_utils.UUID1,
},
{
'rel': 'file',
'href': '/v2/images/%s/file' % test_utils.UUID1,
},
{'rel': 'describedby', 'href': '/v2/schemas/image'}
],
},
@ -150,6 +166,10 @@ class TestImagesSerializer(unittest.TestCase):
'rel': 'self',
'href': '/v2/images/%s' % test_utils.UUID2,
},
{
'rel': 'file',
'href': '/v2/images/%s/file' % test_utils.UUID2,
},
{'rel': 'describedby', 'href': '/v2/schemas/image'}
],
},
@ -171,6 +191,10 @@ class TestImagesSerializer(unittest.TestCase):
'rel': 'self',
'href': '/v2/images/%s' % test_utils.UUID2,
},
{
'rel': 'file',
'href': '/v2/images/%s/file' % test_utils.UUID2,
},
{'rel': 'describedby', 'href': '/v2/schemas/image'}
],
},
@ -187,10 +211,8 @@ class TestImagesSerializer(unittest.TestCase):
'id': test_utils.UUID2,
'name': 'image-2',
'links': [
{
'rel': 'self',
'href': self_link,
},
{'rel': 'self', 'href': self_link},
{'rel': 'file', 'href': '%s/file' % self_link},
{'rel': 'describedby', 'href': '/v2/schemas/image'}
],
},
@ -208,10 +230,8 @@ class TestImagesSerializer(unittest.TestCase):
'id': test_utils.UUID2,
'name': 'image-2',
'links': [
{
'rel': 'self',
'href': self_link,
},
{'rel': 'self', 'href': self_link},
{'rel': 'file', 'href': '%s/file' % self_link},
{'rel': 'describedby', 'href': '/v2/schemas/image'}
],
},