Convert glance to use the new cfg module

The changes here are substantial and widespread, but in summary:

  - We use cfg to parse the CLI and config files, rather than optparse
    and PasteDeploy

  - A schema is defined for all configuration options close to the code
    which uses the option

  - 2 ConfigOpts sub-classes are added to config.py basically just
    defining how to find config files; this means we can now use e.g.
    glance.conf for base config values which glance-api.conf can
    override

  - load_paste_app() is changed to load the paste app from the last
    config file in the stack and pass the app the ConfigOpts instance

  - The generic app and filter factories in wsgi.py are modified to
    pass a ConfigOpts instance to the apps and filters

  - A ConfigOpts subclass is added for the unit tests which writes out
    config values to a temporary config file and uses cfg to parse that

I've tried to keep the switch as unobtrusive as possible leaving further
cleanups for later e.g.

  - Moving PasteDeploy config out of the config files - I think it would
    be good to aim for having users modify the PasteDeploy config files
    only in fairly rare circumstances. To achieve this, we might define
    a number of common pipelines in the PasteDeploy config and allow
    the user to choose between those pipelines in the glance config.

  - We should add help strings to all the opts, even just for the sake
    of documenting them

  - We should move a bunch of the options into groups - e.g. all the
    rabbit options

  - We no longer rely on config files for default values, so the default
    config files could contain nothing but comments - i.e. explaining
    each option and showing what the default for it is - making it
    obvious where a user has explicitly set a value

There are a couple of behavioural changes which I don't think are
signifcant but are worth mentioning:

  - We used to support passing a config file as a positional argument
    but don't anymore; AFAICT, it was only used by glance-manage when
    launching servers and I've changed that to pass --config-file

  - log_opt_values() doesn't log unknown opts, so won't log any values
    for opts which get registered at runtime later

Change-Id: Iafa998a2a8d860f1ad57e2cd2afee69686ed58ba
This commit is contained in:
Mark McLoughlin 2011-11-28 14:37:58 +00:00
parent e5925473cd
commit 57c4e9b6c6
58 changed files with 716 additions and 907 deletions

View File

@ -23,7 +23,6 @@ Glance API Server
""" """
import gettext import gettext
import optparse
import os import os
import sys import sys
@ -37,33 +36,19 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
gettext.install('glance', unicode=1) gettext.install('glance', unicode=1)
from glance import version
from glance.common import config from glance.common import config
from glance.common import wsgi from glance.common import wsgi
def create_options(parser):
"""
Sets up the CLI and config-file options that may be
parsed and program commands.
:param parser: The option parser
"""
config.add_common_options(parser)
config.add_log_options(parser)
if __name__ == '__main__': if __name__ == '__main__':
oparser = optparse.OptionParser(version='%%prog %s'
% version.version_string())
create_options(oparser)
(options, args) = config.parse_options(oparser)
try: try:
conf, app = config.load_paste_app('glance-api', options, args) conf = config.GlanceConfigOpts()
conf()
app = config.load_paste_app(conf)
server = wsgi.Server() server = wsgi.Server()
server.start(app, int(conf['bind_port']), conf['bind_host'], conf) server.start(app, conf, default_port=9292)
server.wait() server.wait()
except RuntimeError, e: except RuntimeError, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)

View File

@ -33,7 +33,6 @@ period, we automatically sweep it up.
""" """
import gettext import gettext
import optparse
import os import os
import sys import sys
@ -47,31 +46,15 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
gettext.install('glance', unicode=1) gettext.install('glance', unicode=1)
from glance import version
from glance.common import config from glance.common import config
from glance.common import wsgi
def create_options(parser):
"""
Sets up the CLI and config-file options that may be
parsed and program commands.
:param parser: The option parser
"""
config.add_common_options(parser)
config.add_log_options(parser)
if __name__ == '__main__': if __name__ == '__main__':
oparser = optparse.OptionParser(version='%%prog %s'
% version.version_string())
create_options(oparser)
(options, args) = config.parse_options(oparser)
try: try:
conf, app = config.load_paste_app('glance-cleaner', options, args, conf = config.GlanceCacheConfigOpts()
'glance-cache') conf()
app = config.load_paste_app(conf, 'glance-cleaner')
app.run() app.run()
except RuntimeError, e: except RuntimeError, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)

View File

@ -24,7 +24,6 @@ images to be pretched.
""" """
import gettext import gettext
import optparse
import os import os
import sys import sys
@ -38,30 +37,15 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
gettext.install('glance', unicode=1) gettext.install('glance', unicode=1)
from glance import version
from glance.common import config from glance.common import config
def create_options(parser):
"""
Sets up the CLI and config-file options that may be
parsed and program commands.
:param parser: The option parser
"""
config.add_common_options(parser)
config.add_log_options(parser)
if __name__ == '__main__': if __name__ == '__main__':
oparser = optparse.OptionParser(version='%%prog %s'
% version.version_string())
create_options(oparser)
(options, args) = config.parse_options(oparser)
try: try:
conf, app = config.load_paste_app('glance-prefetcher', options, args, conf = config.GlanceCacheConfigOpts()
'glance-cache') conf()
app = config.load_paste_app(conf, 'glance-prefetcher')
app.run() app.run()
except RuntimeError, e: except RuntimeError, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)

View File

@ -25,7 +25,6 @@ This is meant to be run as a periodic task, perhaps every half-hour.
""" """
import gettext import gettext
import optparse
import os import os
import sys import sys
@ -39,31 +38,15 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
gettext.install('glance', unicode=1) gettext.install('glance', unicode=1)
from glance import version
from glance.common import config from glance.common import config
from glance.common import wsgi
def create_options(parser):
"""
Sets up the CLI and config-file options that may be
parsed and program commands.
:param parser: The option parser
"""
config.add_common_options(parser)
config.add_log_options(parser)
if __name__ == '__main__': if __name__ == '__main__':
oparser = optparse.OptionParser(version='%%prog %s'
% version.version_string())
create_options(oparser)
(options, args) = config.parse_options(oparser)
try: try:
conf, app = config.load_paste_app('glance-pruner', options, args, conf = config.GlanceCacheConfigOpts()
'glance-cache') conf()
app = config.load_paste_app(conf, 'glance-pruner')
app.run() app.run()
except RuntimeError, e: except RuntimeError, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)

View File

@ -21,7 +21,6 @@ CLI Utility to queue one or more images for prefetching into the image cache
""" """
import gettext import gettext
import optparse
import os import os
import sys import sys
@ -35,7 +34,6 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
gettext.install('glance', unicode=1) gettext.install('glance', unicode=1)
from glance import version
from glance.common import config from glance.common import config
USAGE = """ USAGE = """
@ -44,13 +42,11 @@ USAGE = """
if __name__ == '__main__': if __name__ == '__main__':
oparser = optparse.OptionParser(version='%%prog %s'
% version.version_string(),
usage=USAGE)
(options, args) = config.parse_options(oparser)
try: try:
conf, app = config.load_paste_app('glance-queue-image', options, args) conf = config.GlanceCacheConfigOpts(usage=USAGE)
args = conf()
app = config.load_paste_app(conf, 'glance-queue-image')
app.run(args) app.run(args)
except RuntimeError, e: except RuntimeError, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)

View File

@ -43,6 +43,7 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
gettext.install('glance', unicode=1) gettext.install('glance', unicode=1)
from glance import version from glance import version
from glance.common import cfg
from glance.common import config from glance.common import config
ALL_COMMANDS = ['start', 'stop', 'shutdown', 'restart', ALL_COMMANDS = ['start', 'stop', 'shutdown', 'restart',
@ -65,11 +66,11 @@ And command is one of:
And CONFPATH is the optional configuration file to use.""" And CONFPATH is the optional configuration file to use."""
def pid_files(server, options): def pid_files(server, conf):
pid_files = [] pid_files = []
if options['pid_file']: if conf.pid_file:
if os.path.exists(os.path.abspath(options['pid_file'])): if os.path.exists(os.path.abspath(conf.pid_file)):
pid_files = [os.path.abspath(options['pid_file'])] pid_files = [os.path.abspath(conf.pid_file)]
else: else:
if os.path.exists('/var/run/glance/%s.pid' % server): if os.path.exists('/var/run/glance/%s.pid' % server):
pid_files = ['/var/run/glance/%s.pid' % server] pid_files = ['/var/run/glance/%s.pid' % server]
@ -78,10 +79,10 @@ def pid_files(server, options):
yield pid_file, pid yield pid_file, pid
def do_start(server, options, args): def do_start(server, conf, args):
server_type = '-'.join(server.split('-')[:-1]) server_type = '-'.join(server.split('-')[:-1])
for pid_file, pid in pid_files(server, options): for pid_file, pid in pid_files(server, conf):
if os.path.exists('/proc/%s' % pid): if os.path.exists('/proc/%s' % pid):
print "%s appears to already be running: %s" % (server, pid_file) print "%s appears to already be running: %s" % (server, pid_file)
return return
@ -112,7 +113,6 @@ def do_start(server, options, args):
fp.close() fp.close()
def launch(ini_file, pid_file): def launch(ini_file, pid_file):
args = [server, ini_file]
print 'Starting %s with %s' % (server, ini_file) print 'Starting %s with %s' % (server, ini_file)
pid = os.fork() pid = os.fork()
@ -125,7 +125,7 @@ def do_start(server, options, args):
except OSError: except OSError:
pass pass
try: try:
os.execlp('%s' % server, server, ini_file) os.execlp('%s' % server, server, '--config-file', ini_file)
except OSError, e: except OSError, e:
sys.exit('unable to launch %s. Got error: %s' sys.exit('unable to launch %s. Got error: %s'
% (server, "%s" % e)) % (server, "%s" % e))
@ -133,31 +133,31 @@ def do_start(server, options, args):
else: else:
write_pid_file(pid_file, pid) write_pid_file(pid_file, pid)
if not options['pid_file']: if not conf.pid_file:
pid_file = '/var/run/glance/%s.pid' % server pid_file = '/var/run/glance/%s.pid' % server
else: else:
pid_file = os.path.abspath(options['pid_file']) pid_file = os.path.abspath(conf.pid_file)
try: try:
conf_file = config.find_config_file(server, options, args) if args and os.path.exists(args[0]):
conf_file = os.path.abspath(os.path.expanduser(args[0]))
else:
# Assume paste config is in the last config file
conf_file = conf.config_file[-1]
except RuntimeError, err: except RuntimeError, err:
sys.exit("Could not find any configuration file to use: %s" % err) sys.exit("Could not find any configuration file to use: %s" % err)
launch_args = [(conf_file, pid_file)]
# start all servers
for conf_file, pid_file in launch_args:
launch(conf_file, pid_file) launch(conf_file, pid_file)
def do_stop(server, options, args, graceful=False): def do_stop(server, conf, args, graceful=False):
if graceful and server in GRACEFUL_SHUTDOWN_SERVERS: if graceful and server in GRACEFUL_SHUTDOWN_SERVERS:
sig = signal.SIGHUP sig = signal.SIGHUP
else: else:
sig = signal.SIGTERM sig = signal.SIGTERM
did_anything = False did_anything = False
pfiles = pid_files(server, options) pfiles = pid_files(server, conf)
for pid_file, pid in pfiles: for pid_file, pid in pfiles:
did_anything = True did_anything = True
try: try:
@ -182,13 +182,12 @@ def do_stop(server, options, args, graceful=False):
if __name__ == '__main__': if __name__ == '__main__':
oparser = optparse.OptionParser(usage=USAGE, version='%%prog %s' conf = config.GlanceConfigOpts(usage=USAGE)
% version.version_string()) conf.register_cli_opt(cfg.StrOpt('pid-file',
oparser.add_option('--pid-file', default=None, metavar="PATH", metavar='PATH',
help="File to use as pid file. Default: " help='File to use as pid file. Default: '
"/var/run/glance/$server.pid") '/var/run/glance/$server.pid'))
config.add_common_options(oparser) args = conf()
(options, args) = config.parse_options(oparser)
if len(args) < 2: if len(args) < 2:
oparser.print_usage() oparser.print_usage()
@ -217,23 +216,23 @@ if __name__ == '__main__':
if command == 'start': if command == 'start':
for server in servers: for server in servers:
do_start(server, options, args) do_start(server, conf, args)
if command == 'stop': if command == 'stop':
for server in servers: for server in servers:
do_stop(server, options, args) do_stop(server, conf, args)
if command == 'shutdown': if command == 'shutdown':
for server in servers: for server in servers:
do_stop(server, options, args, graceful=True) do_stop(server, conf, args, graceful=True)
if command == 'restart': if command == 'restart':
for server in servers: for server in servers:
do_stop(server, options, args) do_stop(server, conf, args)
for server in servers: for server in servers:
do_start(server, options, args) do_start(server, conf, args)
if command == 'reload' or command == 'force-reload': if command == 'reload' or command == 'force-reload':
for server in servers: for server in servers:
do_stop(server, options, args, graceful=True) do_stop(server, conf, args, graceful=True)
do_start(server, options, args) do_start(server, conf, args)

View File

@ -27,7 +27,6 @@ Glance Management Utility
# glance-manage (or the other way around) # glance-manage (or the other way around)
import gettext import gettext
import optparse
import os import os
import sys import sys
@ -41,41 +40,29 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
gettext.install('glance', unicode=1) gettext.install('glance', unicode=1)
from glance import version as glance_version from glance.common import cfg
from glance.common import config from glance.common import config
from glance.common import exception from glance.common import exception
import glance.registry.db import glance.registry.db
import glance.registry.db.migration import glance.registry.db.migration
def create_options(parser): def do_db_version(conf, args):
"""
Sets up the CLI and config-file options that may be
parsed and program commands.
:param parser: The option parser
"""
config.add_common_options(parser)
config.add_log_options(parser)
glance.registry.db.add_options(parser)
def do_db_version(options, args):
"""Print database's current migration level""" """Print database's current migration level"""
print glance.registry.db.migration.db_version(options) print glance.registry.db.migration.db_version(conf)
def do_upgrade(options, args): def do_upgrade(conf, args):
"""Upgrade the database's migration level""" """Upgrade the database's migration level"""
try: try:
db_version = args[1] db_version = args[1]
except IndexError: except IndexError:
db_version = None db_version = None
glance.registry.db.migration.upgrade(options, version=db_version) glance.registry.db.migration.upgrade(conf, version=db_version)
def do_downgrade(options, args): def do_downgrade(conf, args):
"""Downgrade the database's migration level""" """Downgrade the database's migration level"""
try: try:
db_version = args[1] db_version = args[1]
@ -83,24 +70,24 @@ def do_downgrade(options, args):
raise exception.MissingArgumentError( raise exception.MissingArgumentError(
"downgrade requires a version argument") "downgrade requires a version argument")
glance.registry.db.migration.downgrade(options, version=db_version) glance.registry.db.migration.downgrade(conf, version=db_version)
def do_version_control(options, args): def do_version_control(conf, args):
"""Place a database under migration control""" """Place a database under migration control"""
glance.registry.db.migration.version_control(options) glance.registry.db.migration.version_control(conf)
def do_db_sync(options, args): def do_db_sync(conf, args):
"""Place a database under migration control and upgrade""" """Place a database under migration control and upgrade"""
try: try:
db_version = args[1] db_version = args[1]
except IndexError: except IndexError:
db_version = None db_version = None
glance.registry.db.migration.db_sync(options, version=db_version) glance.registry.db.migration.db_sync(conf, version=db_version)
def dispatch_cmd(options, args): def dispatch_cmd(conf, args):
"""Search for do_* cmd in this module and then run it""" """Search for do_* cmd in this module and then run it"""
cmd = args[0] cmd = args[0]
try: try:
@ -109,35 +96,32 @@ def dispatch_cmd(options, args):
sys.exit("ERROR: unrecognized command '%s'" % cmd) sys.exit("ERROR: unrecognized command '%s'" % cmd)
try: try:
cmd_func(options, args) cmd_func(conf, args)
except exception.GlanceException, e: except exception.GlanceException, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)
def main(): def main():
version = '%%prog %s' % glance_version.version_string()
usage = "%prog [options] <cmd>"
oparser = optparse.OptionParser(usage, version=version)
create_options(oparser)
(options, args) = config.parse_options(oparser)
try: try:
# We load the glance-registry config section because # We load the glance-registry config section because
# sql_connection is only part of the glance registry. # sql_connection is only part of the glance registry.
conf_file = config.find_config_file('glance-registry', options, args) default_config_files = \
conf = config.load_paste_config(conf_file, 'glance-registry') cfg.find_config_files(project='glance', prog='glance-registry')
config.setup_logging(options, conf)
conf = \
config.GlanceConfigOpts(default_config_files=default_config_files,
usage="%prog [options] <cmd>")
glance.registry.db.add_options(conf)
args = conf()
config.setup_logging(conf)
except RuntimeError, e: except RuntimeError, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)
if not args: if not args:
oparser.print_usage() conf.print_usage()
sys.exit(1) sys.exit(1)
if conf.get('sql_connection') and not options['sql_connection']: dispatch_cmd(conf, args)
options['sql_connection'] = conf.get('sql_connection')
dispatch_cmd(options, args)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -23,7 +23,6 @@ Reference implementation server for Glance Registry
""" """
import gettext import gettext
import optparse
import os import os
import sys import sys
@ -37,33 +36,19 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
gettext.install('glance', unicode=1) gettext.install('glance', unicode=1)
from glance import version
from glance.common import config from glance.common import config
from glance.common import wsgi from glance.common import wsgi
def create_options(parser):
"""
Sets up the CLI and config-file options that may be
parsed and program commands.
:param parser: The option parser
"""
config.add_common_options(parser)
config.add_log_options(parser)
if __name__ == '__main__': if __name__ == '__main__':
oparser = optparse.OptionParser(version='%%prog %s'
% version.version_string())
create_options(oparser)
(options, args) = config.parse_options(oparser)
try: try:
conf, app = config.load_paste_app('glance-registry', options, args) conf = config.GlanceConfigOpts()
conf()
app = config.load_paste_app(conf)
server = wsgi.Server() server = wsgi.Server()
server.start(app, int(conf['bind_port']), conf['bind_host'], conf) server.start(app, conf, default_port=9191)
server.wait() server.wait()
except RuntimeError, e: except RuntimeError, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)

View File

@ -21,7 +21,6 @@ Glance Scrub Service
""" """
import gettext import gettext
import optparse
import os import os
import sys import sys
@ -35,44 +34,30 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
gettext.install('glance', unicode=1) gettext.install('glance', unicode=1)
from glance import version from glance.common import cfg
from glance.common import config from glance.common import config
from glance.store import scrubber from glance.store import scrubber
def create_options(parser):
"""
Sets up the CLI and config-file options that may be
parsed and program commands.
:param parser: The option parser
"""
config.add_common_options(parser)
config.add_log_options(parser)
parser.add_option("-D", "--daemon", default=False, dest="daemon",
action="store_true",
help="Run as a long-running process. When not "
"specified (the default) run the scrub "
"operation once and then exits. When specified "
"do not exit and run scrub on wakeup_time "
"interval as specified in the config file.")
if __name__ == '__main__': if __name__ == '__main__':
oparser = optparse.OptionParser(version='%%prog %s'
% version.version_string())
create_options(oparser)
(options, args) = config.parse_options(oparser)
try: try:
conf, app = config.load_paste_app('glance-scrubber', options, args) conf = config.GlanceConfigOpts()
daemon = options.get('daemon') or \ conf.register_cli_opt(
config.get_option(conf, 'daemon', type='bool', cfg.BoolOpt('daemon',
default=False) short='D',
default=False,
help='Run as a long-running process. When not '
'specified (the default) run the scrub operation '
'once and then exits. When specified do not exit '
'and run scrub on wakeup_time interval as '
'specified in the config.'))
conf.register_opt(cfg.IntOpt('wakeup_time', default=300))
conf()
if daemon: app = config.load_paste_app(conf, 'glance-scrubber')
wakeup_time = int(conf.get('wakeup_time', 300))
server = scrubber.Daemon(wakeup_time) if conf.daemon:
server = scrubber.Daemon(conf.wakeup_time)
server.start(app) server.start(app)
server.wait() server.wait()
else: else:

View File

@ -43,7 +43,7 @@ get_images_re = re.compile(r'^(/v\d+)*/images/(.+)$')
class CacheFilter(wsgi.Middleware): class CacheFilter(wsgi.Middleware):
def __init__(self, app, conf): def __init__(self, app, conf, **local_conf):
self.conf = conf self.conf = conf
self.cache = image_cache.ImageCache(conf) self.cache = image_cache.ImageCache(conf)
self.serializer = images.ImageSerializer() self.serializer = images.ImageSerializer()

View File

@ -28,8 +28,7 @@ logger = logging.getLogger(__name__)
class CacheManageFilter(wsgi.Middleware): class CacheManageFilter(wsgi.Middleware):
def __init__(self, app, conf): def __init__(self, app, conf, **local_conf):
map = app.map map = app.map
resource = cached_images.create_resource(conf) resource = cached_images.create_resource(conf)

View File

@ -35,7 +35,7 @@ logger = logging.getLogger('glance.api.middleware.version_negotiation')
class VersionNegotiationFilter(wsgi.Middleware): class VersionNegotiationFilter(wsgi.Middleware):
def __init__(self, app, conf): def __init__(self, app, conf, **local_conf):
self.versions_app = versions.Controller(conf) self.versions_app = versions.Controller(conf)
self.version_uri_regex = re.compile(r"^v(\d+)\.?(\d+)?") self.version_uri_regex = re.compile(r"^v(\d+)\.?(\d+)?")
self.conf = conf self.conf = conf

View File

@ -34,6 +34,7 @@ from webob.exc import (HTTPNotFound,
import glance.api.v1 import glance.api.v1
from glance.api.v1 import controller from glance.api.v1 import controller
from glance import image_cache from glance import image_cache
from glance.common import cfg
from glance.common import exception from glance.common import exception
from glance.common import notifier from glance.common import notifier
from glance.common import wsgi from glance.common import wsgi
@ -76,8 +77,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)
glance.store.create_stores(conf) glance.store.create_stores(conf)
self.notifier = notifier.Notifier(conf) self.notifier = notifier.Notifier(conf)
registry.configure_registry_client(conf) registry.configure_registry_client(conf)
@ -290,7 +294,7 @@ class Controller(controller.BaseController):
raise HTTPBadRequest(explanation=msg) raise HTTPBadRequest(explanation=msg)
store_name = req.headers.get('x-image-meta-store', store_name = 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, store_name)

View File

@ -30,7 +30,7 @@ class API(wsgi.Router):
"""WSGI router for Glance v1 API requests.""" """WSGI router for Glance v1 API requests."""
def __init__(self, conf): def __init__(self, conf, **local_conf):
self.conf = conf self.conf = conf
mapper = routes.Mapper() mapper = routes.Mapper()

View File

@ -24,6 +24,8 @@ import json
import webob.dec import webob.dec
from glance.common import wsgi
class Controller(object): class Controller(object):
@ -63,5 +65,4 @@ class Controller(object):
return response return response
def get_href(self): def get_href(self):
return "http://%s:%s/v1/" % (self.conf['bind_host'], return "http://%s:%s/v1/" % wsgi.get_bind_addr(self.conf, 9292)
self.conf['bind_port'])

View File

@ -20,159 +20,68 @@
Routines for configuring Glance Routines for configuring Glance
""" """
import ConfigParser
import logging import logging
import logging.config import logging.config
import logging.handlers import logging.handlers
import optparse
import os import os
import re
import sys import sys
from paste import deploy from glance import version
from glance.common import cfg
import glance.common.exception as exception from glance.common import utils
from glance.common import wsgi
DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
def parse_options(parser, cli_args=None): class GlanceConfigOpts(cfg.CommonConfigOpts):
"""
Returns the parsed CLI options, command to run and its arguments, merged
with any same-named options found in a configuration file.
The function returns a tuple of (options, args), where options is a def __init__(self, default_config_files=None, **kwargs):
mapping of option key/str(value) pairs, and args is the set of arguments super(GlanceConfigOpts, self).__init__(
(not options) supplied on the command-line. project='glance',
version='%%prog %s' % version.version_string(),
The reason that the option values are returned as strings only is that default_config_files=default_config_files,
ConfigParser and paste.deploy only accept string values... **kwargs)
:param parser: The option parser
:param cli_args: (Optional) Set of arguments to process. If not present,
sys.argv[1:] is used.
:retval tuple of (options, args)
"""
(options, args) = parser.parse_args(cli_args)
return (vars(options), args)
def add_common_options(parser): class GlanceCacheConfigOpts(GlanceConfigOpts):
"""
Given a supplied optparse.OptionParser, adds an OptionGroup that
represents all common configuration options.
:param parser: optparse.OptionParser def __init__(self, **kwargs):
""" config_files = cfg.find_config_files(project='glance',
help_text = "The following configuration options are common to "\ prog='glance-cache')
"all glance programs." super(GlanceCacheConfigOpts, self).__init__(config_files, **kwargs)
group = optparse.OptionGroup(parser, "Common Options", help_text)
group.add_option('-v', '--verbose', default=False, dest="verbose",
action="store_true",
help="Print more verbose output")
group.add_option('-d', '--debug', default=False, dest="debug",
action="store_true",
help="Print debugging output")
group.add_option('--config-file', default=None, metavar="PATH",
help="Path to the config file to use. When not specified "
"(the default), we generally look at the first "
"argument specified to be a config file, and if "
"that is also missing, we search standard "
"directories for a config file.")
parser.add_option_group(group)
def add_log_options(parser): def setup_logging(conf):
"""
Given a supplied optparse.OptionParser, adds an OptionGroup that
represents all the configuration options around logging.
:param parser: optparse.OptionParser
"""
help_text = "The following configuration options are specific to logging "\
"functionality for this program."
group = optparse.OptionGroup(parser, "Logging Options", help_text)
group.add_option('--log-config', default=None, metavar="PATH",
help="If this option is specified, the logging "
"configuration file specified is used and overrides "
"any other logging options specified. Please see "
"the Python logging module documentation for "
"details on logging configuration files.")
group.add_option('--log-date-format', metavar="FORMAT",
default=DEFAULT_LOG_DATE_FORMAT,
help="Format string for %(asctime)s in log records. "
"Default: %default")
group.add_option('--log-file', default=None, metavar="PATH",
help="(Optional) Name of log file to output to. "
"If not set, logging will go to stdout.")
group.add_option("--log-dir", default=None,
help="(Optional) The directory to keep log files in "
"(will be prepended to --logfile)")
group.add_option('--use-syslog', default=False, dest="use_syslog",
action="store_true",
help="Use syslog for logging.")
parser.add_option_group(group)
def setup_logging(options, conf):
""" """
Sets up the logging options for a log with supplied name Sets up the logging options for a log with supplied name
:param options: Mapping of typed option key/values :param conf: a cfg.ConfOpts object
:param conf: Mapping of untyped key/values from config file
""" """
if options.get('log_config', None): if conf.log_config:
# Use a logging configuration file for all settings... # Use a logging configuration file for all settings...
if os.path.exists(options['log_config']): if os.path.exists(conf.log_config):
logging.config.fileConfig(options['log_config']) logging.config.fileConfig(conf.log_config)
return return
else: else:
raise RuntimeError("Unable to locate specified logging " raise RuntimeError("Unable to locate specified logging "
"config file: %s" % options['log_config']) "config file: %s" % conf.log_config)
# If either the CLI option or the conf value
# is True, we set to True
debug = options.get('debug') or \
get_option(conf, 'debug', type='bool', default=False)
verbose = options.get('verbose') or \
get_option(conf, 'verbose', type='bool', default=False)
root_logger = logging.root root_logger = logging.root
if debug: if conf.debug:
root_logger.setLevel(logging.DEBUG) root_logger.setLevel(logging.DEBUG)
elif verbose: elif conf.verbose:
root_logger.setLevel(logging.INFO) root_logger.setLevel(logging.INFO)
else: else:
root_logger.setLevel(logging.WARNING) root_logger.setLevel(logging.WARNING)
# Set log configuration from options... formatter = logging.Formatter(conf.log_format, conf.log_date_format)
# Note that we use a hard-coded log format in the options
# because of Paste.Deploy bug #379
# http://trac.pythonpaste.org/pythonpaste/ticket/379
log_format = options.get('log_format', DEFAULT_LOG_FORMAT)
log_date_format = options.get('log_date_format', DEFAULT_LOG_DATE_FORMAT)
formatter = logging.Formatter(log_format, log_date_format)
logfile = options.get('log_file') if conf.use_syslog:
if not logfile:
logfile = conf.get('log_file')
use_syslog = options.get('use_syslog') or \
get_option(conf, 'use_syslog', type='bool', default=False)
if use_syslog:
handler = logging.handlers.SysLogHandler(address='/dev/log') handler = logging.handlers.SysLogHandler(address='/dev/log')
elif logfile: elif conf.log_file:
logdir = options.get('log_dir') logfile = conf.log_file
if not logdir: if conf.log_dir:
logdir = conf.get('log_dir') logfile = os.path.join(conf.log_dir, logfile)
if logdir:
logfile = os.path.join(logdir, logfile)
handler = logging.handlers.WatchedFileHandler(logfile) handler = logging.handlers.WatchedFileHandler(logfile)
else: else:
handler = logging.StreamHandler(sys.stdout) handler = logging.StreamHandler(sys.stdout)
@ -181,143 +90,39 @@ def setup_logging(options, conf):
root_logger.addHandler(handler) root_logger.addHandler(handler)
def find_config_file(conf_name, options, args): def load_paste_app(conf, app_name=None):
"""
Return the first config file found for an application.
We search for the paste config file in the following order:
* If --config-file option is used, use that
* If args[0] is a file, use that
* Search for $conf_name.conf in standard directories:
* ~.glance/
* ~
* /etc/glance
* /etc
:retval Full path to config file.
:raises RuntimeError if the config file cannot be found.
"""
fix_path = lambda p: os.path.abspath(os.path.expanduser(p))
if options.get('config_file'):
if os.path.exists(options['config_file']):
return fix_path(options['config_file'])
elif args:
if os.path.exists(args[0]):
return fix_path(args[0])
# Handle standard directory search for $conf_name.conf
config_file_dirs = [fix_path(os.path.join('~', '.glance')),
fix_path('~'),
'/etc/glance/',
'/etc']
for cfg_dir in config_file_dirs:
cfg_file = os.path.join(cfg_dir, '%s.conf' % conf_name)
if os.path.exists(cfg_file):
return cfg_file
raise RuntimeError("Unable to locate %s configuration file." % conf_name)
def load_paste_config(conf_file, app_name):
"""
Load the configuration mapping from a paste config file.
:param conf_file: The path to the paste config file.
:param app_name: Name of the application to load config for, or None.
None signifies to only load the [DEFAULT] section of
the config file.
:retval The configuration mapping.
:raises RuntimeError when there was a problem loading the configuration
file.
"""
try:
return deploy.appconfig("config:%s" % conf_file, name=app_name)
except Exception, e:
raise RuntimeError("Error trying to load config %s: %s"
% (conf_file, e))
def load_paste_app(app_name, options, args, conf_name=None):
""" """
Builds and returns a WSGI app from a paste config file. Builds and returns a WSGI app from a paste config file.
We search for the paste config file in the following order: We assume the last config file specified in the supplied ConfigOpts
* If --config-file option is used, use that object is the paste config file.
* If args[0] is a file, use that
* Search for $conf_name.conf in standard directories:
* ~.glance/
* ~
* /etc/glance
* /etc
:param app_name: Name of the application to load :param conf: a cfg.ConfigOpts object
:param options: Set of typed options returned from parse_options() :param app_name: name of the application to load
:param args: Command line arguments from argv[1:]
:param conf_name: Name of config file to load, defaults to app_name
:raises RuntimeError when config file cannot be located or application :raises RuntimeError when config file cannot be located or application
cannot be loaded from config file cannot be loaded from config file
""" """
if conf_name is None: if app_name is None:
conf_name = app_name app_name = conf.prog
conf_file = find_config_file(conf_name, options, args)
conf = load_paste_config(conf_file, app_name) # Assume paste config is in the last config file
conf_file = os.path.abspath(conf.config_file[-1])
try: try:
# Setup logging early, supplying both the CLI options and the # Setup logging early
# configuration mapping from the config file setup_logging(conf)
setup_logging(options, conf)
# We only update the conf dict for the verbose and debug logger = logging.getLogger(app_name)
# flags. Everything else must be set up in the conf file...
debug = options.get('debug') or \ app = wsgi.paste_deploy_app(conf_file, app_name, conf)
get_option(conf, 'debug', type='bool', default=False)
verbose = options.get('verbose') or \
get_option(conf, 'verbose', type='bool', default=False)
conf['debug'] = debug
conf['verbose'] = verbose
# Log the options used when starting if we're in debug mode... # Log the options used when starting if we're in debug mode...
if debug: if conf.debug:
logger = logging.getLogger(app_name) conf.log_opt_values(logging.getLogger(app_name), logging.DEBUG)
logger.debug("*" * 80)
logger.debug("Configuration options gathered from config file:") return app
logger.debug(conf_file)
logger.debug("================================================")
items = dict([(k, v) for k, v in conf.items()
if k not in ('__file__', 'here')])
for key, value in sorted(items.items()):
logger.debug("%(key)-30s %(value)s" % locals())
logger.debug("*" * 80)
app = deploy.loadapp("config:%s" % conf_file, name=app_name)
except (LookupError, ImportError), e: except (LookupError, ImportError), e:
raise RuntimeError("Unable to load %(app_name)s from " raise RuntimeError("Unable to load %(app_name)s from "
"configuration file %(conf_file)s." "configuration file %(conf_file)s."
"\nGot: %(e)r" % locals()) "\nGot: %(e)r" % locals())
return conf, app
def get_option(options, option, **kwargs):
if option in options:
value = options[option]
type_ = kwargs.get('type', 'str')
if type_ == 'bool':
if hasattr(value, 'lower'):
return value.lower() == 'true'
else:
return value
elif type_ == 'int':
return int(value)
elif type_ == 'float':
return float(value)
else:
return value
elif 'default' in kwargs:
return kwargs['default']
else:
raise KeyError("option '%s' not found" % option)

View File

@ -14,7 +14,8 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from glance.common import config
from glance.common import cfg
from glance.common import exception from glance.common import exception
from glance.common import utils from glance.common import utils
from glance.common import wsgi from glance.common import wsgi
@ -53,26 +54,29 @@ class RequestContext(object):
class ContextMiddleware(wsgi.Middleware): class ContextMiddleware(wsgi.Middleware):
def __init__(self, app, conf):
opts = [
cfg.BoolOpt('owner_is_tenant', default=True),
]
def __init__(self, app, conf, **local_conf):
self.conf = conf self.conf = conf
self.conf.register_opts(self.opts)
# Determine the context class to use
self.ctxcls = RequestContext
if 'context_class' in local_conf:
self.ctxcls = utils.import_class(local_conf['context_class'])
super(ContextMiddleware, self).__init__(app) super(ContextMiddleware, self).__init__(app)
def make_context(self, *args, **kwargs): def make_context(self, *args, **kwargs):
""" """
Create a context with the given arguments. Create a context with the given arguments.
""" """
kwargs.setdefault('owner_is_tenant', self.conf.owner_is_tenant)
# Determine the context class to use return self.ctxcls(*args, **kwargs)
ctxcls = RequestContext
if 'context_class' in self.conf:
ctxcls = utils.import_class(self.conf['context_class'])
# Determine whether to use tenant or owner
owner_is_tenant = config.get_option(self.conf, 'owner_is_tenant',
type='bool', default=True)
kwargs.setdefault('owner_is_tenant', owner_is_tenant)
return ctxcls(*args, **kwargs)
def process_request(self, req): def process_request(self, req):
""" """

View File

@ -22,7 +22,7 @@ import uuid
import kombu.connection import kombu.connection
from glance.common import config from glance.common import cfg
from glance.common import exception from glance.common import exception
@ -61,33 +61,29 @@ class LoggingStrategy(object):
class RabbitStrategy(object): class RabbitStrategy(object):
"""A notifier that puts a message on a queue when called.""" """A notifier that puts a message on a queue when called."""
opts = [
cfg.StrOpt('rabbit_host', default='localhost'),
cfg.IntOpt('rabbit_port', default=5672),
cfg.BoolOpt('rabbit_use_ssl', default=False),
cfg.StrOpt('rabbit_userid', default='guest'),
cfg.StrOpt('rabbit_password', default='guest'),
cfg.StrOpt('rabbit_virtual_host', default='/'),
cfg.StrOpt('rabbit_notification_topic', default='glance_notifications')
]
def __init__(self, conf): def __init__(self, conf):
"""Initialize the rabbit notification strategy.""" """Initialize the rabbit notification strategy."""
self._conf = conf self._conf = conf
host = self._get_option('rabbit_host', 'str', 'localhost') self._conf.register_opts(self.opts)
port = self._get_option('rabbit_port', 'int', 5672)
use_ssl = self._get_option('rabbit_use_ssl', 'bool', False)
userid = self._get_option('rabbit_userid', 'str', 'guest')
password = self._get_option('rabbit_password', 'str', 'guest')
virtual_host = self._get_option('rabbit_virtual_host', 'str', '/')
self.connection = kombu.connection.BrokerConnection( self.connection = kombu.connection.BrokerConnection(
hostname=host, hostname=self._conf.rabbit_host,
userid=userid, userid=self._conf.rabbit_userid,
password=password, password=self._conf.rabbit_password,
virtual_host=virtual_host, virtual_host=self._conf.rabbit_virtual_host,
ssl=use_ssl) ssl=self._conf.rabbit_use_ssl)
self.topic = self._get_option('rabbit_notification_topic', self.topic = self._conf.rabbit_notification_topic
'str',
'glance_notifications')
def _get_option(self, name, datatype, default):
"""Retrieve a configuration option."""
return config.get_option(self._conf,
name,
type=datatype,
default=default)
def _send_message(self, message, priority): def _send_message(self, message, priority):
topic = "%s.%s" % (self.topic, priority) topic = "%s.%s" % (self.topic, priority)
@ -115,9 +111,13 @@ class Notifier(object):
"default": NoopStrategy, "default": NoopStrategy,
} }
opts = [
cfg.StrOpt('notifier_strategy', default='default')
]
def __init__(self, conf, strategy=None): def __init__(self, conf, strategy=None):
strategy = config.get_option(conf, "notifier_strategy", conf.register_opts(self.opts)
type="str", default="default") strategy = conf.notifier_strategy
try: try:
self.strategy = self.STRATEGIES[strategy](conf) self.strategy = self.STRATEGIES[strategy](conf)
except KeyError: except KeyError:

View File

@ -31,15 +31,29 @@ import time
import eventlet import eventlet
from eventlet.green import socket, ssl from eventlet.green import socket, ssl
import eventlet.wsgi import eventlet.wsgi
from paste import deploy
import routes import routes
import routes.middleware import routes.middleware
import webob.dec import webob.dec
import webob.exc import webob.exc
from glance.common import cfg
from glance.common import exception from glance.common import exception
from glance.common import utils from glance.common import utils
bind_opts = [
cfg.StrOpt('bind_host', default='0.0.0.0'),
cfg.IntOpt('bind_port'),
]
socket_opts = [
cfg.IntOpt('backlog', default=4096),
cfg.StrOpt('cert_file'),
cfg.StrOpt('key_file'),
]
class WritableLogger(object): class WritableLogger(object):
"""A thin wrapper that responds to `write` and logs.""" """A thin wrapper that responds to `write` and logs."""
@ -51,30 +65,37 @@ class WritableLogger(object):
self.logger.log(self.level, msg.strip("\n")) self.logger.log(self.level, msg.strip("\n"))
def get_socket(host, port, conf): def get_bind_addr(conf, default_port=None):
"""Return the host and port to bind to."""
conf.register_opts(bind_opts)
return (conf.bind_host, conf.bind_port or default_port)
def get_socket(conf, default_port):
""" """
Bind socket to bind ip:port in conf Bind socket to bind ip:port in conf
note: Mostly comes from Swift with a few small changes... note: Mostly comes from Swift with a few small changes...
:param host: Host to bind to :param conf: a cfg.ConfigOpts object
:param port: Port to bind to :param default_port: port to bind to if none is specified in conf
:param conf: Configuration dict to read settings from
:returns : a socket object as returned from socket.listen or :returns : a socket object as returned from socket.listen or
ssl.wrap_socket if conf specifies cert_file ssl.wrap_socket if conf specifies cert_file
""" """
bind_addr = (host, port) bind_addr = get_bind_addr(conf, default_port)
# TODO(jaypipes): eventlet's greened socket module does not actually # TODO(jaypipes): eventlet's greened socket module does not actually
# support IPv6 in getaddrinfo(). We need to get around this in the # support IPv6 in getaddrinfo(). We need to get around this in the
# future or monitor upstream for a fix # future or monitor upstream for a fix
address_family = [addr[0] for addr in socket.getaddrinfo(bind_addr[0], address_family = [addr[0] for addr in socket.getaddrinfo(bind_addr[0],
bind_addr[1], socket.AF_UNSPEC, socket.SOCK_STREAM) bind_addr[1], socket.AF_UNSPEC, socket.SOCK_STREAM)
if addr[0] in (socket.AF_INET, socket.AF_INET6)][0] if addr[0] in (socket.AF_INET, socket.AF_INET6)][0]
backlog = int(conf.get('backlog', 4096))
cert_file = conf.get('cert_file') conf.register_opts(socket_opts)
key_file = conf.get('key_file')
cert_file = conf.cert_file
key_file = conf.key_file
use_ssl = cert_file or key_file use_ssl = cert_file or key_file
if use_ssl and (not cert_file or not key_file): if use_ssl and (not cert_file or not key_file):
raise RuntimeError(_("When running server in SSL mode, you must " raise RuntimeError(_("When running server in SSL mode, you must "
@ -85,7 +106,7 @@ def get_socket(host, port, conf):
retry_until = time.time() + 30 retry_until = time.time() + 30
while not sock and time.time() < retry_until: while not sock and time.time() < retry_until:
try: try:
sock = eventlet.listen(bind_addr, backlog=backlog, sock = eventlet.listen(bind_addr, backlog=conf.backlog,
family=address_family) family=address_family)
if use_ssl: if use_ssl:
sock = ssl.wrap_socket(sock, certfile=cert_file, sock = ssl.wrap_socket(sock, certfile=cert_file,
@ -114,17 +135,15 @@ class Server(object):
def __init__(self, threads=1000): def __init__(self, threads=1000):
self.pool = eventlet.GreenPool(threads) self.pool = eventlet.GreenPool(threads)
def start(self, application, port, host='0.0.0.0', conf=None): def start(self, application, conf, default_port):
""" """
Run a WSGI server with the given application. Run a WSGI server with the given application.
:param application: The application to run in the WSGI server :param application: The application to run in the WSGI server
:param port: Port to bind to :param conf: a cfg.ConfigOpts object
:param host: Host to bind to :param default_port: Port to bind to if none is specified in conf
:param conf: Mapping of configuration options
""" """
conf = conf or {} socket = get_socket(conf, default_port)
socket = get_socket(host, port, conf)
self.pool.spawn_n(self._run, application, socket) self.pool.spawn_n(self._run, application, socket)
def wait(self): def wait(self):
@ -409,11 +428,45 @@ class Resource(object):
return args return args
def _import_factory(conf, key): class BasePasteFactory(object):
return utils.import_class(conf[key].replace(':', '.').strip())
"""A base class for paste app and filter factories.
Sub-classes must override the KEY class attribute and provide
a __call__ method.
"""
KEY = None
def __init__(self, conf):
self.conf = conf
def __call__(self, global_conf, **local_conf):
raise NotImplementedError
def _import_factory(self, local_conf):
"""Import an app/filter class.
Lookup the KEY from the PasteDeploy local conf and import the
class named there. This class can then be used as an app or
filter factory.
Note we support the <module>:<class> format.
Note also that if you do e.g.
key =
value
then ConfigParser returns a value with a leading newline, so
we strip() the value before using it.
"""
class_name = local_conf[self.KEY].replace(':', '.').strip()
return utils.import_class(class_name)
def app_factory(global_conf, **local_conf): class AppFactory(BasePasteFactory):
"""A Generic paste.deploy app factory. """A Generic paste.deploy app factory.
This requires glance.app_factory to be set to a callable which returns a This requires glance.app_factory to be set to a callable which returns a
@ -423,18 +476,20 @@ def app_factory(global_conf, **local_conf):
paste.app_factory = glance.common.wsgi:app_factory paste.app_factory = glance.common.wsgi:app_factory
glance.app_factory = glance.api.v1:API glance.app_factory = glance.api.v1:API
The WSGI app constructor must accept a configuration dict as its only The WSGI app constructor must accept a ConfigOpts object and a local config
argument. dict as its two arguments.
""" """
factory = _import_factory(local_conf, 'glance.app_factory')
conf = global_conf.copy() KEY = 'glance.app_factory'
conf.update(local_conf)
return factory(conf) def __call__(self, global_conf, **local_conf):
"""The actual paste.app_factory protocol method."""
factory = self._import_factory(local_conf)
return factory(self.conf, **local_conf)
def filter_factory(global_conf, **local_conf): class FilterFactory(AppFactory):
"""A Generic paste.deploy filter factory. """A Generic paste.deploy filter factory.
This requires glance.filter_factory to be set to a callable which returns a This requires glance.filter_factory to be set to a callable which returns a
@ -444,15 +499,66 @@ def filter_factory(global_conf, **local_conf):
paste.filter_factory = glance.common.wsgi:filter_factory paste.filter_factory = glance.common.wsgi:filter_factory
glance.filter_factory = glance.api.middleware.cache:CacheFilter glance.filter_factory = glance.api.middleware.cache:CacheFilter
The WSGI filter constructor must accept a WSGI app and a configuration dict The WSGI filter constructor must accept a WSGI app, a ConfigOpts object and
as its only two arguments. a local config dict as its three arguments.
""" """
factory = _import_factory(local_conf, 'glance.filter_factory')
conf = global_conf.copy() KEY = 'glance.filter_factory'
conf.update(local_conf)
def __call__(self, global_conf, **local_conf):
"""The actual paste.filter_factory protocol method."""
factory = self._import_factory(local_conf)
def filter(app): def filter(app):
return factory(app, conf) return factory(app, self.conf, **local_conf)
return filter return filter
def setup_paste_factories(conf):
"""Set up the generic paste app and filter factories.
Set things up so that:
paste.app_factory = glance.common.wsgi:app_factory
and
paste.filter_factory = glance.common.wsgi:filter_factory
work correctly while loading PasteDeploy configuration.
The app factories are constructed at runtime to allow us to pass a
ConfigOpts object to the WSGI classes.
:param conf: a ConfigOpts object
"""
global app_factory, filter_factory
app_factory = AppFactory(conf)
filter_factory = FilterFactory(conf)
def teardown_paste_factories():
"""Reverse the effect of setup_paste_factories()."""
global app_factory, filter_factory
del app_factory
del filter_factory
def paste_deploy_app(paste_config_file, app_name, conf):
"""Load a WSGI app from a PasteDeploy configuration.
Use deploy.loadapp() to load the app from the PasteDeploy configuration,
ensuring that the supplied ConfigOpts object is passed to the app and
filter constructors.
:param paste_config_file: a PasteDeploy config file
:param app_name: the name of the app/pipeline to load from the file
:param conf: a ConfigOpts object to supply to the app and its filters
:returns: the WSGI app
"""
setup_paste_factories(conf)
try:
return deploy.loadapp("config:%s" % paste_config_file, name=app_name)
finally:
teardown_paste_factories()

View File

@ -22,7 +22,7 @@ LRU Cache for Image Data
import logging import logging
import os import os
from glance.common import config from glance.common import cfg
from glance.common import exception from glance.common import exception
from glance.common import utils from glance.common import utils
@ -34,15 +34,23 @@ class ImageCache(object):
"""Provides an LRU cache for image data.""" """Provides an LRU cache for image data."""
opts = [
cfg.StrOpt('image_cache_driver', default='sqlite'),
cfg.IntOpt('image_cache_max_size', default=10 * (1024 ** 3)), # 10 GB
cfg.IntOpt('image_cache_stall_time', default=86400), # 24 hours
cfg.StrOpt('image_cache_dir'),
]
def __init__(self, conf): def __init__(self, conf):
self.conf = conf self.conf = conf
self.conf.register_opts(self.opts)
self.init_driver() self.init_driver()
def init_driver(self): def init_driver(self):
""" """
Create the driver for the cache Create the driver for the cache
""" """
driver_name = self.conf.get('image_cache_driver', 'sqlite') driver_name = self.conf.image_cache_driver
driver_module = (__name__ + '.drivers.' + driver_name + '.Driver') driver_module = (__name__ + '.drivers.' + driver_name + '.Driver')
try: try:
self.driver_class = utils.import_class(driver_module) self.driver_class = utils.import_class(driver_module)
@ -150,8 +158,7 @@ class ImageCache(object):
size. Returns a tuple containing the total number of cached size. Returns a tuple containing the total number of cached
files removed and the total size of all pruned image files. files removed and the total size of all pruned image files.
""" """
max_size = int(self.conf.get('image_cache_max_size', max_size = self.conf.image_cache_max_size
DEFAULT_MAX_CACHE_SIZE))
current_size = self.driver.get_cache_size() current_size = self.driver.get_cache_size()
if max_size > current_size: if max_size > current_size:
logger.debug(_("Image cache has free space, skipping prune...")) logger.debug(_("Image cache has free space, skipping prune..."))
@ -180,12 +187,12 @@ class ImageCache(object):
"%(total_bytes_pruned)d.") % locals()) "%(total_bytes_pruned)d.") % locals())
return total_files_pruned, total_bytes_pruned return total_files_pruned, total_bytes_pruned
def clean(self): def clean(self, stall_time=None):
""" """
Cleans up any invalid or incomplete cached images. The cache driver Cleans up any invalid or incomplete cached images. The cache driver
decides what that means... decides what that means...
""" """
self.driver.clean() self.driver.clean(stall_time)
def queue_image(self, image_id): def queue_image(self, image_id):
""" """

View File

@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
class Cleaner(object): class Cleaner(object):
def __init__(self, conf): def __init__(self, conf, **local_conf):
self.conf = conf self.conf = conf
self.cache = ImageCache(conf) self.cache = ImageCache(conf)

View File

@ -60,11 +60,9 @@ class Driver(object):
Creates all necessary directories under the base cache directory Creates all necessary directories under the base cache directory
""" """
try: self.base_dir = self.conf.image_cache_dir
key = 'image_cache_dir' if self.base_dir is None:
self.base_dir = self.conf[key] msg = _('Failed to read %s from config') % 'image_cache_dir'
except KeyError:
msg = _('Failed to read %s from config') % key
logger.error(msg) logger.error(msg)
driver = self.__class__.__module__ driver = self.__class__.__module__
raise exception.BadDriverConfiguration(driver_name=driver, raise exception.BadDriverConfiguration(driver_name=driver,
@ -168,7 +166,7 @@ class Driver(object):
:param image_id: Image ID :param image_id: Image ID
""" """
def clean(self): def clean(self, stall_time=None):
""" """
Dependent on the driver, clean up and destroy any invalid or incomplete Dependent on the driver, clean up and destroy any invalid or incomplete
cached images cached images

View File

@ -31,13 +31,12 @@ import time
from eventlet import sleep, timeout from eventlet import sleep, timeout
import sqlite3 import sqlite3
from glance.common import cfg
from glance.common import exception from glance.common import exception
from glance.image_cache.drivers import base from glance.image_cache.drivers import base
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DEFAULT_SQL_CALL_TIMEOUT = 2 DEFAULT_SQL_CALL_TIMEOUT = 2
DEFAULT_STALL_TIME = 86400 # 24 hours
DEFAULT_SQLITE_DB = 'cache.db'
class SqliteConnection(sqlite3.Connection): class SqliteConnection(sqlite3.Connection):
@ -82,6 +81,10 @@ class Driver(base.Driver):
that has atimes set. that has atimes set.
""" """
opts = [
cfg.StrOpt('image_cache_sqlite_db', default='cache.db'),
]
def configure(self): def configure(self):
""" """
Configure the driver to use the stored configuration options Configure the driver to use the stored configuration options
@ -91,11 +94,13 @@ class Driver(base.Driver):
""" """
super(Driver, self).configure() super(Driver, self).configure()
self.conf.register_opts(self.opts)
# Create the SQLite database that will hold our cache attributes # Create the SQLite database that will hold our cache attributes
self.initialize_db() self.initialize_db()
def initialize_db(self): def initialize_db(self):
db = self.conf.get('image_cache_sqlite_db', DEFAULT_SQLITE_DB) db = self.conf.image_cache_sqlite_db
self.db_path = os.path.join(self.base_dir, db) self.db_path = os.path.join(self.base_dir, db)
try: try:
conn = sqlite3.connect(self.db_path, check_same_thread=False, conn = sqlite3.connect(self.db_path, check_same_thread=False,
@ -244,7 +249,7 @@ class Driver(base.Driver):
if os.path.exists(path): if os.path.exists(path):
os.unlink(path) os.unlink(path)
def clean(self): def clean(self, stall_time=None):
""" """
Delete any image files in the invalid directory and any Delete any image files in the invalid directory and any
files in the incomplete directory that are older than a files in the incomplete directory that are older than a
@ -252,10 +257,11 @@ class Driver(base.Driver):
""" """
self.delete_invalid_files() self.delete_invalid_files()
incomplete_stall_time = int(self.conf.get('image_cache_stall_time', if stall_time is None:
DEFAULT_STALL_TIME)) stall_time = self.conf.image_cache_stall_time
now = time.time() now = time.time()
older_than = now - incomplete_stall_time older_than = now - stall_time
self.delete_stalled_files(older_than) self.delete_stalled_files(older_than)
def get_least_recently_accessed(self): def get_least_recently_accessed(self):

View File

@ -70,8 +70,6 @@ from glance.image_cache.drivers import base
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DEFAULT_STALL_TIME = 86400 # 24 hours
class Driver(base.Driver): class Driver(base.Driver):
@ -416,7 +414,7 @@ class Driver(base.Driver):
return self._reap_old_files(self.incomplete_dir, 'stalled', return self._reap_old_files(self.incomplete_dir, 'stalled',
grace=grace) grace=grace)
def clean(self): def clean(self, stall_time=None):
""" """
Delete any image files in the invalid directory and any Delete any image files in the invalid directory and any
files in the incomplete directory that are older than a files in the incomplete directory that are older than a
@ -424,9 +422,10 @@ class Driver(base.Driver):
""" """
self.reap_invalid() self.reap_invalid()
incomplete_stall_time = int(self.conf.get('image_cache_stall_time', if stall_time is None:
DEFAULT_STALL_TIME)) stall_time = self.conf.image_cache_stall_time
self.reap_stalled(incomplete_stall_time)
self.reap_stalled(stall_time)
def get_all_regular_files(basepath): def get_all_regular_files(basepath):

View File

@ -23,8 +23,6 @@ import logging
import eventlet import eventlet
from glance.common import config
from glance.common import context
from glance.common import exception from glance.common import exception
from glance.image_cache import ImageCache from glance.image_cache import ImageCache
from glance import registry from glance import registry
@ -42,16 +40,15 @@ logger = logging.getLogger(__name__)
class Prefetcher(object): class Prefetcher(object):
def __init__(self, conf): def __init__(self, conf, **local_conf):
self.conf = conf self.conf = conf
glance.store.create_stores(conf) glance.store.create_stores(conf)
self.cache = ImageCache(conf) self.cache = ImageCache(conf)
registry.configure_registry_client(conf) registry.configure_registry_client(conf)
def fetch_image_into_cache(self, image_id): def fetch_image_into_cache(self, image_id):
auth_tok = self.conf.get('admin_token') ctx = registry.get_client_context(self.conf,
ctx = context.RequestContext(is_admin=True, show_deleted=True, is_admin=True, show_deleted=True)
auth_tok=auth_tok)
try: try:
image_meta = registry.get_image_metadata(ctx, image_id) image_meta = registry.get_image_metadata(ctx, image_id)
if image_meta['status'] != 'active': if image_meta['status'] != 'active':

View File

@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
class Pruner(object): class Pruner(object):
def __init__(self, conf): def __init__(self, conf, **local_conf):
self.conf = conf self.conf = conf
self.cache = ImageCache(conf) self.cache = ImageCache(conf)

View File

@ -23,8 +23,6 @@ import logging
import eventlet import eventlet
from glance.common import config
from glance.common import context
from glance.common import exception from glance.common import exception
from glance.image_cache import ImageCache from glance.image_cache import ImageCache
from glance import registry from glance import registry
@ -35,15 +33,14 @@ logger = logging.getLogger(__name__)
class Queuer(object): class Queuer(object):
def __init__(self, conf): def __init__(self, conf, **local_conf):
self.conf = conf self.conf = conf
self.cache = ImageCache(conf) self.cache = ImageCache(conf)
registry.configure_registry_client(conf) registry.configure_registry_client(conf)
def queue_image(self, image_id): def queue_image(self, image_id):
auth_tok = self.conf.get('admin_token') ctx = \
ctx = context.RequestContext(is_admin=True, show_deleted=True, registry.get_client_context(conf, is_admin=True, show_deleted=True)
auth_tok=auth_tok)
try: try:
image_meta = registry.get_image_metadata(ctx, image_id) image_meta = registry.get_image_metadata(ctx, image_id)
if image_meta['status'] != 'active': if image_meta['status'] != 'active':

View File

@ -21,7 +21,7 @@ Registry API
import logging import logging
from glance.common import config from glance.common import cfg
from glance.common import exception from glance.common import exception
from glance.registry import client from glance.registry import client
@ -34,6 +34,25 @@ _CLIENT_KWARGS = {}
_METADATA_ENCRYPTION_KEY = None _METADATA_ENCRYPTION_KEY = None
registry_addr_opts = [
cfg.StrOpt('registry_host', default='0.0.0.0'),
cfg.IntOpt('registry_port', default=9191),
]
registry_client_opts = [
cfg.StrOpt('registry_client_protocol', default='http'),
cfg.StrOpt('registry_client_key_file'),
cfg.StrOpt('registry_client_cert_file'),
cfg.StrOpt('registry_client_ca_file'),
cfg.StrOpt('metadata_encryption_key'),
]
admin_token_opt = cfg.StrOpt('admin_token')
def get_registry_addr(conf):
conf.register_opts(registry_addr_opts)
return (conf.registry_host, conf.registry_port)
def configure_registry_client(conf): def configure_registry_client(conf):
""" """
Sets up a registry client for use in registry lookups Sets up a registry client for use in registry lookups
@ -42,9 +61,8 @@ def configure_registry_client(conf):
""" """
global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT, _METADATA_ENCRYPTION_KEY global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT, _METADATA_ENCRYPTION_KEY
try: try:
host = conf['registry_host'] host, port = get_registry_addr(conf)
port = int(conf['registry_port']) except cfg.ConfigFileValueError:
except (TypeError, ValueError):
msg = _("Configuration option was not valid") msg = _("Configuration option was not valid")
logger.error(msg) logger.error(msg)
raise exception.BadRegistryConnectionConfiguration(msg) raise exception.BadRegistryConnectionConfiguration(msg)
@ -53,18 +71,23 @@ def configure_registry_client(conf):
logger.error(msg) logger.error(msg)
raise exception.BadRegistryConnectionConfiguration(msg) raise exception.BadRegistryConnectionConfiguration(msg)
use_ssl = config.get_option(conf, 'registry_client_protocol', conf.register_opts(registry_client_opts)
default='http').lower() == 'https'
key_file = conf.get('registry_client_key_file')
cert_file = conf.get('registry_client_cert_file')
ca_file = conf.get('registry_client_ca_file')
_METADATA_ENCRYPTION_KEY = conf.get('metadata_encryption_key')
_CLIENT_HOST = host _CLIENT_HOST = host
_CLIENT_PORT = port _CLIENT_PORT = port
_CLIENT_KWARGS = {'use_ssl': use_ssl, _METADATA_ENCRYPTION_KEY = conf.metadata_encryption_key
'key_file': key_file, _CLIENT_KWARGS = {
'cert_file': cert_file, 'use_ssl': conf.registry_client_protocol.lower() == 'https',
'ca_file': ca_file} 'key_file': conf.registry_client_key_file,
'cert_file': conf.registry_client_cert_file,
'ca_file': conf.registry_client_ca_file
}
def get_client_context(conf, **kwargs):
conf.register_opt(admin_token_opt)
from glance.common import context
return context.RequestContext(auth_tok=conf.admin_token, **kwargs)
def get_registry_client(cxt): def get_registry_client(cxt):

View File

@ -25,7 +25,7 @@ from glance.common import wsgi
class API(wsgi.Router): class API(wsgi.Router):
"""WSGI entry point for all Registry requests.""" """WSGI entry point for all Registry requests."""
def __init__(self, conf): def __init__(self, conf, **local_conf):
mapper = routes.Mapper() mapper = routes.Mapper()
images_resource = images.create_resource(conf) images_resource = images.create_resource(conf)

View File

@ -23,6 +23,7 @@ import logging
from webob import exc from webob import exc
from glance.common import cfg
from glance.common import exception from glance.common import exception
from glance.common import utils from glance.common import utils
from glance.common import wsgi from glance.common import wsgi
@ -49,8 +50,14 @@ SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
class Controller(object): class Controller(object):
opts = [
cfg.IntOpt('limit_param_default', default=25),
cfg.IntOpt('api_limit_max', default=1000),
]
def __init__(self, conf): def __init__(self, conf):
self.conf = conf self.conf = conf
self.conf.register_opts(self.opts)
db_api.configure_db(conf) db_api.configure_db(conf)
def _get_images(self, context, **params): def _get_images(self, context, **params):
@ -181,31 +188,15 @@ class Controller(object):
def _get_limit(self, req): def _get_limit(self, req):
"""Parse a limit query param into something usable.""" """Parse a limit query param into something usable."""
try: try:
default = self.conf['limit_param_default'] limit = int(req.str_params.get('limit',
except KeyError: self.conf.limit_param_default))
# if no value is configured, provide a sane default
default = 25
msg = _("Failed to read limit_param_default from config. "
"Defaulting to %s") % default
logger.debug(msg)
try:
limit = int(req.str_params.get('limit', default))
except ValueError: except ValueError:
raise exc.HTTPBadRequest(_("limit param must be an integer")) raise exc.HTTPBadRequest(_("limit param must be an integer"))
if limit < 0: if limit < 0:
raise exc.HTTPBadRequest(_("limit param must be positive")) raise exc.HTTPBadRequest(_("limit param must be positive"))
try: return min(self.conf.api_limit_max, limit)
api_limit_max = int(self.conf['api_limit_max'])
except (KeyError, ValueError):
api_limit_max = 1000
msg = _("Failed to read api_limit_max from config. "
"Defaulting to %s") % api_limit_max
logger.debug(msg)
return min(api_limit_max, limit)
def _get_marker(self, req): def _get_marker(self, req):
"""Parse a marker query param into something usable.""" """Parse a marker query param into something usable."""

View File

@ -17,23 +17,24 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import optparse from glance.common import cfg
def add_conf(parser): def add_options(conf):
""" """
Adds any configuration options that the db layer might have. Adds any configuration options that the db layer might have.
:param parser: An optparse.OptionParser object :param conf: A ConfigOpts object
:retval None :retval None
""" """
help_text = "The following configuration options are specific to the "\ conf.add_group(cfg.OptGroup('registrydb',
"Glance image registry database." title='Registry Database Options',
help='The following configuration options '
group = optparse.OptionGroup(parser, "Registry Database Options", 'are specific to the Glance image '
help_text) 'registry database.'))
group.add_option('--sql-connection', metavar="CONNECTION", conf.register_cli_opt(cfg.StrOpt('sql-connection',
default=None, group='registrydb',
help="A valid SQLAlchemy connection string for the " metavar='CONNECTION',
"registry database. Default: %default") help='A valid SQLAlchemy connection '
parser.add_option_group(group) 'string for the registry database. '
'Default: %default'))

View File

@ -31,7 +31,7 @@ from sqlalchemy.orm import joinedload
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql import or_, and_ from sqlalchemy.sql import or_, and_
from glance.common import config from glance.common import cfg
from glance.common import exception from glance.common import exception
from glance.common import utils from glance.common import utils
from glance.registry.db import models from glance.registry.db import models
@ -57,6 +57,11 @@ DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi',
STATUSES = ['active', 'saving', 'queued', 'killed', 'pending_delete', STATUSES = ['active', 'saving', 'queued', 'killed', 'pending_delete',
'deleted'] 'deleted']
db_opts = [
cfg.IntOpt('sql_idle_timeout', default=3600),
cfg.StrOpt('sql_connection', default='sqlite:///glance.sqlite'),
]
def configure_db(conf): def configure_db(conf):
""" """
@ -67,13 +72,9 @@ def configure_db(conf):
""" """
global _ENGINE, sa_logger, logger global _ENGINE, sa_logger, logger
if not _ENGINE: if not _ENGINE:
debug = config.get_option( conf.register_opts(db_opts)
conf, 'debug', type='bool', default=False) timeout = conf.sql_idle_timeout
verbose = config.get_option( sql_connection = conf.sql_connection
conf, 'verbose', type='bool', default=False)
timeout = config.get_option(
conf, 'sql_idle_timeout', type='int', default=3600)
sql_connection = config.get_option(conf, 'sql_connection')
try: try:
_ENGINE = create_engine(sql_connection, pool_recycle=timeout) _ENGINE = create_engine(sql_connection, pool_recycle=timeout)
except Exception, err: except Exception, err:
@ -84,9 +85,9 @@ def configure_db(conf):
raise raise
sa_logger = logging.getLogger('sqlalchemy.engine') sa_logger = logging.getLogger('sqlalchemy.engine')
if debug: if conf.debug:
sa_logger.setLevel(logging.DEBUG) sa_logger.setLevel(logging.DEBUG)
elif verbose: elif conf.verbose:
sa_logger.setLevel(logging.INFO) sa_logger.setLevel(logging.INFO)
models.register_models(_ENGINE) models.register_models(_ENGINE)

View File

@ -40,7 +40,7 @@ def db_version(conf):
:retval version number :retval version number
""" """
repo_path = get_migrate_repo_path() repo_path = get_migrate_repo_path()
sql_connection = conf['sql_connection'] sql_connection = conf.sql_connection
try: try:
return versioning_api.db_version(sql_connection, repo_path) return versioning_api.db_version(sql_connection, repo_path)
except versioning_exceptions.DatabaseNotControlledError, e: except versioning_exceptions.DatabaseNotControlledError, e:
@ -59,7 +59,7 @@ def upgrade(conf, version=None):
""" """
db_version(conf) # Ensure db is under migration control db_version(conf) # Ensure db is under migration control
repo_path = get_migrate_repo_path() repo_path = get_migrate_repo_path()
sql_connection = conf['sql_connection'] sql_connection = conf.sql_connection
version_str = version or 'latest' version_str = version or 'latest'
logger.info(_("Upgrading %(sql_connection)s to version %(version_str)s") % logger.info(_("Upgrading %(sql_connection)s to version %(version_str)s") %
locals()) locals())
@ -76,7 +76,7 @@ def downgrade(conf, version):
""" """
db_version(conf) # Ensure db is under migration control db_version(conf) # Ensure db is under migration control
repo_path = get_migrate_repo_path() repo_path = get_migrate_repo_path()
sql_connection = conf['sql_connection'] sql_connection = conf.sql_connection
logger.info(_("Downgrading %(sql_connection)s to version %(version)s") % logger.info(_("Downgrading %(sql_connection)s to version %(version)s") %
locals()) locals())
return versioning_api.downgrade(sql_connection, repo_path, version) return versioning_api.downgrade(sql_connection, repo_path, version)
@ -88,7 +88,7 @@ def version_control(conf):
:param conf: conf dict :param conf: conf dict
""" """
sql_connection = conf['sql_connection'] sql_connection = conf.sql_connection
try: try:
_version_control(conf) _version_control(conf)
except versioning_exceptions.DatabaseAlreadyControlledError, e: except versioning_exceptions.DatabaseAlreadyControlledError, e:
@ -104,7 +104,7 @@ def _version_control(conf):
:param conf: conf dict :param conf: conf dict
""" """
repo_path = get_migrate_repo_path() repo_path = get_migrate_repo_path()
sql_connection = conf['sql_connection'] sql_connection = conf.sql_connection
return versioning_api.version_control(sql_connection, repo_path) return versioning_api.version_control(sql_connection, repo_path)

View File

@ -22,7 +22,7 @@ import time
import urlparse import urlparse
from glance import registry from glance import registry
from glance.common import config from glance.common import cfg
from glance.common import exception from glance.common import exception
from glance.common import utils from glance.common import utils
from glance.store import location from glance.store import location
@ -154,13 +154,27 @@ def get_store_from_location(uri):
return loc.store_name return loc.store_name
scrubber_datadir_opt = cfg.StrOpt('scrubber_datadir',
default='/var/lib/glance/scrubber')
def get_scrubber_datadir(conf):
conf.register_opt(scrubber_datadir_opt)
return conf.scrubber_datadir
delete_opts = [
cfg.BoolOpt('delayed_delete', default=False),
cfg.IntOpt('scrub_time', default=0)
]
def schedule_delete_from_backend(uri, conf, context, image_id, **kwargs): def schedule_delete_from_backend(uri, conf, context, image_id, **kwargs):
""" """
Given a uri and a time, schedule the deletion of an image. Given a uri and a time, schedule the deletion of an image.
""" """
use_delay = config.get_option(conf, 'delayed_delete', type='bool', conf.register_opts(delete_opts)
default=False) if not conf.delayed_delete:
if not use_delay:
registry.update_image_metadata(context, image_id, registry.update_image_metadata(context, image_id,
{'status': 'deleted'}) {'status': 'deleted'})
try: try:
@ -169,10 +183,8 @@ def schedule_delete_from_backend(uri, conf, context, image_id, **kwargs):
msg = _("Failed to delete image from store (%(uri)s).") % locals() msg = _("Failed to delete image from store (%(uri)s).") % locals()
logger.error(msg) logger.error(msg)
datadir = config.get_option(conf, 'scrubber_datadir') datadir = get_scrubber_datadir(conf)
scrub_time = config.get_option(conf, 'scrub_time', type='int', delete_time = time.time() + conf.scrub_time
default=0)
delete_time = time.time() + scrub_time
file_path = os.path.join(datadir, str(image_id)) file_path = os.path.join(datadir, str(image_id))
utils.safe_mkdirs(datadir) utils.safe_mkdirs(datadir)

View File

@ -28,13 +28,13 @@ class Store(object):
CHUNKSIZE = (16 * 1024 * 1024) # 16M CHUNKSIZE = (16 * 1024 * 1024) # 16M
def __init__(self, conf=None): def __init__(self, conf):
""" """
Initialize the Store Initialize the Store
:param conf: Optional dictionary of configuration options :param conf: Optional dictionary of configuration options
""" """
self.conf = conf or {} self.conf = conf
self.configure() self.configure()

View File

@ -24,6 +24,7 @@ import logging
import os import os
import urlparse import urlparse
from glance.common import cfg
from glance.common import exception from glance.common import exception
import glance.store import glance.store
import glance.store.base import glance.store.base
@ -93,6 +94,8 @@ class ChunkedFile(object):
class Store(glance.store.base.Store): class Store(glance.store.base.Store):
datadir_opt = cfg.StrOpt('filesystem_store_datadir')
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
@ -100,7 +103,15 @@ class Store(glance.store.base.Store):
this method. If the store was not able to successfully configure this method. If the store was not able to successfully configure
itself, it should raise `exception.BadStoreConfiguration` itself, it should raise `exception.BadStoreConfiguration`
""" """
self.datadir = self._option_get('filesystem_store_datadir') self.conf.register_opt(self.datadir_opt)
self.datadir = self.conf.filesystem_store_datadir
if self.datadir is None:
reason = _("Could not find %s in configuration options.") % \
'filesystem_store_datadir'
logger.error(reason)
raise exception.BadStoreConfiguration(store_name="filesystem",
reason=reason)
if not os.path.exists(self.datadir): if not os.path.exists(self.datadir):
msg = _("Directory to write image files does not exist " msg = _("Directory to write image files does not exist "
@ -114,15 +125,6 @@ class Store(glance.store.base.Store):
raise exception.BadStoreConfiguration(store_name="filesystem", raise exception.BadStoreConfiguration(store_name="filesystem",
reason=reason) reason=reason)
def _option_get(self, param):
result = self.conf.get(param)
if not result:
reason = _("Could not find %s in configuration options.") % param
logger.error(reason)
raise exception.BadStoreConfiguration(store_name="filesystem",
reason=reason)
return result
def get(self, location): def get(self, location):
""" """
Takes a `glance.store.location.Location` object that indicates Takes a `glance.store.location.Location` object that indicates

View File

@ -24,6 +24,7 @@ import hashlib
import logging import logging
import math import math
from glance.common import cfg
from glance.common import exception from glance.common import exception
import glance.store import glance.store
import glance.store.base import glance.store.base
@ -100,6 +101,13 @@ class Store(glance.store.base.Store):
EXAMPLE_URL = "rbd://<IMAGE>" EXAMPLE_URL = "rbd://<IMAGE>"
opts = [
cfg.IntOpt('rbd_store_chunk_size', default=DEFAULT_CHUNKSIZE),
cfg.StrOpt('rbd_store_pool', default=DEFAULT_POOL),
cfg.StrOpt('rbd_store_user', default=DEFAULT_USER),
cfg.StrOpt('rbd_store_ceph_conf', default=DEFAULT_CONFFILE),
]
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
@ -107,20 +115,16 @@ class Store(glance.store.base.Store):
this method. If the store was not able to successfully configure this method. If the store was not able to successfully configure
itself, it should raise `exception.BadStoreConfiguration` itself, it should raise `exception.BadStoreConfiguration`
""" """
self.conf.register_opts(self.opts)
try: try:
self.chunk_size = int( self.chunk_size = self.conf.rbd_store_chunk_size * 1024 * 1024
self.conf.get(
'rbd_store_chunk_size',
DEFAULT_CHUNKSIZE)) * 1024 * 1024
# these must not be unicode since they will be passed to a # these must not be unicode since they will be passed to a
# non-unicode-aware C library # non-unicode-aware C library
self.pool = str(self.conf.get('rbd_store_pool', self.pool = str(self.conf.rbd_store_pool)
DEFAULT_POOL)) self.user = str(self.conf.rbd_store_user)
self.user = str(self.conf.get('rbd_store_user', self.conf_file = str(self.conf.rbd_store_ceph_conf)
DEFAULT_USER)) except cfg.ConfigFileValueError, e:
self.conf_file = str(self.conf.get('rbd_store_ceph_conf',
DEFAULT_CONFFILE))
except Exception, e:
reason = _("Error in store configuration: %s") % e reason = _("Error in store configuration: %s") % e
logger.error(reason) logger.error(reason)
raise exception.BadStoreConfiguration(store_name='rbd', raise exception.BadStoreConfiguration(store_name='rbd',

View File

@ -23,7 +23,7 @@ import httplib
import tempfile import tempfile
import urlparse import urlparse
from glance.common import config from glance.common import cfg
from glance.common import exception from glance.common import exception
import glance.store import glance.store
import glance.store.base import glance.store.base
@ -183,6 +183,15 @@ class Store(glance.store.base.Store):
EXAMPLE_URL = "s3://<ACCESS_KEY>:<SECRET_KEY>@<S3_URL>/<BUCKET>/<OBJ>" EXAMPLE_URL = "s3://<ACCESS_KEY>:<SECRET_KEY>@<S3_URL>/<BUCKET>/<OBJ>"
opts = [
cfg.StrOpt('s3_store_host'),
cfg.StrOpt('s3_store_access_key'),
cfg.StrOpt('s3_store_secret_key'),
cfg.StrOpt('s3_store_bucket'),
cfg.StrOpt('s3_store_object_buffer_dir'),
cfg.BoolOpt('s3_store_create_bucket_on_put', default=False),
]
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
@ -190,6 +199,7 @@ class Store(glance.store.base.Store):
this method. If the store was not able to successfully configure this method. If the store was not able to successfully configure
itself, it should raise `exception.BadStoreConfiguration` itself, it should raise `exception.BadStoreConfiguration`
""" """
self.conf.register_opts(self.opts)
self.s3_host = self._option_get('s3_store_host') self.s3_host = self._option_get('s3_store_host')
access_key = self._option_get('s3_store_access_key') access_key = self._option_get('s3_store_access_key')
secret_key = self._option_get('s3_store_secret_key') secret_key = self._option_get('s3_store_secret_key')
@ -210,14 +220,11 @@ class Store(glance.store.base.Store):
else: # Defaults http else: # Defaults http
self.full_s3_host = 'http://' + self.s3_host self.full_s3_host = 'http://' + self.s3_host
if self.conf.get('s3_store_object_buffer_dir'): self.s3_store_object_buffer_dir = \
self.s3_store_object_buffer_dir = self.conf.get( self.conf.s3_store_object_buffer_dir
's3_store_object_buffer_dir')
else:
self.s3_store_object_buffer_dir = None
def _option_get(self, param): def _option_get(self, param):
result = self.conf.get(param) result = getattr(self.conf, param)
if not result: if not result:
reason = _("Could not find %(param)s in configuration " reason = _("Could not find %(param)s in configuration "
"options.") % locals() "options.") % locals()
@ -417,10 +424,7 @@ def create_bucket_if_missing(bucket, s3_conn, conf):
s3_conn.get_bucket(bucket) s3_conn.get_bucket(bucket)
except S3ResponseError, e: except S3ResponseError, e:
if e.status == httplib.NOT_FOUND: if e.status == httplib.NOT_FOUND:
add_bucket = config.get_option(conf, if conf.s3_store_create_bucket_on_put:
's3_store_create_bucket_on_put',
type='bool', default=False)
if add_bucket:
try: try:
s3_conn.create_bucket(bucket) s3_conn.create_bucket(bucket)
except S3ResponseError, e: except S3ResponseError, e:

View File

@ -27,7 +27,7 @@ import glance.store.s3
import glance.store.swift import glance.store.swift
from glance import registry from glance import registry
from glance import store from glance import store
from glance.common import config from glance.common import cfg
from glance.common import utils from glance.common import utils
from glance.common import exception from glance.common import exception
from glance.registry import context from glance.registry import context
@ -65,22 +65,30 @@ class Daemon(object):
class Scrubber(object): class Scrubber(object):
CLEANUP_FILE = ".cleanup" CLEANUP_FILE = ".cleanup"
def __init__(self, conf): opts = [
logger.info(_("Initializing scrubber with conf: %s") % conf) cfg.BoolOpt('cleanup_scrubber', default=False),
cfg.IntOpt('cleanup_scrubber_time', default=86400)
]
def __init__(self, conf, **local_conf):
self.conf = conf self.conf = conf
self.datadir = config.get_option(conf, 'scrubber_datadir') self.conf.register_opts(self.opts)
self.cleanup = config.get_option(conf, 'cleanup_scrubber',
type='bool', default=False) self.datadir = store.get_scrubber_datadir(conf)
host = config.get_option(conf, 'registry_host') self.cleanup = self.conf.cleanup_scrubber
port = config.get_option(conf, 'registry_port', type='int') self.cleanup_time = self.conf.cleanup_scrubber_time
host, port = registry.get_registry_addr(conf)
logger.info(_("Initializing scrubber with conf: %s") %
{'datadir': self.datadir, 'cleanup': self.cleanup,
'cleanup_time': self.cleanup_time,
'registry_host': host, 'registry_port': port})
self.registry = client.RegistryClient(host, port) self.registry = client.RegistryClient(host, port)
utils.safe_mkdirs(self.datadir) utils.safe_mkdirs(self.datadir)
if self.cleanup:
self.cleanup_time = config.get_option(conf,
'cleanup_scrubber_time',
type='int', default=86400)
store.create_stores(conf) store.create_stores(conf)
def run(self, pool, event=None): def run(self, pool, event=None):

View File

@ -26,7 +26,7 @@ import math
import tempfile import tempfile
import urlparse import urlparse
from glance.common import config from glance.common import cfg
from glance.common import exception from glance.common import exception
import glance.store import glance.store
import glance.store.base import glance.store.base
@ -38,8 +38,8 @@ except ImportError:
pass pass
DEFAULT_CONTAINER = 'glance' DEFAULT_CONTAINER = 'glance'
DEFAULT_LARGE_OBJECT_SIZE = 5 * 1024 * 1024 * 1024 # 5GB DEFAULT_LARGE_OBJECT_SIZE = 5 * 1024 # 5GB
DEFAULT_LARGE_OBJECT_CHUNK_SIZE = 200 * 1024 * 1024 # 200M DEFAULT_LARGE_OBJECT_CHUNK_SIZE = 200 # 200M
logger = logging.getLogger('glance.store.swift') logger = logging.getLogger('glance.store.swift')
@ -185,9 +185,24 @@ class Store(glance.store.base.Store):
CHUNKSIZE = 65536 CHUNKSIZE = 65536
opts = [
cfg.BoolOpt('swift_enable_snet', default=False),
cfg.StrOpt('swift_store_auth_address'),
cfg.StrOpt('swift_store_user'),
cfg.StrOpt('swift_store_key'),
cfg.StrOpt('swift_store_container',
default=DEFAULT_CONTAINER),
cfg.IntOpt('swift_store_large_object_size',
default=DEFAULT_LARGE_OBJECT_SIZE),
cfg.IntOpt('swift_store_large_object_chunk_size',
default=DEFAULT_LARGE_OBJECT_CHUNK_SIZE),
cfg.StrOpt('swift_store_object_buffer_dir'),
cfg.BoolOpt('swift_store_create_container_on_put', default=False),
]
def configure(self): def configure(self):
self.snet = config.get_option( self.conf.register_opts(self.opts)
self.conf, 'swift_enable_snet', type='bool', default=False) self.snet = self.conf.swift_enable_snet
def configure_add(self): def configure_add(self):
""" """
@ -199,29 +214,14 @@ class Store(glance.store.base.Store):
self.auth_address = self._option_get('swift_store_auth_address') self.auth_address = self._option_get('swift_store_auth_address')
self.user = self._option_get('swift_store_user') self.user = self._option_get('swift_store_user')
self.key = self._option_get('swift_store_key') self.key = self._option_get('swift_store_key')
self.container = self.conf.get('swift_store_container', self.container = self.conf.swift_store_container
DEFAULT_CONTAINER)
try: try:
if self.conf.get('swift_store_large_object_size'): self.large_object_size = self.conf.swift_store_large_object_size
self.large_object_size = int( self.large_object_chunk_size = \
self.conf.get('swift_store_large_object_size') self.conf.swift_store_large_object_chunk_size
) * (1024 * 1024) # Size specified in MB in conf files self.swift_store_object_buffer_dir = \
else: self.conf.swift_store_object_buffer_dir
self.large_object_size = DEFAULT_LARGE_OBJECT_SIZE except cfg.ConfigFileValueError, e:
if self.conf.get('swift_store_large_object_chunk_size'):
self.large_object_chunk_size = int(
self.conf.get('swift_store_large_object_chunk_size')
) * (1024 * 1024) # Size specified in MB in conf files
else:
self.large_object_chunk_size = DEFAULT_LARGE_OBJECT_CHUNK_SIZE
if self.conf.get('swift_store_object_buffer_dir'):
self.swift_store_object_buffer_dir = (
self.conf.get('swift_store_object_buffer_dir'))
else:
self.swift_store_object_buffer_dir = None
except Exception, e:
reason = _("Error in configuration conf: %s") % e reason = _("Error in configuration conf: %s") % e
logger.error(reason) logger.error(reason)
raise exception.BadStoreConfiguration(store_name="swift", raise exception.BadStoreConfiguration(store_name="swift",
@ -283,7 +283,7 @@ class Store(glance.store.base.Store):
authurl=auth_url, user=user, key=key, snet=snet) authurl=auth_url, user=user, key=key, snet=snet)
def _option_get(self, param): def _option_get(self, param):
result = self.conf.get(param) result = getattr(self.conf, param)
if not result: if not result:
reason = (_("Could not find %(param)s in configuration " reason = (_("Could not find %(param)s in configuration "
"options.") % locals()) "options.") % locals())
@ -495,10 +495,7 @@ def create_container_if_missing(container, swift_conn, conf):
swift_conn.head_container(container) swift_conn.head_container(container)
except swift_client.ClientException, e: except swift_client.ClientException, e:
if e.http_status == httplib.NOT_FOUND: if e.http_status == httplib.NOT_FOUND:
add_container = config.get_option(conf, if conf.swift_store_create_container_on_put:
'swift_store_create_container_on_put',
type='bool', default=False)
if add_container:
try: try:
swift_conn.put_container(container) swift_conn.put_container(container)
except ClientException, e: except ClientException, e:

View File

@ -226,7 +226,8 @@ glance.app_factory = glance.image_cache.queue_image:Queuer
""" % cache_file_options) """ % cache_file_options)
cache_file.flush() cache_file.flush()
cmd = "bin/glance-cache-prefetcher %s" % cache_config_filepath cmd = "bin/glance-cache-prefetcher --config-file %s" % \
cache_config_filepath
exitcode, out, err = execute(cmd) exitcode, out, err = execute(cmd)

View File

@ -374,7 +374,8 @@ glance.app_factory = glance.image_cache.queue_image:Queuer
self.verify_no_cached_images() self.verify_no_cached_images()
cmd = "bin/glance-cache-prefetcher %s" % cache_config_filepath cmd = "bin/glance-cache-prefetcher --config-file %s" % \
cache_config_filepath
exitcode, out, err = execute(cmd) exitcode, out, err = execute(cmd)

View File

@ -86,8 +86,9 @@ class TestClientRedirects(functional.FunctionalTest):
self.port_two = utils.get_unused_port() self.port_two = utils.get_unused_port()
server_one = wsgi.Server() server_one = wsgi.Server()
server_two = wsgi.Server() server_two = wsgi.Server()
server_one.start(RedirectTestApp("one"), self.port_one, "127.0.0.1") conf = utils.TestConfigOpts({'bind_host': '127.0.0.1'})
server_two.start(RedirectTestApp("two"), self.port_two, "127.0.0.1") server_one.start(RedirectTestApp("one"), conf, self.port_one)
server_two.start(RedirectTestApp("two"), conf, self.port_two)
self.client = client.BaseClient("127.0.0.1", self.port_one) self.client = client.BaseClient("127.0.0.1", self.port_one)
def test_get_without_redirect(self): def test_get_without_redirect(self):

View File

@ -28,6 +28,7 @@ import glance.common.client
from glance.common import context from glance.common import context
from glance.common import exception from glance.common import exception
from glance.registry.api import v1 as rserver from glance.registry.api import v1 as rserver
from glance.tests import utils
FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests') FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests')
@ -97,9 +98,13 @@ def stub_out_registry_and_store_server(stubs):
sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', sql_connection = os.environ.get('GLANCE_SQL_CONNECTION',
"sqlite://") "sqlite://")
context_class = 'glance.registry.context.RequestContext' context_class = 'glance.registry.context.RequestContext'
conf = {'sql_connection': sql_connection, 'verbose': VERBOSE, conf = utils.TestConfigOpts({
'debug': DEBUG, 'context_class': context_class} 'sql_connection': sql_connection,
api = context.ContextMiddleware(rserver.API(conf), conf) 'verbose': VERBOSE,
'debug': DEBUG
})
api = context.ContextMiddleware(rserver.API(conf),
conf, context_class=context_class)
res = self.req.get_response(api) res = self.req.get_response(api)
# httplib.Response has a read() method...fake it out # httplib.Response has a read() method...fake it out
@ -145,14 +150,16 @@ def stub_out_registry_and_store_server(stubs):
self.req.body = body self.req.body = body
def getresponse(self): def getresponse(self):
conf = {'verbose': VERBOSE, conf = utils.TestConfigOpts({
'verbose': VERBOSE,
'debug': DEBUG, 'debug': DEBUG,
'bind_host': '0.0.0.0', 'bind_host': '0.0.0.0',
'bind_port': '9999999', 'bind_port': '9999999',
'registry_host': '0.0.0.0', 'registry_host': '0.0.0.0',
'registry_port': '9191', 'registry_port': '9191',
'default_store': 'file', 'default_store': 'file',
'filesystem_store_datadir': FAKE_FILESYSTEM_ROOTDIR} 'filesystem_store_datadir': FAKE_FILESYSTEM_ROOTDIR
})
api = version_negotiation.VersionNegotiationFilter( api = version_negotiation.VersionNegotiationFilter(
context.ContextMiddleware(router.API(conf), conf), context.ContextMiddleware(router.API(conf), conf),
conf) conf)
@ -218,9 +225,13 @@ 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:///")
context_class = 'glance.registry.context.RequestContext' context_class = 'glance.registry.context.RequestContext'
conf = {'sql_connection': sql_connection, 'verbose': VERBOSE, conf = utils.TestConfigOpts({
'debug': DEBUG, 'context_class': context_class} 'sql_connection': sql_connection,
api = context.ContextMiddleware(rserver.API(conf), conf) 'verbose': VERBOSE,
'debug': DEBUG
})
api = context.ContextMiddleware(rserver.API(conf),
conf, context_class=context_class)
res = self.req.get_response(api) res = self.req.get_response(api)
# httplib.Response has a read() method...fake it out # httplib.Response has a read() method...fake it out

View File

@ -34,6 +34,7 @@ from glance.registry.api import v1 as rserver
from glance.registry.db import api as db_api from glance.registry.db import api as db_api
from glance.registry.db import models as db_models from glance.registry.db import models as db_models
from glance.tests import stubs from glance.tests import stubs
from glance.tests import utils as test_utils
_gen_uuid = utils.generate_uuid _gen_uuid = utils.generate_uuid
@ -48,8 +49,7 @@ CONF = {'sql_connection': 'sqlite://',
'registry_host': '0.0.0.0', 'registry_host': '0.0.0.0',
'registry_port': '9191', 'registry_port': '9191',
'default_store': 'file', 'default_store': 'file',
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR, 'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
'context_class': 'glance.registry.context.RequestContext'}
class TestRegistryDb(unittest.TestCase): class TestRegistryDb(unittest.TestCase):
@ -64,9 +64,11 @@ class TestRegistryDb(unittest.TestCase):
API controller results in a) an Exception being thrown and b) API controller results in a) an Exception being thrown and b)
a message being logged to the registry log file... a message being logged to the registry log file...
""" """
bad_conf = {'verbose': True, bad_conf = test_utils.TestConfigOpts({
'verbose': True,
'debug': True, 'debug': True,
'sql_connection': 'baddriver:///'} 'sql_connection': 'baddriver:///'
})
# We set this to None to trigger a reconfigure, otherwise # We set this to None to trigger a reconfigure, otherwise
# other modules may have already correctly configured the DB # other modules may have already correctly configured the DB
orig_engine = db_api._ENGINE orig_engine = db_api._ENGINE
@ -101,7 +103,10 @@ class TestRegistryAPI(unittest.TestCase):
self.stubs = stubout.StubOutForTesting() self.stubs = stubout.StubOutForTesting()
stubs.stub_out_registry_and_store_server(self.stubs) stubs.stub_out_registry_and_store_server(self.stubs)
stubs.stub_out_filesystem_backend() stubs.stub_out_filesystem_backend()
self.api = context.ContextMiddleware(rserver.API(CONF), CONF) conf = test_utils.TestConfigOpts(CONF)
context_class = 'glance.registry.context.RequestContext'
self.api = context.ContextMiddleware(rserver.API(conf),
conf, context_class=context_class)
self.FIXTURES = [ self.FIXTURES = [
{'id': UUID1, {'id': UUID1,
'name': 'fake image #1', 'name': 'fake image #1',
@ -136,7 +141,7 @@ class TestRegistryAPI(unittest.TestCase):
'location': "file:///tmp/glance-tests/2", 'location': "file:///tmp/glance-tests/2",
'properties': {}}] 'properties': {}}]
self.context = rcontext.RequestContext(is_admin=True) self.context = rcontext.RequestContext(is_admin=True)
db_api.configure_db(CONF) db_api.configure_db(conf)
self.destroy_fixtures() self.destroy_fixtures()
self.create_fixtures() self.create_fixtures()
@ -1935,7 +1940,8 @@ class TestGlanceAPI(unittest.TestCase):
stubs.stub_out_registry_and_store_server(self.stubs) stubs.stub_out_registry_and_store_server(self.stubs)
stubs.stub_out_filesystem_backend() stubs.stub_out_filesystem_backend()
sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', "sqlite://") sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', "sqlite://")
self.api = context.ContextMiddleware(router.API(CONF), CONF) conf = test_utils.TestConfigOpts(CONF)
self.api = context.ContextMiddleware(router.API(conf), conf)
self.FIXTURES = [ self.FIXTURES = [
{'id': UUID1, {'id': UUID1,
'name': 'fake image #1', 'name': 'fake image #1',
@ -1966,7 +1972,7 @@ class TestGlanceAPI(unittest.TestCase):
'location': "file:///tmp/glance-tests/2", 'location': "file:///tmp/glance-tests/2",
'properties': {}}] 'properties': {}}]
self.context = rcontext.RequestContext(is_admin=True) self.context = rcontext.RequestContext(is_admin=True)
db_api.configure_db(CONF) db_api.configure_db(conf)
self.destroy_fixtures() self.destroy_fixtures()
self.create_fixtures() self.create_fixtures()

View File

@ -34,6 +34,7 @@ from glance.registry.db import models as db_models
from glance.registry import client as rclient from glance.registry import client as rclient
from glance.registry import context as rcontext from glance.registry import context as rcontext
from glance.tests import stubs from glance.tests import stubs
from glance.tests import utils as test_utils
CONF = {'sql_connection': 'sqlite://'} CONF = {'sql_connection': 'sqlite://'}
@ -138,7 +139,8 @@ class TestRegistryClient(unittest.TestCase):
"""Establish a clean test environment""" """Establish a clean test environment"""
self.stubs = stubout.StubOutForTesting() self.stubs = stubout.StubOutForTesting()
stubs.stub_out_registry_and_store_server(self.stubs) stubs.stub_out_registry_and_store_server(self.stubs)
db_api.configure_db(CONF) conf = test_utils.TestConfigOpts(CONF)
db_api.configure_db(conf)
self.context = rcontext.RequestContext(is_admin=True) self.context = rcontext.RequestContext(is_admin=True)
self.FIXTURES = [ self.FIXTURES = [
{'id': UUID1, {'id': UUID1,
@ -1138,7 +1140,8 @@ class TestClient(unittest.TestCase):
self.stubs = stubout.StubOutForTesting() self.stubs = stubout.StubOutForTesting()
stubs.stub_out_registry_and_store_server(self.stubs) stubs.stub_out_registry_and_store_server(self.stubs)
stubs.stub_out_filesystem_backend() stubs.stub_out_filesystem_backend()
db_api.configure_db(CONF) conf = test_utils.TestConfigOpts(CONF)
db_api.configure_db(conf)
self.client = client.Client("0.0.0.0") self.client = client.Client("0.0.0.0")
self.FIXTURES = [ self.FIXTURES = [
{'id': UUID1, {'id': UUID1,

View File

@ -16,8 +16,6 @@
# under the License. # under the License.
import os.path import os.path
import optparse
import tempfile
import unittest import unittest
import stubout import stubout
@ -26,125 +24,9 @@ from glance.api.middleware import version_negotiation
from glance.api.v1 import images from glance.api.v1 import images
from glance.api.v1 import members from glance.api.v1 import members
from glance.common import config from glance.common import config
from glance.common import wsgi
from glance.image_cache import pruner from glance.image_cache import pruner
class TestOptionParsing(unittest.TestCase):
def test_common_options(self):
parser = optparse.OptionParser()
self.assertEquals(0, len(parser.option_groups))
config.add_common_options(parser)
self.assertEquals(1, len(parser.option_groups))
expected_options = ['--verbose', '--debug', '--config-file']
for e in expected_options:
self.assertTrue(parser.option_groups[0].get_option(e),
"Missing required common option: %s" % e)
def test_parse_options(self):
# test empty args and that parse_options() returns a mapping
# of typed values
parser = optparse.OptionParser()
config.add_common_options(parser)
parsed_conf, args = config.parse_options(parser, [])
expected_conf = {'verbose': False, 'debug': False,
'config_file': None}
self.assertEquals(expected_conf, parsed_conf)
# test non-empty args and that parse_options() returns a mapping
# of typed values matching supplied args
parser = optparse.OptionParser()
config.add_common_options(parser)
parsed_conf, args = config.parse_options(parser, ['--verbose'])
expected_conf = {'verbose': True, 'debug': False,
'config_file': None}
self.assertEquals(expected_conf, parsed_conf)
# test non-empty args that contain unknown options raises
# a SystemExit exception. Not ideal, but unfortunately optparse
# raises a sys.exit() when it runs into an error instead of raising
# something a bit more useful for libraries. optparse must have been
# written by the same group that wrote unittest ;)
parser = optparse.OptionParser()
config.add_common_options(parser)
self.assertRaises(SystemExit, config.parse_options,
parser, ['--unknown'])
class TestConfigFiles(unittest.TestCase):
def setUp(self):
self.stubs = stubout.StubOutForTesting()
def tearDown(self):
self.stubs.UnsetAll()
def test_config_file_default(self):
expected_path = '/etc/glance/glance-api.conf'
self.stubs.Set(os.path, 'exists', lambda p: p == expected_path)
path = config.find_config_file('glance-api', {}, [])
self.assertEquals(expected_path, path)
def test_config_file_option(self):
expected_path = '/etc/glance/my-glance-api.conf'
self.stubs.Set(os.path, 'exists', lambda p: p == expected_path)
path = config.find_config_file('glance-api',
{'config_file': expected_path}, [])
self.assertEquals(expected_path, path)
def test_config_file_arg(self):
expected_path = '/etc/glance/my-glance-api.conf'
self.stubs.Set(os.path, 'exists', lambda p: p == expected_path)
path = config.find_config_file('glance-api', {}, [expected_path])
self.assertEquals(expected_path, path)
def test_config_file_tilde_arg(self):
supplied_path = '~/my-glance-api.conf'
expected_path = '/tmp/my-glance-api.conf'
def fake_expanduser(p):
if p[0] == '~':
p = '/tmp' + p[1:]
return p
self.stubs.Set(os.path, 'expanduser', fake_expanduser)
self.stubs.Set(os.path, 'exists', lambda p: p == supplied_path)
path = config.find_config_file('glance-api', {}, [supplied_path])
self.assertEquals(expected_path, path)
def test_config_file_not_found(self):
self.stubs.Set(os.path, 'exists', lambda p: False)
self.assertRaises(RuntimeError,
config.find_config_file,
'glance-foo', {}, [])
class TestPasteConfig(unittest.TestCase):
def test_load_paste_config(self):
path = os.path.join(os.getcwd(), 'etc/glance-api.conf')
conf = config.load_paste_config(path, 'glance-api')
self.assertEquals('file', conf['default_store'])
class TestPasteApp(unittest.TestCase): class TestPasteApp(unittest.TestCase):
def setUp(self): def setUp(self):
@ -154,15 +36,16 @@ class TestPasteApp(unittest.TestCase):
self.stubs.UnsetAll() self.stubs.UnsetAll()
def test_load_paste_app(self): def test_load_paste_app(self):
path = os.path.join(os.getcwd(), 'etc/glance-api.conf') conf = config.GlanceConfigOpts()
conf(['--config-file',
os.path.join(os.getcwd(), 'etc/glance-api.conf')])
self.stubs.Set(config, 'setup_logging', lambda *a: None) self.stubs.Set(config, 'setup_logging', lambda *a: None)
self.stubs.Set(images, 'create_resource', lambda *a: None) self.stubs.Set(images, 'create_resource', lambda *a: None)
self.stubs.Set(members, 'create_resource', lambda *a: None) self.stubs.Set(members, 'create_resource', lambda *a: None)
conf, app = config.load_paste_app('glance-api', {}, [path]) app = config.load_paste_app(conf, 'glance-api')
self.assertEquals('file', conf['default_store'])
self.assertEquals(version_negotiation.VersionNegotiationFilter, self.assertEquals(version_negotiation.VersionNegotiationFilter,
type(app)) type(app))
@ -178,11 +61,12 @@ class TestPasteApp(unittest.TestCase):
orig_join = os.path.join orig_join = os.path.join
self.stubs.Set(os.path, 'join', fake_join) self.stubs.Set(os.path, 'join', fake_join)
conf = config.GlanceCacheConfigOpts()
conf([])
self.stubs.Set(config, 'setup_logging', lambda *a: None) self.stubs.Set(config, 'setup_logging', lambda *a: None)
self.stubs.Set(wsgi, 'app_factory', lambda *a, **kw: 'pruner') self.stubs.Set(pruner, 'Pruner', lambda conf, **lc: 'pruner')
conf, app = config.load_paste_app('glance-pruner', {}, [], app = config.load_paste_app(conf, 'glance-pruner')
'glance-cache')
self.assertEquals('86400', conf['image_cache_stall_time'])
self.assertEquals('pruner', app) self.assertEquals('pruner', app)

View File

@ -28,6 +28,7 @@ from glance.common import utils
from glance.store.location import get_location_from_uri from glance.store.location import get_location_from_uri
from glance.store.filesystem import Store, ChunkedFile from glance.store.filesystem import Store, ChunkedFile
from glance.tests import stubs from glance.tests import stubs
from glance.tests import utils as test_utils
FILESYSTEM_CONF = { FILESYSTEM_CONF = {
'verbose': True, 'verbose': True,
@ -43,7 +44,7 @@ class TestStore(unittest.TestCase):
stubs.stub_out_filesystem_backend() stubs.stub_out_filesystem_backend()
self.orig_chunksize = ChunkedFile.CHUNKSIZE self.orig_chunksize = ChunkedFile.CHUNKSIZE
ChunkedFile.CHUNKSIZE = 10 ChunkedFile.CHUNKSIZE = 10
self.store = Store(FILESYSTEM_CONF) self.store = Store(test_utils.TestConfigOpts(FILESYSTEM_CONF))
def tearDown(self): def tearDown(self):
"""Clear the test environment""" """Clear the test environment"""

View File

@ -24,6 +24,7 @@ from glance.common import exception
from glance.store import create_stores, delete_from_backend from glance.store import create_stores, 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 import utils
def stub_out_http_backend(stubs): def stub_out_http_backend(stubs):
@ -104,6 +105,6 @@ class TestHttpStore(unittest.TestCase):
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({}) create_stores(utils.TestConfigOpts({}))
self.assertRaises(exception.StoreDeleteNotSupported, self.assertRaises(exception.StoreDeleteNotSupported,
delete_from_backend, uri) delete_from_backend, uri)

View File

@ -26,6 +26,7 @@ import stubout
from glance import image_cache from glance import image_cache
from glance.common import exception from glance.common import exception
from glance.common import utils from glance.common import utils
from glance.tests import utils as test_utils
from glance.tests.utils import skip_if_disabled, xattr_writes_supported from glance.tests.utils import skip_if_disabled, xattr_writes_supported
FIXTURE_DATA = '*' * 1024 FIXTURE_DATA = '*' * 1024
@ -136,8 +137,7 @@ class ImageCacheTestCase(object):
self.assertTrue(os.path.exists(incomplete_file_path)) self.assertTrue(os.path.exists(incomplete_file_path))
self.cache.conf['image_cache_stall_time'] = 0 self.cache.clean(stall_time=0)
self.cache.clean()
self.assertFalse(os.path.exists(incomplete_file_path)) self.assertFalse(os.path.exists(incomplete_file_path))
@ -250,11 +250,12 @@ class TestImageCacheXattr(unittest.TestCase,
self.inited = True self.inited = True
self.disabled = False self.disabled = False
self.conf = {'image_cache_dir': self.cache_dir, self.conf = test_utils.TestConfigOpts({
'image_cache_dir': self.cache_dir,
'image_cache_driver': 'xattr', 'image_cache_driver': 'xattr',
'image_cache_max_size': 1024 * 5, 'image_cache_max_size': 1024 * 5,
'registry_host': '0.0.0.0', 'registry_host': '0.0.0.0',
'registry_port': 9191} 'registry_port': 9191})
self.cache = image_cache.ImageCache(self.conf) self.cache = image_cache.ImageCache(self.conf)
if not xattr_writes_supported(self.cache_dir): if not xattr_writes_supported(self.cache_dir):
@ -294,11 +295,12 @@ class TestImageCacheSqlite(unittest.TestCase,
self.disabled = False self.disabled = False
self.cache_dir = os.path.join("/", "tmp", "test.cache.%d" % self.cache_dir = os.path.join("/", "tmp", "test.cache.%d" %
random.randint(0, 1000000)) random.randint(0, 1000000))
self.conf = {'image_cache_dir': self.cache_dir, self.conf = test_utils.TestConfigOpts({
'image_cache_dir': self.cache_dir,
'image_cache_driver': 'sqlite', 'image_cache_driver': 'sqlite',
'image_cache_max_size': 1024 * 5, 'image_cache_max_size': 1024 * 5,
'registry_host': '0.0.0.0', 'registry_host': '0.0.0.0',
'registry_port': 9191} 'registry_port': 9191})
self.cache = image_cache.ImageCache(self.conf) self.cache = image_cache.ImageCache(self.conf)
def tearDown(self): def tearDown(self):

View File

@ -34,9 +34,10 @@ from migrate.versioning.repository import Repository
from sqlalchemy import * from sqlalchemy import *
from sqlalchemy.pool import NullPool from sqlalchemy.pool import NullPool
from glance.common import cfg
from glance.common import exception from glance.common import exception
import glance.registry.db.migration as migration_api import glance.registry.db.migration as migration_api
from glance.tests.utils import execute from glance.tests import utils
class TestMigrations(unittest.TestCase): class TestMigrations(unittest.TestCase):
@ -115,7 +116,7 @@ class TestMigrations(unittest.TestCase):
"create database %(database)s;") % locals() "create database %(database)s;") % locals()
cmd = ("mysql -u%(user)s %(password)s -h%(host)s " cmd = ("mysql -u%(user)s %(password)s -h%(host)s "
"-e\"%(sql)s\"") % locals() "-e\"%(sql)s\"") % locals()
exitcode, out, err = execute(cmd) exitcode, out, err = utils.execute(cmd)
self.assertEqual(0, exitcode) self.assertEqual(0, exitcode)
def test_walk_versions(self): def test_walk_versions(self):
@ -124,7 +125,9 @@ class TestMigrations(unittest.TestCase):
that there are no errors in the version scripts for each engine that there are no errors in the version scripts for each engine
""" """
for key, engine in self.engines.items(): for key, engine in self.engines.items():
conf = {'sql_connection': TestMigrations.TEST_DATABASES[key]} conf = utils.TestConfigOpts({
'sql_connection': TestMigrations.TEST_DATABASES[key]})
conf.register_opt(cfg.StrOpt('sql_connection'))
self._walk_versions(conf) self._walk_versions(conf)
def _walk_versions(self, conf): def _walk_versions(self, conf):
@ -165,7 +168,9 @@ class TestMigrations(unittest.TestCase):
the image_properties table back into the base image table. the image_properties table back into the base image table.
""" """
for key, engine in self.engines.items(): for key, engine in self.engines.items():
conf = {'sql_connection': TestMigrations.TEST_DATABASES[key]} conf = utils.TestConfigOpts({
'sql_connection': TestMigrations.TEST_DATABASES[key]})
conf.register_opt(cfg.StrOpt('sql_connection'))
self._no_data_loss_2_to_3_to_2(engine, conf) self._no_data_loss_2_to_3_to_2(engine, conf)
def _no_data_loss_2_to_3_to_2(self, engine, conf): def _no_data_loss_2_to_3_to_2(self, engine, conf):

View File

@ -111,9 +111,9 @@ class UtilsTestCase(unittest.TestCase):
# Try importing an object by supplying a class and # Try importing an object by supplying a class and
# verify the object's class name is the same as that supplied # verify the object's class name is the same as that supplied
store_obj = utils.import_object('glance.store.s3.Store') ex_obj = utils.import_object('glance.common.exception.GlanceException')
self.assertTrue(store_obj.__class__.__name__ == 'Store') self.assertTrue(ex_obj.__class__.__name__ == 'GlanceException')
# Try importing a module itself # Try importing a module itself
module_obj = utils.import_object('glance.registry') module_obj = utils.import_object('glance.registry')

View File

@ -20,13 +20,14 @@ import unittest
from glance.common import exception from glance.common import exception
from glance.common import notifier from glance.common import notifier
from glance.tests import utils
class TestInvalidNotifier(unittest.TestCase): class TestInvalidNotifier(unittest.TestCase):
"""Test that notifications are generated appropriately""" """Test that notifications are generated appropriately"""
def test_cannot_create(self): def test_cannot_create(self):
conf = {"notifier_strategy": "invalid_notifier"} conf = utils.TestConfigOpts({"notifier_strategy": "invalid_notifier"})
self.assertRaises(exception.InvalidNotifierStrategy, self.assertRaises(exception.InvalidNotifierStrategy,
notifier.Notifier, notifier.Notifier,
conf) conf)
@ -36,7 +37,7 @@ class TestLoggingNotifier(unittest.TestCase):
"""Test the logging notifier is selected and works properly.""" """Test the logging notifier is selected and works properly."""
def setUp(self): def setUp(self):
conf = {"notifier_strategy": "logging"} conf = utils.TestConfigOpts({"notifier_strategy": "logging"})
self.called = False self.called = False
self.logger = logging.getLogger("glance.notifier.logging_notifier") self.logger = logging.getLogger("glance.notifier.logging_notifier")
self.notifier = notifier.Notifier(conf) self.notifier = notifier.Notifier(conf)
@ -67,7 +68,7 @@ class TestNoopNotifier(unittest.TestCase):
"""Test that the noop notifier works...and does nothing?""" """Test that the noop notifier works...and does nothing?"""
def setUp(self): def setUp(self):
conf = {"notifier_strategy": "noop"} conf = utils.TestConfigOpts({"notifier_strategy": "noop"})
self.notifier = notifier.Notifier(conf) self.notifier = notifier.Notifier(conf)
def test_warn(self): def test_warn(self):
@ -86,7 +87,7 @@ class TestRabbitNotifier(unittest.TestCase):
def setUp(self): def setUp(self):
notifier.RabbitStrategy._send_message = self._send_message notifier.RabbitStrategy._send_message = self._send_message
self.called = False self.called = False
conf = {"notifier_strategy": "rabbit"} conf = utils.TestConfigOpts({"notifier_strategy": "rabbit"})
self.notifier = notifier.Notifier(conf) self.notifier = notifier.Notifier(conf)
def _send_message(self, message, priority): def _send_message(self, message, priority):

View File

@ -32,6 +32,7 @@ from glance.common import utils
from glance.store import BackendException, UnsupportedBackend from glance.store import BackendException, 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 from glance.store.s3 import Store
from glance.tests import utils as test_utils
FAKE_UUID = utils.generate_uuid() FAKE_UUID = utils.generate_uuid()
@ -163,7 +164,7 @@ class TestStore(unittest.TestCase):
"""Establish a clean test environment""" """Establish a clean test environment"""
self.stubs = stubout.StubOutForTesting() self.stubs = stubout.StubOutForTesting()
stub_out_s3(self.stubs) stub_out_s3(self.stubs)
self.store = Store(S3_CONF) self.store = Store(test_utils.TestConfigOpts(S3_CONF))
def tearDown(self): def tearDown(self):
"""Clear the test environment""" """Clear the test environment"""
@ -260,7 +261,7 @@ class TestStore(unittest.TestCase):
expected_image_id) expected_image_id)
image_s3 = StringIO.StringIO(expected_s3_contents) image_s3 = StringIO.StringIO(expected_s3_contents)
self.store = Store(new_conf) self.store = Store(test_utils.TestConfigOpts(new_conf))
location, size, checksum = self.store.add(expected_image_id, location, size, checksum = self.store.add(expected_image_id,
image_s3, image_s3,
expected_s3_size) expected_s3_size)
@ -292,7 +293,7 @@ class TestStore(unittest.TestCase):
del conf[key] del conf[key]
try: try:
self.store = Store(conf) self.store = Store(test_utils.TestConfigOpts(conf))
return self.store.add == self.store.add_disabled return self.store.add == self.store.add_disabled
except: except:
return False return False

View File

@ -24,8 +24,9 @@ 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 import utils
glance.store.create_stores({}) glance.store.create_stores(utils.TestConfigOpts({}))
class TestStoreLocation(unittest.TestCase): class TestStoreLocation(unittest.TestCase):

View File

@ -30,6 +30,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 import utils as test_utils
FAKE_UUID = utils.generate_uuid FAKE_UUID = utils.generate_uuid
@ -182,7 +183,7 @@ class TestStore(unittest.TestCase):
"""Establish a clean test environment""" """Establish a clean test environment"""
self.stubs = stubout.StubOutForTesting() self.stubs = stubout.StubOutForTesting()
stub_out_swift_common_client(self.stubs) stub_out_swift_common_client(self.stubs)
self.store = Store(SWIFT_CONF) self.store = Store(test_utils.TestConfigOpts(SWIFT_CONF))
def tearDown(self): def tearDown(self):
"""Clear the test environment""" """Clear the test environment"""
@ -293,7 +294,7 @@ class TestStore(unittest.TestCase):
image_swift = StringIO.StringIO(expected_swift_contents) image_swift = StringIO.StringIO(expected_swift_contents)
self.store = Store(new_conf) self.store = Store(test_utils.TestConfigOpts(new_conf))
location, size, checksum = self.store.add(image_id, image_swift, location, size, checksum = self.store.add(image_id, image_swift,
expected_swift_size) expected_swift_size)
@ -318,7 +319,7 @@ class TestStore(unittest.TestCase):
conf['swift_store_create_container_on_put'] = 'False' conf['swift_store_create_container_on_put'] = 'False'
conf['swift_store_container'] = 'noexist' conf['swift_store_container'] = 'noexist'
image_swift = StringIO.StringIO("nevergonnamakeit") image_swift = StringIO.StringIO("nevergonnamakeit")
self.store = Store(conf) self.store = Store(test_utils.TestConfigOpts(conf))
# We check the exception text to ensure the container # We check the exception text to ensure the container
# missing text is found in it, otherwise, we would have # missing text is found in it, otherwise, we would have
@ -348,7 +349,7 @@ class TestStore(unittest.TestCase):
'/noexist/%s' % expected_image_id '/noexist/%s' % expected_image_id
image_swift = StringIO.StringIO(expected_swift_contents) image_swift = StringIO.StringIO(expected_swift_contents)
self.store = Store(conf) self.store = Store(test_utils.TestConfigOpts(conf))
location, size, checksum = self.store.add(expected_image_id, location, size, checksum = self.store.add(expected_image_id,
image_swift, image_swift,
expected_swift_size) expected_swift_size)
@ -387,7 +388,7 @@ class TestStore(unittest.TestCase):
try: try:
glance.store.swift.DEFAULT_LARGE_OBJECT_SIZE = 1024 glance.store.swift.DEFAULT_LARGE_OBJECT_SIZE = 1024
glance.store.swift.DEFAULT_LARGE_OBJECT_CHUNK_SIZE = 1024 glance.store.swift.DEFAULT_LARGE_OBJECT_CHUNK_SIZE = 1024
self.store = Store(conf) self.store = Store(test_utils.TestConfigOpts(conf))
location, size, checksum = self.store.add(expected_image_id, location, size, checksum = self.store.add(expected_image_id,
image_swift, image_swift,
expected_swift_size) expected_swift_size)
@ -440,7 +441,7 @@ class TestStore(unittest.TestCase):
MAX_SWIFT_OBJECT_SIZE = 1024 MAX_SWIFT_OBJECT_SIZE = 1024
glance.store.swift.DEFAULT_LARGE_OBJECT_SIZE = 1024 glance.store.swift.DEFAULT_LARGE_OBJECT_SIZE = 1024
glance.store.swift.DEFAULT_LARGE_OBJECT_CHUNK_SIZE = 1024 glance.store.swift.DEFAULT_LARGE_OBJECT_CHUNK_SIZE = 1024
self.store = Store(conf) self.store = Store(test_utils.TestConfigOpts(conf))
location, size, checksum = self.store.add(expected_image_id, location, size, checksum = self.store.add(expected_image_id,
image_swift, 0) image_swift, 0)
finally: finally:
@ -475,7 +476,7 @@ class TestStore(unittest.TestCase):
del conf[key] del conf[key]
try: try:
self.store = Store(conf) self.store = Store(test_utils.TestConfigOpts(conf))
return self.store.add == self.store.add_disabled return self.store.add == self.store.add_disabled
except: except:
return False return False

View File

@ -22,9 +22,11 @@ import stubout
import webob import webob
from glance import client from glance import client
from glance.common import config
from glance.common import exception from glance.common import exception
from glance.api import versions from glance.api import versions
from glance.tests import stubs from glance.tests import stubs
from glance.tests import utils
class VersionsTest(unittest.TestCase): class VersionsTest(unittest.TestCase):
@ -46,8 +48,10 @@ class VersionsTest(unittest.TestCase):
def test_get_version_list(self): def test_get_version_list(self):
req = webob.Request.blank('/') req = webob.Request.blank('/')
req.accept = "application/json" req.accept = "application/json"
conf = {'bind_host': '0.0.0.0', conf = utils.TestConfigOpts({
'bind_port': 9292} 'bind_host': '0.0.0.0',
'bind_port': 9292
})
res = req.get_response(versions.Controller(conf)) res = req.get_response(versions.Controller(conf))
self.assertEqual(res.status_int, 300) self.assertEqual(res.status_int, 300)
self.assertEqual(res.content_type, "application/json") self.assertEqual(res.content_type, "application/json")

View File

@ -22,9 +22,44 @@ import functools
import os import os
import socket import socket
import subprocess import subprocess
import tempfile
import nose.plugins.skip import nose.plugins.skip
from glance.common import config
class TestConfigOpts(config.GlanceConfigOpts):
def __init__(self, test_values):
super(TestConfigOpts, self).__init__()
self._test_values = test_values
self()
def __call__(self):
config_file = self._write_tmp_config_file()
try:
super(TestConfigOpts, self).\
__call__(['--config-file', config_file])
finally:
os.remove(config_file)
def _write_tmp_config_file(self):
contents = '[DEFAULT]\n'
for key, value in self._test_values.items():
contents += '%s = %s\n' % (key, value)
(fd, path) = tempfile.mkstemp(prefix='testcfg')
try:
os.write(fd, contents)
except Exception, e:
os.close(fd)
os.remove(path)
raise e
os.close(fd)
return path
class skip_test(object): class skip_test(object):
"""Decorator that skips a test.""" """Decorator that skips a test."""