diff --git a/etc/glance-api.conf b/etc/glance-api.conf index b8fc82b01d..8d144744fa 100644 --- a/etc/glance-api.conf +++ b/etc/glance-api.conf @@ -5,11 +5,20 @@ verbose = True # Show debugging output in logs (sets DEBUG log level output) debug = False -# Which backend store should Glance use by default is not specified -# in a request to add a new image to Glance? Default: 'file' -# Available choices are 'file', 'swift', and 's3' +# Which backend scheme should Glance use by default is not specified +# in a request to add a new image to Glance? Known schemes are determined +# by the known_stores option below. +# Default: 'file' default_store = file +# List of which store classes and store class locations are +# currently known to glance at startup. +known_stores = glance.store.filesystem.Store, + glance.store.http.Store, + glance.store.rbd.Store, + glance.store.s3.Store, + glance.store.swift.Store, + # Address to bind the API server bind_host = 0.0.0.0 diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py index dad9af4aad..11cf9d470e 100644 --- a/glance/api/v1/images.py +++ b/glance/api/v1/images.py @@ -41,13 +41,8 @@ from glance.common import exception from glance.common import wsgi from glance.common import utils from glance.openstack.common import cfg -import glance.store -import glance.store.filesystem -import glance.store.http -import glance.store.rbd -import glance.store.s3 -import glance.store.swift -from glance.store import (get_from_backend, +from glance.store import (create_stores, + get_from_backend, get_size_from_backend, schedule_delete_from_backend, get_store_from_location, @@ -67,6 +62,10 @@ SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS # as a BigInteger. IMAGE_SIZE_CAP = 1 << 50 +# Defined at module level due to _is_opt_registered +# identity check (not equality). +default_store_opt = cfg.StrOpt('default_store', default='file') + class Controller(controller.BaseController): """ @@ -87,13 +86,11 @@ class Controller(controller.BaseController): DELETE /images/ -- Delete the image with id """ - default_store_opt = cfg.StrOpt('default_store', default='file') - def __init__(self, conf): self.conf = conf - self.conf.register_opt(self.default_store_opt) - glance.store.create_stores(conf) - self.verify_store_or_exit(self.conf.default_store) + self.conf.register_opt(default_store_opt) + create_stores(self.conf) + self.verify_scheme_or_exit(self.conf.default_store) self.notifier = notifier.Notifier(conf) registry.configure_registry_client(conf) self.policy = policy.Enforcer(conf) @@ -333,8 +330,8 @@ class Controller(controller.BaseController): """ Uploads the payload of the request to a backend store in Glance. If the `x-image-meta-store` header is set, Glance - will attempt to use that store, if not, Glance will use the - store set by the flag `default_store`. + will attempt to use that scheme; if not, Glance will use the + scheme set by the flag `default_store` to find the backing store. :param req: The WSGI/Webob Request object :param image_meta: Mapping of metadata about image @@ -367,10 +364,10 @@ class Controller(controller.BaseController): "x-image-meta-size header")) image_size = 0 - store_name = req.headers.get('x-image-meta-store', - self.conf.default_store) + scheme = req.headers.get('x-image-meta-store', + self.conf.default_store) - store = self.get_store_or_400(req, store_name) + store = self.get_store_or_400(req, scheme) image_id = image_meta['id'] logger.debug(_("Setting image %s to status 'saving'"), image_id) @@ -378,7 +375,7 @@ class Controller(controller.BaseController): {'status': 'saving'}) try: logger.debug(_("Uploading image data for image %(image_id)s " - "to %(store_name)s store"), locals()) + "to %(scheme)s store"), locals()) if image_size > IMAGE_SIZE_CAP: max_image_size = IMAGE_SIZE_CAP @@ -750,41 +747,39 @@ class Controller(controller.BaseController): else: self.notifier.info('image.delete', id) - def get_store_or_400(self, request, store_name): + def get_store_or_400(self, request, scheme): """ Grabs the storage backend for the supplied store name or raises an HTTPBadRequest (400) response :param request: The WSGI/Webob Request object - :param store_name: The backend store name + :param scheme: The backend store scheme :raises HTTPNotFound if store does not exist """ try: - return get_store_from_scheme(store_name) + return get_store_from_scheme(scheme) except exception.UnknownScheme: - msg = (_("Requested store %s not available on this Glance server") - % store_name) - logger.error(msg) + msg = _("Store for scheme %s not found") + logger.error(msg % scheme) raise HTTPBadRequest(msg, request=request, content_type='text/plain') - def verify_store_or_exit(self, store_name): + def verify_scheme_or_exit(self, scheme): """ Verifies availability of the storage backend for the - given store name or exits + given scheme or exits - :param store_name: The backend store name + :param scheme: The backend store scheme """ try: - get_store_from_scheme(store_name) + get_store_from_scheme(scheme) except exception.UnknownScheme: - msg = (_("Default store %s not available on this Glance server\n") - % store_name) - logger.error(msg) + msg = _("Store for scheme %s not found") + logger.error(msg % scheme) # message on stderr will only be visible if started directly via # bin/glance-api, as opposed to being daemonized by glance-control - sys.stderr.write(msg) + sys.stderr.write(msg % scheme) sys.exit(255) diff --git a/glance/api/v2/image_data.py b/glance/api/v2/image_data.py index c1e851f4a8..c68076ad86 100644 --- a/glance/api/v2/image_data.py +++ b/glance/api/v2/image_data.py @@ -20,11 +20,6 @@ from glance.common import exception from glance.common import wsgi import glance.registry.db.api import glance.store -import glance.store.filesystem -import glance.store.http -import glance.store.rbd -import glance.store.s3 -import glance.store.swift class ImageDataController(base.Controller): diff --git a/glance/store/__init__.py b/glance/store/__init__.py index c21f6e2cc5..a128fd6125 100644 --- a/glance/store/__init__.py +++ b/glance/store/__init__.py @@ -28,9 +28,6 @@ from glance.store import location logger = logging.getLogger('glance.store') -# Set of known store modules -REGISTERED_STORE_MODULES = [] - # Set of store objects, constructed in create_stores() STORES = {} @@ -128,50 +125,68 @@ class Indexable(object): return self.size -def register_store(store_module, schemes): - """ - Registers a store module and a set of schemes - for which a particular URI request should be routed. - - :param store_module: String representing the store module - :param schemes: List of strings representing schemes for - which this store should be used in routing - """ +def _get_store_class(store_entry): + store_cls = None try: - utils.import_class(store_module + '.Store') + logger.debug("Attempting to import store %s", store_entry) + store_cls = utils.import_class(store_entry) except exception.NotFound: - raise BackendException('Unable to register store. Could not find ' - 'a class named Store in module %s.' - % store_module) - REGISTERED_STORE_MODULES.append(store_module) - scheme_map = {} - for scheme in schemes: - scheme_map[scheme] = store_module - location.register_scheme_map(scheme_map) + raise BackendException('Unable to load store. ' + 'Could not find a class named %s.' + % store_entry) + return store_cls + + +known_stores_opt = cfg.ListOpt('known_stores', + default=('glance.store.filesystem.Store',)) def create_stores(conf): """ - Construct the store objects with supplied configuration options + Registers all store modules and all schemes + from the given config. Duplicates are not re-registered. """ - for store_module in REGISTERED_STORE_MODULES: - try: - store_class = utils.import_class(store_module + '.Store') - except exception.NotFound: - raise BackendException('Unable to create store. Could not find ' - 'a class named Store in module %s.' - % store_module) - STORES[store_module] = store_class(conf) + conf.register_opt(known_stores_opt) + store_count = 0 + for store_entry in conf.known_stores: + store_entry = store_entry.strip() + if not store_entry: + continue + store_cls = _get_store_class(store_entry) + store_instance = store_cls(conf) + schemes = store_instance.get_schemes() + if not schemes: + raise BackendException('Unable to register store %s. ' + 'No schemes associated with it.' + % store_cls) + else: + if store_cls not in STORES: + logger.debug("Registering store %s with schemes %s", + store_cls, schemes) + STORES[store_cls] = store_instance + scheme_map = {} + for scheme in schemes: + loc_cls = store_instance.get_store_location_class() + scheme_map[scheme] = { + 'store_class': store_cls, + 'location_class': loc_cls, + } + location.register_scheme_map(scheme_map) + store_count += 1 + else: + logger.debug("Store %s already registered", store_cls) + return store_count def get_store_from_scheme(scheme): """ Given a scheme, return the appropriate store object - for handling that scheme + for handling that scheme. """ - if scheme not in location.SCHEME_TO_STORE_MAP: + if scheme not in location.SCHEME_TO_CLS_MAP: raise exception.UnknownScheme(scheme=scheme) - return STORES[location.SCHEME_TO_STORE_MAP[scheme]] + scheme_info = location.SCHEME_TO_CLS_MAP[scheme] + return STORES[scheme_info['store_class']] def get_store_from_uri(uri): diff --git a/glance/store/base.py b/glance/store/base.py index 4bbd942f88..1bb80bd303 100644 --- a/glance/store/base.py +++ b/glance/store/base.py @@ -20,6 +20,7 @@ import logging from glance.common import exception +from glance.common import utils logger = logging.getLogger('glance.store.base') @@ -35,7 +36,7 @@ class Store(object): :param conf: Optional dictionary of configuration options """ self.conf = conf - + self.store_location_class = None self.configure() try: @@ -54,6 +55,22 @@ class Store(object): """ pass + def get_schemes(self): + """ + Returns a tuple of schemes which this store can handle. + """ + raise NotImplementedError + + def get_store_location_class(self): + """ + Returns the store location class that is used by this store. + """ + if not self.store_location_class: + class_name = "%s.StoreLocation" % (self.__module__) + logger.debug("Late loading location class %s", class_name) + self.store_location_class = utils.import_class(class_name) + return self.store_location_class + def configure_add(self): """ This is like `configure` except that it's specifically for diff --git a/glance/store/filesystem.py b/glance/store/filesystem.py index ccdcf45ae0..09452d0a65 100644 --- a/glance/store/filesystem.py +++ b/glance/store/filesystem.py @@ -98,6 +98,9 @@ class Store(glance.store.base.Store): datadir_opt = cfg.StrOpt('filesystem_store_datadir') + def get_schemes(self): + return ('file', 'filesystem') + def configure_add(self): """ Configure the Store to use the stored configuration options @@ -216,6 +219,3 @@ class Store(glance.store.base.Store): logger.debug(_("Wrote %(bytes_written)d bytes to %(filepath)s with " "checksum %(checksum_hex)s") % locals()) return ('file://%s' % filepath, bytes_written, checksum_hex) - - -glance.store.register_store(__name__, ['filesystem', 'file']) diff --git a/glance/store/http.py b/glance/store/http.py index 8c56d95bf8..96a26900ed 100644 --- a/glance/store/http.py +++ b/glance/store/http.py @@ -126,6 +126,9 @@ class Store(glance.store.base.Store): return (ResponseIndexable(iterator, content_length), content_length) + def get_schemes(self): + return ('http', 'https') + def get_size(self, location): """ Takes a `glance.store.location.Location` object that indicates @@ -155,6 +158,3 @@ class Store(glance.store.base.Store): """ return {'http': httplib.HTTPConnection, 'https': httplib.HTTPSConnection}[loc.scheme] - - -glance.store.register_store(__name__, ['http', 'https']) diff --git a/glance/store/location.py b/glance/store/location.py index 08cd135072..5aad4793ea 100644 --- a/glance/store/location.py +++ b/glance/store/location.py @@ -47,7 +47,7 @@ from glance.common import utils logger = logging.getLogger('glance.store.location') -SCHEME_TO_STORE_MAP = {} +SCHEME_TO_CLS_MAP = {} def get_location_from_uri(uri): @@ -68,21 +68,23 @@ def get_location_from_uri(uri): file:///var/lib/glance/images/1 """ pieces = urlparse.urlparse(uri) - if pieces.scheme not in SCHEME_TO_STORE_MAP.keys(): + if pieces.scheme not in SCHEME_TO_CLS_MAP.keys(): raise exception.UnknownScheme(pieces.scheme) - loc = Location(pieces.scheme, uri=uri) - return loc + scheme_info = SCHEME_TO_CLS_MAP[pieces.scheme] + return Location(pieces.scheme, uri=uri, + store_location_class=scheme_info['location_class']) def register_scheme_map(scheme_map): """ Given a mapping of 'scheme' to store_name, adds the mapping to the known list of schemes. - - Each store should call this method and let Glance know about which - schemes to map to a store name. """ - SCHEME_TO_STORE_MAP.update(scheme_map) + for (k, v) in scheme_map.items(): + logger.debug("Registering scheme %s with %s", k, v) + if k in SCHEME_TO_CLS_MAP: + logger.warn("Overwriting scheme %s with %s", k, v) + SCHEME_TO_CLS_MAP[k] = v class Location(object): @@ -91,11 +93,14 @@ class Location(object): Class describing the location of an image that Glance knows about """ - def __init__(self, store_name, uri=None, image_id=None, store_specs=None): + def __init__(self, store_name, store_location_class, + uri=None, image_id=None, store_specs=None): """ Create a new Location object. - :param store_name: The string identifier of the storage backend + :param store_name: The string identifier/scheme of the storage backend + :param store_location_class: The store location class to use + for this location instance. :param image_id: The identifier of the image in whatever storage backend is used. :param uri: Optional URI to construct location from @@ -106,25 +111,10 @@ class Location(object): self.store_name = store_name self.image_id = image_id self.store_specs = store_specs or {} - self.store_location = self._get_store_location() + self.store_location = store_location_class(self.store_specs) if uri: self.store_location.parse_uri(uri) - def _get_store_location(self): - """ - We find the store module and then grab an instance of the store's - StoreLocation class which handles store-specific location information - """ - try: - cls = utils.import_class('%s.StoreLocation' - % SCHEME_TO_STORE_MAP[self.store_name]) - return cls(self.store_specs) - except exception.NotFound: - msg = _("Unable to find StoreLocation class in store " - "%s") % self.store_name - logger.error(msg) - return None - def get_store_uri(self): """ Returns the Glance image URI, which is the host:port of the API server diff --git a/glance/store/rbd.py b/glance/store/rbd.py index 21fcd8ccf1..570c089efe 100644 --- a/glance/store/rbd.py +++ b/glance/store/rbd.py @@ -108,6 +108,9 @@ class Store(glance.store.base.Store): cfg.StrOpt('rbd_store_ceph_conf', default=DEFAULT_CONFFILE), ] + def get_schemes(self): + return ('rbd',) + def configure_add(self): """ Configure the Store to use the stored configuration options @@ -200,6 +203,3 @@ class Store(glance.store.base.Store): except rbd.ImageNotFound: raise exception.NotFound( _('RBD image %s does not exist') % loc.image) - - -glance.store.register_store(__name__, ['rbd']) diff --git a/glance/store/s3.py b/glance/store/s3.py index 73e820bf0d..1e0f4a2235 100644 --- a/glance/store/s3.py +++ b/glance/store/s3.py @@ -198,6 +198,9 @@ class Store(glance.store.base.Store): cfg.BoolOpt('s3_store_create_bucket_on_put', default=False), ] + def get_schemes(self): + return ('s3', 's3+http', 's3+https') + def configure_add(self): """ Configure the Store to use the stored configuration options @@ -504,6 +507,3 @@ def get_key(bucket, obj): logger.error(msg) raise exception.NotFound(msg) return key - - -glance.store.register_store(__name__, ['s3', 's3+http', 's3+https']) diff --git a/glance/store/swift.py b/glance/store/swift.py index c4b10e392b..e6645a9bde 100644 --- a/glance/store/swift.py +++ b/glance/store/swift.py @@ -202,6 +202,9 @@ class Store(glance.store.base.Store): cfg.BoolOpt('swift_store_create_container_on_put', default=False), ] + def get_schemes(self): + return ('swift+https', 'swift', 'swift+http') + def configure(self): self.conf.register_opts(self.opts) self.snet = self.conf.swift_enable_snet @@ -571,6 +574,3 @@ def create_container_if_missing(container, swift_conn, conf): raise glance.store.BackendException(msg) else: raise - - -glance.store.register_store(__name__, ['swift', 'swift+http', 'swift+https']) diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py index 9f89517933..09b9b89a24 100644 --- a/glance/tests/functional/__init__.py +++ b/glance/tests/functional/__init__.py @@ -183,6 +183,7 @@ class ApiServer(Server): super(ApiServer, self).__init__(test_dir, port) self.server_name = 'api' self.default_store = 'file' + self.known_stores = test_utils.get_default_stores() self.key_file = "" self.cert_file = "" self.metadata_encryption_key = "012345678901234567890123456789ab" @@ -227,6 +228,7 @@ verbose = %(verbose)s debug = %(debug)s filesystem_store_datadir=%(image_dir)s default_store = %(default_store)s +known_stores = %(known_stores)s bind_host = 0.0.0.0 bind_port = %(bind_port)s key_file = %(key_file)s diff --git a/glance/tests/functional/test_bin_glance.py b/glance/tests/functional/test_bin_glance.py index e65b58c21d..e33e1e9ab1 100644 --- a/glance/tests/functional/test_bin_glance.py +++ b/glance/tests/functional/test_bin_glance.py @@ -536,8 +536,12 @@ class TestBinGlance(functional.FunctionalTest): self.cleanup() # Start servers with a Swift backend and a bad auth URL - options = {'default_store': 'swift', - 'swift_store_auth_address': 'badurl'} + override_options = { + 'default_store': 'swift', + 'swift_store_auth_address': 'badurl', + } + options = self.__dict__.copy() + options.update(override_options) self.start_servers(**options) api_port = self.api_port diff --git a/glance/tests/functional/test_scrubber.py b/glance/tests/functional/test_scrubber.py index 7d17058492..663094c999 100644 --- a/glance/tests/functional/test_scrubber.py +++ b/glance/tests/functional/test_scrubber.py @@ -47,7 +47,7 @@ class TestScrubber(functional.FunctionalTest): """ self.cleanup() - self.start_servers() + self.start_servers(**self.__dict__.copy()) client = self._get_client() registry = self._get_registry_client() diff --git a/glance/tests/stubs.py b/glance/tests/stubs.py index 03179e3ce3..e3b745909c 100644 --- a/glance/tests/stubs.py +++ b/glance/tests/stubs.py @@ -38,7 +38,7 @@ VERBOSE = False DEBUG = False -def stub_out_registry_and_store_server(stubs, base_dir): +def stub_out_registry_and_store_server(stubs, conf, base_dir): """ Mocks calls to 127.0.0.1 on 9191 and 9292 for testing so that a real Glance server does not need to be up and @@ -67,11 +67,6 @@ def stub_out_registry_and_store_server(stubs, base_dir): def getresponse(self): sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', "sqlite://") - conf = utils.TestConfigOpts({ - 'sql_connection': sql_connection, - 'verbose': VERBOSE, - 'debug': DEBUG - }) api = context.UnauthenticatedContextMiddleware( rserver.API(conf), conf) res = self.req.get_response(api) @@ -150,17 +145,7 @@ def stub_out_registry_and_store_server(stubs, base_dir): self.req.body = body def getresponse(self): - conf = utils.TestConfigOpts({ - 'verbose': VERBOSE, - 'debug': DEBUG, - 'bind_host': '0.0.0.0', - 'bind_port': '9999999', - 'registry_host': '0.0.0.0', - 'registry_port': '9191', - 'default_store': 'file', - 'filesystem_store_datadir': base_dir, - 'policy_file': os.path.join(base_dir, 'policy.json'), - }) + api = context.UnauthenticatedContextMiddleware( router.API(conf), conf) res = self.req.get_response(api) @@ -209,7 +194,7 @@ def stub_out_registry_and_store_server(stubs, base_dir): fake_image_iter) -def stub_out_registry_server(stubs, **kwargs): +def stub_out_registry_server(stubs, conf, **kwargs): """ Mocks calls to 127.0.0.1 on 9191 for testing so that a real Glance Registry server does not need to be up and @@ -237,11 +222,6 @@ def stub_out_registry_server(stubs, **kwargs): def getresponse(self): sql_connection = kwargs.get('sql_connection', "sqlite:///") - conf = utils.TestConfigOpts({ - 'sql_connection': sql_connection, - 'verbose': VERBOSE, - 'debug': DEBUG - }) api = context.UnauthenticatedContextMiddleware( rserver.API(conf), conf) res = self.req.get_response(api) diff --git a/glance/tests/unit/base.py b/glance/tests/unit/base.py index 8cd3bf2904..6276c4c290 100644 --- a/glance/tests/unit/base.py +++ b/glance/tests/unit/base.py @@ -22,11 +22,26 @@ import unittest import stubout +from glance import store +from glance.store import location from glance.tests import stubs from glance.tests import utils as test_utils -class IsolatedUnitTest(unittest.TestCase): +class StoreClearingUnitTest(unittest.TestCase): + + def setUp(self): + # Ensure stores + locations cleared + store.STORES = {} + location.SCHEME_TO_CLS_MAP = {} + + def tearDown(self): + # Ensure stores + locations cleared + store.STORES = {} + location.SCHEME_TO_CLS_MAP = {} + + +class IsolatedUnitTest(StoreClearingUnitTest): """ Unit test case that establishes a mock environment within @@ -34,18 +49,21 @@ class IsolatedUnitTest(unittest.TestCase): """ def setUp(self): + super(IsolatedUnitTest, self).setUp() self.test_id, self.test_dir = test_utils.get_isolated_test_env() self.stubs = stubout.StubOutForTesting() - stubs.stub_out_registry_and_store_server(self.stubs, self.test_dir) policy_file = self._copy_data_file('policy.json', self.test_dir) options = {'sql_connection': 'sqlite://', 'verbose': False, 'debug': False, 'default_store': 'filesystem', + 'known_stores': test_utils.get_default_stores(), 'filesystem_store_datadir': os.path.join(self.test_dir), 'policy_file': policy_file} - self.conf = test_utils.TestConfigOpts(options) + stubs.stub_out_registry_and_store_server(self.stubs, + self.conf, + self.test_dir) def _copy_data_file(self, file_name, dst_dir): src_file_name = os.path.join('glance/tests/etc', file_name) @@ -59,6 +77,7 @@ class IsolatedUnitTest(unittest.TestCase): fap.close() def tearDown(self): + super(IsolatedUnitTest, self).tearDown() self.stubs.UnsetAll() if os.path.exists(self.test_dir): shutil.rmtree(self.test_dir) diff --git a/glance/tests/unit/test_http_store.py b/glance/tests/unit/test_http_store.py index a28b27c96d..a68705d6b7 100644 --- a/glance/tests/unit/test_http_store.py +++ b/glance/tests/unit/test_http_store.py @@ -20,13 +20,15 @@ import unittest import stubout +from glance.common import config from glance.common import exception, context from glance.registry.db import api as db_api -from glance.store import (create_stores, - delete_from_backend, +from glance.registry import configure_registry_client +from glance.store import (delete_from_backend, schedule_delete_from_backend) from glance.store.http import Store from glance.store.location import get_location_from_uri +from glance.tests.unit import base from glance.tests import utils, stubs as test_stubs @@ -73,13 +75,13 @@ def stub_out_http_backend(stubs): stubs.Set(Store, '_get_conn_class', fake_get_conn_class) -def stub_out_registry_image_update(stubs): +def stub_out_registry_image_update(stubs, conf): """ Stubs an image update on the registry. :param stubs: Set of stubout stubs """ - test_stubs.stub_out_registry_server(stubs) + test_stubs.stub_out_registry_server(stubs, conf) def fake_image_update(ctx, image_id, values, purge_props=False): return {'properties': {}} @@ -87,13 +89,19 @@ def stub_out_registry_image_update(stubs): stubs.Set(db_api, 'image_update', fake_image_update) -class TestHttpStore(unittest.TestCase): +class TestHttpStore(base.StoreClearingUnitTest): def setUp(self): + super(TestHttpStore, self).setUp() self.stubs = stubout.StubOutForTesting() stub_out_http_backend(self.stubs) Store.CHUNKSIZE = 2 self.store = Store({}) + self.conf = utils.TestConfigOpts({ + 'default_store': 'http', + 'known_stores': "glance.store.http.Store", + }) + configure_registry_client(self.conf) def test_http_get(self): uri = "http://netloc/path/to/file.tar.gz" @@ -102,7 +110,6 @@ class TestHttpStore(unittest.TestCase): loc = get_location_from_uri(uri) (image_file, image_size) = self.store.get(loc) self.assertEqual(image_size, 31) - chunks = [c for c in image_file] self.assertEqual(chunks, expected_returns) @@ -121,19 +128,14 @@ class TestHttpStore(unittest.TestCase): uri = "https://netloc/path/to/file.tar.gz" loc = get_location_from_uri(uri) self.assertRaises(NotImplementedError, self.store.delete, loc) - - create_stores(utils.TestConfigOpts({})) self.assertRaises(exception.StoreDeleteNotSupported, delete_from_backend, uri) def test_http_schedule_delete_swallows_error(self): - stub_out_registry_image_update(self.stubs) uri = "https://netloc/path/to/file.tar.gz" ctx = context.RequestContext() - conf = utils.TestConfigOpts({}) - create_stores(conf) - + stub_out_registry_image_update(self.stubs, self.conf) try: - schedule_delete_from_backend(uri, conf, ctx, 'image_id') + schedule_delete_from_backend(uri, self.conf, ctx, 'image_id') except exception.StoreDeleteNotSupported: self.fail('StoreDeleteNotSupported should be swallowed') diff --git a/glance/tests/unit/test_s3_store.py b/glance/tests/unit/test_s3_store.py index 74f1bf14ea..afcf5f0c92 100644 --- a/glance/tests/unit/test_s3_store.py +++ b/glance/tests/unit/test_s3_store.py @@ -29,6 +29,7 @@ from glance.common import utils from glance.store import UnsupportedBackend from glance.store.location import get_location_from_uri from glance.store.s3 import Store, get_s3_location +from glance.tests.unit import base from glance.tests import utils as test_utils @@ -37,6 +38,8 @@ FAKE_UUID = utils.generate_uuid() FIVE_KB = (5 * 1024) S3_CONF = {'verbose': True, 'debug': True, + 'default_store': 's3', + 'known_stores': test_utils.get_default_stores(), 's3_store_access_key': 'user', 's3_store_secret_key': 'key', 's3_store_host': 'localhost:8080', @@ -155,16 +158,18 @@ def format_s3_location(user, key, authurl, bucket, obj): bucket, obj) -class TestStore(unittest.TestCase): +class TestStore(base.StoreClearingUnitTest): def setUp(self): """Establish a clean test environment""" + super(TestStore, self).setUp() self.stubs = stubout.StubOutForTesting() stub_out_s3(self.stubs) self.store = Store(test_utils.TestConfigOpts(S3_CONF)) def tearDown(self): """Clear the test environment""" + super(TestStore, self).tearDown() self.stubs.UnsetAll() def test_get(self): diff --git a/glance/tests/unit/test_store_location.py b/glance/tests/unit/test_store_location.py index 312b93601e..e289612bda 100644 --- a/glance/tests/unit/test_store_location.py +++ b/glance/tests/unit/test_store_location.py @@ -24,12 +24,18 @@ import glance.store.http import glance.store.filesystem import glance.store.swift import glance.store.s3 +from glance.tests.unit import base from glance.tests import utils -glance.store.create_stores(utils.TestConfigOpts({})) +class TestStoreLocation(base.StoreClearingUnitTest): -class TestStoreLocation(unittest.TestCase): + def setUp(self): + super(TestStoreLocation, self).setUp() + self.conf = utils.TestConfigOpts({ + 'known_stores': utils.get_default_stores(), + 'default_store': 'file', + }) def test_get_location_from_uri_back_to_uri(self): """ diff --git a/glance/tests/unit/test_swift_store.py b/glance/tests/unit/test_swift_store.py index 743bd37849..152bc82969 100644 --- a/glance/tests/unit/test_swift_store.py +++ b/glance/tests/unit/test_swift_store.py @@ -31,6 +31,7 @@ from glance.common import utils from glance.store import BackendException import glance.store.swift from glance.store.location import get_location_from_uri +from glance.tests.unit import base from glance.tests import utils as test_utils @@ -43,6 +44,8 @@ MAX_SWIFT_OBJECT_SIZE = FIVE_GB SWIFT_PUT_OBJECT_CALLS = 0 SWIFT_CONF = {'verbose': True, 'debug': True, + 'known_stores': "glance.store.swift.Store", + 'default_store': 'swift', 'swift_store_user': 'user', 'swift_store_key': 'key', 'swift_store_auth_address': 'localhost:8080', @@ -562,44 +565,48 @@ class SwiftTests(object): self.assertRaises(exception.NotFound, self.store.delete, loc) -class TestStoreAuthV1(unittest.TestCase, SwiftTests): +class TestStoreAuthV1(base.StoreClearingUnitTest, SwiftTests): + + def getConfig(self): + conf = SWIFT_CONF.copy() + conf['swift_store_auth_version'] = '1' + conf['swift_store_user'] = 'user' + return conf def setUp(self): """Establish a clean test environment""" - self.conf = SWIFT_CONF.copy() - self.conf['swift_store_auth_version'] = '1' - self.conf['swift_store_user'] = 'user' + super(TestStoreAuthV1, self).setUp() + self.conf = self.getConfig() self.stubs = stubout.StubOutForTesting() stub_out_swift_common_client(self.stubs, self.conf) self.store = Store(test_utils.TestConfigOpts(self.conf)) def tearDown(self): """Clear the test environment""" + super(TestStoreAuthV1, self).tearDown() self.stubs.UnsetAll() class TestStoreAuthV2(TestStoreAuthV1): - def setUp(self): - """Establish a clean test environment""" - self.conf = SWIFT_CONF.copy() - self.conf['swift_store_user'] = 'tenant:user' - self.conf['swift_store_auth_version'] = '2' - self.stubs = stubout.StubOutForTesting() - stub_out_swift_common_client(self.stubs, self.conf) - self.store = Store(test_utils.TestConfigOpts(self.conf)) + def getConfig(self): + conf = super(TestStoreAuthV2, self).getConfig() + conf['swift_store_user'] = 'tenant:user' + conf['swift_store_auth_version'] = '2' + return conf def test_v2_with_no_tenant(self): - self.conf['swift_store_user'] = 'failme' + conf = self.getConfig() + conf['swift_store_user'] = 'failme' uri = "swift://%s:key@auth_address/glance/%s" % ( - self.conf['swift_store_user'], FAKE_UUID) + conf['swift_store_user'], FAKE_UUID) loc = get_location_from_uri(uri) self.assertRaises(exception.BadStoreUri, self.store.get, loc) -class TestChunkReader(unittest.TestCase): +class TestChunkReader(base.StoreClearingUnitTest): def test_read_all_data(self): """ diff --git a/glance/tests/unit/v2/test_image_data_resource.py b/glance/tests/unit/v2/test_image_data_resource.py index 4aeb02bd48..7f1ddb5b5c 100644 --- a/glance/tests/unit/v2/test_image_data_resource.py +++ b/glance/tests/unit/v2/test_image_data_resource.py @@ -21,10 +21,11 @@ import webob import glance.api.v2.image_data from glance.common import utils import glance.tests.unit.utils as test_utils +from glance.tests.unit import base import glance.tests.utils -class TestImagesController(unittest.TestCase): +class TestImagesController(base.StoreClearingUnitTest): def setUp(self): super(TestImagesController, self).setUp() diff --git a/glance/tests/utils.py b/glance/tests/utils.py index 7db686cfc4..45189c5487 100644 --- a/glance/tests/utils.py +++ b/glance/tests/utils.py @@ -30,6 +30,7 @@ import nose.plugins.skip from glance.common import config from glance.common import utils from glance.common import wsgi +from glance import store def get_isolated_test_env(): @@ -81,6 +82,7 @@ class TestConfigOpts(config.GlanceConfigOpts): self.temp_file = os.path.join(tempfile.mkdtemp(), 'testcfg.conf') self() + store.create_stores(self) def __call__(self): self._write_tmp_config_file() @@ -314,6 +316,19 @@ def find_executable(cmdname): return None +def get_default_stores(): + # Default test stores + known_stores = [ + "glance.store.filesystem.Store", + "glance.store.http.Store", + "glance.store.rbd.Store", + "glance.store.s3.Store", + "glance.store.swift.Store", + ] + # Made in a format that the config can read + return ", ".join(known_stores) + + def get_unused_port(): """ Returns an unused port on localhost.