Implements blueprint import-dynamic-stores.

Make glance more pluggable with regard to stores.

Change-Id: I7b264d1b047a321f7b60857bb73154f831b82a7b
This commit is contained in:
Joshua Harlow 2012-05-08 17:28:42 -07:00 committed by Brian Waldon
parent b5bae3d040
commit ef475e17d5
22 changed files with 238 additions and 176 deletions

View File

@ -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

View File

@ -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/<ID> -- Delete the image with id <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',
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)

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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'])

View File

@ -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'])

View File

@ -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

View File

@ -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'])

View File

@ -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'])

View File

@ -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'])

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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):

View File

@ -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):
"""

View File

@ -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):
"""

View File

@ -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()

View File

@ -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.