Support zero-size image creation via the v1 API
Addresses LP 1025353 for the v1 API. Transition image status to active immediately on creation (as opposed to leaving it queued forever) if the size is set to zero from the get-go. The v2 implementation is left unchanged for now, as the image status does not appear to ever transition from queued to active in that case. This change allows an image to be created that simply acts as a properties bucket, but requires no image data. For example, an image created from a booted-from-volume instance where only the kernel, ramdisk ID, and block device mappings are required. Change-Id: I61e96f3fe5f5245fec791170b4a8b4c72135c3de
This commit is contained in:
parent
00a7683555
commit
6873195cd1
@ -22,7 +22,8 @@ Images in Glance can be in one the following statuses:
|
|||||||
* ``queued``
|
* ``queued``
|
||||||
|
|
||||||
The image identifier has been reserved for an image in the Glance
|
The image identifier has been reserved for an image in the Glance
|
||||||
registry. No image data has been uploaded to Glance.
|
registry. No image data has been uploaded to Glance and the image
|
||||||
|
size was not explicitly set to zero on creation.
|
||||||
|
|
||||||
* ``saving``
|
* ``saving``
|
||||||
|
|
||||||
@ -34,7 +35,9 @@ Images in Glance can be in one the following statuses:
|
|||||||
|
|
||||||
* ``active``
|
* ``active``
|
||||||
|
|
||||||
Denotes an image that is fully available in Glance.
|
Denotes an image that is fully available in Glance. This occurs when
|
||||||
|
the image data is uploaded, or the image size is explicitly set to
|
||||||
|
zero on creation.
|
||||||
|
|
||||||
* ``killed``
|
* ``killed``
|
||||||
|
|
||||||
|
@ -272,12 +272,16 @@ class Controller(controller.BaseController):
|
|||||||
self._enforce(req, 'get_image')
|
self._enforce(req, 'get_image')
|
||||||
image_meta = self.get_active_image_meta_or_404(req, id)
|
image_meta = self.get_active_image_meta_or_404(req, id)
|
||||||
|
|
||||||
|
if image_meta.get('size') == 0:
|
||||||
|
image_iterator = iter([])
|
||||||
|
else:
|
||||||
image_iterator, size = self._get_from_store(image_meta['location'])
|
image_iterator, size = self._get_from_store(image_meta['location'])
|
||||||
|
image_iterator = utils.cooperative_iter(image_iterator)
|
||||||
image_meta['size'] = size or image_meta['size']
|
image_meta['size'] = size or image_meta['size']
|
||||||
|
|
||||||
del image_meta['location']
|
del image_meta['location']
|
||||||
return {
|
return {
|
||||||
'image_iterator': utils.cooperative_iter(image_iterator),
|
'image_iterator': image_iterator,
|
||||||
'image_meta': image_meta,
|
'image_meta': image_meta,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,6 +299,10 @@ class Controller(controller.BaseController):
|
|||||||
:raises HTTPBadRequest if image metadata is not valid
|
:raises HTTPBadRequest if image metadata is not valid
|
||||||
"""
|
"""
|
||||||
location = self._external_source(image_meta, req)
|
location = self._external_source(image_meta, req)
|
||||||
|
|
||||||
|
image_meta['status'] = ('active' if image_meta.get('size') == 0
|
||||||
|
else 'queued')
|
||||||
|
|
||||||
if location:
|
if location:
|
||||||
store = get_store_from_location(location)
|
store = get_store_from_location(location)
|
||||||
# check the store exists before we hit the registry, but we
|
# check the store exists before we hit the registry, but we
|
||||||
@ -309,8 +317,6 @@ class Controller(controller.BaseController):
|
|||||||
# to a non-zero value during upload
|
# to a non-zero value during upload
|
||||||
image_meta['size'] = image_meta.get('size', 0)
|
image_meta['size'] = image_meta.get('size', 0)
|
||||||
|
|
||||||
image_meta['status'] = 'queued'
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image_meta = registry.add_image_metadata(req.context, image_meta)
|
image_meta = registry.add_image_metadata(req.context, image_meta)
|
||||||
return image_meta
|
return image_meta
|
||||||
|
@ -445,6 +445,48 @@ class TestApi(functional.FunctionalTest):
|
|||||||
|
|
||||||
self.stop_servers()
|
self.stop_servers()
|
||||||
|
|
||||||
|
@skip_if_disabled
|
||||||
|
def test_zero_initial_size(self):
|
||||||
|
"""
|
||||||
|
A test to ensure that an image with size explicitly set to zero
|
||||||
|
has status that immediately transitions to active.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.cleanup()
|
||||||
|
self.start_servers(**self.__dict__.copy())
|
||||||
|
|
||||||
|
# 1. POST /images with public image named Image1
|
||||||
|
# attribute and a size of zero.
|
||||||
|
# Verify a 201 OK is returned
|
||||||
|
headers = {'Content-Type': 'application/octet-stream',
|
||||||
|
'X-Image-Meta-Size': '0',
|
||||||
|
'X-Image-Meta-Name': 'Image1',
|
||||||
|
'X-Image-Meta-disk_format': 'raw',
|
||||||
|
'X-image-Meta-container_format': 'ovf',
|
||||||
|
'X-Image-Meta-Is-Public': 'True'}
|
||||||
|
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
||||||
|
http = httplib2.Http()
|
||||||
|
response, content = http.request(path, 'POST', headers=headers)
|
||||||
|
self.assertEqual(response.status, 201)
|
||||||
|
|
||||||
|
# 2. HEAD image-location
|
||||||
|
# Verify image size is zero and the status is active
|
||||||
|
path = response.get('location')
|
||||||
|
http = httplib2.Http()
|
||||||
|
response, content = http.request(path, 'HEAD')
|
||||||
|
self.assertEqual(response.status, 200)
|
||||||
|
self.assertEqual(response['x-image-meta-size'], '0')
|
||||||
|
self.assertEqual(response['x-image-meta-status'], 'active')
|
||||||
|
|
||||||
|
# 3. GET image-location
|
||||||
|
# Verify image content is empty
|
||||||
|
http = httplib2.Http()
|
||||||
|
response, content = http.request(path, 'GET')
|
||||||
|
self.assertEqual(response.status, 200)
|
||||||
|
self.assertEqual(len(content), 0)
|
||||||
|
|
||||||
|
self.stop_servers()
|
||||||
|
|
||||||
@skip_if_disabled
|
@skip_if_disabled
|
||||||
def test_traceback_not_consumed(self):
|
def test_traceback_not_consumed(self):
|
||||||
"""
|
"""
|
||||||
|
@ -2158,6 +2158,30 @@ class TestGlanceAPI(base.IsolatedUnitTest):
|
|||||||
res = req.get_response(self.api)
|
res = req.get_response(self.api)
|
||||||
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
|
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
|
||||||
|
|
||||||
|
def test_add_image_zero_size(self):
|
||||||
|
"""Tests creating an active image with explicitly zero size"""
|
||||||
|
fixture_headers = {'x-image-meta-disk-format': 'ami',
|
||||||
|
'x-image-meta-container-format': 'ami',
|
||||||
|
'x-image-meta-size': '0',
|
||||||
|
'x-image-meta-name': 'empty image'}
|
||||||
|
|
||||||
|
req = webob.Request.blank("/images")
|
||||||
|
req.method = 'POST'
|
||||||
|
for k, v in fixture_headers.iteritems():
|
||||||
|
req.headers[k] = v
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
self.assertEquals(res.status_int, httplib.CREATED)
|
||||||
|
|
||||||
|
res_body = json.loads(res.body)['image']
|
||||||
|
self.assertEquals('active', res_body['status'])
|
||||||
|
image_id = res_body['id']
|
||||||
|
|
||||||
|
# GET empty image
|
||||||
|
req = webob.Request.blank("/images/%s" % image_id)
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
self.assertEqual(res.status_int, 200)
|
||||||
|
self.assertEqual(len(res.body), 0)
|
||||||
|
|
||||||
def test_add_image_bad_store(self):
|
def test_add_image_bad_store(self):
|
||||||
"""Tests raises BadRequest for invalid store header"""
|
"""Tests raises BadRequest for invalid store header"""
|
||||||
fixture_headers = {'x-image-meta-store': 'bad',
|
fixture_headers = {'x-image-meta-store': 'bad',
|
||||||
|
Loading…
Reference in New Issue
Block a user