# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Functional tests for the Swift store interface Set the GLANCE_TEST_SWIFT_CONF environment variable to the location of a Glance config that defines how to connect to a functional Swift backend """ import ConfigParser import hashlib import os import os.path import random import string import StringIO import uuid import oslo.config.cfg import six.moves.urllib.parse as urlparse import testtools from glance.common import exception import glance.common.utils as common_utils import glance.store.swift import glance.tests.functional.store as store_tests try: import swiftclient except ImportError: swiftclient = None class SwiftStoreError(RuntimeError): pass def _uniq(value): return '%s.%d' % (value, random.randint(0, 99999)) def read_config(path): cp = ConfigParser.RawConfigParser() cp.read(path) return cp def parse_config(config): out = {} options = [ 'swift_store_auth_address', 'swift_store_auth_version', 'swift_store_user', 'swift_store_key', 'swift_store_container', ] for option in options: out[option] = config.defaults()[option] return out def swift_connect(auth_url, auth_version, user, key): try: return swiftclient.Connection(authurl=auth_url, auth_version=auth_version, user=user, key=key, snet=False, retries=1) except AttributeError: raise SwiftStoreError("Could not find swiftclient module") def swift_list_containers(swift_conn): try: _, containers = swift_conn.get_account() except Exception as e: msg = ("Failed to list containers (get_account) " "from Swift. Got error: %s" % e) raise SwiftStoreError(msg) else: return containers def swift_create_container(swift_conn, container_name): try: swift_conn.put_container(container_name) except swiftclient.ClientException as e: msg = "Failed to create container. Got error: %s" % e raise SwiftStoreError(msg) def swift_get_container(swift_conn, container_name, **kwargs): return swift_conn.get_container(container_name, **kwargs) def swift_delete_container(swift_conn, container_name): try: swift_conn.delete_container(container_name) except swiftclient.ClientException as e: msg = "Failed to delete container from Swift. Got error: %s" % e raise SwiftStoreError(msg) def swift_put_object(swift_conn, container_name, object_name, contents): return swift_conn.put_object(container_name, object_name, contents) def swift_head_object(swift_conn, container_name, obj_name): return swift_conn.head_object(container_name, obj_name) def keystone_authenticate(auth_url, auth_version, tenant_name, username, password): assert int(auth_version) == 2, 'Only auth version 2 is supported' import keystoneclient.v2_0.client ksclient = keystoneclient.v2_0.client.Client(tenant_name=tenant_name, username=username, password=password, auth_url=auth_url) auth_resp = ksclient.service_catalog.catalog tenant_id = auth_resp['token']['tenant']['id'] service_catalog = auth_resp['serviceCatalog'] return tenant_id, ksclient.auth_token, service_catalog class TestSwiftStore(store_tests.BaseTestCase, testtools.TestCase): store_cls_path = 'glance.store.swift.Store' store_cls = glance.store.swift.Store store_name = 'swift' def setUp(self): config_path = os.environ.get('GLANCE_TEST_SWIFT_CONF') if not config_path: msg = "GLANCE_TEST_SWIFT_CONF environ not set." self.skipTest(msg) oslo.config.cfg.CONF(args=[], default_config_files=[config_path]) raw_config = read_config(config_path) config = parse_config(raw_config) swift = swift_connect(config['swift_store_auth_address'], config['swift_store_auth_version'], config['swift_store_user'], config['swift_store_key']) #NOTE(bcwaldon): Ensure we have a functional swift connection swift_list_containers(swift) self.swift_client = swift self.swift_config = config self.swift_config['swift_store_create_container_on_put'] = True super(TestSwiftStore, self).setUp() def get_store(self, **kwargs): store = glance.store.swift.Store(context=kwargs.get('context')) store.configure() store.configure_add() return store def test_object_chunking(self): """Upload an image that is split into multiple swift objects. We specifically check the case that image_size % swift_store_large_object_chunk_size != 0 to ensure we aren't losing image data. """ self.config( swift_store_large_object_size=2, # 2 MB swift_store_large_object_chunk_size=2, # 2 MB ) store = self.get_store() image_id = str(uuid.uuid4()) image_size = 5242880 # 5 MB image_data = StringIO.StringIO('X' * image_size) image_checksum = 'eb7f8c3716b9f059cee7617a4ba9d0d3' uri, add_size, add_checksum, _ = store.add(image_id, image_data, image_size) self.assertEqual(image_size, add_size) self.assertEqual(image_checksum, add_checksum) location = glance.store.location.Location( self.store_name, store.get_store_location_class(), uri=uri, image_id=image_id) # Store interface should still be respected even though # we are storing images in multiple Swift objects (get_iter, get_size) = store.get(location) self.assertEqual(5242880, get_size) self.assertEqual('X' * 5242880, ''.join(get_iter)) # The object should have a manifest pointing to the chunks # of image data swift_location = location.store_location headers = swift_head_object(self.swift_client, swift_location.container, swift_location.obj) manifest = headers.get('x-object-manifest') self.assertTrue(manifest) # Verify the objects in the manifest exist manifest_container, manifest_prefix = manifest.split('/', 1) container = swift_get_container(self.swift_client, manifest_container, prefix=manifest_prefix) segments = [segment['name'] for segment in container[1]] for segment in segments: headers = swift_head_object(self.swift_client, manifest_container, segment) self.assertTrue(headers.get('content-length')) # Since we used a 5 MB image with a 2 MB chunk size, we should # expect to see three data objects self.assertEqual(3, len(segments), 'Got segments %s' % segments) # Add an object that should survive the delete operation non_image_obj = image_id + '0' swift_put_object(self.swift_client, manifest_container, non_image_obj, 'XXX') store.delete(location) # Verify the segments in the manifest are all gone for segment in segments: self.assertRaises(swiftclient.ClientException, swift_head_object, self.swift_client, manifest_container, segment) # Verify the manifest is gone too self.assertRaises(swiftclient.ClientException, swift_head_object, self.swift_client, manifest_container, swift_location.obj) # Verify that the non-image object was not deleted headers = swift_head_object(self.swift_client, manifest_container, non_image_obj) self.assertTrue(headers.get('content-length')) # Clean up self.swift_client.delete_object(manifest_container, non_image_obj) # Simulate exceeding 'image_size_cap' setting image_data = StringIO.StringIO('X' * image_size) image_data = common_utils.LimitingReader(image_data, image_size - 1) image_id = str(uuid.uuid4()) self.assertRaises(exception.ImageSizeLimitExceeded, store.add, image_id, image_data, image_size) # Verify written segments have been deleted container = swift_get_container(self.swift_client, manifest_container, prefix=image_id) segments = [segment['name'] for segment in container[1]] self.assertEqual(0, len(segments), 'Got segments %s' % segments) def test_retries_fail_start_of_download(self): """ Get an object from Swift where Swift does not complete the request in one attempt. Fails at the start of the download. """ self.config( swift_store_retry_get_count=1, ) store = self.get_store() image_id = str(uuid.uuid4()) image_size = 1024 * 1024 * 5 # 5 MB chars = string.ascii_uppercase + string.digits image_data = ''.join(random.choice(chars) for x in range(image_size)) image_checksum = hashlib.md5(image_data) uri, add_size, add_checksum, _ = store.add(image_id, image_data, image_size) location = glance.store.location.Location( self.store_name, store.get_store_location_class(), uri=uri, image_id=image_id) def iter_wrapper(iterable): # raise StopIteration as soon as iteration begins yield '' (get_iter, get_size) = store.get(location) get_iter.wrapped = glance.store.swift.swift_retry_iter( iter_wrapper(get_iter.wrapped), image_size, store, location.store_location) self.assertEqual(image_size, get_size) received_data = ''.join(get_iter.wrapped) self.assertEqual(image_data, received_data) self.assertEqual(image_checksum.hexdigest(), hashlib.md5(received_data).hexdigest()) def test_retries_fail_partway_through_download(self): """ Get an object from Swift where Swift does not complete the request in one attempt. Fails partway through the download. """ self.config( swift_store_retry_get_count=1, ) store = self.get_store() image_id = str(uuid.uuid4()) image_size = 1024 * 1024 * 5 # 5 MB chars = string.ascii_uppercase + string.digits image_data = ''.join(random.choice(chars) for x in range(image_size)) image_checksum = hashlib.md5(image_data) uri, add_size, add_checksum, _ = store.add(image_id, image_data, image_size) location = glance.store.location.Location( self.store_name, store.get_store_location_class(), uri=uri, image_id=image_id) def iter_wrapper(iterable): bytes_received = 0 for chunk in iterable: yield chunk bytes_received += len(chunk) if bytes_received > (image_size / 2): raise StopIteration (get_iter, get_size) = store.get(location) get_iter.wrapped = glance.store.swift.swift_retry_iter( iter_wrapper(get_iter.wrapped), image_size, store, location.store_location) self.assertEqual(image_size, get_size) received_data = ''.join(get_iter.wrapped) self.assertEqual(image_data, received_data) self.assertEqual(image_checksum.hexdigest(), hashlib.md5(received_data).hexdigest()) def test_retries_fail_end_of_download(self): """ Get an object from Swift where Swift does not complete the request in one attempt. Fails at the end of the download """ self.config( swift_store_retry_get_count=1, ) store = self.get_store() image_id = str(uuid.uuid4()) image_size = 1024 * 1024 * 5 # 5 MB chars = string.ascii_uppercase + string.digits image_data = ''.join(random.choice(chars) for x in range(image_size)) image_checksum = hashlib.md5(image_data) uri, add_size, add_checksum, _ = store.add(image_id, image_data, image_size) location = glance.store.location.Location( self.store_name, store.get_store_location_class(), uri=uri, image_id=image_id) def iter_wrapper(iterable): bytes_received = 0 for chunk in iterable: yield chunk bytes_received += len(chunk) if bytes_received == image_size: raise StopIteration (get_iter, get_size) = store.get(location) get_iter.wrapped = glance.store.swift.swift_retry_iter( iter_wrapper(get_iter.wrapped), image_size, store, location.store_location) self.assertEqual(image_size, get_size) received_data = ''.join(get_iter.wrapped) self.assertEqual(image_data, received_data) self.assertEqual(image_checksum.hexdigest(), hashlib.md5(received_data).hexdigest()) def stash_image(self, image_id, image_data): container_name = self.swift_config['swift_store_container'] swift_put_object(self.swift_client, container_name, image_id, 'XXX') #NOTE(bcwaldon): This is a hack until we find a better way to # build this URL auth_url = self.swift_config['swift_store_auth_address'] auth_url = urlparse.urlparse(auth_url) user = urlparse.quote(self.swift_config['swift_store_user']) key = self.swift_config['swift_store_key'] netloc = ''.join(('%s:%s' % (user, key), '@', auth_url.netloc)) path = os.path.join(auth_url.path, container_name, image_id) # This is an auth url with // on the end return 'swift+http://%s%s' % (netloc, path) def test_multitenant(self): """Ensure an image is properly configured when using multitenancy.""" fake_swift_admin = 'd2f68325-8e2c-4fb1-8c8b-89de2f3d9c4a' self.config( swift_store_multi_tenant=True, ) swift_store_user = self.swift_config['swift_store_user'] tenant_name, username = swift_store_user.split(':') tenant_id, auth_token, service_catalog = keystone_authenticate( self.swift_config['swift_store_auth_address'], self.swift_config['swift_store_auth_version'], tenant_name, username, self.swift_config['swift_store_key']) context = glance.context.RequestContext( tenant=tenant_id, service_catalog=service_catalog, auth_tok=auth_token) store = self.get_store(context=context) image_id = str(uuid.uuid4()) image_data = StringIO.StringIO('XXX') uri, _, _, _ = store.add(image_id, image_data, 3) location = glance.store.location.Location( self.store_name, store.get_store_location_class(), uri=uri, image_id=image_id) read_tenant = str(uuid.uuid4()) write_tenant = str(uuid.uuid4()) store.set_acls(location, public=False, read_tenants=[read_tenant], write_tenants=[write_tenant]) container_name = location.store_location.container container, _ = swift_get_container(self.swift_client, container_name) self.assertEqual(read_tenant + ':*', container.get('x-container-read')) self.assertEqual(write_tenant + ':*', container.get('x-container-write')) store.set_acls(location, public=True, read_tenants=[read_tenant]) container_name = location.store_location.container container, _ = swift_get_container(self.swift_client, container_name) self.assertEqual('.r:*,.rlistings', container.get('x-container-read')) self.assertEqual('', container.get('x-container-write', '')) (get_iter, get_size) = store.get(location) self.assertEqual(3, get_size) self.assertEqual('XXX', ''.join(get_iter)) store.delete(location) def test_delayed_delete_with_auth(self): """Ensure delete works with delayed delete and auth Reproduces LP bug 1238604. """ swift_store_user = self.swift_config['swift_store_user'] tenant_name, username = swift_store_user.split(':') tenant_id, auth_token, service_catalog = keystone_authenticate( self.swift_config['swift_store_auth_address'], self.swift_config['swift_store_auth_version'], tenant_name, username, self.swift_config['swift_store_key']) context = glance.context.RequestContext( tenant=tenant_id, service_catalog=service_catalog, auth_tok=auth_token) store = self.get_store(context=context) image_id = str(uuid.uuid4()) image_data = StringIO.StringIO('data') uri, _, _, _ = store.add(image_id, image_data, 4) location = glance.store.location.Location( self.store_name, store.get_store_location_class(), uri=uri, image_id=image_id) container_name = location.store_location.container container, _ = swift_get_container(self.swift_client, container_name) (get_iter, get_size) = store.get(location) self.assertEqual(4, get_size) self.assertEqual('data', ''.join(get_iter)) glance.store.schedule_delayed_delete_from_backend(context, uri, image_id) store.delete(location)