Implements blueprint import-dynamic-stores.
Make glance more pluggable with regard to stores. Change-Id: I7b264d1b047a321f7b60857bb73154f831b82a7b
This commit is contained in:
parent
b5bae3d040
commit
ef475e17d5
|
@ -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
|
||||
|
||||
|
|
|
@ -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',
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue