Fixes Bug#696375: x-image-meta-size is not optional, contrary to

documentation. 

The image's size is set to zero now during reservation of the image ID if the
image's size is not passed in with headers. In addition,
glance.store.Backend.add() now returns a tuple of (location, size) and
the image's size attribute in the registry is updated to this value if 
previously set to zero.

Adds a new test case that ensures the size attribute is set properly
when not included in the image meta headers.

Adds documentation for the new _reserve(), _upload(), and _activate()
methods in glance.server.Controller.
This commit is contained in:
jaypipes@gmail.com 2011-01-18 12:58:23 -05:00
parent f73ee950a1
commit 56fa9a39bf
4 changed files with 98 additions and 8 deletions

View File

@ -163,9 +163,25 @@ class Controller(wsgi.Controller):
return req.get_response(res)
def _reserve(self, req):
"""
Adds the image metadata to the registry and assigns
an image identifier if one is not supplied in the request
headers. Sets the image's status to `queued`
:param request: The WSGI/Webob Request object
:param id: The opaque image identifier
:raises HTTPConflict if image already exists
:raises HTTPBadRequest if image metadata is not valid
"""
image_meta = util.get_image_meta_from_headers(req)
image_meta['status'] = 'queued'
# Ensure that the size attribute is set to zero for all
# queued instances. The size will be set to a non-zero
# value during upload
image_meta['size'] = image_meta.get('size', 0)
try:
image_meta = registry.add_image_metadata(image_meta)
return image_meta
@ -178,6 +194,18 @@ class Controller(wsgi.Controller):
raise HTTPBadRequest()
def _upload(self, req, image_meta):
"""
Uploads the payload of the request to a backend store in
Glance. If the `x-image-meta-store` header is set, Glance
will attempt to use that store, if not, Glance will use the
store set by the flag `default_store`.
:param request: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image
:raises HTTPConflict if image already exists
:retval The location where the image was stored
"""
content_type = req.headers.get('content-type', 'notset')
if content_type != 'application/octet-stream':
raise HTTPBadRequest(
@ -192,26 +220,49 @@ class Controller(wsgi.Controller):
registry.update_image_metadata(image_meta['id'], image_meta)
try:
location = store.add(image_meta['id'], req.body_file)
location, size = store.add(image_meta['id'], req.body_file)
# 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
registry.update_image_metadata(image_meta['id'], image_meta)
return location
except exception.Duplicate, e:
logging.error("Error adding image to store: %s", str(e))
raise HTTPConflict(str(e), request=req)
def _activate(self, req, image_meta, location):
"""
Sets the image status to `active` and the image's location
attribute.
:param request: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image
:param location: Location of where Glance stored this image
"""
image_meta['location'] = location
image_meta['status'] = 'active'
registry.update_image_metadata(image_meta['id'], image_meta)
def _kill(self, req, image_meta):
"""
Marks the image status to `killed`
:param request: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image
"""
image_meta['status'] = 'killed'
registry.update_image_metadata(image_meta['id'], image_meta)
def _safe_kill(self, req, image_meta):
"""Mark image killed without raising exceptions if it fails.
"""
Mark image killed without raising exceptions if it fails.
Since _kill is meant to be called from exceptions handlers, it should
not raise itself, rather it should just log its error.
:param request: The WSGI/Webob Request object
"""
try:
self._kill(req, image_meta)
@ -220,6 +271,14 @@ class Controller(wsgi.Controller):
image_meta['id'], repr(e))
def _upload_and_activate(self, req, image_meta):
"""
Safely uploads the image data in the request payload
and activates the image in the registry after a successful
upload.
:param request: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image
"""
try:
location = self._upload(req, image_meta)
self._activate(req, image_meta, location)
@ -282,7 +341,6 @@ class Controller(wsgi.Controller):
:param id: The opaque image identifier
:retval Returns the updated image information as a mapping
"""
orig_image_meta = self.get_image_meta_or_404(req, id)
new_image_meta = util.get_image_meta_from_headers(req)

View File

@ -112,7 +112,9 @@ class FilesystemBackend(glance.store.Backend):
:param id: The opaque image identifier
:param data: The image data to write, as a file-like object
:retval The location that was written, with file:// scheme prepended
:retval Tuple with (location, size)
The location that was written, with file:// scheme prepended
and the size in bytes of the data written
"""
datadir = FLAGS.filesystem_store_datadir
@ -125,11 +127,13 @@ class FilesystemBackend(glance.store.Backend):
raise exception.Duplicate("Image file %s already exists!"
% filepath)
bytes_written = 0
with open(filepath, 'wb') as f:
while True:
buf = data.read(ChunkedFile.CHUNKSIZE)
if not buf:
break
bytes_written += len(buf)
f.write(buf)
return 'file://%s' % filepath
return ('file://%s' % filepath, bytes_written)

View File

@ -361,6 +361,7 @@ def stub_out_registry_db_image_api(stubs):
raise exception.Invalid("Invalid status '%s' for image" %
values['status'])
values['size'] = values.get('size', 0)
values['deleted'] = False
values['properties'] = values.get('properties', {})
values['created_at'] = datetime.datetime.utcnow()

View File

@ -391,6 +391,7 @@ class TestClient(unittest.TestCase):
image_meta = self.client.add_image(fixture)
self.assertEquals('queued', image_meta['status'])
self.assertEquals(0, image_meta['size'])
def test_add_image_basic(self):
"""Tests that we can add image metadata and returns the new id"""
@ -487,7 +488,7 @@ class TestClient(unittest.TestCase):
'properties': {'distro': 'Ubuntu 10.04 LTS'}
}
image_data_fixture = r"chunk0000remainder"
image_data_fixture = r"chunk00000remainder"
new_image = self.client.add_image(fixture, image_data_fixture)
new_image_id = new_image['id']
@ -512,7 +513,7 @@ class TestClient(unittest.TestCase):
'properties': {'distro': 'Ubuntu 10.04 LTS'}
}
image_data_fixture = r"chunk0000remainder"
image_data_fixture = r"chunk00000remainder"
tmp_image_filepath = '/tmp/rubbish-image'
@ -540,6 +541,32 @@ class TestClient(unittest.TestCase):
for k, v in fixture.iteritems():
self.assertEquals(v, new_meta[k])
def test_add_image_with_image_data_as_string_and_no_size(self):
"""Tests add image by passing image data as string w/ no size attr"""
fixture = {'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'properties': {'distro': 'Ubuntu 10.04 LTS'}
}
image_data_fixture = r"chunk00000remainder"
new_image = self.client.add_image(fixture, image_data_fixture)
new_image_id = new_image['id']
self.assertEquals(3, new_image_id)
new_meta, new_image_chunks = self.client.get_image(3)
new_image_data = ""
for image_chunk in new_image_chunks:
new_image_data += image_chunk
self.assertEquals(image_data_fixture, new_image_data)
for k, v in fixture.iteritems():
self.assertEquals(v, new_meta[k])
self.assertEquals(19, new_meta['size'])
def test_add_image_with_bad_store(self):
"""Tests BadRequest raised when supplying bad store name in meta"""
fixture = {'name': 'fake public image',
@ -550,7 +577,7 @@ class TestClient(unittest.TestCase):
'properties': {'distro': 'Ubuntu 10.04 LTS'}
}
image_data_fixture = r"chunk0000remainder"
image_data_fixture = r"chunk00000remainder"
self.assertRaises(exception.BadInputError,
self.client.add_image,