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:
parent
a3690f0c9a
commit
af11621170
@ -67,6 +67,7 @@ JSON-encoded mapping in the following format::
|
|||||||
'disk_format': 'vhd',
|
'disk_format': 'vhd',
|
||||||
'container_format': 'ovf',
|
'container_format': 'ovf',
|
||||||
'size': '5368709120',
|
'size': '5368709120',
|
||||||
|
'checksum': 'c2e5db72bd7fd153f53ede5da5a06de3',
|
||||||
'location': 'swift://account:key/container/image.tar.gz.0',
|
'location': 'swift://account:key/container/image.tar.gz.0',
|
||||||
'created_at': '2010-02-03 09:34:01',
|
'created_at': '2010-02-03 09:34:01',
|
||||||
'updated_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-disk-format vhd
|
||||||
x-image-meta-container-format ovf
|
x-image-meta-container-format ovf
|
||||||
x-image-meta-size 5368709120
|
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-location swift://account:key/container/image.tar.gz.0
|
||||||
x-image-meta-created_at 2010-02-03 09:34:01
|
x-image-meta-created_at 2010-02-03 09:34:01
|
||||||
x-image-meta-updated_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
|
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
|
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
|
Retrieving a Virtual Machine Image
|
||||||
----------------------------------
|
----------------------------------
|
||||||
@ -166,6 +171,7 @@ returned from the above ``GET`` request::
|
|||||||
x-image-meta-disk-format vhd
|
x-image-meta-disk-format vhd
|
||||||
x-image-meta-container-format ovf
|
x-image-meta-container-format ovf
|
||||||
x-image-meta-size 5368709120
|
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-location swift://account:key/container/image.tar.gz.0
|
||||||
x-image-meta-created_at 2010-02-03 09:34:01
|
x-image-meta-created_at 2010-02-03 09:34:01
|
||||||
x-image-meta-updated_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 response's `Content-Length` header shall be equal to the value of
|
||||||
the `x-image-meta-size` header
|
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
|
The image data itself will be the body of the HTTP response returned
|
||||||
from the request, which will have content-type of
|
from the request, which will have content-type of
|
||||||
`application/octet-stream`.
|
`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
|
When not present, Glance will calculate the image's size based on the size
|
||||||
of the request body.
|
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``
|
* ``x-image-meta-is-public``
|
||||||
|
|
||||||
This header is optional.
|
This header is optional.
|
||||||
|
@ -142,7 +142,10 @@ class BaseClient(object):
|
|||||||
c.request(method, action, body, headers)
|
c.request(method, action, body, headers)
|
||||||
res = c.getresponse()
|
res = c.getresponse()
|
||||||
status_code = self.get_status_code(res)
|
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
|
return res
|
||||||
elif status_code == httplib.UNAUTHORIZED:
|
elif status_code == httplib.UNAUTHORIZED:
|
||||||
raise exception.NotAuthorized
|
raise exception.NotAuthorized
|
||||||
|
@ -42,7 +42,7 @@ BASE_MODEL_ATTRS = set(['id', 'created_at', 'updated_at', 'deleted_at',
|
|||||||
|
|
||||||
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size',
|
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size',
|
||||||
'disk_format', 'container_format',
|
'disk_format', 'container_format',
|
||||||
'is_public', 'location'])
|
'is_public', 'location', 'checksum'])
|
||||||
|
|
||||||
CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf']
|
CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf']
|
||||||
DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi']
|
DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi']
|
||||||
|
@ -104,6 +104,7 @@ class Image(BASE, ModelBase):
|
|||||||
status = Column(String(30), nullable=False)
|
status = Column(String(30), nullable=False)
|
||||||
is_public = Column(Boolean, nullable=False, default=False)
|
is_public = Column(Boolean, nullable=False, default=False)
|
||||||
location = Column(Text)
|
location = Column(Text)
|
||||||
|
checksum = Column(String(32))
|
||||||
|
|
||||||
|
|
||||||
class ImageProperty(BASE, ModelBase):
|
class ImageProperty(BASE, ModelBase):
|
||||||
|
@ -31,6 +31,8 @@ from glance.registry.db import api as db_api
|
|||||||
|
|
||||||
logger = logging.getLogger('glance.registry.server')
|
logger = logging.getLogger('glance.registry.server')
|
||||||
|
|
||||||
|
DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size', 'checksum']
|
||||||
|
|
||||||
|
|
||||||
class Controller(wsgi.Controller):
|
class Controller(wsgi.Controller):
|
||||||
"""Controller for the reference implementation registry server"""
|
"""Controller for the reference implementation registry server"""
|
||||||
@ -49,14 +51,22 @@ class Controller(wsgi.Controller):
|
|||||||
|
|
||||||
Where image_list is a sequence of mappings::
|
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)
|
images = db_api.image_get_all_public(None)
|
||||||
image_dicts = [dict(id=i['id'],
|
results = []
|
||||||
name=i['name'],
|
for image in images:
|
||||||
size=i['size']) for i in images]
|
result = {}
|
||||||
return dict(images=image_dicts)
|
for field in DISPLAY_FIELDS_IN_INDEX:
|
||||||
|
result[field] = image[field]
|
||||||
|
results.append(result)
|
||||||
|
return dict(images=results)
|
||||||
|
|
||||||
def detail(self, req):
|
def detail(self, req):
|
||||||
"""Return detailed information for all public, non-deleted images
|
"""Return detailed information for all public, non-deleted images
|
||||||
|
106
glance/server.py
106
glance/server.py
@ -29,6 +29,7 @@ Configuration Options
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import httplib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
@ -68,8 +69,8 @@ class Controller(wsgi.Controller):
|
|||||||
GET /images/<ID> -- Return image data for image with id <ID>
|
GET /images/<ID> -- Return image data for image with id <ID>
|
||||||
POST /images -- Store image data and return metadata about the
|
POST /images -- Store image data and return metadata about the
|
||||||
newly-stored image
|
newly-stored image
|
||||||
PUT /images/<ID> -- Update image metadata (not image data, since
|
PUT /images/<ID> -- Update image metadata and/or upload image
|
||||||
image data is immutable once stored)
|
data for a previously-reserved image
|
||||||
DELETE /images/<ID> -- Delete the image with id <ID>
|
DELETE /images/<ID> -- Delete the image with id <ID>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -90,7 +91,9 @@ class Controller(wsgi.Controller):
|
|||||||
{'images': [
|
{'images': [
|
||||||
{'id': <ID>,
|
{'id': <ID>,
|
||||||
'name': <NAME>,
|
'name': <NAME>,
|
||||||
'size': <SIZE>}, ...
|
'size': <SIZE>,
|
||||||
|
'checksum': <CHECKSUM>
|
||||||
|
}, ...
|
||||||
]}
|
]}
|
||||||
"""
|
"""
|
||||||
images = registry.get_images_list(self.options)
|
images = registry.get_images_list(self.options)
|
||||||
@ -109,6 +112,7 @@ class Controller(wsgi.Controller):
|
|||||||
'size': <SIZE>,
|
'size': <SIZE>,
|
||||||
'disk_format': <DISK_FORMAT>,
|
'disk_format': <DISK_FORMAT>,
|
||||||
'container_format': <CONTAINER_FORMAT>,
|
'container_format': <CONTAINER_FORMAT>,
|
||||||
|
'checksum': <CHECKSUM>,
|
||||||
'store': <STORE>,
|
'store': <STORE>,
|
||||||
'status': <STATUS>,
|
'status': <STATUS>,
|
||||||
'created_at': <TIMESTAMP>,
|
'created_at': <TIMESTAMP>,
|
||||||
@ -134,6 +138,8 @@ class Controller(wsgi.Controller):
|
|||||||
|
|
||||||
res = Response(request=req)
|
res = Response(request=req)
|
||||||
utils.inject_image_meta_into_headers(res, image)
|
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)
|
return req.get_response(res)
|
||||||
|
|
||||||
@ -163,6 +169,8 @@ class Controller(wsgi.Controller):
|
|||||||
res = Response(app_iter=image_iterator(),
|
res = Response(app_iter=image_iterator(),
|
||||||
content_type="text/plain")
|
content_type="text/plain")
|
||||||
utils.inject_image_meta_into_headers(res, image)
|
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)
|
return req.get_response(res)
|
||||||
|
|
||||||
def _reserve(self, req):
|
def _reserve(self, req):
|
||||||
@ -223,36 +231,45 @@ class Controller(wsgi.Controller):
|
|||||||
|
|
||||||
store = self.get_store_or_400(req, store_name)
|
store = self.get_store_or_400(req, store_name)
|
||||||
|
|
||||||
image_meta['status'] = 'saving'
|
|
||||||
image_id = image_meta['id']
|
image_id = image_meta['id']
|
||||||
logger.debug("Updating image metadata for image %s"
|
logger.debug("Setting image %s to status 'saving'"
|
||||||
% image_id)
|
% image_id)
|
||||||
registry.update_image_metadata(self.options,
|
registry.update_image_metadata(self.options, image_id,
|
||||||
image_meta['id'],
|
{'status': 'saving'})
|
||||||
image_meta)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug("Uploading image data for image %(image_id)s "
|
logger.debug("Uploading image data for image %(image_id)s "
|
||||||
"to %(store_name)s store" % locals())
|
"to %(store_name)s store" % locals())
|
||||||
location, size = store.add(image_meta['id'],
|
location, size, checksum = store.add(image_meta['id'],
|
||||||
req.body_file,
|
req.body_file,
|
||||||
self.options)
|
self.options)
|
||||||
# If size returned from store is different from size
|
|
||||||
# already stored in registry, update the registry with
|
# Verify any supplied checksum value matches checksum
|
||||||
# the new size of the image
|
# returned from store when adding image
|
||||||
if image_meta.get('size', 0) != size:
|
supplied_checksum = image_meta.get('checksum')
|
||||||
image_meta['size'] = size
|
if supplied_checksum and supplied_checksum != checksum:
|
||||||
logger.debug("Updating image metadata for image %s"
|
msg = ("Supplied checksum (%(supplied_checksum)s) and "
|
||||||
% image_id)
|
"checksum generated from uploaded image "
|
||||||
registry.update_image_metadata(self.options,
|
"(%(checksum)s) did not match. Setting image "
|
||||||
image_meta['id'],
|
"status to 'killed'.") % locals()
|
||||||
image_meta)
|
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
|
return location
|
||||||
except exception.Duplicate, e:
|
except exception.Duplicate, e:
|
||||||
logger.error("Error adding image to store: %s", str(e))
|
logger.error("Error adding image to store: %s", str(e))
|
||||||
raise HTTPConflict(str(e), request=req)
|
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
|
Sets the image status to `active` and the image's location
|
||||||
attribute.
|
attribute.
|
||||||
@ -261,25 +278,25 @@ class Controller(wsgi.Controller):
|
|||||||
:param image_meta: Mapping of metadata about image
|
:param image_meta: Mapping of metadata about image
|
||||||
:param location: Location of where Glance stored this image
|
:param location: Location of where Glance stored this image
|
||||||
"""
|
"""
|
||||||
|
image_meta = {}
|
||||||
image_meta['location'] = location
|
image_meta['location'] = location
|
||||||
image_meta['status'] = 'active'
|
image_meta['status'] = 'active'
|
||||||
registry.update_image_metadata(self.options,
|
return registry.update_image_metadata(self.options,
|
||||||
image_meta['id'],
|
image_id,
|
||||||
image_meta)
|
image_meta)
|
||||||
|
|
||||||
def _kill(self, req, image_meta):
|
def _kill(self, req, image_id):
|
||||||
"""
|
"""
|
||||||
Marks the image status to `killed`
|
Marks the image status to `killed`
|
||||||
|
|
||||||
:param request: The WSGI/Webob Request object
|
: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,
|
registry.update_image_metadata(self.options,
|
||||||
image_meta['id'],
|
image_id,
|
||||||
image_meta)
|
{'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.
|
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.
|
not raise itself, rather it should just log its error.
|
||||||
|
|
||||||
:param request: The WSGI/Webob Request object
|
:param request: The WSGI/Webob Request object
|
||||||
|
:param image_id: Opaque image identifier
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self._kill(req, image_meta)
|
self._kill(req, image_id)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
logger.error("Unable to kill image %s: %s",
|
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):
|
def _upload_and_activate(self, req, image_meta):
|
||||||
"""
|
"""
|
||||||
@ -302,13 +320,16 @@ class Controller(wsgi.Controller):
|
|||||||
|
|
||||||
:param request: The WSGI/Webob Request object
|
:param request: The WSGI/Webob Request object
|
||||||
:param image_meta: Mapping of metadata about image
|
:param image_meta: Mapping of metadata about image
|
||||||
|
|
||||||
|
:retval Mapping of updated image data
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
image_id = image_meta['id']
|
||||||
location = self._upload(req, image_meta)
|
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
|
except: # unqualified b/c we're re-raising it
|
||||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
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
|
# NOTE(sirp): _safe_kill uses httplib which, in turn, uses
|
||||||
# Eventlet's GreenSocket. Eventlet subsequently clears exceptions
|
# Eventlet's GreenSocket. Eventlet subsequently clears exceptions
|
||||||
# by calling `sys.exc_clear()`.
|
# by calling `sys.exc_clear()`.
|
||||||
@ -352,19 +373,21 @@ class Controller(wsgi.Controller):
|
|||||||
image data.
|
image data.
|
||||||
"""
|
"""
|
||||||
image_meta = self._reserve(req)
|
image_meta = self._reserve(req)
|
||||||
|
image_id = image_meta['id']
|
||||||
|
|
||||||
if utils.has_body(req):
|
if utils.has_body(req):
|
||||||
self._upload_and_activate(req, image_meta)
|
image_meta = self._upload_and_activate(req, image_meta)
|
||||||
else:
|
else:
|
||||||
if 'x-image-meta-location' in req.headers:
|
if 'x-image-meta-location' in req.headers:
|
||||||
location = req.headers['x-image-meta-location']
|
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
|
# APP states we should return a Location: header with the edit
|
||||||
# URI of the resource newly-created.
|
# URI of the resource newly-created.
|
||||||
res = Response(request=req, body=json.dumps(dict(image=image_meta)),
|
res = Response(request=req, body=json.dumps(dict(image=image_meta)),
|
||||||
content_type="text/plain")
|
status=httplib.CREATED, content_type="text/plain")
|
||||||
res.headers.add('Location', "/images/%s" % image_meta['id'])
|
res.headers.add('Location', "/images/%s" % image_id)
|
||||||
|
res.headers.add('ETag', image_meta['checksum'])
|
||||||
|
|
||||||
return req.get_response(res)
|
return req.get_response(res)
|
||||||
|
|
||||||
@ -392,9 +415,14 @@ class Controller(wsgi.Controller):
|
|||||||
new_image_meta)
|
new_image_meta)
|
||||||
|
|
||||||
if has_body:
|
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:
|
except exception.Invalid, e:
|
||||||
msg = ("Failed to update image metadata. Got error: %(e)s"
|
msg = ("Failed to update image metadata. Got error: %(e)s"
|
||||||
% locals())
|
% locals())
|
||||||
|
@ -140,40 +140,3 @@ def parse_uri_tokens(parsed_uri, example_url):
|
|||||||
authurl = "https://%s" % '/'.join(path_parts)
|
authurl = "https://%s" % '/'.join(path_parts)
|
||||||
|
|
||||||
return user, key, authurl, container, obj
|
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)
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
A simple filesystem-backed store
|
A simple filesystem-backed store
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import urlparse
|
import urlparse
|
||||||
@ -110,9 +111,10 @@ class FilesystemBackend(glance.store.Backend):
|
|||||||
:param data: The image data to write, as a file-like object
|
:param data: The image data to write, as a file-like object
|
||||||
:param options: Conf mapping
|
:param options: Conf mapping
|
||||||
|
|
||||||
:retval Tuple with (location, size)
|
:retval Tuple with (location, size, checksum)
|
||||||
The location that was written, with file:// scheme prepended
|
The location that was written, with file:// scheme prepended,
|
||||||
and the size in bytes of the data written
|
the size in bytes of the data written, and the checksum of
|
||||||
|
the image added.
|
||||||
"""
|
"""
|
||||||
datadir = options['filesystem_store_datadir']
|
datadir = options['filesystem_store_datadir']
|
||||||
|
|
||||||
@ -127,6 +129,7 @@ class FilesystemBackend(glance.store.Backend):
|
|||||||
raise exception.Duplicate("Image file %s already exists!"
|
raise exception.Duplicate("Image file %s already exists!"
|
||||||
% filepath)
|
% filepath)
|
||||||
|
|
||||||
|
checksum = hashlib.md5()
|
||||||
bytes_written = 0
|
bytes_written = 0
|
||||||
with open(filepath, 'wb') as f:
|
with open(filepath, 'wb') as f:
|
||||||
while True:
|
while True:
|
||||||
@ -134,23 +137,11 @@ class FilesystemBackend(glance.store.Backend):
|
|||||||
if not buf:
|
if not buf:
|
||||||
break
|
break
|
||||||
bytes_written += len(buf)
|
bytes_written += len(buf)
|
||||||
|
checksum.update(buf)
|
||||||
f.write(buf)
|
f.write(buf)
|
||||||
|
|
||||||
logger.debug("Wrote %(bytes_written)d bytes to %(filepath)s"
|
checksum_hex = checksum.hexdigest()
|
||||||
% locals())
|
|
||||||
return ('file://%s' % filepath, bytes_written)
|
|
||||||
|
|
||||||
@classmethod
|
logger.debug("Wrote %(bytes_written)d bytes to %(filepath)s with "
|
||||||
def add_options(cls, parser):
|
"checksum %(checksum_hex)s" % locals())
|
||||||
"""
|
return ('file://%s' % filepath, bytes_written, checksum_hex)
|
||||||
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")
|
|
||||||
|
@ -161,7 +161,7 @@ class SwiftBackend(glance.store.Backend):
|
|||||||
# header keys are lowercased by Swift
|
# header keys are lowercased by Swift
|
||||||
if 'content-length' in resp_headers:
|
if 'content-length' in resp_headers:
|
||||||
size = int(resp_headers['content-length'])
|
size = int(resp_headers['content-length'])
|
||||||
return (location, size)
|
return (location, size, obj_etag)
|
||||||
except ClientException, e:
|
except ClientException, e:
|
||||||
if e.http_status == httplib.CONFLICT:
|
if e.http_status == httplib.CONFLICT:
|
||||||
raise exception.Duplicate("Swift already has an image at "
|
raise exception.Duplicate("Swift already has an image at "
|
||||||
|
@ -220,6 +220,7 @@ def stub_out_registry_and_store_server(stubs):
|
|||||||
'registry_host': '0.0.0.0',
|
'registry_host': '0.0.0.0',
|
||||||
'registry_port': '9191',
|
'registry_port': '9191',
|
||||||
'default_store': 'file',
|
'default_store': 'file',
|
||||||
|
'checksum': True,
|
||||||
'filesystem_store_datadir': FAKE_FILESYSTEM_ROOTDIR}
|
'filesystem_store_datadir': FAKE_FILESYSTEM_ROOTDIR}
|
||||||
res = self.req.get_response(server.API(options))
|
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(),
|
'updated_at': datetime.datetime.utcnow(),
|
||||||
'deleted_at': None,
|
'deleted_at': None,
|
||||||
'deleted': False,
|
'deleted': False,
|
||||||
|
'checksum': None,
|
||||||
'size': 13,
|
'size': 13,
|
||||||
'location': "swift://user:passwd@acct/container/obj.tar.0",
|
'location': "swift://user:passwd@acct/container/obj.tar.0",
|
||||||
'properties': [{'key': 'type',
|
'properties': [{'key': 'type',
|
||||||
@ -292,6 +294,7 @@ def stub_out_registry_db_image_api(stubs):
|
|||||||
'updated_at': datetime.datetime.utcnow(),
|
'updated_at': datetime.datetime.utcnow(),
|
||||||
'deleted_at': None,
|
'deleted_at': None,
|
||||||
'deleted': False,
|
'deleted': False,
|
||||||
|
'checksum': None,
|
||||||
'size': 19,
|
'size': 19,
|
||||||
'location': "file:///tmp/glance-tests/2",
|
'location': "file:///tmp/glance-tests/2",
|
||||||
'properties': []}]
|
'properties': []}]
|
||||||
@ -311,6 +314,7 @@ def stub_out_registry_db_image_api(stubs):
|
|||||||
glance.registry.db.api.validate_image(values)
|
glance.registry.db.api.validate_image(values)
|
||||||
|
|
||||||
values['size'] = values.get('size', 0)
|
values['size'] = values.get('size', 0)
|
||||||
|
values['checksum'] = values.get('checksum')
|
||||||
values['deleted'] = False
|
values['deleted'] = False
|
||||||
values['properties'] = values.get('properties', {})
|
values['properties'] = values.get('properties', {})
|
||||||
values['created_at'] = datetime.datetime.utcnow()
|
values['created_at'] = datetime.datetime.utcnow()
|
||||||
@ -343,6 +347,7 @@ def stub_out_registry_db_image_api(stubs):
|
|||||||
copy_image.update(values)
|
copy_image.update(values)
|
||||||
glance.registry.db.api.validate_image(copy_image)
|
glance.registry.db.api.validate_image(copy_image)
|
||||||
props = []
|
props = []
|
||||||
|
orig_properties = image['properties']
|
||||||
|
|
||||||
if 'properties' in values.keys():
|
if 'properties' in values.keys():
|
||||||
for k, v in values['properties'].items():
|
for k, v in values['properties'].items():
|
||||||
@ -355,7 +360,8 @@ def stub_out_registry_db_image_api(stubs):
|
|||||||
p['deleted_at'] = None
|
p['deleted_at'] = None
|
||||||
props.append(p)
|
props.append(p)
|
||||||
|
|
||||||
values['properties'] = props
|
orig_properties = orig_properties + props
|
||||||
|
values['properties'] = orig_properties
|
||||||
|
|
||||||
image.update(values)
|
image.update(values)
|
||||||
return image
|
return image
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import httplib
|
import httplib
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
@ -47,7 +48,9 @@ class TestRegistryAPI(unittest.TestCase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
fixture = {'id': 2,
|
fixture = {'id': 2,
|
||||||
'name': 'fake image #2'}
|
'name': 'fake image #2',
|
||||||
|
'size': 19,
|
||||||
|
'checksum': None}
|
||||||
req = webob.Request.blank('/')
|
req = webob.Request.blank('/')
|
||||||
res = req.get_response(self.api)
|
res = req.get_response(self.api)
|
||||||
res_dict = json.loads(res.body)
|
res_dict = json.loads(res.body)
|
||||||
@ -65,7 +68,9 @@ class TestRegistryAPI(unittest.TestCase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
fixture = {'id': 2,
|
fixture = {'id': 2,
|
||||||
'name': 'fake image #2'}
|
'name': 'fake image #2',
|
||||||
|
'size': 19,
|
||||||
|
'checksum': None}
|
||||||
req = webob.Request.blank('/images')
|
req = webob.Request.blank('/images')
|
||||||
res = req.get_response(self.api)
|
res = req.get_response(self.api)
|
||||||
res_dict = json.loads(res.body)
|
res_dict = json.loads(res.body)
|
||||||
@ -85,6 +90,8 @@ class TestRegistryAPI(unittest.TestCase):
|
|||||||
fixture = {'id': 2,
|
fixture = {'id': 2,
|
||||||
'name': 'fake image #2',
|
'name': 'fake image #2',
|
||||||
'is_public': True,
|
'is_public': True,
|
||||||
|
'size': 19,
|
||||||
|
'checksum': None,
|
||||||
'disk_format': 'vhd',
|
'disk_format': 'vhd',
|
||||||
'container_format': 'ovf',
|
'container_format': 'ovf',
|
||||||
'status': 'active'}
|
'status': 'active'}
|
||||||
@ -388,7 +395,7 @@ class TestGlanceAPI(unittest.TestCase):
|
|||||||
for k, v in fixture_headers.iteritems():
|
for k, v in fixture_headers.iteritems():
|
||||||
req.headers[k] = v
|
req.headers[k] = v
|
||||||
res = req.get_response(self.api)
|
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']
|
res_body = json.loads(res.body)['image']
|
||||||
self.assertEquals('queued', res_body['status'])
|
self.assertEquals('queued', res_body['status'])
|
||||||
@ -423,7 +430,7 @@ class TestGlanceAPI(unittest.TestCase):
|
|||||||
req.headers['Content-Type'] = 'application/octet-stream'
|
req.headers['Content-Type'] = 'application/octet-stream'
|
||||||
req.body = "chunk00000remainder"
|
req.body = "chunk00000remainder"
|
||||||
res = req.get_response(self.api)
|
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']
|
res_body = json.loads(res.body)['image']
|
||||||
self.assertEquals(res_body['location'],
|
self.assertEquals(res_body['location'],
|
||||||
@ -437,6 +444,97 @@ class TestGlanceAPI(unittest.TestCase):
|
|||||||
"res.headerlist = %r" % res.headerlist)
|
"res.headerlist = %r" % res.headerlist)
|
||||||
self.assertTrue('/images/3' in res.headers['location'])
|
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):
|
def test_image_meta(self):
|
||||||
"""Test for HEAD /images/<ID>"""
|
"""Test for HEAD /images/<ID>"""
|
||||||
expected_headers = {'x-image-meta-id': '2',
|
expected_headers = {'x-image-meta-id': '2',
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"""Tests the filesystem backend store"""
|
"""Tests the filesystem backend store"""
|
||||||
|
|
||||||
import StringIO
|
import StringIO
|
||||||
|
import hashlib
|
||||||
import unittest
|
import unittest
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
@ -27,6 +28,11 @@ from glance.common import exception
|
|||||||
from glance.store.filesystem import FilesystemBackend, ChunkedFile
|
from glance.store.filesystem import FilesystemBackend, ChunkedFile
|
||||||
from tests import stubs
|
from tests import stubs
|
||||||
|
|
||||||
|
FILESYSTEM_OPTIONS = {
|
||||||
|
'verbose': True,
|
||||||
|
'debug': True,
|
||||||
|
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
|
||||||
|
|
||||||
|
|
||||||
class TestFilesystemBackend(unittest.TestCase):
|
class TestFilesystemBackend(unittest.TestCase):
|
||||||
|
|
||||||
@ -75,17 +81,17 @@ class TestFilesystemBackend(unittest.TestCase):
|
|||||||
expected_image_id = 42
|
expected_image_id = 42
|
||||||
expected_file_size = 1024 * 5 # 5K
|
expected_file_size = 1024 * 5 # 5K
|
||||||
expected_file_contents = "*" * expected_file_size
|
expected_file_contents = "*" * expected_file_size
|
||||||
|
expected_checksum = hashlib.md5(expected_file_contents).hexdigest()
|
||||||
expected_location = "file://%s/%s" % (stubs.FAKE_FILESYSTEM_ROOTDIR,
|
expected_location = "file://%s/%s" % (stubs.FAKE_FILESYSTEM_ROOTDIR,
|
||||||
expected_image_id)
|
expected_image_id)
|
||||||
image_file = StringIO.StringIO(expected_file_contents)
|
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_location, location)
|
||||||
self.assertEquals(expected_file_size, size)
|
self.assertEquals(expected_file_size, size)
|
||||||
|
self.assertEquals(expected_checksum, checksum)
|
||||||
|
|
||||||
url_pieces = urlparse.urlparse("file:///tmp/glance-tests/42")
|
url_pieces = urlparse.urlparse("file:///tmp/glance-tests/42")
|
||||||
new_image_file = FilesystemBackend.get(url_pieces)
|
new_image_file = FilesystemBackend.get(url_pieces)
|
||||||
@ -110,7 +116,7 @@ class TestFilesystemBackend(unittest.TestCase):
|
|||||||
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
|
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
|
||||||
self.assertRaises(exception.Duplicate,
|
self.assertRaises(exception.Duplicate,
|
||||||
FilesystemBackend.add,
|
FilesystemBackend.add,
|
||||||
2, image_file, options)
|
2, image_file, FILESYSTEM_OPTIONS)
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
"""
|
"""
|
||||||
|
@ -68,15 +68,20 @@ def stub_out_swift_common_client(stubs):
|
|||||||
if hasattr(contents, 'read'):
|
if hasattr(contents, 'read'):
|
||||||
fixture_object = StringIO.StringIO()
|
fixture_object = StringIO.StringIO()
|
||||||
chunk = contents.read(SwiftBackend.CHUNKSIZE)
|
chunk = contents.read(SwiftBackend.CHUNKSIZE)
|
||||||
|
checksum = hashlib.md5()
|
||||||
while chunk:
|
while chunk:
|
||||||
fixture_object.write(chunk)
|
fixture_object.write(chunk)
|
||||||
|
checksum.update(chunk)
|
||||||
chunk = contents.read(SwiftBackend.CHUNKSIZE)
|
chunk = contents.read(SwiftBackend.CHUNKSIZE)
|
||||||
|
etag = checksum.hexdigest()
|
||||||
else:
|
else:
|
||||||
fixture_object = StringIO.StringIO(contents)
|
fixture_object = StringIO.StringIO(contents)
|
||||||
|
etag = hashlib.md5(fixture_object.getvalue()).hexdigest()
|
||||||
|
read_len = fixture_object.len
|
||||||
fixture_objects[fixture_key] = fixture_object
|
fixture_objects[fixture_key] = fixture_object
|
||||||
fixture_headers[fixture_key] = {
|
fixture_headers[fixture_key] = {
|
||||||
'content-length': fixture_object.len,
|
'content-length': read_len,
|
||||||
'etag': hashlib.md5(fixture_object.read()).hexdigest()}
|
'etag': etag}
|
||||||
return fixture_headers[fixture_key]['etag']
|
return fixture_headers[fixture_key]['etag']
|
||||||
else:
|
else:
|
||||||
msg = ("Object PUT failed - Object with key %s already exists"
|
msg = ("Object PUT failed - Object with key %s already exists"
|
||||||
@ -189,8 +194,9 @@ class TestSwiftBackend(unittest.TestCase):
|
|||||||
def test_add(self):
|
def test_add(self):
|
||||||
"""Test that we can add an image via the swift backend"""
|
"""Test that we can add an image via the swift backend"""
|
||||||
expected_image_id = 42
|
expected_image_id = 42
|
||||||
expected_swift_size = 1024 * 5 # 5K
|
expected_swift_size = FIVE_KB
|
||||||
expected_swift_contents = "*" * expected_swift_size
|
expected_swift_contents = "*" * expected_swift_size
|
||||||
|
expected_checksum = hashlib.md5(expected_swift_contents).hexdigest()
|
||||||
expected_location = format_swift_location(
|
expected_location = format_swift_location(
|
||||||
SWIFT_OPTIONS['swift_store_user'],
|
SWIFT_OPTIONS['swift_store_user'],
|
||||||
SWIFT_OPTIONS['swift_store_key'],
|
SWIFT_OPTIONS['swift_store_key'],
|
||||||
@ -199,10 +205,12 @@ class TestSwiftBackend(unittest.TestCase):
|
|||||||
expected_image_id)
|
expected_image_id)
|
||||||
image_swift = StringIO.StringIO(expected_swift_contents)
|
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_location, location)
|
||||||
self.assertEquals(expected_swift_size, size)
|
self.assertEquals(expected_swift_size, size)
|
||||||
|
self.assertEquals(expected_checksum, checksum)
|
||||||
|
|
||||||
url_pieces = urlparse.urlparse(expected_location)
|
url_pieces = urlparse.urlparse(expected_location)
|
||||||
new_image_swift = SwiftBackend.get(url_pieces)
|
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_create_container_on_put'] = 'True'
|
||||||
options['swift_store_container'] = 'noexist'
|
options['swift_store_container'] = 'noexist'
|
||||||
expected_image_id = 42
|
expected_image_id = 42
|
||||||
expected_swift_size = 1024 * 5 # 5K
|
expected_swift_size = FIVE_KB
|
||||||
expected_swift_contents = "*" * expected_swift_size
|
expected_swift_contents = "*" * expected_swift_size
|
||||||
|
expected_checksum = hashlib.md5(expected_swift_contents).hexdigest()
|
||||||
expected_location = format_swift_location(
|
expected_location = format_swift_location(
|
||||||
options['swift_store_user'],
|
options['swift_store_user'],
|
||||||
options['swift_store_key'],
|
options['swift_store_key'],
|
||||||
@ -253,10 +262,12 @@ class TestSwiftBackend(unittest.TestCase):
|
|||||||
expected_image_id)
|
expected_image_id)
|
||||||
image_swift = StringIO.StringIO(expected_swift_contents)
|
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_location, location)
|
||||||
self.assertEquals(expected_swift_size, size)
|
self.assertEquals(expected_swift_size, size)
|
||||||
|
self.assertEquals(expected_checksum, checksum)
|
||||||
|
|
||||||
url_pieces = urlparse.urlparse(expected_location)
|
url_pieces = urlparse.urlparse(expected_location)
|
||||||
new_image_swift = SwiftBackend.get(url_pieces)
|
new_image_swift = SwiftBackend.get(url_pieces)
|
||||||
|
Loading…
Reference in New Issue
Block a user