From 6873195cd19d519a5f6f31461b7b683e33b19784 Mon Sep 17 00:00:00 2001 From: Eoghan Glynn Date: Thu, 12 Jul 2012 15:04:22 +0100 Subject: [PATCH] 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 --- doc/source/statuses.rst | 7 +++-- glance/api/v1/images.py | 16 +++++++--- glance/tests/functional/v1/test_api.py | 42 ++++++++++++++++++++++++++ glance/tests/unit/v1/test_api.py | 24 +++++++++++++++ 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/doc/source/statuses.rst b/doc/source/statuses.rst index 431913639a..0dd1fc3581 100644 --- a/doc/source/statuses.rst +++ b/doc/source/statuses.rst @@ -22,7 +22,8 @@ Images in Glance can be in one the following statuses: * ``queued`` 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`` @@ -34,7 +35,9 @@ Images in Glance can be in one the following statuses: * ``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`` diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py index c7dbd46ea9..caeb5e71ef 100644 --- a/glance/api/v1/images.py +++ b/glance/api/v1/images.py @@ -272,12 +272,16 @@ class Controller(controller.BaseController): self._enforce(req, 'get_image') image_meta = self.get_active_image_meta_or_404(req, id) - image_iterator, size = self._get_from_store(image_meta['location']) - image_meta['size'] = size or image_meta['size'] + if image_meta.get('size') == 0: + image_iterator = iter([]) + else: + 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'] del image_meta['location'] return { - 'image_iterator': utils.cooperative_iter(image_iterator), + 'image_iterator': image_iterator, 'image_meta': image_meta, } @@ -295,6 +299,10 @@ class Controller(controller.BaseController): :raises HTTPBadRequest if image metadata is not valid """ location = self._external_source(image_meta, req) + + image_meta['status'] = ('active' if image_meta.get('size') == 0 + else 'queued') + if location: store = get_store_from_location(location) # 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 image_meta['size'] = image_meta.get('size', 0) - image_meta['status'] = 'queued' - try: image_meta = registry.add_image_metadata(req.context, image_meta) return image_meta diff --git a/glance/tests/functional/v1/test_api.py b/glance/tests/functional/v1/test_api.py index 86b0776c5a..8adaf9407f 100644 --- a/glance/tests/functional/v1/test_api.py +++ b/glance/tests/functional/v1/test_api.py @@ -445,6 +445,48 @@ class TestApi(functional.FunctionalTest): 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 def test_traceback_not_consumed(self): """ diff --git a/glance/tests/unit/v1/test_api.py b/glance/tests/unit/v1/test_api.py index f7b8076ea5..d9331ee68c 100644 --- a/glance/tests/unit/v1/test_api.py +++ b/glance/tests/unit/v1/test_api.py @@ -2158,6 +2158,30 @@ class TestGlanceAPI(base.IsolatedUnitTest): res = req.get_response(self.api) 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): """Tests raises BadRequest for invalid store header""" fixture_headers = {'x-image-meta-store': 'bad',