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 @@ def show(self, req, id): 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 @@ def _reserve(self, req, image_meta): :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 @@ def _reserve(self, req, image_meta): # 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 @@ def test_size_greater_2G_mysql(self): 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 @@ def test_add_image_size_too_big(self): 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',