From a4e467f20e391664d5603a55869f18347518ff5f Mon Sep 17 00:00:00 2001 From: Ricky Grassmuck Date: Sun, 5 Jun 2016 12:00:00 +0200 Subject: [PATCH] Add the ability to set default runtime parameters via config file ## Feature Description Currently, If a user wishes to customize their runtime configurations the only way to do so is by passing in arguments via commandline parameters each time the service is started. Since these options are unlikely to change for an individual user, a configuration file able to be parsed at runtime to set these options can be added to the CouchPotatoServer application root directory. Once configured, the user no longer has to pass any arguments via the commandline. ## Config File Setup An example template for users too customize can be found at `CouchPotatoServer/init/runtime_config.ini` An example configuration: ``` [default] daemon: data_dir: /home/couchpotato config_file: /etc/couchpotato/settings.conf pid_file: /home/couchpotato/cp.pid ``` This example is equivilant to executing: ``` python CouchPotato.py --daemon \ --data_dir /home/couchpotato \ --config_file /etc/couchpotato/settings.conf \ --pid_file /home/couchpotato/cp.pid ``` For boolean parameters such as --daemon or --debug, provide just the name of the option on its own line with or without the ":" on the end. Parameters that have corresponding arguments require the name of the parameter, a ":", followed by a space and the argument to be passed in. Lines beginning with a # are considered comments and not parsed. ## Note Parameters passed as command line arguments WILL take precedence over the one specified in the config.ini file. ## Changes * added package argparse_config.py to the lib folder * Modified runner.py to parse config.ini file * Added config.ini file which sets daemon mode by default * Added init/runtime-config.ini which contains all available options with comments --- CouchPotato.py | 2 +- config.ini | 9 +++ couchpotato/runner.py | 11 ++- init/runtime-config.ini | 25 ++++++ libs/argparse_config.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 252 insertions(+), 4 deletions(-) create mode 100644 config.ini create mode 100644 init/runtime-config.ini create mode 100644 libs/argparse_config.py diff --git a/CouchPotato.py b/CouchPotato.py index b4a6421..20d3bb0 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -33,7 +33,7 @@ class Loader(object): # Get options via arg from couchpotato.runner import getOptions - self.options = getOptions(sys.argv[1:]) + self.options = getOptions(sys.argv[1:], base_path) # Load settings settings = Env.get('settings') diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..f056a0a --- /dev/null +++ b/config.ini @@ -0,0 +1,9 @@ +# Runtime configuration file +# For all available options copy thefile located at +# "/init/runtime-config.ini" to +# "/config.ini" +# and edit it to fit your needs. + + +[default] +daemon: diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 900b1f8..10e0efd 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -13,6 +13,7 @@ import shutil from CodernityDB.database_super_thread_safe import SuperThreadSafeDatabase from argparse import ArgumentParser +import argparse_config from cache import FileSystemCache from couchpotato import KeyHandler, LoginHandler, LogoutHandler from couchpotato.api import NonBlockHandler, ApiHandler @@ -27,9 +28,9 @@ from couchpotato.core.softchroot import SoftChrootInitError try: from tornado.netutil import bind_unix_socket except: pass -def getOptions(args): - +def getOptions(args, base_path): # Options + conf = os.path.join(base_path, "config.ini") parser = ArgumentParser(prog = 'CouchPotato.py') parser.add_argument('--data_dir', dest = 'data_dir', help = 'Absolute or ~/ path of the data dir') @@ -45,7 +46,11 @@ def getOptions(args): dest = 'daemon', help = 'Daemonize the app') parser.add_argument('--pid_file', dest = 'pid_file', help = 'Path to pidfile needed for daemon') - + + # parse applicaions runtime configuration file and set options + if os.path.exists(conf): + argparse_config.read_config_file(parser, conf) + options = parser.parse_args(args) data_dir = os.path.expanduser(options.data_dir if options.data_dir else getDataDir()) diff --git a/init/runtime-config.ini b/init/runtime-config.ini new file mode 100644 index 0000000..def8a5e --- /dev/null +++ b/init/runtime-config.ini @@ -0,0 +1,25 @@ +# A template for the CouchPotatoServer config.ini file + +# Do not change the [default] section heading +[default] + +# Uncomment to run CouchPotato as a daemon +# daemon: + +# Uncomment to specify CouchPotato's data directory +# data_dir: /apps/configs/couchpotato + +# Uncomment to specify the location of CouchPotato's settings file +# config_file: /home/couchpotato/settings.conf + +# Uncomment to specifiy the location of the PID file +# pid_file: /home/couchpotato/cp.pid + +# Uncomment to run CP in debug mode +# debug: + +# Uncomment to log messages to the console instead of the log files +# console_log: + +# Uncomment to limit logging +# quiet: diff --git a/libs/argparse_config.py b/libs/argparse_config.py new file mode 100644 index 0000000..40e2b0f --- /dev/null +++ b/libs/argparse_config.py @@ -0,0 +1,209 @@ +""" +Author: tikitu +""" +import ConfigParser +from argparse import _SubParsersAction, _StoreAction, _StoreConstAction +import argparse + +__version__ = '0.5.1' + + +def get_config_parser(filename): + config_parser = ConfigParser.SafeConfigParser(allow_no_value=True) + config_parser.read([filename]) + return config_parser + + +def read_config_file(arg_parser, filename): + config_parser = get_config_parser(filename) + read_config_parser(arg_parser, config_parser) + + +def read_config_parser(arg_parser, config_parser): + ReadConfig(config_parser=config_parser).walk_parser(arg_parser) + + +def add_config_block_subcommand(arg_parser, subparsers, + config_parser=None, + only_non_defaults=False): + """ + Add a subcommand "config-block" to the arg_parser, to be used as follows: + + In myprog.py: + + subparsers = arg_parser.add_subparsers(..., dest='command') + add_config_block_subcommand(arg_parser, subparsers) + # ... + parsed_args = arg_parser.parse_args() + if parsed_args.command == 'config': + print parsed_args.func(parsed_args) + exit(0) + + On the commandline: + + $ myprog.py config default --username tikitu --secret xyzzy + [default] + username: tikitu + secret: xyzzy + + :param arg_parser: + :param config_parser: + :param dest: + :param only_non_defaults: + :return: None + """ + config_command_parser = subparsers.add_parser('config') + config_command_parser.add_argument('block') + config_command_parser.add_argument('commandline', nargs=argparse.REMAINDER) + + def handle_args(orig_parsed_args): + if config_parser is not None: + read_config_parser(arg_parser, config_parser) + args_for_commandline = list(orig_parsed_args.commandline) + if orig_parsed_args.block == 'default': + subparsers.add_parser('dummy-command') + args_for_commandline.append('dummy-command') + else: + args_for_commandline.insert(0, orig_parsed_args.block) + parsed_args = arg_parser.parse_args(args_for_commandline) + return generate_config(arg_parser, parsed_args, + section=orig_parsed_args.block, + only_non_defaults=only_non_defaults) + + config_command_parser.set_defaults(func=handle_args) + + +def generate_config(arg_parser, parsed_args, section='default', + only_non_defaults=False): + action = GenerateConfig(parsed_args, section, + only_non_defaults=only_non_defaults) + action.walk_parser(arg_parser) + return action.contents + + +class ArgParserWalker(object): + def start_section(self, section_name): + raise NotImplementedError() + + def end_section(self): + raise NotImplementedError() + + def process_parser_action(self, action, is_store_const=False): + raise NotImplementedError() + + def walk_parser(self, arg_parser): + try: + self.start_section('default') + for action in arg_parser._actions: + if isinstance(action, _StoreAction): + self.process_parser_action(action) + elif isinstance(action, _StoreConstAction): + self.process_parser_action(action, is_store_const=True) + elif isinstance(action, _SubParsersAction): + for command, sub_parser in action.choices.items(): + self.start_section(command) + for sub_action in sub_parser._actions: + self.process_parser_action(sub_action) + self.end_section() + self.end_section() + except DefaultError as e: + arg_parser.error( + u'[{section_name}] config option "{option_string}" ' + u'must be {type_transformer}() value, got: {value}'.format( + section_name=e.section_name, + option_string=e.option_string, + type_transformer=e.type_transformer.__name__, + value=e.value + )) + + +class GenerateConfig(ArgParserWalker): + def __init__(self, parsed_args, section, only_non_defaults=False): + self.parsed_args = parsed_args + self._contents = [] + self._only_non_defaults = only_non_defaults + self._section = section + self._in_sections = [] + + def start_section(self, section_name): + self._in_sections.append(section_name) + if section_name == self._section: + if self._contents: + self._contents.append(u'') + self._contents.append(u'[{0}]'.format(section_name)) + + def end_section(self): + self._in_sections.pop() + + @property + def contents(self): + return u'\n'.join(self._contents + [u'']) + + def process_parser_action(self, action, is_store_const=False): + if self._in_sections[-1] != self._section: + return + # take the longest string, likely the most informative + action_name = list(action.option_strings) + action_name.sort(key=lambda s: len(s), reverse=True) + action_name = _convert_option_string(action_name[0]) + + action_value = getattr(self.parsed_args, action.dest, None) + if self._only_non_defaults and action_value == action.default: + action_value = None + if action_value is not None: + if is_store_const: + self._contents.append(action_name) + else: + self._contents.append(u'{action_name}: {default_value}'.format( + action_name=action_name, + default_value=action_value, # hope it prints as wanted... + )) + + +class ReadConfig(ArgParserWalker): + def __init__(self, config_parser=None): + self.sections = [] + self.config_parser = config_parser + + def start_section(self, section_name): + self.sections.append(section_name) + + def end_section(self): + self.sections.pop() + + @property + def current_section(self): + return self.sections[-1] if self.sections else None + + def process_parser_action(self, action, is_store_const=False): + for option_string in action.option_strings: + option_string = _convert_option_string(option_string) + if self.config_parser.has_option(self.current_section, + option_string): + if is_store_const: + action.default = action.const + else: + value = self.config_parser.get(self.current_section, + option_string) + type_transformer = (action.type if action.type is not None + else lambda x: x) + try: + action.default = type_transformer(value) + except: + raise DefaultError(self.current_section, + option_string, + value, + type_transformer) + action.required = False + + +class DefaultError(Exception): + def __init__(self, section_name, option_string, value, type_transformer): + self.section_name = section_name + self.option_string = option_string + self.value = value + self.type_transformer = type_transformer + + +def _convert_option_string(op_s): + return op_s.lstrip('-').replace('-', '_')