#!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Routines for configuring OpenStack Service """ import logging.config import optparse import os from paste import deploy import sys import ConfigParser from keystone.common.wsgi import add_console_handler 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): """ 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 mapping of option key/str(value) pairs, and args is the set of arguments (not options) supplied on the command-line. The reason that the option values are returned as strings only is that ConfigParser and paste.deploy only accept string values... :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): """ Given a supplied optparse.OptionParser, adds an OptionGroup that represents all common configuration options. :param parser: optparse.OptionParser """ help_text = "The following configuration options are common to "\ "all keystone programs." 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 to console") group.add_option('-c', '--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.""") group.add_option('-p', '--port', '--bind-port', default=5000, dest="bind_port", help="specifies port to listen on (default is 5000)") group.add_option('--host', '--bind-host', default="0.0.0.0", dest="bind_host", help="specifies host address to listen on "\ "(default is all or 0.0.0.0)") # This one is handled by keystone/tools/tracer.py (if loaded) group.add_option('-t', '--trace-calls', default=False, dest="trace_calls", action="store_true", help="Turns on call tracing for troubleshooting") parser.add_option_group(group) return group def add_log_options(parser): """ 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)") parser.add_option_group(group) return group def setup_logging(options, conf): """ Sets up the logging options for a log with supplied name :param options: Mapping of typed option key/values :param conf: Mapping of untyped key/values from config file """ if options.get('log_config', None): # Use a logging configuration file for all settings... if os.path.exists(options['log_config']): logging.config.fileConfig(options['log_config']) return else: raise RuntimeError("Unable to locate specified logging "\ "config file: %s" % options['log_config']) # If either the CLI option or the conf value # is True, we set to True debug = options.get('debug') or conf.get('debug', False) debug = debug in [True, "True", "1"] verbose = options.get('verbose') or conf.get('verbose', False) verbose = verbose in [True, "True", "1"] root_logger = logging.root if debug: level = logging.DEBUG elif verbose: level = logging.INFO else: level = logging.WARNING root_logger.setLevel(level) # Set log configuration from options... # 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 not logfile: logfile = conf.get('log_file') if logfile: logdir = options.get('log_dir') if not logdir: logdir = conf.get('log_dir') if logdir: logfile = os.path.join(logdir, logfile) logfile = logging.FileHandler(logfile) logfile.setFormatter(formatter) root_logger.addHandler(logfile) # Mirror to console if verbose or debug if debug or verbose: add_console_handler(root_logger, logging.INFO) else: handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) root_logger.addHandler(handler) def find_config_file(options, args): """ Return the first config file found. 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 keystone.conf in standard directories: * . * ~.keystone/ * ~ * /etc/keystone * /etc :if no config file is given get from possible_topdir/etc/keystone.conf :retval Full path to config file, or None if no config file found """ POSSIBLE_TOPDIR = os.path.normpath(os.path.join(\ os.path.abspath(sys.argv[0]), os.pardir, os.pardir)) 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 keystone.conf config_file_dirs = [fix_path(os.getcwd()), fix_path(os.path.join('~', '.keystone')), fix_path('~'), '/etc/keystone/', '/etc'] for cfg_dir in config_file_dirs: cfg_file = os.path.join(cfg_dir, 'keystone.conf') if os.path.exists(cfg_file): return cfg_file else: if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'etc', \ 'keystone.conf')): # For debug only config_file = os.path.join(POSSIBLE_TOPDIR, 'etc', \ 'keystone.conf') return config_file def load_paste_config(app_name, options, args): """ Looks for a config file to use for an app and returns the config file path and a configuration mapping from a paste config file. 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 keystone.conf in standard directories: * . * ~.keystone/ * ~ * /etc/keystone * /etc :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. :param options: Set of typed options returned from parse_options() :param args: Command line arguments from argv[1:] :retval Tuple of (conf_file, conf) :raises RuntimeError when config file cannot be located or there was a problem loading the configuration file. """ conf_file = find_config_file(options, args) if not conf_file: raise RuntimeError("Unable to locate any configuration file. "\ "Cannot load application %s" % app_name) try: conf = deploy.appconfig("config:%s" % conf_file, name=app_name) conf.global_conf.update(get_non_paste_configs(conf_file)) return conf_file, conf except Exception, e: raise RuntimeError("Error loading config %s: %s" % (conf_file, e)) def get_non_paste_configs(conf_file): load_config_files(conf_file) complete_conf = load_config_files(conf_file) #Add Non Paste global sections.Need to find a better way. global_conf = {} if complete_conf != None: for section in complete_conf.sections(): if not (section.startswith('filter:') or \ section.startswith('app:') or \ section.startswith('pipeline:')): section_items = complete_conf.items(section) section_items_dict = {} for section_item in section_items: section_items_dict[section_item[0]] = section_item[1] global_conf[section] = section_items_dict return global_conf def load_config_files(config_files): '''Load the config files.''' config = ConfigParser.ConfigParser() if config_files is not None: config.read(config_files) return config def load_paste_app(app_name, options, args): """ Builds and returns a WSGI app from a paste config file. 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 keystone.conf in standard directories: * . * ~.keystone/ * ~ * /etc/keystone * /etc :param app_name: Name of the application to load (server, admin, proxy, ..) :param options: Set of typed options returned from parse_options() :param args: Command line arguments from argv[1:] :raises RuntimeError when config file cannot be located or application cannot be loaded from config file """ conf_file, conf = load_paste_config(app_name, options, args) try: # Setup logging early, supplying both the CLI options and the # configuration mapping from the config file setup_logging(options, conf) # We only update the conf dict for the verbose and debug # flags. Everything else must be set up in the conf file... debug = options.get('debug') or conf.get('debug', False) debug = debug in [True, "True", "1"] verbose = options.get('verbose') or conf.get('verbose', False) verbose = verbose in [True, "True", "1"] conf['debug'] = debug conf['verbose'] = verbose # Log the options used when starting if we're in debug mode... if debug: logger = logging.getLogger(app_name) logger.info("*" * 50) logger.info("Configuration options gathered from config file:") logger.info(conf_file) logger.info("================================================") items = dict([(k, v) for k, v in conf.items() if k not in ('__file__', 'here')]) for key, value in sorted(items.items()): logger.info("%(key)-20s %(value)s" % locals()) logger.info("*" * 50) app = deploy.loadapp("config:%s" % conf_file, name=app_name, global_conf=conf.global_conf) except (LookupError, ImportError), e: raise RuntimeError("Unable to load %(app_name)s from " "configuration file %(conf_file)s." "\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)