Adds checksumming to Glance.

When adding an image (or uploading an image during PUT operations),
you may now supply an optional X-Image-Meta-Checksum header. When
storing the uploaded image, the backend image stores now are required
to return a checksum of the data they just stored. The optional
X-Image-Meta-Checksum header is compared against this generated checksum
and returns a 409 Bad Request if there is a mismatch.

The ETag header is now properly set to the image's checksum now
for all GET /images/<ID>, HEAD /images/<ID>, POST /images and
PUT /images/<ID> operations.

Adds unit tests verifying the checksumming behaviour in the API, and
in the Swift and Filesystem backend stores.
This commit is contained in:
jaypipes@gmail.com 2011-03-08 10:22:44 -05:00
parent a3690f0c9a
commit af11621170
13 changed files with 256 additions and 122 deletions

View File

@ -67,6 +67,7 @@ JSON-encoded mapping in the following format::
'disk_format': 'vhd',
'container_format': 'ovf',
'size': '5368709120',
'checksum': 'c2e5db72bd7fd153f53ede5da5a06de3',
'location': 'swift://account:key/container/image.tar.gz.0',
'created_at': '2010-02-03 09:34:01',
'updated_at': '2010-02-03 09:34:01',
@ -116,6 +117,7 @@ following shows an example of the HTTP headers returned from the above
x-image-meta-disk-format vhd
x-image-meta-container-format ovf
x-image-meta-size 5368709120
x-image-meta-checksum c2e5db72bd7fd153f53ede5da5a06de3
x-image-meta-location swift://account:key/container/image.tar.gz.0
x-image-meta-created_at 2010-02-03 09:34:01
x-image-meta-updated_at 2010-02-03 09:34:01
@ -137,6 +139,9 @@ following shows an example of the HTTP headers returned from the above
that have been saved with the image metadata. The key is the string
after `x-image-meta-property-` and the value is the value of the header
The response's `ETag` header will always be equal to the
`x-image-meta-checksum` value
Retrieving a Virtual Machine Image
----------------------------------
@ -166,6 +171,7 @@ returned from the above ``GET`` request::
x-image-meta-disk-format vhd
x-image-meta-container-format ovf
x-image-meta-size 5368709120
x-image-meta-checksum c2e5db72bd7fd153f53ede5da5a06de3
x-image-meta-location swift://account:key/container/image.tar.gz.0
x-image-meta-created_at 2010-02-03 09:34:01
x-image-meta-updated_at 2010-02-03 09:34:01
@ -190,6 +196,9 @@ returned from the above ``GET`` request::
The response's `Content-Length` header shall be equal to the value of
the `x-image-meta-size` header
The response's `ETag` header will always be equal to the
`x-image-meta-checksum` value
The image data itself will be the body of the HTTP response returned
from the request, which will have content-type of
`application/octet-stream`.
@ -284,6 +293,14 @@ The list of metadata headers that Glance accepts are listed below.
When not present, Glance will calculate the image's size based on the size
of the request body.
* ``x-image-meta-checksum``
This header is optional.
When present, Glance will verify the checksum generated from the backend
store when storing your image against this value and return a
**400 Bad Request** if the values do not match.
* ``x-image-meta-is-public``
This header is optional.

View File

@ -142,7 +142,10 @@ class BaseClient(object):
c.request(method, action, body, headers)
res = c.getresponse()
status_code = self.get_status_code(res)
if status_code == httplib.OK:
if status_code in (httplib.OK,
httplib.CREATED,
httplib.ACCEPTED,
httplib.NO_CONTENT):
return res
elif status_code == httplib.UNAUTHORIZED:
raise exception.NotAuthorized

View File

@ -42,7 +42,7 @@ BASE_MODEL_ATTRS = set(['id', 'created_at', 'updated_at', 'deleted_at',
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size',
'disk_format', 'container_format',
'is_public', 'location'])
'is_public', 'location', 'checksum'])
CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf']
DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi']

View File

@ -104,6 +104,7 @@ class Image(BASE, ModelBase):
status = Column(String(30), nullable=False)
is_public = Column(Boolean, nullable=False, default=False)
location = Column(Text)
checksum = Column(String(32))
class ImageProperty(BASE, ModelBase):

View File

@ -31,6 +31,8 @@ from glance.registry.db import api as db_api
logger = logging.getLogger('glance.registry.server')
DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size', 'checksum']
class Controller(wsgi.Controller):
"""Controller for the reference implementation registry server"""
@ -49,14 +51,22 @@ class Controller(wsgi.Controller):
Where image_list is a sequence of mappings::
{'id': image_id, 'name': image_name}
{
'id': <ID>,
'name': <NAME>,
'size': <SIZE>,
'checksum': <CHECKSUM>
}
"""
images = db_api.image_get_all_public(None)
image_dicts = [dict(id=i['id'],
name=i['name'],
size=i['size']) for i in images]
return dict(images=image_dicts)
results = []
for image in images:
result = {}
for field in DISPLAY_FIELDS_IN_INDEX:
result[field] = image[field]
results.append(result)
return dict(images=results)
def detail(self, req):
"""Return detailed information for all public, non-deleted images

View File

@ -29,6 +29,7 @@ Configuration Options
"""
import httplib
import json
import logging
import sys
@ -68,8 +69,8 @@ class Controller(wsgi.Controller):
GET /images/<ID> -- Return image data for image with id <ID>
POST /images -- Store image data and return metadata about the
newly-stored image
PUT /images/<ID> -- Update image metadata (not image data, since
image data is immutable once stored)
PUT /images/<ID> -- Update image metadata and/or upload image
data for a previously-reserved image
DELETE /images/<ID> -- Delete the image with id <ID>
"""
@ -90,7 +91,9 @@ class Controller(wsgi.Controller):
{'images': [
{'id': <ID>,
'name': <NAME>,
'size': <SIZE>}, ...
'size': <SIZE>,
'checksum': <CHECKSUM>
}, ...
]}
"""
images = registry.get_images_list(self.options)
@ -109,6 +112,7 @@ class Controller(wsgi.Controller):
'size': <SIZE>,
'disk_format': <DISK_FORMAT>,
'container_format': <CONTAINER_FORMAT>,
'checksum': <CHECKSUM>,
'store': <STORE>,
'status': <STATUS>,
'created_at': <TIMESTAMP>,
@ -134,6 +138,8 @@ class Controller(wsgi.Controller):
res = Response(request=req)
utils.inject_image_meta_into_headers(res, image)
res.headers.add('Location', "/images/%s" % id)
res.headers.add('ETag', image['checksum'])
return req.get_response(res)
@ -163,6 +169,8 @@ class Controller(wsgi.Controller):
res = Response(app_iter=image_iterator(),
content_type="text/plain")
utils.inject_image_meta_into_headers(res, image)
res.headers.add('Location', "/images/%s" % id)
res.headers.add('ETag', image['checksum'])
return req.get_response(res)
def _reserve(self, req):
@ -223,36 +231,45 @@ class Controller(wsgi.Controller):
store = self.get_store_or_400(req, store_name)
image_meta['status'] = 'saving'
image_id = image_meta['id']
logger.debug("Updating image metadata for image %s"
logger.debug("Setting image %s to status 'saving'"
% image_id)
registry.update_image_metadata(self.options,
image_meta['id'],
image_meta)
registry.update_image_metadata(self.options, image_id,
{'status': 'saving'})
try:
logger.debug("Uploading image data for image %(image_id)s "
"to %(store_name)s store" % locals())
location, size = store.add(image_meta['id'],
location, size, checksum = store.add(image_meta['id'],
req.body_file,
self.options)
# If size returned from store is different from size
# already stored in registry, update the registry with
# the new size of the image
if image_meta.get('size', 0) != size:
image_meta['size'] = size
logger.debug("Updating image metadata for image %s"
% image_id)
registry.update_image_metadata(self.options,
image_meta['id'],
image_meta)
# Verify any supplied checksum value matches checksum
# returned from store when adding image
supplied_checksum = image_meta.get('checksum')
if supplied_checksum and supplied_checksum != checksum:
msg = ("Supplied checksum (%(supplied_checksum)s) and "
"checksum generated from uploaded image "
"(%(checksum)s) did not match. Setting image "
"status to 'killed'.") % locals()
self._safe_kill(req, image_meta)
raise HTTPBadRequest(msg, content_type="text/plain",
request=req)
# Update the database with the checksum returned
# from the backend store
logger.debug("Updating image %(image_id)s data. "
"Checksum set to %(checksum)s, size set "
"to %(size)d" % locals())
m = registry.update_image_metadata(self.options, image_id,
{'checksum': checksum,
'size': size})
return location
except exception.Duplicate, e:
logger.error("Error adding image to store: %s", str(e))
raise HTTPConflict(str(e), request=req)
def _activate(self, req, image_meta, location):
def _activate(self, req, image_id, location):
"""
Sets the image status to `active` and the image's location
attribute.
@ -261,25 +278,25 @@ class Controller(wsgi.Controller):
:param image_meta: Mapping of metadata about image
:param location: Location of where Glance stored this image
"""
image_meta = {}
image_meta['location'] = location
image_meta['status'] = 'active'
registry.update_image_metadata(self.options,
image_meta['id'],
return registry.update_image_metadata(self.options,
image_id,
image_meta)
def _kill(self, req, image_meta):
def _kill(self, req, image_id):
"""
Marks the image status to `killed`
:param request: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image
:param image_id: Opaque image identifier
"""
image_meta['status'] = 'killed'
registry.update_image_metadata(self.options,
image_meta['id'],
image_meta)
image_id,
{'status': 'killed'})
def _safe_kill(self, req, image_meta):
def _safe_kill(self, req, image_id):
"""
Mark image killed without raising exceptions if it fails.
@ -287,12 +304,13 @@ class Controller(wsgi.Controller):
not raise itself, rather it should just log its error.
:param request: The WSGI/Webob Request object
:param image_id: Opaque image identifier
"""
try:
self._kill(req, image_meta)
self._kill(req, image_id)
except Exception, e:
logger.error("Unable to kill image %s: %s",
image_meta['id'], repr(e))
image_id, repr(e))
def _upload_and_activate(self, req, image_meta):
"""
@ -302,13 +320,16 @@ class Controller(wsgi.Controller):
:param request: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image
:retval Mapping of updated image data
"""
try:
image_id = image_meta['id']
location = self._upload(req, image_meta)
self._activate(req, image_meta, location)
return self._activate(req, image_id, location)
except: # unqualified b/c we're re-raising it
exc_type, exc_value, exc_traceback = sys.exc_info()
self._safe_kill(req, image_meta)
self._safe_kill(req, image_id)
# NOTE(sirp): _safe_kill uses httplib which, in turn, uses
# Eventlet's GreenSocket. Eventlet subsequently clears exceptions
# by calling `sys.exc_clear()`.
@ -352,19 +373,21 @@ class Controller(wsgi.Controller):
image data.
"""
image_meta = self._reserve(req)
image_id = image_meta['id']
if utils.has_body(req):
self._upload_and_activate(req, image_meta)
image_meta = self._upload_and_activate(req, image_meta)
else:
if 'x-image-meta-location' in req.headers:
location = req.headers['x-image-meta-location']
self._activate(req, image_meta, location)
image_meta = self._activate(req, image_id, location)
# APP states we should return a Location: header with the edit
# URI of the resource newly-created.
res = Response(request=req, body=json.dumps(dict(image=image_meta)),
content_type="text/plain")
res.headers.add('Location', "/images/%s" % image_meta['id'])
status=httplib.CREATED, content_type="text/plain")
res.headers.add('Location', "/images/%s" % image_id)
res.headers.add('ETag', image_meta['checksum'])
return req.get_response(res)
@ -392,9 +415,14 @@ class Controller(wsgi.Controller):
new_image_meta)
if has_body:
self._upload_and_activate(req, image_meta)
image_meta = self._upload_and_activate(req, image_meta)
return dict(image=image_meta)
res = Response(request=req,
body=json.dumps(dict(image=image_meta)),
content_type="text/plain")
res.headers.add('Location', "/images/%s" % id)
res.headers.add('ETag', image_meta['checksum'])
return res
except exception.Invalid, e:
msg = ("Failed to update image metadata. Got error: %(e)s"
% locals())

View File

@ -140,40 +140,3 @@ def parse_uri_tokens(parsed_uri, example_url):
authurl = "https://%s" % '/'.join(path_parts)
return user, key, authurl, container, obj
def add_options(parser):
"""
Adds any configuration options that each store might
have.
:param parser: An optparse.OptionParser object
:retval None
"""
# TODO(jaypipes): Remove these imports...
from glance.store.http import HTTPBackend
from glance.store.s3 import S3Backend
from glance.store.swift import SwiftBackend
from glance.store.filesystem import FilesystemBackend
help_text = "The following configuration options are specific to the "\
"Glance image store."
DEFAULT_STORE_CHOICES = ['file', 'swift', 's3']
group = optparse.OptionGroup(parser, "Image Store Options", help_text)
group.add_option('--default-store', metavar="STORE",
default="file",
choices=DEFAULT_STORE_CHOICES,
help="The backend store that Glance will use to store "
"virtual machine images to. Choices: ('%s') "
"Default: %%default" % "','".join(DEFAULT_STORE_CHOICES))
backend_classes = [FilesystemBackend,
HTTPBackend,
SwiftBackend,
S3Backend]
for backend_class in backend_classes:
if hasattr(backend_class, 'add_options'):
backend_class.add_options(group)
parser.add_option_group(group)

View File

@ -19,6 +19,7 @@
A simple filesystem-backed store
"""
import hashlib
import logging
import os
import urlparse
@ -110,9 +111,10 @@ class FilesystemBackend(glance.store.Backend):
:param data: The image data to write, as a file-like object
:param options: Conf mapping
:retval Tuple with (location, size)
The location that was written, with file:// scheme prepended
and the size in bytes of the data written
:retval Tuple with (location, size, checksum)
The location that was written, with file:// scheme prepended,
the size in bytes of the data written, and the checksum of
the image added.
"""
datadir = options['filesystem_store_datadir']
@ -127,6 +129,7 @@ class FilesystemBackend(glance.store.Backend):
raise exception.Duplicate("Image file %s already exists!"
% filepath)
checksum = hashlib.md5()
bytes_written = 0
with open(filepath, 'wb') as f:
while True:
@ -134,23 +137,11 @@ class FilesystemBackend(glance.store.Backend):
if not buf:
break
bytes_written += len(buf)
checksum.update(buf)
f.write(buf)
logger.debug("Wrote %(bytes_written)d bytes to %(filepath)s"
% locals())
return ('file://%s' % filepath, bytes_written)
checksum_hex = checksum.hexdigest()
@classmethod
def add_options(cls, parser):
"""
Adds specific configuration options for this store
:param parser: An optparse.OptionParser object
:retval None
"""
parser.add_option('--filesystem-store-datadir', metavar="DIR",
default="/var/lib/glance/images/",
help="Location to write image data. This directory "
"should be writeable by the user that runs the "
"glance-api program. Default: %default")
logger.debug("Wrote %(bytes_written)d bytes to %(filepath)s with "
"checksum %(checksum_hex)s" % locals())
return ('file://%s' % filepath, bytes_written, checksum_hex)

View File

@ -161,7 +161,7 @@ class SwiftBackend(glance.store.Backend):
# header keys are lowercased by Swift
if 'content-length' in resp_headers:
size = int(resp_headers['content-length'])
return (location, size)
return (location, size, obj_etag)
except ClientException, e:
if e.http_status == httplib.CONFLICT:
raise exception.Duplicate("Swift already has an image at "

View File

@ -220,6 +220,7 @@ def stub_out_registry_and_store_server(stubs):
'registry_host': '0.0.0.0',
'registry_port': '9191',
'default_store': 'file',
'checksum': True,
'filesystem_store_datadir': FAKE_FILESYSTEM_ROOTDIR}
res = self.req.get_response(server.API(options))
@ -277,6 +278,7 @@ def stub_out_registry_db_image_api(stubs):
'updated_at': datetime.datetime.utcnow(),
'deleted_at': None,
'deleted': False,
'checksum': None,
'size': 13,
'location': "swift://user:passwd@acct/container/obj.tar.0",
'properties': [{'key': 'type',
@ -292,6 +294,7 @@ def stub_out_registry_db_image_api(stubs):
'updated_at': datetime.datetime.utcnow(),
'deleted_at': None,
'deleted': False,
'checksum': None,
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': []}]
@ -311,6 +314,7 @@ def stub_out_registry_db_image_api(stubs):
glance.registry.db.api.validate_image(values)
values['size'] = values.get('size', 0)
values['checksum'] = values.get('checksum')
values['deleted'] = False
values['properties'] = values.get('properties', {})
values['created_at'] = datetime.datetime.utcnow()
@ -343,6 +347,7 @@ def stub_out_registry_db_image_api(stubs):
copy_image.update(values)
glance.registry.db.api.validate_image(copy_image)
props = []
orig_properties = image['properties']
if 'properties' in values.keys():
for k, v in values['properties'].items():
@ -355,7 +360,8 @@ def stub_out_registry_db_image_api(stubs):
p['deleted_at'] = None
props.append(p)
values['properties'] = props
orig_properties = orig_properties + props
values['properties'] = orig_properties
image.update(values)
return image

View File

@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import hashlib
import httplib
import json
import unittest
@ -47,7 +48,9 @@ class TestRegistryAPI(unittest.TestCase):
"""
fixture = {'id': 2,
'name': 'fake image #2'}
'name': 'fake image #2',
'size': 19,
'checksum': None}
req = webob.Request.blank('/')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
@ -65,7 +68,9 @@ class TestRegistryAPI(unittest.TestCase):
"""
fixture = {'id': 2,
'name': 'fake image #2'}
'name': 'fake image #2',
'size': 19,
'checksum': None}
req = webob.Request.blank('/images')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
@ -85,6 +90,8 @@ class TestRegistryAPI(unittest.TestCase):
fixture = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'size': 19,
'checksum': None,
'disk_format': 'vhd',
'container_format': 'ovf',
'status': 'active'}
@ -388,7 +395,7 @@ class TestGlanceAPI(unittest.TestCase):
for k, v in fixture_headers.iteritems():
req.headers[k] = v
res = req.get_response(self.api)
self.assertEquals(res.status_int, httplib.OK)
self.assertEquals(res.status_int, httplib.CREATED)
res_body = json.loads(res.body)['image']
self.assertEquals('queued', res_body['status'])
@ -423,7 +430,7 @@ class TestGlanceAPI(unittest.TestCase):
req.headers['Content-Type'] = 'application/octet-stream'
req.body = "chunk00000remainder"
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
self.assertEquals(res.status_int, httplib.CREATED)
res_body = json.loads(res.body)['image']
self.assertEquals(res_body['location'],
@ -437,6 +444,97 @@ class TestGlanceAPI(unittest.TestCase):
"res.headerlist = %r" % res.headerlist)
self.assertTrue('/images/3' in res.headers['location'])
def test_image_is_checksummed(self):
"""Test that the image contents are checksummed properly"""
fixture_headers = {'x-image-meta-store': 'file',
'x-image-meta-disk-format': 'vhd',
'x-image-meta-container-format': 'ovf',
'x-image-meta-name': 'fake image #3'}
image_contents = "chunk00000remainder"
image_checksum = hashlib.md5(image_contents).hexdigest()
req = webob.Request.blank("/images")
req.method = 'POST'
for k, v in fixture_headers.iteritems():
req.headers[k] = v
req.headers['Content-Type'] = 'application/octet-stream'
req.body = image_contents
res = req.get_response(self.api)
self.assertEquals(res.status_int, httplib.CREATED)
res_body = json.loads(res.body)['image']
self.assertEquals(res_body['location'],
'file:///tmp/glance-tests/3')
self.assertEquals(image_checksum, res_body['checksum'],
"Mismatched checksum. Expected %s, got %s" %
(image_checksum, res_body['checksum']))
def test_etag_equals_checksum_header(self):
"""Test that the ETag header matches the x-image-meta-checksum"""
fixture_headers = {'x-image-meta-store': 'file',
'x-image-meta-disk-format': 'vhd',
'x-image-meta-container-format': 'ovf',
'x-image-meta-name': 'fake image #3'}
image_contents = "chunk00000remainder"
image_checksum = hashlib.md5(image_contents).hexdigest()
req = webob.Request.blank("/images")
req.method = 'POST'
for k, v in fixture_headers.iteritems():
req.headers[k] = v
req.headers['Content-Type'] = 'application/octet-stream'
req.body = image_contents
res = req.get_response(self.api)
self.assertEquals(res.status_int, httplib.CREATED)
# HEAD the image and check the ETag equals the checksum header...
expected_headers = {'x-image-meta-checksum': image_checksum,
'etag': image_checksum}
req = webob.Request.blank("/images/3")
req.method = 'HEAD'
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
for key in expected_headers.keys():
self.assertTrue(key in res.headers,
"required header '%s' missing from "
"returned headers" % key)
for key, value in expected_headers.iteritems():
self.assertEquals(value, res.headers[key])
def test_bad_checksum_kills_image(self):
"""Test that the image contents are checksummed properly"""
image_contents = "chunk00000remainder"
bad_checksum = hashlib.md5("invalid").hexdigest()
fixture_headers = {'x-image-meta-store': 'file',
'x-image-meta-disk-format': 'vhd',
'x-image-meta-container-format': 'ovf',
'x-image-meta-name': 'fake image #3',
'x-image-meta-checksum': bad_checksum}
req = webob.Request.blank("/images")
req.method = 'POST'
for k, v in fixture_headers.iteritems():
req.headers[k] = v
req.headers['Content-Type'] = 'application/octet-stream'
req.body = image_contents
res = req.get_response(self.api)
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
# Test the image was killed...
expected_headers = {'x-image-meta-id': '3',
'x-image-meta-status': 'killed'}
req = webob.Request.blank("/images/3")
req.method = 'HEAD'
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
for key, value in expected_headers.iteritems():
self.assertEquals(value, res.headers[key])
def test_image_meta(self):
"""Test for HEAD /images/<ID>"""
expected_headers = {'x-image-meta-id': '2',

View File

@ -18,6 +18,7 @@
"""Tests the filesystem backend store"""
import StringIO
import hashlib
import unittest
import urlparse
@ -27,6 +28,11 @@ from glance.common import exception
from glance.store.filesystem import FilesystemBackend, ChunkedFile
from tests import stubs
FILESYSTEM_OPTIONS = {
'verbose': True,
'debug': True,
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
class TestFilesystemBackend(unittest.TestCase):
@ -75,17 +81,17 @@ class TestFilesystemBackend(unittest.TestCase):
expected_image_id = 42
expected_file_size = 1024 * 5 # 5K
expected_file_contents = "*" * expected_file_size
expected_checksum = hashlib.md5(expected_file_contents).hexdigest()
expected_location = "file://%s/%s" % (stubs.FAKE_FILESYSTEM_ROOTDIR,
expected_image_id)
image_file = StringIO.StringIO(expected_file_contents)
options = {'verbose': True,
'debug': True,
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
location, size = FilesystemBackend.add(42, image_file, options)
location, size, checksum = FilesystemBackend.add(42, image_file,
FILESYSTEM_OPTIONS)
self.assertEquals(expected_location, location)
self.assertEquals(expected_file_size, size)
self.assertEquals(expected_checksum, checksum)
url_pieces = urlparse.urlparse("file:///tmp/glance-tests/42")
new_image_file = FilesystemBackend.get(url_pieces)
@ -110,7 +116,7 @@ class TestFilesystemBackend(unittest.TestCase):
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
self.assertRaises(exception.Duplicate,
FilesystemBackend.add,
2, image_file, options)
2, image_file, FILESYSTEM_OPTIONS)
def test_delete(self):
"""

View File

@ -68,15 +68,20 @@ def stub_out_swift_common_client(stubs):
if hasattr(contents, 'read'):
fixture_object = StringIO.StringIO()
chunk = contents.read(SwiftBackend.CHUNKSIZE)
checksum = hashlib.md5()
while chunk:
fixture_object.write(chunk)
checksum.update(chunk)
chunk = contents.read(SwiftBackend.CHUNKSIZE)
etag = checksum.hexdigest()
else:
fixture_object = StringIO.StringIO(contents)
etag = hashlib.md5(fixture_object.getvalue()).hexdigest()
read_len = fixture_object.len
fixture_objects[fixture_key] = fixture_object
fixture_headers[fixture_key] = {
'content-length': fixture_object.len,
'etag': hashlib.md5(fixture_object.read()).hexdigest()}
'content-length': read_len,
'etag': etag}
return fixture_headers[fixture_key]['etag']
else:
msg = ("Object PUT failed - Object with key %s already exists"
@ -189,8 +194,9 @@ class TestSwiftBackend(unittest.TestCase):
def test_add(self):
"""Test that we can add an image via the swift backend"""
expected_image_id = 42
expected_swift_size = 1024 * 5 # 5K
expected_swift_size = FIVE_KB
expected_swift_contents = "*" * expected_swift_size
expected_checksum = hashlib.md5(expected_swift_contents).hexdigest()
expected_location = format_swift_location(
SWIFT_OPTIONS['swift_store_user'],
SWIFT_OPTIONS['swift_store_key'],
@ -199,10 +205,12 @@ class TestSwiftBackend(unittest.TestCase):
expected_image_id)
image_swift = StringIO.StringIO(expected_swift_contents)
location, size = SwiftBackend.add(42, image_swift, SWIFT_OPTIONS)
location, size, checksum = SwiftBackend.add(42, image_swift,
SWIFT_OPTIONS)
self.assertEquals(expected_location, location)
self.assertEquals(expected_swift_size, size)
self.assertEquals(expected_checksum, checksum)
url_pieces = urlparse.urlparse(expected_location)
new_image_swift = SwiftBackend.get(url_pieces)
@ -243,8 +251,9 @@ class TestSwiftBackend(unittest.TestCase):
options['swift_store_create_container_on_put'] = 'True'
options['swift_store_container'] = 'noexist'
expected_image_id = 42
expected_swift_size = 1024 * 5 # 5K
expected_swift_size = FIVE_KB
expected_swift_contents = "*" * expected_swift_size
expected_checksum = hashlib.md5(expected_swift_contents).hexdigest()
expected_location = format_swift_location(
options['swift_store_user'],
options['swift_store_key'],
@ -253,10 +262,12 @@ class TestSwiftBackend(unittest.TestCase):
expected_image_id)
image_swift = StringIO.StringIO(expected_swift_contents)
location, size = SwiftBackend.add(42, image_swift, options)
location, size, checksum = SwiftBackend.add(42, image_swift,
options)
self.assertEquals(expected_location, location)
self.assertEquals(expected_swift_size, size)
self.assertEquals(expected_checksum, checksum)
url_pieces = urlparse.urlparse(expected_location)
new_image_swift = SwiftBackend.get(url_pieces)