From 4b5e795e085dd2ade9d2b8e93a0572878397d826 Mon Sep 17 00:00:00 2001 From: Jay Pipes Date: Thu, 15 Sep 2011 22:01:29 -0400 Subject: [PATCH] Fixes LP Bug #833285 Large objects, uploaded to Swift using the large object manifest method, are uploaded in chunks/segments in the Swift store driver. Unfortunately, doing a DELETE on the object only deletes the manifest, leaving the chunks/segments orphaned. This patch makes the Swift store driver check for the existence of an object manifest, and if found, deletes all intermediate chunks, then deletes the object manifest itself. Adds a functional test to check the segments are actually deleted. Change-Id: Ib5787581a57e419393faca198d867db757fc03b3 --- glance/store/swift.py | 22 +++++++++ glance/tests/functional/test_swift.py | 64 ++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/glance/store/swift.py b/glance/store/swift.py index d1e5d23305..7277be0ff7 100644 --- a/glance/store/swift.py +++ b/glance/store/swift.py @@ -424,6 +424,28 @@ def delete(self, location): auth_url=loc.swift_auth_url, user=loc.user, key=loc.key) try: + # We request the manifest for the object. If one exists, + # that means the object was uploaded in chunks/segments, + # and we need to delete all the chunks as well as the + # manifest. + manifest = None + try: + headers = swift_conn.head_object(loc.container, loc.obj) + manifest = headers.get('x-object-manifest') + except swift_client.ClientException, e: + if e.http_status != httplib.NOT_FOUND: + raise + if manifest: + # Delete all the chunks before the object manifest itself + obj_container, obj_prefix = manifest.split('/', 1) + for segment in swift_conn.get_container(obj_container, + prefix=obj_prefix)[1]: + # TODO(jaypipes): This would be an easy area to parallelize + # since we're simply sending off parallelizable requests + # to Swift to delete stuff. It's not like we're going to + # be hogging up network or file I/O here... + swift_conn.delete_object(obj_container, segment['name']) + swift_conn.delete_object(loc.container, loc.obj) except swift_client.ClientException, e: if e.http_status == httplib.NOT_FOUND: diff --git a/glance/tests/functional/test_swift.py b/glance/tests/functional/test_swift.py index 064f6c4dd1..fc0f9aa75a 100644 --- a/glance/tests/functional/test_swift.py +++ b/glance/tests/functional/test_swift.py @@ -39,6 +39,8 @@ import tempfile import unittest +import glance.store.swift # Needed to register driver for location +from glance.store.location import get_location_from_uri from glance.tests.functional import test_api from glance.tests.utils import execute, skip_if_disabled @@ -153,12 +155,15 @@ def clear_container(self): self.swift_conn.put_container(self.swift_store_container) @skip_if_disabled - def test_add_large_object_manifest(self): + def test_large_objects(self): """ We test the large object manifest code path in the Swift driver. In the case where an image file is bigger than the config variable swift_store_large_object_size, then we chunk the image into Swift, and add a manifest put_object at the end. + + We test that the delete of the large object cleans up all the + chunks in Swift, in addition to the manifest file (LP Bug# 833285) """ self.cleanup() @@ -169,7 +174,7 @@ def test_add_large_object_manifest(self): api_port = self.api_port registry_port = self.registry_port - # 0. GET /images + # GET /images # Verify no public images path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port) http = httplib2.Http() @@ -177,7 +182,7 @@ def test_add_large_object_manifest(self): self.assertEqual(response.status, 200) self.assertEqual(content, '{"images": []}') - # 1. POST /images with public image named Image1 + # POST /images with public image named Image1 # attribute and no custom properties. Verify a 200 OK is returned image_data = "*" * FIVE_MB headers = {'Content-Type': 'application/octet-stream', @@ -195,7 +200,7 @@ def test_add_large_object_manifest(self): self.assertEqual(data['image']['name'], "Image1") self.assertEqual(data['image']['is_public'], True) - # 4. HEAD /images/1 + # HEAD /images/1 # Verify image found now path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port) http = httplib2.Http() @@ -203,7 +208,7 @@ def test_add_large_object_manifest(self): self.assertEqual(response.status, 200) self.assertEqual(response['x-image-meta-name'], "Image1") - # 5. GET /images/1 + # GET /images/1 # Verify all information on image we just added is correct path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port) http = httplib2.Http() @@ -241,6 +246,55 @@ def test_add_large_object_manifest(self): self.assertEqual(hashlib.md5(content).hexdigest(), hashlib.md5("*" * FIVE_MB).hexdigest()) + # We test that the delete of the large object cleans up all the + # chunks in Swift, in addition to the manifest file (LP Bug# 833285) + + # Grab the actual Swift location and query the object manifest for + # the chunks/segments. We will check that the segments don't exist + # after we delete the object through Glance... + path = "http://%s:%d/images/1" % ("0.0.0.0", self.registry_port) + http = httplib2.Http() + response, content = http.request(path, 'GET') + self.assertEqual(response.status, 200) + data = json.loads(content) + image_loc = get_location_from_uri(data['image']['location']) + swift_loc = image_loc.store_location + + from swift.common import client as swift_client + swift_conn = swift_client.Connection( + authurl=swift_loc.swift_auth_url, + user=swift_loc.user, key=swift_loc.key) + + # Verify the object manifest exists + headers = swift_conn.head_object(swift_loc.container, swift_loc.obj) + manifest = headers.get('x-object-manifest') + self.assertTrue(manifest is not None, "Manifest could not be found!") + + # Grab the segment identifiers + obj_container, obj_prefix = manifest.split('/', 1) + segments = [segment['name'] for segment in + swift_conn.get_container(obj_container, + prefix=obj_prefix)[1]] + + # Verify the segments exist + for segment in segments: + headers = swift_conn.head_object(obj_container, segment) + self.assertTrue(headers.get('content-length') is not None, + headers) + + # DELETE /images/1 + # Verify image and all chunks are gone... + path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port) + http = httplib2.Http() + response, content = http.request(path, 'DELETE') + self.assertEqual(response.status, 200) + + # Verify the segments no longer exist + for segment in segments: + self.assertRaises(swift_client.ClientException, + swift_conn.head_object, + obj_container, segment) + self.stop_servers() @skip_if_disabled