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) # Show debugging output in logs (sets DEBUG log level output)
debug = False debug = False
# Which backend store should Glance use by default is not specified # Which backend scheme should Glance use by default is not specified
# in a request to add a new image to Glance? Default: 'file' # in a request to add a new image to Glance? Known schemes are determined
# Available choices are 'file', 'swift', and 's3' # by the known_stores option below.
# Default: 'file'
default_store = 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 # Address to bind the API server
bind_host = 0.0.0.0 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 wsgi
from glance.common import utils from glance.common import utils
from glance.openstack.common import cfg from glance.openstack.common import cfg
import glance.store from glance.store import (create_stores,
import glance.store.filesystem get_from_backend,
import glance.store.http
import glance.store.rbd
import glance.store.s3
import glance.store.swift
from glance.store import (get_from_backend,
get_size_from_backend, get_size_from_backend,
schedule_delete_from_backend, schedule_delete_from_backend,
get_store_from_location, get_store_from_location,
@ -67,6 +62,10 @@ SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS
# as a BigInteger. # as a BigInteger.
IMAGE_SIZE_CAP = 1 << 50 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): class Controller(controller.BaseController):
""" """
@ -87,13 +86,11 @@ class Controller(controller.BaseController):
DELETE /images/<ID> -- Delete the image with id <ID> DELETE /images/<ID> -- Delete the image with id <ID>
""" """
default_store_opt = cfg.StrOpt('default_store', default='file')
def __init__(self, conf): def __init__(self, conf):
self.conf = conf self.conf = conf
self.conf.register_opt(self.default_store_opt) self.conf.register_opt(default_store_opt)
glance.store.create_stores(conf) create_stores(self.conf)
self.verify_store_or_exit(self.conf.default_store) self.verify_scheme_or_exit(self.conf.default_store)
self.notifier = notifier.Notifier(conf) self.notifier = notifier.Notifier(conf)
registry.configure_registry_client(conf) registry.configure_registry_client(conf)
self.policy = policy.Enforcer(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 Uploads the payload of the request to a backend store in
Glance. If the `x-image-meta-store` header is set, Glance Glance. If the `x-image-meta-store` header is set, Glance
will attempt to use that store, if not, Glance will use the will attempt to use that scheme; if not, Glance will use the
store set by the flag `default_store`. scheme set by the flag `default_store` to find the backing store.
:param req: The WSGI/Webob Request object :param req: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image :param image_meta: Mapping of metadata about image
@ -367,10 +364,10 @@ class Controller(controller.BaseController):
"x-image-meta-size header")) "x-image-meta-size header"))
image_size = 0 image_size = 0
store_name = req.headers.get('x-image-meta-store', scheme = req.headers.get('x-image-meta-store',
self.conf.default_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'] image_id = image_meta['id']
logger.debug(_("Setting image %s to status 'saving'"), image_id) logger.debug(_("Setting image %s to status 'saving'"), image_id)
@ -378,7 +375,7 @@ class Controller(controller.BaseController):
{'status': 'saving'}) {'status': 'saving'})
try: try:
logger.debug(_("Uploading image data for image %(image_id)s " 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: if image_size > IMAGE_SIZE_CAP:
max_image_size = IMAGE_SIZE_CAP max_image_size = IMAGE_SIZE_CAP
@ -750,41 +747,39 @@ class Controller(controller.BaseController):
else: else:
self.notifier.info('image.delete', id) 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 Grabs the storage backend for the supplied store name
or raises an HTTPBadRequest (400) response or raises an HTTPBadRequest (400) response
:param request: The WSGI/Webob Request object :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 :raises HTTPNotFound if store does not exist
""" """
try: try:
return get_store_from_scheme(store_name) return get_store_from_scheme(scheme)
except exception.UnknownScheme: except exception.UnknownScheme:
msg = (_("Requested store %s not available on this Glance server") msg = _("Store for scheme %s not found")
% store_name) logger.error(msg % scheme)
logger.error(msg)
raise HTTPBadRequest(msg, request=request, raise HTTPBadRequest(msg, request=request,
content_type='text/plain') 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 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: try:
get_store_from_scheme(store_name) get_store_from_scheme(scheme)
except exception.UnknownScheme: except exception.UnknownScheme:
msg = (_("Default store %s not available on this Glance server\n") msg = _("Store for scheme %s not found")
% store_name) logger.error(msg % scheme)
logger.error(msg)
# message on stderr will only be visible if started directly via # message on stderr will only be visible if started directly via
# bin/glance-api, as opposed to being daemonized by glance-control # bin/glance-api, as opposed to being daemonized by glance-control
sys.stderr.write(msg) sys.stderr.write(msg % scheme)
sys.exit(255) sys.exit(255)

View File

@ -20,11 +20,6 @@ from glance.common import exception
from glance.common import wsgi from glance.common import wsgi
import glance.registry.db.api import glance.registry.db.api
import glance.store 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): class ImageDataController(base.Controller):

View File

@ -28,9 +28,6 @@ from glance.store import location
logger = logging.getLogger('glance.store') logger = logging.getLogger('glance.store')
# Set of known store modules
REGISTERED_STORE_MODULES = []
# Set of store objects, constructed in create_stores() # Set of store objects, constructed in create_stores()
STORES = {} STORES = {}
@ -128,50 +125,68 @@ class Indexable(object):
return self.size return self.size
def register_store(store_module, schemes): def _get_store_class(store_entry):
""" store_cls = None
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
"""
try: 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: except exception.NotFound:
raise BackendException('Unable to register store. Could not find ' raise BackendException('Unable to load store. '
'a class named Store in module %s.' 'Could not find a class named %s.'
% store_module) % store_entry)
REGISTERED_STORE_MODULES.append(store_module) return store_cls
scheme_map = {}
for scheme in schemes:
scheme_map[scheme] = store_module known_stores_opt = cfg.ListOpt('known_stores',
location.register_scheme_map(scheme_map) default=('glance.store.filesystem.Store',))
def create_stores(conf): 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: conf.register_opt(known_stores_opt)
try: store_count = 0
store_class = utils.import_class(store_module + '.Store') for store_entry in conf.known_stores:
except exception.NotFound: store_entry = store_entry.strip()
raise BackendException('Unable to create store. Could not find ' if not store_entry:
'a class named Store in module %s.' continue
% store_module) store_cls = _get_store_class(store_entry)
STORES[store_module] = store_class(conf) 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): def get_store_from_scheme(scheme):
""" """
Given a scheme, return the appropriate store object 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) 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): def get_store_from_uri(uri):

View File

@ -20,6 +20,7 @@
import logging import logging
from glance.common import exception from glance.common import exception
from glance.common import utils
logger = logging.getLogger('glance.store.base') logger = logging.getLogger('glance.store.base')
@ -35,7 +36,7 @@ class Store(object):
:param conf: Optional dictionary of configuration options :param conf: Optional dictionary of configuration options
""" """
self.conf = conf self.conf = conf
self.store_location_class = None
self.configure() self.configure()
try: try:
@ -54,6 +55,22 @@ class Store(object):
""" """
pass 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): def configure_add(self):
""" """
This is like `configure` except that it's specifically for 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') datadir_opt = cfg.StrOpt('filesystem_store_datadir')
def get_schemes(self):
return ('file', 'filesystem')
def configure_add(self): def configure_add(self):
""" """
Configure the Store to use the stored configuration options 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 " logger.debug(_("Wrote %(bytes_written)d bytes to %(filepath)s with "
"checksum %(checksum_hex)s") % locals()) "checksum %(checksum_hex)s") % locals())
return ('file://%s' % filepath, bytes_written, checksum_hex) 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) return (ResponseIndexable(iterator, content_length), content_length)
def get_schemes(self):
return ('http', 'https')
def get_size(self, location): def get_size(self, location):
""" """
Takes a `glance.store.location.Location` object that indicates Takes a `glance.store.location.Location` object that indicates
@ -155,6 +158,3 @@ class Store(glance.store.base.Store):
""" """
return {'http': httplib.HTTPConnection, return {'http': httplib.HTTPConnection,
'https': httplib.HTTPSConnection}[loc.scheme] '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') logger = logging.getLogger('glance.store.location')
SCHEME_TO_STORE_MAP = {} SCHEME_TO_CLS_MAP = {}
def get_location_from_uri(uri): def get_location_from_uri(uri):
@ -68,21 +68,23 @@ def get_location_from_uri(uri):
file:///var/lib/glance/images/1 file:///var/lib/glance/images/1
""" """
pieces = urlparse.urlparse(uri) 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) raise exception.UnknownScheme(pieces.scheme)
loc = Location(pieces.scheme, uri=uri) scheme_info = SCHEME_TO_CLS_MAP[pieces.scheme]
return loc return Location(pieces.scheme, uri=uri,
store_location_class=scheme_info['location_class'])
def register_scheme_map(scheme_map): def register_scheme_map(scheme_map):
""" """
Given a mapping of 'scheme' to store_name, adds the mapping to the Given a mapping of 'scheme' to store_name, adds the mapping to the
known list of schemes. 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): class Location(object):
@ -91,11 +93,14 @@ class Location(object):
Class describing the location of an image that Glance knows about 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. 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 :param image_id: The identifier of the image in whatever storage
backend is used. backend is used.
:param uri: Optional URI to construct location from :param uri: Optional URI to construct location from
@ -106,25 +111,10 @@ class Location(object):
self.store_name = store_name self.store_name = store_name
self.image_id = image_id self.image_id = image_id
self.store_specs = store_specs or {} self.store_specs = store_specs or {}
self.store_location = self._get_store_location() self.store_location = store_location_class(self.store_specs)
if uri: if uri:
self.store_location.parse_uri(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): def get_store_uri(self):
""" """
Returns the Glance image URI, which is the host:port of the API server 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), cfg.StrOpt('rbd_store_ceph_conf', default=DEFAULT_CONFFILE),
] ]
def get_schemes(self):
return ('rbd',)
def configure_add(self): def configure_add(self):
""" """
Configure the Store to use the stored configuration options Configure the Store to use the stored configuration options
@ -200,6 +203,3 @@ class Store(glance.store.base.Store):
except rbd.ImageNotFound: except rbd.ImageNotFound:
raise exception.NotFound( raise exception.NotFound(
_('RBD image %s does not exist') % loc.image) _('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), cfg.BoolOpt('s3_store_create_bucket_on_put', default=False),
] ]
def get_schemes(self):
return ('s3', 's3+http', 's3+https')
def configure_add(self): def configure_add(self):
""" """
Configure the Store to use the stored configuration options Configure the Store to use the stored configuration options
@ -504,6 +507,3 @@ def get_key(bucket, obj):
logger.error(msg) logger.error(msg)
raise exception.NotFound(msg) raise exception.NotFound(msg)
return key 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), cfg.BoolOpt('swift_store_create_container_on_put', default=False),
] ]
def get_schemes(self):
return ('swift+https', 'swift', 'swift+http')
def configure(self): def configure(self):
self.conf.register_opts(self.opts) self.conf.register_opts(self.opts)
self.snet = self.conf.swift_enable_snet 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) raise glance.store.BackendException(msg)
else: else:
raise 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) super(ApiServer, self).__init__(test_dir, port)
self.server_name = 'api' self.server_name = 'api'
self.default_store = 'file' self.default_store = 'file'
self.known_stores = test_utils.get_default_stores()
self.key_file = "" self.key_file = ""
self.cert_file = "" self.cert_file = ""
self.metadata_encryption_key = "012345678901234567890123456789ab" self.metadata_encryption_key = "012345678901234567890123456789ab"
@ -227,6 +228,7 @@ verbose = %(verbose)s
debug = %(debug)s debug = %(debug)s
filesystem_store_datadir=%(image_dir)s filesystem_store_datadir=%(image_dir)s
default_store = %(default_store)s default_store = %(default_store)s
known_stores = %(known_stores)s
bind_host = 0.0.0.0 bind_host = 0.0.0.0
bind_port = %(bind_port)s bind_port = %(bind_port)s
key_file = %(key_file)s key_file = %(key_file)s

View File

@ -536,8 +536,12 @@ class TestBinGlance(functional.FunctionalTest):
self.cleanup() self.cleanup()
# Start servers with a Swift backend and a bad auth URL # Start servers with a Swift backend and a bad auth URL
options = {'default_store': 'swift', override_options = {
'swift_store_auth_address': 'badurl'} 'default_store': 'swift',
'swift_store_auth_address': 'badurl',
}
options = self.__dict__.copy()
options.update(override_options)
self.start_servers(**options) self.start_servers(**options)
api_port = self.api_port api_port = self.api_port

View File

@ -47,7 +47,7 @@ class TestScrubber(functional.FunctionalTest):
""" """
self.cleanup() self.cleanup()
self.start_servers() self.start_servers(**self.__dict__.copy())
client = self._get_client() client = self._get_client()
registry = self._get_registry_client() registry = self._get_registry_client()

View File

@ -38,7 +38,7 @@ VERBOSE = False
DEBUG = 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 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 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): def getresponse(self):
sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', sql_connection = os.environ.get('GLANCE_SQL_CONNECTION',
"sqlite://") "sqlite://")
conf = utils.TestConfigOpts({
'sql_connection': sql_connection,
'verbose': VERBOSE,
'debug': DEBUG
})
api = context.UnauthenticatedContextMiddleware( api = context.UnauthenticatedContextMiddleware(
rserver.API(conf), conf) rserver.API(conf), conf)
res = self.req.get_response(api) res = self.req.get_response(api)
@ -150,17 +145,7 @@ def stub_out_registry_and_store_server(stubs, base_dir):
self.req.body = body self.req.body = body
def getresponse(self): 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( api = context.UnauthenticatedContextMiddleware(
router.API(conf), conf) router.API(conf), conf)
res = self.req.get_response(api) res = self.req.get_response(api)
@ -209,7 +194,7 @@ def stub_out_registry_and_store_server(stubs, base_dir):
fake_image_iter) 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 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 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): def getresponse(self):
sql_connection = kwargs.get('sql_connection', "sqlite:///") sql_connection = kwargs.get('sql_connection', "sqlite:///")
conf = utils.TestConfigOpts({
'sql_connection': sql_connection,
'verbose': VERBOSE,
'debug': DEBUG
})
api = context.UnauthenticatedContextMiddleware( api = context.UnauthenticatedContextMiddleware(
rserver.API(conf), conf) rserver.API(conf), conf)
res = self.req.get_response(api) res = self.req.get_response(api)

View File

@ -22,11 +22,26 @@ import unittest
import stubout import stubout
from glance import store
from glance.store import location
from glance.tests import stubs from glance.tests import stubs
from glance.tests import utils as test_utils 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 Unit test case that establishes a mock environment within
@ -34,18 +49,21 @@ class IsolatedUnitTest(unittest.TestCase):
""" """
def setUp(self): def setUp(self):
super(IsolatedUnitTest, self).setUp()
self.test_id, self.test_dir = test_utils.get_isolated_test_env() self.test_id, self.test_dir = test_utils.get_isolated_test_env()
self.stubs = stubout.StubOutForTesting() 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) policy_file = self._copy_data_file('policy.json', self.test_dir)
options = {'sql_connection': 'sqlite://', options = {'sql_connection': 'sqlite://',
'verbose': False, 'verbose': False,
'debug': False, 'debug': False,
'default_store': 'filesystem', 'default_store': 'filesystem',
'known_stores': test_utils.get_default_stores(),
'filesystem_store_datadir': os.path.join(self.test_dir), 'filesystem_store_datadir': os.path.join(self.test_dir),
'policy_file': policy_file} 'policy_file': policy_file}
self.conf = test_utils.TestConfigOpts(options) 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): def _copy_data_file(self, file_name, dst_dir):
src_file_name = os.path.join('glance/tests/etc', file_name) src_file_name = os.path.join('glance/tests/etc', file_name)
@ -59,6 +77,7 @@ class IsolatedUnitTest(unittest.TestCase):
fap.close() fap.close()
def tearDown(self): def tearDown(self):
super(IsolatedUnitTest, self).tearDown()
self.stubs.UnsetAll() self.stubs.UnsetAll()
if os.path.exists(self.test_dir): if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir) shutil.rmtree(self.test_dir)

View File

@ -20,13 +20,15 @@ import unittest
import stubout import stubout
from glance.common import config
from glance.common import exception, context from glance.common import exception, context
from glance.registry.db import api as db_api from glance.registry.db import api as db_api
from glance.store import (create_stores, from glance.registry import configure_registry_client
delete_from_backend, from glance.store import (delete_from_backend,
schedule_delete_from_backend) schedule_delete_from_backend)
from glance.store.http import Store from glance.store.http import Store
from glance.store.location import get_location_from_uri from glance.store.location import get_location_from_uri
from glance.tests.unit import base
from glance.tests import utils, stubs as test_stubs 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) 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. Stubs an image update on the registry.
:param stubs: Set of stubout stubs :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): def fake_image_update(ctx, image_id, values, purge_props=False):
return {'properties': {}} return {'properties': {}}
@ -87,13 +89,19 @@ def stub_out_registry_image_update(stubs):
stubs.Set(db_api, 'image_update', fake_image_update) stubs.Set(db_api, 'image_update', fake_image_update)
class TestHttpStore(unittest.TestCase): class TestHttpStore(base.StoreClearingUnitTest):
def setUp(self): def setUp(self):
super(TestHttpStore, self).setUp()
self.stubs = stubout.StubOutForTesting() self.stubs = stubout.StubOutForTesting()
stub_out_http_backend(self.stubs) stub_out_http_backend(self.stubs)
Store.CHUNKSIZE = 2 Store.CHUNKSIZE = 2
self.store = Store({}) 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): def test_http_get(self):
uri = "http://netloc/path/to/file.tar.gz" uri = "http://netloc/path/to/file.tar.gz"
@ -102,7 +110,6 @@ class TestHttpStore(unittest.TestCase):
loc = get_location_from_uri(uri) loc = get_location_from_uri(uri)
(image_file, image_size) = self.store.get(loc) (image_file, image_size) = self.store.get(loc)
self.assertEqual(image_size, 31) self.assertEqual(image_size, 31)
chunks = [c for c in image_file] chunks = [c for c in image_file]
self.assertEqual(chunks, expected_returns) self.assertEqual(chunks, expected_returns)
@ -121,19 +128,14 @@ class TestHttpStore(unittest.TestCase):
uri = "https://netloc/path/to/file.tar.gz" uri = "https://netloc/path/to/file.tar.gz"
loc = get_location_from_uri(uri) loc = get_location_from_uri(uri)
self.assertRaises(NotImplementedError, self.store.delete, loc) self.assertRaises(NotImplementedError, self.store.delete, loc)
create_stores(utils.TestConfigOpts({}))
self.assertRaises(exception.StoreDeleteNotSupported, self.assertRaises(exception.StoreDeleteNotSupported,
delete_from_backend, uri) delete_from_backend, uri)
def test_http_schedule_delete_swallows_error(self): def test_http_schedule_delete_swallows_error(self):
stub_out_registry_image_update(self.stubs)
uri = "https://netloc/path/to/file.tar.gz" uri = "https://netloc/path/to/file.tar.gz"
ctx = context.RequestContext() ctx = context.RequestContext()
conf = utils.TestConfigOpts({}) stub_out_registry_image_update(self.stubs, self.conf)
create_stores(conf)
try: try:
schedule_delete_from_backend(uri, conf, ctx, 'image_id') schedule_delete_from_backend(uri, self.conf, ctx, 'image_id')
except exception.StoreDeleteNotSupported: except exception.StoreDeleteNotSupported:
self.fail('StoreDeleteNotSupported should be swallowed') 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 import UnsupportedBackend
from glance.store.location import get_location_from_uri from glance.store.location import get_location_from_uri
from glance.store.s3 import Store, get_s3_location from glance.store.s3 import Store, get_s3_location
from glance.tests.unit import base
from glance.tests import utils as test_utils from glance.tests import utils as test_utils
@ -37,6 +38,8 @@ FAKE_UUID = utils.generate_uuid()
FIVE_KB = (5 * 1024) FIVE_KB = (5 * 1024)
S3_CONF = {'verbose': True, S3_CONF = {'verbose': True,
'debug': True, 'debug': True,
'default_store': 's3',
'known_stores': test_utils.get_default_stores(),
's3_store_access_key': 'user', 's3_store_access_key': 'user',
's3_store_secret_key': 'key', 's3_store_secret_key': 'key',
's3_store_host': 'localhost:8080', 's3_store_host': 'localhost:8080',
@ -155,16 +158,18 @@ def format_s3_location(user, key, authurl, bucket, obj):
bucket, obj) bucket, obj)
class TestStore(unittest.TestCase): class TestStore(base.StoreClearingUnitTest):
def setUp(self): def setUp(self):
"""Establish a clean test environment""" """Establish a clean test environment"""
super(TestStore, self).setUp()
self.stubs = stubout.StubOutForTesting() self.stubs = stubout.StubOutForTesting()
stub_out_s3(self.stubs) stub_out_s3(self.stubs)
self.store = Store(test_utils.TestConfigOpts(S3_CONF)) self.store = Store(test_utils.TestConfigOpts(S3_CONF))
def tearDown(self): def tearDown(self):
"""Clear the test environment""" """Clear the test environment"""
super(TestStore, self).tearDown()
self.stubs.UnsetAll() self.stubs.UnsetAll()
def test_get(self): def test_get(self):

View File

@ -24,12 +24,18 @@ import glance.store.http
import glance.store.filesystem import glance.store.filesystem
import glance.store.swift import glance.store.swift
import glance.store.s3 import glance.store.s3
from glance.tests.unit import base
from glance.tests import utils 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): 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 from glance.store import BackendException
import glance.store.swift import glance.store.swift
from glance.store.location import get_location_from_uri from glance.store.location import get_location_from_uri
from glance.tests.unit import base
from glance.tests import utils as test_utils from glance.tests import utils as test_utils
@ -43,6 +44,8 @@ MAX_SWIFT_OBJECT_SIZE = FIVE_GB
SWIFT_PUT_OBJECT_CALLS = 0 SWIFT_PUT_OBJECT_CALLS = 0
SWIFT_CONF = {'verbose': True, SWIFT_CONF = {'verbose': True,
'debug': True, 'debug': True,
'known_stores': "glance.store.swift.Store",
'default_store': 'swift',
'swift_store_user': 'user', 'swift_store_user': 'user',
'swift_store_key': 'key', 'swift_store_key': 'key',
'swift_store_auth_address': 'localhost:8080', 'swift_store_auth_address': 'localhost:8080',
@ -562,44 +565,48 @@ class SwiftTests(object):
self.assertRaises(exception.NotFound, self.store.delete, loc) 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): def setUp(self):
"""Establish a clean test environment""" """Establish a clean test environment"""
self.conf = SWIFT_CONF.copy() super(TestStoreAuthV1, self).setUp()
self.conf['swift_store_auth_version'] = '1' self.conf = self.getConfig()
self.conf['swift_store_user'] = 'user'
self.stubs = stubout.StubOutForTesting() self.stubs = stubout.StubOutForTesting()
stub_out_swift_common_client(self.stubs, self.conf) stub_out_swift_common_client(self.stubs, self.conf)
self.store = Store(test_utils.TestConfigOpts(self.conf)) self.store = Store(test_utils.TestConfigOpts(self.conf))
def tearDown(self): def tearDown(self):
"""Clear the test environment""" """Clear the test environment"""
super(TestStoreAuthV1, self).tearDown()
self.stubs.UnsetAll() self.stubs.UnsetAll()
class TestStoreAuthV2(TestStoreAuthV1): class TestStoreAuthV2(TestStoreAuthV1):
def setUp(self): def getConfig(self):
"""Establish a clean test environment""" conf = super(TestStoreAuthV2, self).getConfig()
self.conf = SWIFT_CONF.copy() conf['swift_store_user'] = 'tenant:user'
self.conf['swift_store_user'] = 'tenant:user' conf['swift_store_auth_version'] = '2'
self.conf['swift_store_auth_version'] = '2' return conf
self.stubs = stubout.StubOutForTesting()
stub_out_swift_common_client(self.stubs, self.conf)
self.store = Store(test_utils.TestConfigOpts(self.conf))
def test_v2_with_no_tenant(self): 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" % ( 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) loc = get_location_from_uri(uri)
self.assertRaises(exception.BadStoreUri, self.assertRaises(exception.BadStoreUri,
self.store.get, self.store.get,
loc) loc)
class TestChunkReader(unittest.TestCase): class TestChunkReader(base.StoreClearingUnitTest):
def test_read_all_data(self): def test_read_all_data(self):
""" """

View File

@ -21,10 +21,11 @@ import webob
import glance.api.v2.image_data import glance.api.v2.image_data
from glance.common import utils from glance.common import utils
import glance.tests.unit.utils as test_utils import glance.tests.unit.utils as test_utils
from glance.tests.unit import base
import glance.tests.utils import glance.tests.utils
class TestImagesController(unittest.TestCase): class TestImagesController(base.StoreClearingUnitTest):
def setUp(self): def setUp(self):
super(TestImagesController, self).setUp() super(TestImagesController, self).setUp()

View File

@ -30,6 +30,7 @@ import nose.plugins.skip
from glance.common import config from glance.common import config
from glance.common import utils from glance.common import utils
from glance.common import wsgi from glance.common import wsgi
from glance import store
def get_isolated_test_env(): def get_isolated_test_env():
@ -81,6 +82,7 @@ class TestConfigOpts(config.GlanceConfigOpts):
self.temp_file = os.path.join(tempfile.mkdtemp(), 'testcfg.conf') self.temp_file = os.path.join(tempfile.mkdtemp(), 'testcfg.conf')
self() self()
store.create_stores(self)
def __call__(self): def __call__(self):
self._write_tmp_config_file() self._write_tmp_config_file()
@ -314,6 +316,19 @@ def find_executable(cmdname):
return None 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(): def get_unused_port():
""" """
Returns an unused port on localhost. Returns an unused port on localhost.