diff options
Diffstat (limited to 'src/lib/Bcfg2')
-rw-r--r-- | src/lib/Bcfg2/Options.py | 1328 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/Actions.py | 164 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/Common.py | 135 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/OptionGroups.py | 209 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/Options.py | 305 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/Parser.py | 282 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/Subcommands.py | 237 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/Types.py | 87 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/__init__.py | 10 |
9 files changed, 1429 insertions, 1328 deletions
diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py deleted file mode 100644 index 64408693a..000000000 --- a/src/lib/Bcfg2/Options.py +++ /dev/null @@ -1,1328 +0,0 @@ -"""Option parsing library for utilities.""" - -import copy -import getopt -import inspect -import os -import re -import shlex -import sys -import grp -import pwd -from Bcfg2.Client.Tools import __path__ as toolpath -from Bcfg2.Compat import ConfigParser, walk_packages -from Bcfg2.version import __version__ - - -class OptionFailure(Exception): - """ raised when malformed Option objects are instantiated """ - pass - -DEFAULT_CONFIG_LOCATION = '/etc/bcfg2.conf' -DEFAULT_INSTALL_PREFIX = '/usr' - - -class DefaultConfigParser(ConfigParser.ConfigParser): - """ A config parser that can be used to query options with default - values in the event that the option is not found """ - - def __init__(self, *args, **kwargs): - """Make configuration options case sensitive""" - ConfigParser.ConfigParser.__init__(self, *args, **kwargs) - self.optionxform = str - - def get(self, section, option, **kwargs): - """ convenience method for getting config items """ - default = None - if 'default' in kwargs: - default = kwargs['default'] - del kwargs['default'] - try: - return ConfigParser.ConfigParser.get(self, section, option, - **kwargs) - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): - if default is not None: - return default - else: - raise - - def getboolean(self, section, option, **kwargs): - """ convenience method for getting boolean config items """ - default = None - if 'default' in kwargs: - default = kwargs['default'] - del kwargs['default'] - try: - return ConfigParser.ConfigParser.getboolean(self, section, - option, **kwargs) - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError, - ValueError): - if default is not None: - return default - else: - raise - - -class Option(object): - """ a single option, which might be read from the command line, - environment, or config file """ - - # pylint: disable=C0103,R0913 - def __init__(self, desc, default, cmd=None, odesc=False, - env=False, cf=False, cook=False, long_arg=False, - deprecated_cf=None): - self.desc = desc - self.default = default - self.cmd = cmd - self.long = long_arg - if not self.long: - if cmd and (cmd[0] != '-' or len(cmd) != 2): - raise OptionFailure("Poorly formed command %s" % cmd) - elif cmd and not cmd.startswith('--'): - raise OptionFailure("Poorly formed command %s" % cmd) - self.odesc = odesc - self.env = env - self.cf = cf - self.deprecated_cf = deprecated_cf - self.boolean = False - if not odesc and not cook and isinstance(self.default, bool): - self.boolean = True - self.cook = cook - self.value = None - # pylint: enable=C0103,R0913 - - def get_cooked_value(self, value): - """ get the value of this option after performing any option - munging specified in the 'cook' keyword argument to the - constructor """ - if self.boolean: - return True - if self.cook: - return self.cook(value) - else: - return value - - def __str__(self): - rv = ["%s: " % self.__class__.__name__, self.desc] - if self.cmd or self.cf: - rv.append(" (") - if self.cmd: - if self.odesc: - if self.long: - rv.append("%s=%s" % (self.cmd, self.odesc)) - else: - rv.append("%s %s" % (self.cmd, self.odesc)) - else: - rv.append("%s" % self.cmd) - - if self.cf: - if self.cmd: - rv.append("; ") - rv.append("[%s].%s" % self.cf) - if self.cmd or self.cf: - rv.append(")") - if hasattr(self, "value"): - rv.append(": %s" % self.value) - return "".join(rv) - - def buildHelpMessage(self): - """ build the help message for this option """ - vals = [] - if not self.cmd: - return '' - if self.odesc: - if self.long: - vals.append("%s=%s" % (self.cmd, self.odesc)) - else: - vals.append("%s %s" % (self.cmd, self.odesc)) - else: - vals.append(self.cmd) - vals.append(self.desc) - return " %-28s %s\n" % tuple(vals) - - def buildGetopt(self): - """ build a string suitable for describing this short option - to getopt """ - gstr = '' - if self.long: - return gstr - if self.cmd: - gstr = self.cmd[1] - if self.odesc: - gstr += ':' - return gstr - - def buildLongGetopt(self): - """ build a string suitable for describing this long option to - getopt """ - if self.odesc: - return self.cmd[2:] + '=' - else: - return self.cmd[2:] - - def parse(self, opts, rawopts, configparser=None): - """ parse a single option. try parsing the data out of opts - (the results of getopt), rawopts (the raw option string), the - environment, and finally the config parser. either opts or - rawopts should be provided, but not both """ - if self.cmd and opts: - # Processing getopted data - optinfo = [opt[1] for opt in opts if opt[0] == self.cmd] - if optinfo: - if optinfo[0]: - self.value = self.get_cooked_value(optinfo[0]) - else: - self.value = True - return - if self.cmd and self.cmd in rawopts: - if self.odesc: - data = rawopts[rawopts.index(self.cmd) + 1] - else: - data = True - self.value = self.get_cooked_value(data) - return - # No command line option found - if self.env and self.env in os.environ: - self.value = self.get_cooked_value(os.environ[self.env]) - return - if self.cf and configparser: - try: - self.value = self.get_cooked_value(configparser.get(*self.cf)) - return - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): - pass - if self.deprecated_cf: - try: - self.value = self.get_cooked_value( - configparser.get(*self.deprecated_cf)) - print("Warning: [%s] %s is deprecated, use [%s] %s instead" - % (self.deprecated_cf[0], self.deprecated_cf[1], - self.cf[0], self.cf[1])) - return - except (ConfigParser.NoSectionError, - ConfigParser.NoOptionError): - pass - - # Default value not cooked - self.value = self.default - - -class OptionSet(dict): - """ a set of Option objects that interfaces with getopt and - DefaultConfigParser to populate a dict of <option name>:<value> - """ - - def __init__(self, *args, **kwargs): - dict.__init__(self, *args) - self.hm = self.buildHelpMessage() # pylint: disable=C0103 - if 'configfile' in kwargs: - self.cfile = kwargs['configfile'] - else: - self.cfile = DEFAULT_CONFIG_LOCATION - if 'quiet' in kwargs: - self.quiet = kwargs['quiet'] - else: - self.quiet = False - self.cfp = DefaultConfigParser() - if len(self.cfp.read(self.cfile)) == 0 and not self.quiet: - # suppress warnings if called from bcfg2-admin init - caller = inspect.stack()[-1][1].split('/')[-1] - if caller == 'bcfg2-admin' and len(sys.argv) > 1: - if sys.argv[1] == 'init': - return - else: - print("Warning! Unable to read specified configuration file: " - "%s" % self.cfile) - - def buildGetopt(self): - """ build a short option description string suitable for use - by getopt.getopt """ - return ''.join([opt.buildGetopt() for opt in list(self.values())]) - - def buildLongGetopt(self): - """ build a list of long options suitable for use by - getopt.getopt """ - return [opt.buildLongGetopt() for opt in list(self.values()) - if opt.long] - - def buildHelpMessage(self): - """ Build the help mesage for this option set, or use self.hm - if it is set """ - if hasattr(self, 'hm'): - return self.hm - hlist = [] # list of _non-empty_ help messages - for opt in list(self.values()): - helpmsg = opt.buildHelpMessage() - if helpmsg: - hlist.append(helpmsg) - return ''.join(hlist) - - def helpExit(self, msg='', code=1): - """ print help and exit """ - if msg: - print(msg) - print("Usage:") - print(self.buildHelpMessage()) - raise SystemExit(code) - - def versionExit(self, code=0): - """ print the version of bcfg2 and exit """ - print("%s %s on Python %s" % - (os.path.basename(sys.argv[0]), - __version__, - ".".join(str(v) for v in sys.version_info[0:3]))) - raise SystemExit(code) - - def parse(self, argv, do_getopt=True): - '''Parse options from command line.''' - if VERSION not in self.values(): - self['__version__'] = VERSION - if do_getopt: - try: - opts, args = getopt.getopt(argv, self.buildGetopt(), - self.buildLongGetopt()) - except getopt.GetoptError: - err = sys.exc_info()[1] - self.helpExit(err) - if '-h' in argv: - self.helpExit('', 0) - if '--version' in argv: - self.versionExit() - self['args'] = args - for key in list(self.keys()): - if key == 'args': - continue - option = self[key] - if do_getopt: - option.parse(opts, [], configparser=self.cfp) - else: - option.parse([], argv, configparser=self.cfp) - if hasattr(option, 'value'): - val = option.value - self[key] = val - if "__version__" in self: - del self['__version__'] - - -def list_split(c_string): - """ split an option string on commas, optionally surrounded by - whitespace, returning a list """ - if c_string: - return re.split(r'\s*,\s*', c_string) - return [] - - -def colon_split(c_string): - """ split an option string on colons, returning a list """ - if c_string: - return c_string.split(r':') - return [] - - -def get_bool(val): - """ given a string value of a boolean configuration option, return - an actual bool (True or False) """ - # these values copied from ConfigParser.RawConfigParser.getboolean - # with the addition of True and False - truelist = ["1", "yes", "True", "true", "on"] - falselist = ["0", "no", "False", "false", "off"] - if val in truelist: - return True - elif val in falselist: - return False - else: - raise ValueError("Not a boolean value", val) - - -def get_int(val): - """ given a string value of an integer configuration option, - return an actual int """ - return int(val) - - -def get_timeout(val): - """ convert the timeout value into a float or None """ - if val is None: - return val - timeout = float(val) # pass ValueError up the stack - if timeout <= 0: - return None - return timeout - - -def get_size(value): - """ Given a number of bytes in a human-readable format (e.g., - '512m', '2g'), get the absolute number of bytes as an integer """ - if value == -1: - return value - mat = re.match(r'(\d+)([KkMmGg])?', value) - if not mat: - raise ValueError("Not a valid size", value) - rvalue = int(mat.group(1)) - mult = mat.group(2).lower() - if mult == 'k': - return rvalue * 1024 - elif mult == 'm': - return rvalue * 1024 * 1024 - elif mult == 'g': - return rvalue * 1024 * 1024 * 1024 - else: - return rvalue - - -def get_gid(val): - """ This takes a group name or gid and returns the corresponding - gid. """ - try: - return int(val) - except ValueError: - return int(grp.getgrnam(val)[2]) - - -def get_uid(val): - """ This takes a group name or gid and returns the corresponding - gid. """ - try: - return int(val) - except ValueError: - return int(pwd.getpwnam(val)[2]) - - -# Options accepts keyword argument list with the following values: -# default: default value for the option -# cmd: command line switch -# odesc: option description -# cf: tuple containing section/option -# cook: method for parsing option -# long_arg: (True|False) specifies whether cmd is a long argument - -# General options -CFILE = \ - Option('Specify configuration file', - default=DEFAULT_CONFIG_LOCATION, - cmd='-C', - odesc='<conffile>', - env="BCFG2_CONFIG") -LOCKFILE = \ - Option('Specify lockfile', - default='/var/lock/bcfg2.run', - odesc='<Path to lockfile>', - cf=('components', 'lockfile')) -HELP = \ - Option('Print this usage message', - default=False, - cmd='-h') -VERSION = \ - Option('Print the version and exit', - default=False, - cmd='--version', long_arg=True) -DAEMON = \ - Option("Daemonize process, storing pid", - default=None, - cmd='-D', - odesc='<pidfile>') -INSTALL_PREFIX = \ - Option('Installation location', - default=DEFAULT_INSTALL_PREFIX, - odesc='</path>', - cf=('server', 'prefix')) -SENDMAIL_PATH = \ - Option('Path to sendmail', - default='/usr/lib/sendmail', - cf=('reports', 'sendmailpath')) -INTERACTIVE = \ - Option('Run interactively, prompting the user for each change', - default=False, - cmd='-I', ) -ENCODING = \ - Option('Encoding of cfg files', - default='UTF-8', - cmd='-E', - odesc='<encoding>', - cf=('components', 'encoding')) -PARANOID_PATH = \ - Option('Specify path for paranoid file backups', - default='/var/cache/bcfg2', - odesc='<paranoid backup path>', - cf=('paranoid', 'path')) -PARANOID_MAX_COPIES = \ - Option('Specify the number of paranoid copies you want', - default=1, - odesc='<max paranoid copies>', - cf=('paranoid', 'max_copies')) -OMIT_LOCK_CHECK = \ - Option('Omit lock check', - default=False, - cmd='-O') -CORE_PROFILE = \ - Option('profile', - default=False, - cmd='-p') -SCHEMA_PATH = \ - Option('Path to XML Schema files', - default='%s/share/bcfg2/schemas' % DEFAULT_INSTALL_PREFIX, - cmd='--schema', - odesc='<schema path>', - cf=('lint', 'schema'), - long_arg=True) -INTERPRETER = \ - Option("Python interpreter to use", - default='best', - cmd="--interpreter", - odesc='<python|bpython|ipython|best>', - cf=('bcfg2-info', 'interpreter'), - long_arg=True) - -# Metadata options (mdata section) -MDATA_OWNER = \ - Option('Default Path owner', - default='root', - odesc='owner permissions', - cf=('mdata', 'owner')) -MDATA_GROUP = \ - Option('Default Path group', - default='root', - odesc='group permissions', - cf=('mdata', 'group')) -MDATA_IMPORTANT = \ - Option('Default Path priority (importance)', - default='False', - odesc='Important entries are installed first', - cf=('mdata', 'important')) -MDATA_MODE = \ - Option('Default mode for Path', - default='644', - odesc='octal file mode', - cf=('mdata', 'mode')) -MDATA_SECONTEXT = \ - Option('Default SELinux context', - default='__default__', - odesc='SELinux context', - cf=('mdata', 'secontext')) -MDATA_PARANOID = \ - Option('Default Path paranoid setting', - default='true', - odesc='Path paranoid setting', - cf=('mdata', 'paranoid')) -MDATA_SENSITIVE = \ - Option('Default Path sensitive setting', - default='false', - odesc='Path sensitive setting', - cf=('mdata', 'sensitive')) - -# Server options -SERVER_REPOSITORY = \ - Option('Server repository path', - default='/var/lib/bcfg2', - cmd='-Q', - odesc='<repository path>', - cf=('server', 'repository')) -SERVER_PLUGINS = \ - Option('Server plugin list', - # default server plugins - default=['Bundler', 'Cfg', 'Metadata', 'Pkgmgr', 'Rules', - 'SSHbase'], - cf=('server', 'plugins'), - cook=list_split) -SERVER_FILEMONITOR = \ - Option('Server file monitor', - default='default', - odesc='File monitoring driver', - cf=('server', 'filemonitor')) -SERVER_FAM_IGNORE = \ - Option('File globs to ignore', - default=['*~', '*#', '.#*', '*.swp', '*.swpx', '.*.swx', - 'SCCS', '.svn', '4913', '.gitignore'], - cf=('server', 'ignore_files'), - cook=list_split) -SERVER_FAM_BLOCK = \ - Option('FAM blocks on startup until all events are processed', - default=False, - cook=get_bool, - cf=('server', 'fam_blocking')) -SERVER_LISTEN_ALL = \ - Option('Listen on all interfaces', - default=False, - cmd='--listen-all', - cf=('server', 'listen_all'), - cook=get_bool, - long_arg=True) -SERVER_LOCATION = \ - Option('Server Location', - default='https://localhost:6789', - cmd='-S', - odesc='https://server:port', - cf=('components', 'bcfg2')) -SERVER_KEY = \ - Option('Path to SSL key', - default="/etc/pki/tls/private/bcfg2.key", - cmd='--ssl-key', - odesc='<ssl key>', - cf=('communication', 'key'), - long_arg=True) -SERVER_CERT = \ - Option('Path to SSL certificate', - default="/etc/pki/tls/certs/bcfg2.crt", - odesc='<ssl cert>', - cf=('communication', 'certificate')) -SERVER_CA = \ - Option('Path to SSL CA Cert', - default=None, - odesc='<ca cert>', - cf=('communication', 'ca')) -SERVER_PASSWORD = \ - Option('Communication Password', - default=None, - cmd='-x', - odesc='<password>', - cf=('communication', 'password')) -SERVER_PROTOCOL = \ - Option('Server Protocol', - default='xmlrpc/ssl', - cf=('communication', 'protocol')) -SERVER_BACKEND = \ - Option('Server Backend', - default='best', - cf=('server', 'backend')) -SERVER_DAEMON_USER = \ - Option('User to run the server daemon as', - default=0, - cf=('server', 'user'), - cook=get_uid) -SERVER_DAEMON_GROUP = \ - Option('Group to run the server daemon as', - default=0, - cf=('server', 'group'), - cook=get_gid) -SERVER_VCS_ROOT = \ - Option('Server VCS repository root', - default=None, - odesc='<VCS repository root>', - cf=('server', 'vcs_root')) -SERVER_UMASK = \ - Option('Server umask', - default='0077', - odesc='<Server umask>', - cf=('server', 'umask')) -SERVER_AUTHENTICATION = \ - Option('Default client authentication method', - default='cert+password', - odesc='{cert|bootstrap|cert+password}', - cf=('communication', 'authentication')) -SERVER_CHILDREN = \ - Option('Spawn this number of children for the multiprocessing core. ' - 'By default spawns children equivalent to the number of processors ' - 'in the machine.', - default=None, - cmd='--children', - odesc='<children>', - cf=('server', 'children'), - cook=get_int, - long_arg=True) - -# database options -DB_ENGINE = \ - Option('Database engine', - default='sqlite3', - cf=('database', 'engine')) -DB_NAME = \ - Option('Database name', - default=os.path.join(SERVER_REPOSITORY.default, "etc/bcfg2.sqlite"), - cf=('database', 'name')) -DB_USER = \ - Option('Database username', - default=None, - cf=('database', 'user')) -DB_PASSWORD = \ - Option('Database password', - default=None, - cf=('database', 'password')) -DB_HOST = \ - Option('Database host', - default='localhost', - cf=('database', 'host')) -DB_PORT = \ - Option('Database port', - default='', - cf=('database', 'port')) - -# Django options -WEB_CFILE = \ - Option('Web interface configuration file', - default="/etc/bcfg2-web.conf", - cmd='-W', - odesc='<conffile>', - cf=('reporting', 'config'), - deprecated_cf=('statistics', 'web_prefix'),) -DJANGO_TIME_ZONE = \ - Option('Django timezone', - default=None, - cf=('reporting', 'time_zone'), - deprecated_cf=('statistics', 'web_prefix'),) -DJANGO_DEBUG = \ - Option('Django debug', - default=None, - cf=('reporting', 'web_debug'), - deprecated_cf=('statistics', 'web_prefix'), - cook=get_bool,) -DJANGO_WEB_PREFIX = \ - Option('Web prefix', - default=None, - cf=('reporting', 'web_prefix')) - -# Reporting options -REPORTING_FILE_LIMIT = \ - Option('Reporting file size limit', - default=get_size('1m'), - cf=('reporting', 'file_limit'), - cook=get_size,) - -# Reporting options -REPORTING_TRANSPORT = \ - Option('Reporting transport', - default='DirectStore', - cf=('reporting', 'transport'),) - -# Client options -CLIENT_KEY = \ - Option('Path to SSL key', - default=None, - cmd='--ssl-key', - odesc='<ssl key>', - cf=('communication', 'key'), - long_arg=True) -CLIENT_CERT = \ - Option('Path to SSL certificate', - default=None, - cmd='--ssl-cert', - odesc='<ssl cert>', - cf=('communication', 'certificate'), - long_arg=True) -CLIENT_CA = \ - Option('Path to SSL CA Cert', - default=None, - cmd='--ca-cert', - odesc='<ca cert>', - cf=('communication', 'ca'), - long_arg=True) -CLIENT_SCNS = \ - Option('List of server commonNames', - default=None, - cmd='--ssl-cns', - odesc='<CN1:CN2>', - cf=('communication', 'serverCommonNames'), - cook=list_split, - long_arg=True) -CLIENT_PROFILE = \ - Option('Assert the given profile for the host', - default=None, - cmd='-p', - odesc='<profile>', - cf=('client', 'profile')) -CLIENT_RETRIES = \ - Option('The number of times to retry network communication', - default='3', - cmd='-R', - odesc='<retry count>', - cf=('communication', 'retries')) -CLIENT_RETRY_DELAY = \ - Option('The time in seconds to wait between retries', - default='1', - cmd='-y', - odesc='<retry delay>', - cf=('communication', 'retry_delay')) -CLIENT_DRYRUN = \ - Option('Do not actually change the system', - default=False, - cmd='-n') -CLIENT_EXTRA_DISPLAY = \ - Option('enable extra entry output', - default=False, - cmd='-e') -CLIENT_PARANOID = \ - Option('Make automatic backups of config files', - default=False, - cmd='-P', - cf=('client', 'paranoid'), - cook=get_bool) -CLIENT_DRIVERS = \ - Option('Specify tool driver set', - default=[m[1] for m in walk_packages(path=toolpath)], - cmd='-D', - odesc='<driver1,driver2>', - cf=('client', 'drivers'), - cook=list_split) -CLIENT_CACHE = \ - Option('Store the configuration in a file', - default=None, - cmd='-c', - odesc='<cache path>') -CLIENT_REMOVE = \ - Option('Force removal of additional configuration items', - default=None, - cmd='-r', - odesc='<entry type|all>') -CLIENT_BUNDLE = \ - Option('Only configure the given bundle(s)', - default=[], - cmd='-b', - odesc='<bundle:bundle>', - cook=colon_split) -CLIENT_SKIPBUNDLE = \ - Option('Configure everything except the given bundle(s)', - default=[], - cmd='-B', - odesc='<bundle:bundle>', - cook=colon_split) -CLIENT_BUNDLEQUICK = \ - Option('Only verify/configure the given bundle(s)', - default=False, - cmd='-Q') -CLIENT_INDEP = \ - Option('Only configure independent entries, ignore bundles', - default=False, - cmd='-z') -CLIENT_SKIPINDEP = \ - Option('Do not configure independent entries', - default=False, - cmd='-Z') -CLIENT_KEVLAR = \ - Option('Run in kevlar (bulletproof) mode', - default=False, - cmd='-k', ) -CLIENT_FILE = \ - Option('Configure from a file rather than querying the server', - default=None, - cmd='-f', - odesc='<specification path>') -CLIENT_QUICK = \ - Option('Disable some checksum verification', - default=False, - cmd='-q') -CLIENT_USER = \ - Option('The user to provide for authentication', - default='root', - cmd='-u', - odesc='<user>', - cf=('communication', 'user')) -CLIENT_SERVICE_MODE = \ - Option('Set client service mode', - default='default', - cmd='-s', - odesc='<default|disabled|build>') -CLIENT_TIMEOUT = \ - Option('Set the client XML-RPC timeout', - default=90, - cmd='-t', - odesc='<timeout>', - cf=('communication', 'timeout')) -CLIENT_DLIST = \ - Option('Run client in server decision list mode', - default='none', - cmd='-l', - odesc='<whitelist|blacklist|none>', - cf=('client', 'decision')) -CLIENT_DECISION_LIST = \ - Option('Decision List', - default=False, - cmd='--decision-list', - odesc='<file>', - long_arg=True) -CLIENT_EXIT_ON_PROBE_FAILURE = \ - Option("The client should exit if a probe fails", - default=True, - cmd='--exit-on-probe-failure', - long_arg=True, - cf=('client', 'exit_on_probe_failure'), - cook=get_bool) -CLIENT_PROBE_TIMEOUT = \ - Option("Timeout when running client probes", - default=None, - cf=('client', 'probe_timeout'), - cook=get_timeout) -CLIENT_COMMAND_TIMEOUT = \ - Option("Timeout when client runs other external commands (not probes)", - default=None, - cf=('client', 'command_timeout'), - cook=get_timeout) - -# bcfg2-test and bcfg2-lint options -TEST_NOSEOPTS = \ - Option('Options to pass to nosetests. Only honored with --children 0', - default=[], - cmd='--nose-options', - odesc='<opts>', - cf=('bcfg2_test', 'nose_options'), - cook=shlex.split, - long_arg=True) -TEST_IGNORE = \ - Option('Ignore these entries if they fail to build.', - default=[], - cmd='--ignore', - odesc='<Type>:<name>,<Type>:<name>', - cf=('bcfg2_test', 'ignore_entries'), - cook=list_split, - long_arg=True) -TEST_CHILDREN = \ - Option('Spawn this number of children for bcfg2-test (python 2.6+)', - default=0, - cmd='--children', - odesc='<children>', - cf=('bcfg2_test', 'children'), - cook=get_int, - long_arg=True) -TEST_XUNIT = \ - Option('Output an XUnit result file with --children', - default=None, - cmd='--xunit', - odesc='<xunit file>', - cf=('bcfg2_test', 'xunit'), - long_arg=True) -LINT_CONFIG = \ - Option('Specify bcfg2-lint configuration file', - default='/etc/bcfg2-lint.conf', - cmd='--lint-config', - odesc='<conffile>', - long_arg=True) -LINT_PLUGINS = \ - Option('bcfg2-lint plugin list', - default=None, # default is Bcfg2.Server.Lint.__all__ - cf=('lint', 'plugins'), - cook=list_split) -LINT_SHOW_ERRORS = \ - Option('Show error handling', - default=False, - cmd='--list-errors', - long_arg=True) -LINT_FILES_ON_STDIN = \ - Option('Operate on a list of files supplied on stdin', - default=False, - cmd='--stdin', - long_arg=True) - -# individual client tool options -CLIENT_APT_TOOLS_INSTALL_PATH = \ - Option('Apt tools install path', - default='/usr', - cf=('APT', 'install_path')) -CLIENT_APT_TOOLS_VAR_PATH = \ - Option('Apt tools var path', - default='/var', - cf=('APT', 'var_path')) -CLIENT_SYSTEM_ETC_PATH = \ - Option('System etc path', - default='/etc', - cf=('APT', 'etc_path')) -CLIENT_PORTAGE_BINPKGONLY = \ - Option('Portage binary packages only', - default=False, - cf=('Portage', 'binpkgonly'), - cook=get_bool) -CLIENT_RPM_INSTALLONLY = \ - Option('RPM install-only packages', - default=['kernel', 'kernel-bigmem', 'kernel-enterprise', - 'kernel-smp', 'kernel-modules', 'kernel-debug', - 'kernel-unsupported', 'kernel-devel', 'kernel-source', - 'kernel-default', 'kernel-largesmp-devel', - 'kernel-largesmp', 'kernel-xen', 'gpg-pubkey'], - cf=('RPM', 'installonlypackages'), - cook=list_split) -CLIENT_RPM_PKG_CHECKS = \ - Option("Perform RPM package checks", - default=True, - cf=('RPM', 'pkg_checks'), - cook=get_bool) -CLIENT_RPM_PKG_VERIFY = \ - Option("Perform RPM package verify", - default=True, - cf=('RPM', 'pkg_verify'), - cook=get_bool) -CLIENT_RPM_INSTALLED_ACTION = \ - Option("RPM installed action", - default="install", - cf=('RPM', 'installed_action')) -CLIENT_RPM_ERASE_FLAGS = \ - Option("RPM erase flags", - default=["allmatches"], - cf=('RPM', 'erase_flags'), - cook=list_split) -CLIENT_RPM_VERSION_FAIL_ACTION = \ - Option("RPM version fail action", - default="upgrade", - cf=('RPM', 'version_fail_action')) -CLIENT_RPM_VERIFY_FAIL_ACTION = \ - Option("RPM verify fail action", - default="reinstall", - cf=('RPM', 'verify_fail_action')) -CLIENT_RPM_VERIFY_FLAGS = \ - Option("RPM verify flags", - default=[], - cf=('RPM', 'verify_flags'), - cook=list_split) -CLIENT_YUM_PKG_CHECKS = \ - Option("Perform YUM package checks", - default=True, - cf=('YUM', 'pkg_checks'), - cook=get_bool) -CLIENT_YUM_PKG_VERIFY = \ - Option("Perform YUM package verify", - default=True, - cf=('YUM', 'pkg_verify'), - cook=get_bool) -CLIENT_YUM_INSTALLED_ACTION = \ - Option("YUM installed action", - default="install", - cf=('YUM', 'installed_action')) -CLIENT_YUM_VERSION_FAIL_ACTION = \ - Option("YUM version fail action", - default="upgrade", - cf=('YUM', 'version_fail_action')) -CLIENT_YUM_VERIFY_FAIL_ACTION = \ - Option("YUM verify fail action", - default="reinstall", - cf=('YUM', 'verify_fail_action')) -CLIENT_YUM_VERIFY_FLAGS = \ - Option("YUM verify flags", - default=[], - cf=('YUM', 'verify_flags'), - cook=list_split) -CLIENT_POSIX_UID_WHITELIST = \ - Option("UID ranges the POSIXUsers tool will manage", - default=[], - cf=('POSIXUsers', 'uid_whitelist'), - cook=list_split) -CLIENT_POSIX_GID_WHITELIST = \ - Option("GID ranges the POSIXUsers tool will manage", - default=[], - cf=('POSIXUsers', 'gid_whitelist'), - cook=list_split) -CLIENT_POSIX_UID_BLACKLIST = \ - Option("UID ranges the POSIXUsers tool will not manage", - default=[], - cf=('POSIXUsers', 'uid_blacklist'), - cook=list_split) -CLIENT_POSIX_GID_BLACKLIST = \ - Option("GID ranges the POSIXUsers tool will not manage", - default=[], - cf=('POSIXUsers', 'gid_blacklist'), - cook=list_split) - -# Logging options -LOGGING_FILE_PATH = \ - Option('Set path of file log', - default=None, - cmd='-o', - odesc='<path>', - cf=('logging', 'path')) -LOGGING_SYSLOG = \ - Option('Log to syslog', - default=True, - cook=get_bool, - cf=('logging', 'syslog')) -DEBUG = \ - Option("Enable debugging output", - default=False, - cmd='-d', - cook=get_bool, - cf=('logging', 'debug')) -VERBOSE = \ - Option("Enable verbose output", - default=False, - cmd='-v', - cook=get_bool, - cf=('logging', 'verbose')) -LOG_PERFORMANCE = \ - Option("Periodically log performance statistics", - default=False, - cf=('logging', 'performance')) -PERFLOG_INTERVAL = \ - Option("Performance statistics logging interval in seconds", - default=300.0, - cook=get_timeout, - cf=('logging', 'performance_interval')) - -# Plugin-specific options -CFG_VALIDATION = \ - Option('Run validation on Cfg files', - default=True, - cmd='--cfg-validation', - cf=('cfg', 'validation'), - long_arg=True, - cook=get_bool) - -# bcfg2-crypt options -ENCRYPT = \ - Option('Encrypt the specified file', - default=False, - cmd='--encrypt', - long_arg=True) -DECRYPT = \ - Option('Decrypt the specified file', - default=False, - cmd='--decrypt', - long_arg=True) -CRYPT_STDOUT = \ - Option('Decrypt or encrypt the specified file to stdout', - default=False, - cmd='--stdout', - long_arg=True) -CRYPT_PASSPHRASE = \ - Option('Encryption passphrase name', - default=None, - cmd='-p', - odesc='<passphrase>') -CRYPT_XPATH = \ - Option('XPath expression to select elements to encrypt', - default=None, - cmd='--xpath', - odesc='<xpath>', - long_arg=True) -CRYPT_PROPERTIES = \ - Option('Encrypt the specified file as a Properties file', - default=False, - cmd="--properties", - long_arg=True) -CRYPT_CFG = \ - Option('Encrypt the specified file as a Cfg file', - default=False, - cmd="--cfg", - long_arg=True) -CRYPT_REMOVE = \ - Option('Remove the plaintext file after encrypting', - default=False, - cmd="--remove", - long_arg=True) - -# Option groups -CLI_COMMON_OPTIONS = dict(configfile=CFILE, - debug=DEBUG, - help=HELP, - version=VERSION, - verbose=VERBOSE, - encoding=ENCODING, - logging=LOGGING_FILE_PATH, - syslog=LOGGING_SYSLOG) - -DAEMON_COMMON_OPTIONS = dict(daemon=DAEMON, - umask=SERVER_UMASK, - listen_all=SERVER_LISTEN_ALL, - daemon_uid=SERVER_DAEMON_USER, - daemon_gid=SERVER_DAEMON_GROUP) - -SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY, - plugins=SERVER_PLUGINS, - password=SERVER_PASSWORD, - filemonitor=SERVER_FILEMONITOR, - ignore=SERVER_FAM_IGNORE, - fam_blocking=SERVER_FAM_BLOCK, - location=SERVER_LOCATION, - key=SERVER_KEY, - cert=SERVER_CERT, - ca=SERVER_CA, - protocol=SERVER_PROTOCOL, - web_configfile=WEB_CFILE, - backend=SERVER_BACKEND, - vcs_root=SERVER_VCS_ROOT, - authentication=SERVER_AUTHENTICATION, - perflog=LOG_PERFORMANCE, - perflog_interval=PERFLOG_INTERVAL, - children=SERVER_CHILDREN) - -CRYPT_OPTIONS = dict(encrypt=ENCRYPT, - decrypt=DECRYPT, - crypt_stdout=CRYPT_STDOUT, - passphrase=CRYPT_PASSPHRASE, - xpath=CRYPT_XPATH, - properties=CRYPT_PROPERTIES, - cfg=CRYPT_CFG, - remove=CRYPT_REMOVE) - -PATH_METADATA_OPTIONS = dict(owner=MDATA_OWNER, - group=MDATA_GROUP, - mode=MDATA_MODE, - secontext=MDATA_SECONTEXT, - important=MDATA_IMPORTANT, - paranoid=MDATA_PARANOID, - sensitive=MDATA_SENSITIVE) - -DRIVER_OPTIONS = \ - dict(apt_install_path=CLIENT_APT_TOOLS_INSTALL_PATH, - apt_var_path=CLIENT_APT_TOOLS_VAR_PATH, - apt_etc_path=CLIENT_SYSTEM_ETC_PATH, - portage_binpkgonly=CLIENT_PORTAGE_BINPKGONLY, - rpm_installonly=CLIENT_RPM_INSTALLONLY, - rpm_pkg_checks=CLIENT_RPM_PKG_CHECKS, - rpm_pkg_verify=CLIENT_RPM_PKG_VERIFY, - rpm_installed_action=CLIENT_RPM_INSTALLED_ACTION, - rpm_erase_flags=CLIENT_RPM_ERASE_FLAGS, - rpm_version_fail_action=CLIENT_RPM_VERSION_FAIL_ACTION, - rpm_verify_fail_action=CLIENT_RPM_VERIFY_FAIL_ACTION, - rpm_verify_flags=CLIENT_RPM_VERIFY_FLAGS, - yum_pkg_checks=CLIENT_YUM_PKG_CHECKS, - yum_pkg_verify=CLIENT_YUM_PKG_VERIFY, - yum_installed_action=CLIENT_YUM_INSTALLED_ACTION, - yum_version_fail_action=CLIENT_YUM_VERSION_FAIL_ACTION, - yum_verify_fail_action=CLIENT_YUM_VERIFY_FAIL_ACTION, - yum_verify_flags=CLIENT_YUM_VERIFY_FLAGS, - posix_uid_whitelist=CLIENT_POSIX_UID_WHITELIST, - posix_gid_whitelist=CLIENT_POSIX_UID_WHITELIST, - posix_uid_blacklist=CLIENT_POSIX_UID_BLACKLIST, - posix_gid_blacklist=CLIENT_POSIX_UID_BLACKLIST) - -CLIENT_COMMON_OPTIONS = \ - dict(extra=CLIENT_EXTRA_DISPLAY, - quick=CLIENT_QUICK, - lockfile=LOCKFILE, - drivers=CLIENT_DRIVERS, - dryrun=CLIENT_DRYRUN, - paranoid=CLIENT_PARANOID, - ppath=PARANOID_PATH, - max_copies=PARANOID_MAX_COPIES, - bundle=CLIENT_BUNDLE, - skipbundle=CLIENT_SKIPBUNDLE, - bundle_quick=CLIENT_BUNDLEQUICK, - indep=CLIENT_INDEP, - skipindep=CLIENT_SKIPINDEP, - file=CLIENT_FILE, - interactive=INTERACTIVE, - cache=CLIENT_CACHE, - profile=CLIENT_PROFILE, - remove=CLIENT_REMOVE, - server=SERVER_LOCATION, - user=CLIENT_USER, - password=SERVER_PASSWORD, - retries=CLIENT_RETRIES, - retry_delay=CLIENT_RETRY_DELAY, - kevlar=CLIENT_KEVLAR, - omit_lock_check=OMIT_LOCK_CHECK, - decision=CLIENT_DLIST, - servicemode=CLIENT_SERVICE_MODE, - key=CLIENT_KEY, - certificate=CLIENT_CERT, - ca=CLIENT_CA, - serverCN=CLIENT_SCNS, - timeout=CLIENT_TIMEOUT, - decision_list=CLIENT_DECISION_LIST, - probe_exit=CLIENT_EXIT_ON_PROBE_FAILURE, - probe_timeout=CLIENT_PROBE_TIMEOUT, - command_timeout=CLIENT_COMMAND_TIMEOUT) -CLIENT_COMMON_OPTIONS.update(DRIVER_OPTIONS) -CLIENT_COMMON_OPTIONS.update(CLI_COMMON_OPTIONS) - -DATABASE_COMMON_OPTIONS = dict(web_configfile=WEB_CFILE, - configfile=CFILE, - db_engine=DB_ENGINE, - db_name=DB_NAME, - db_user=DB_USER, - db_password=DB_PASSWORD, - db_host=DB_HOST, - db_port=DB_PORT, - time_zone=DJANGO_TIME_ZONE, - django_debug=DJANGO_DEBUG, - web_prefix=DJANGO_WEB_PREFIX) - -REPORTING_COMMON_OPTIONS = dict(reporting_file_limit=REPORTING_FILE_LIMIT, - reporting_transport=REPORTING_TRANSPORT) - -TEST_COMMON_OPTIONS = dict(noseopts=TEST_NOSEOPTS, - test_ignore=TEST_IGNORE, - children=TEST_CHILDREN, - xunit=TEST_XUNIT, - validate=CFG_VALIDATION) - -INFO_COMMON_OPTIONS = dict(ppath=PARANOID_PATH, - max_copies=PARANOID_MAX_COPIES) -INFO_COMMON_OPTIONS.update(CLI_COMMON_OPTIONS) -INFO_COMMON_OPTIONS.update(SERVER_COMMON_OPTIONS) - - -class OptionParser(OptionSet): - """ OptionParser bootstraps option parsing, getting the value of - the config file. This should only be instantiated by - :func:`get_option_parser`, below, not by individual plugins or - scripts. """ - - def __init__(self, args, argv=None, quiet=False): - if argv is None: - argv = sys.argv[1:] - # the bootstrap is always quiet, since it's running with a - # default config file and so might produce warnings otherwise - self.bootstrap = OptionSet([('configfile', CFILE)], quiet=True) - self.bootstrap.parse(argv, do_getopt=False) - OptionSet.__init__(self, args, configfile=self.bootstrap['configfile'], - quiet=quiet) - self.optinfo = copy.copy(args) - # these will be set by parse() and then used by reparse() - self.argv = [] - self.do_getopt = True - - def reparse(self, argv=None, do_getopt=None): - """ parse the options again, taking any changes (e.g., to the - config file) into account """ - self.parse(argv=argv, do_getopt=do_getopt) - - def parse(self, argv=None, do_getopt=None): - for key, opt in self.optinfo.items(): - self[key] = opt - if "args" not in self.optinfo and "args" in self: - del self['args'] - self.argv = argv or sys.argv[1:] - if self.do_getopt is None: - if do_getopt: - self.do_getopt = do_getopt - else: - self.do_getopt = True - if do_getopt is None: - do_getopt = self.do_getopt - OptionSet.parse(self, self.argv, do_getopt=do_getopt) - - def add_option(self, name, opt): - """ Add an option to the parser """ - self[name] = opt - self.optinfo[name] = opt - - def add_options(self, options): - """ Add a set of options to the parser """ - self.update(options) - self.optinfo.update(options) - - def update(self, optdict): - dict.update(self, optdict) - self.optinfo.update(optdict) - - -#: A module-level OptionParser object that all plugins, etc., can use. -#: This should not be used directly, but retrieved via -#: :func:`get_option_parser`. -_PARSER = None - - -def load_option_parser(args, argv=None, quiet=False): - """ Load an :class:`Bcfg2.Options.OptionParser` object, caching it - in :attr:`_PARSER` for later retrieval via - :func:`get_option_parser`. - - :param args: The argument set to parse. - :type args: dict of :class:`Bcfg2.Options.Option` objects - :param argv: The command-line argument list. If this is not - provided, :attr:`sys.argv` will be used. - :type argv: list of strings - :param quiet: Be quiet when bootstrapping the argument parser. - :type quiet: bool - :returns: :class:`Bcfg2.Options.OptionParser` - """ - global _PARSER # pylint: disable=W0603 - _PARSER = OptionParser(args, argv=argv, quiet=quiet) - return _PARSER - - -def get_option_parser(): - """ Get an already-created :class:`Bcfg2.Options.OptionParser` object. If - :attr:`_PARSER` has not been populated, then a new OptionParser - will be created with basic arguments. - - :returns: :class:`Bcfg2.Options.OptionParser` - """ - if _PARSER is None: - return load_option_parser(CLI_COMMON_OPTIONS) - return _PARSER diff --git a/src/lib/Bcfg2/Options/Actions.py b/src/lib/Bcfg2/Options/Actions.py new file mode 100644 index 000000000..cb83f3ae7 --- /dev/null +++ b/src/lib/Bcfg2/Options/Actions.py @@ -0,0 +1,164 @@ +""" Custom argparse actions """ + +import sys +import argparse +from Parser import get_parser + +__all__ = ["ConfigFileAction", "ComponentAction", "PluginsAction"] + + +class ComponentAction(argparse.Action): + """ ComponentAction automatically imports classes and modules + based on the value of the option, and automatically collects + options from the loaded classes and modules. It cannot be used by + itself, but must be subclassed, with either :attr:`mapping` or + :attr:`bases` overridden. See + :class:`Bcfg2.Options.PluginsAction` for an example. + + ComponentActions expect to be given a list of class names. If + :attr:`bases` is overridden, then it will attempt to import those + classes from identically named modules within the given bases. + For instance: + + .. code-block:: python + + class FooComponentAction(Bcfg2.Options.ComponentAction): + bases = ["Bcfg2.Server.Foo"] + + + class FooLoader(object): + options = [ + Bcfg2.Options.Option( + "--foo", + type=Bcfg2.Options.Types.comma_list, + default=["One"], + action=FooComponentAction)] + + If "--foo One,Two,Three" were given on the command line, then + ``FooComponentAction`` would attempt to import + ``Bcfg2.Server.Foo.One.One``, ``Bcfg2.Server.Foo.Two.Two``, and + ``Bcfg2.Server.Foo.Three.Three``. (It would also call + :func:`Bcfg2.Options.Parser.add_component` with each of those + classes as arguments.) + + Note that, although ComponentActions expect lists of components + (by default; this can be overridden by setting :attr:`islist`), + you must still explicitly specify a ``type`` argument to the + :class:`Bcfg2.Options.Option` constructor to split the value into + a list. + + Note also that, unlike other actions, the default value of a + ComponentAction option does not need to be the actual literal + final value. (I.e., you don't have to import + ``Bcfg2.Server.Foo.One.One`` and set it as the default in the + example above; the string "One" suffices.) + """ + + #: A list of parent modules where modules or classes should be + #: imported from. + bases = [] + + #: A mapping of ``<name> => <object>`` that components will be + #: loaded from. This can be used to permit much more complex + #: behavior than just a list of :attr:`bases`. + mapping = dict() + + #: If ``module`` is True, then only the module will be loaded, not + #: a class from the module. For instance, in the example above, + #: ``FooComponentAction`` would attempt instead to import + #: ``Bcfg2.Server.Foo.One``, ``Bcfg2.Server.Foo.Two``, and + #: ``Bcfg2.Server.Foo.Three``. + module = False + + #: By default, ComponentActions expect a list of components to + #: load. If ``islist`` is False, then it will only expect a + #: single component. + islist = True + + #: If ``fail_silently`` is True, then failures to import modules + #: or classes will not be logged. This is useful when the default + #: is to import everything, some of which are expected to fail. + fail_silently = False + + def __init__(self, *args, **kwargs): + if self.mapping: + if 'choices' not in kwargs: + kwargs['choices'] = self.mapping.keys() + self._final = False + argparse.Action.__init__(self, *args, **kwargs) + + def _import(self, module, name): + try: + return getattr(__import__(module, fromlist=[name]), name) + except (AttributeError, ImportError): + if not self.fail_silently: + print("Failed to load %s from %s: %s" % + (name, module, sys.exc_info()[1])) + return None + + def _load_component(self, name): + """ Import a single class or module, adding it as a component to + the parser. + + :param name: The name of the class or module to import, without + the base prepended. + :type name: string + :returns: the imported class or module + """ + cls = None + if self.mapping and name in self.mapping: + cls = self.mapping[name] + elif "." in name: + cls = self._import(*name.rsplit(".", 1)) + else: + for base in self.bases: + if self.module: + mod = base + else: + mod = "%s.%s" % (base, name) + cls = self._import(mod, name) + if cls is not None: + break + if cls: + get_parser().add_component(cls) + else: + print("Could not load component %s" % name) + return cls + + def finalize(self, parser, namespace): + """ Finalize a default value by loading the components given + in it. This lets a default be specified with a list of + strings instead of a list of classes. """ + if not self._final: + self.__call__(parser, namespace, self.default) + + def __call__(self, parser, namespace, values, option_string=None): + if values is None: + result = None + else: + if self.islist: + result = [] + for val in values: + cls = self._load_component(val) + if cls is not None: + result.append(cls) + else: + result = self._load_component(values) + self._final = True + setattr(namespace, self.dest, values) + + +class ConfigFileAction(argparse.Action): + """ ConfigFileAction automatically loads and parses a + supplementary config file (e.g., ``bcfg2-web.conf`` or + ``bcfg2-lint.conf``). """ + + def __call__(self, parser, namespace, values, option_string=None): + get_parser().add_config_file(self.dest, values) + setattr(namespace, self.dest, values) + + +class PluginsAction(ComponentAction): + """ :class:`Bcfg2.Options.ComponentAction` subclass for loading + Bcfg2 server plugins. """ + bases = ['Bcfg2.Server.Plugins'] diff --git a/src/lib/Bcfg2/Options/Common.py b/src/lib/Bcfg2/Options/Common.py new file mode 100644 index 000000000..302be61f4 --- /dev/null +++ b/src/lib/Bcfg2/Options/Common.py @@ -0,0 +1,135 @@ +""" Common options used in multiple different contexts. """ + +import Types +from Actions import PluginsAction, ComponentAction +from Parser import repository as _repository_option +from Options import Option, PathOption, BooleanOption + +__all__ = ["Common"] + + +class classproperty(object): + """ Decorator that can be used to create read-only class + properties. """ + + def __init__(self, getter): + self.getter = getter + + def __get__(self, instance, owner): + return self.getter(owner) + + +class ReportingTransportAction(ComponentAction): + """ :class:`Bcfg2.Options.ComponentAction` that loads a single + reporting transport from :mod:`Bcfg2.Reporting.Transport`. """ + islist = False + bases = ['Bcfg2.Reporting.Transport'] + + +class ReportingStorageAction(ComponentAction): + """ :class:`Bcfg2.Options.ComponentAction` that loads a single + reporting storage driver from :mod:`Bcfg2.Reporting.Storage`. """ + islist = False + bases = ['Bcfg2.Reporting.Storage'] + + +class Common(object): + """ Common options used in multiple different contexts. """ + _plugins = None + _filemonitor = None + _reporting_storage = None + _reporting_transport = None + + @classproperty + def plugins(cls): + """ Load a list of Bcfg2 server plugins """ + if cls._plugins is None: + cls._plugins = Option( + cf=('server', 'plugins'), + type=Types.comma_list, help="Server plugin list", + action=PluginsAction, + default=['Bundler', 'Cfg', 'Metadata', 'Pkgmgr', 'Rules', + 'SSHbase']) + return cls._plugins + + @classproperty + def filemonitor(cls): + """ Load a single Bcfg2 file monitor (from + :attr:`Bcfg2.Server.FileMonitor.available`) """ + if cls._filemonitor is None: + import Bcfg2.Server.FileMonitor + + class FileMonitorAction(ComponentAction): + islist = False + mapping = Bcfg2.Server.FileMonitor.available + + cls._filemonitor = Option( + cf=('server', 'filemonitor'), action=FileMonitorAction, + default='default', help='Server file monitoring driver') + return cls._filemonitor + + @classproperty + def reporting_storage(cls): + """ Load a Reporting storage backend """ + if cls._reporting_storage is None: + cls._reporting_storage = Option( + cf=('reporting', 'storage'), dest="reporting_storage", + help='Reporting storage engine', + action=ReportingStorageAction, default='DjangoORM') + return cls._reporting_storage + + @classproperty + def reporting_transport(cls): + """ Load a Reporting transport backend """ + if cls._reporting_transport is None: + cls._reporting_transport = Option( + cf=('reporting', 'transport'), dest="reporting_transport", + help='Reporting transport', + action=ReportingTransportAction, default='DirectStore') + return cls._reporting_transport + + #: Set the path to the Bcfg2 repository + repository = _repository_option + + #: Daemonize process, storing PID + daemon = PathOption( + '-D', '--daemon', help="Daemonize process, storing PID") + + #: Run interactively, prompting the user for each change + interactive = BooleanOption( + "-I", "--interactive", + help='Run interactively, prompting the user for each change') + + #: Log to syslog + syslog = BooleanOption( + cf=('logging', 'syslog'), help="Log to syslog") + + #: Server location + location = Option( + '-S', '--server', cf=('components', 'bcfg2'), + default='https://localhost:6789', metavar='<https://server:port>', + help="Server location") + + #: Communication password + password = Option( + '-x', '--password', cf=('communication', 'password'), + metavar='<password>', help="Communication Password") + + #: Path to SSL key + ssl_key = PathOption( + '--ssl-key', cf=('communication', 'key'), dest="key", + help='Path to SSL key', default="/etc/pki/tls/private/bcfg2.key") + + #: Path to SSL certificate + ssl_cert = PathOption( + cf=('communication', 'certificate'), dest="cert", + help='Path to SSL certificate', default="/etc/pki/tls/certs/bcfg2.crt") + + #: Path to SSL CA certificate + ssl_ca = PathOption( + cf=('communication', 'ca'), help='Path to SSL CA Cert') + + #: Default Path paranoid setting + default_paranoid = Option( + cf=('mdata', 'paranoid'), dest="default_paranoid", default='true', + choices=['true', 'false'], help='Default Path paranoid setting') diff --git a/src/lib/Bcfg2/Options/OptionGroups.py b/src/lib/Bcfg2/Options/OptionGroups.py new file mode 100644 index 000000000..b14c523f4 --- /dev/null +++ b/src/lib/Bcfg2/Options/OptionGroups.py @@ -0,0 +1,209 @@ +""" Option grouping classes """ + +import re +import copy +import fnmatch +from Options import Option +from itertools import chain + +__all__ = ["OptionGroup", "ExclusiveOptionGroup", "Subparser", + "WildcardSectionGroup"] + +#: A dict that records a mapping of argparse action name (e.g., +#: "store_true") to the argparse Action class for it. See +#: :func:`_get_action_class` +_action_map = dict() + + +class OptionContainer(list): + """ Parent class of all option groups """ + + def list_options(self): + """ Get a list of all options contained in this group, + including options contained in option groups in this group, + and so on. """ + return list(chain(*[o.list_options() for o in self])) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, list.__repr__(self)) + + def add_to_parser(self, parser): + """ Add this option group to a :class:`Bcfg2.Options.Parser` + object. """ + for opt in self: + opt.add_to_parser(parser) + + +class OptionGroup(OptionContainer): + """ Generic option group that is used only to organize options. + This uses :meth:`argparse.ArgumentParser.add_argument_group` + behind the scenes. """ + + def __init__(self, *items, **kwargs): + """ + :param \*args: Child options + :type \*args: Bcfg2.Options.Option + :param title: The title of the option group + :type title: string + :param description: A longer description of the option group + :param description: string + """ + OptionContainer.__init__(self, items) + self.title = kwargs.pop('title') + self.description = kwargs.pop('description', None) + + def add_to_parser(self, parser): + group = parser.add_argument_group(self.title, self.description) + OptionContainer.add_to_parser(self, group) + + +class ExclusiveOptionGroup(OptionContainer): + """ Option group that ensures that only one argument in the group + is present. This uses + :meth:`argparse.ArgumentParser.add_mutually_exclusive_group` + behind the scenes.""" + + def __init__(self, *items, **kwargs): + """ + :param \*args: Child options + :type \*args: Bcfg2.Options.Option + :param required: Exactly one argument in the group *must* be + specified. + :type required: boolean + """ + OptionContainer.__init__(self, items) + self.required = kwargs.pop('required', False) + + def add_to_parser(self, parser): + group = parser.add_mutually_exclusive_group(required=self.required) + OptionContainer.add_to_parser(self, group) + + +class Subparser(OptionContainer): + """ Option group that adds options in it to a subparser. This + uses a lot of functionality tied to `argparse Sub-commands + <http://docs.python.org/dev/library/argparse.html#sub-commands>`_. + + The subcommand string itself is stored in the + :attr:`Bcfg2.Options.setup` namespace as ``subcommand``. + + This is commonly used with :class:`Bcfg2.Options.Subcommand` + groups. + """ + + _subparsers = dict() + + def __init__(self, *items, **kwargs): + """ + :param \*args: Child options + :type \*args: Bcfg2.Options.Option + :param name: The name of the subparser. Required. + :type name: string + :param help: A help message for the subparser + :param help: string + """ + self.name = kwargs.pop('name') + self.help = kwargs.pop('help', None) + OptionContainer.__init__(self, items) + + def __repr__(self): + return "%s %s(%s)" % (self.__class__.__name__, + self.name, + list.__repr__(self)) + + def add_to_parser(self, parser): + if parser not in self._subparsers: + self._subparsers[parser] = parser.add_subparsers(dest='subcommand') + subparser = self._subparsers[parser].add_parser(self.name, + help=self.help) + OptionContainer.add_to_parser(self, subparser) + + +class WildcardSectionGroup(OptionContainer, Option): + """ WildcardSectionGroups contain options that may exist in + several different sections of the config that match a glob. It + works by creating options on the fly to match the sections + described in the glob. For example, consider: + + .. code-block:: python + + options = [ + Bcfg2.Options.WildcardSectionGroup( + Bcfg2.Options.Option(cf=("myplugin:*", "number"), type=int), + Bcfg2.Options.Option(cf=("myplugin:*", "description"))] + + If the config file contained ``[myplugin:foo]`` and + ``[myplugin:bar]`` sections, then this would automagically create + options for each of those. The end result would be: + + .. code-block:: python + + >>> Bcfg2.Options.setup + Namespace(myplugin_bar_description='Bar description', myplugin_bar_number=2, myplugin_foo_description='Foo description', myplugin_foo_number=1, myplugin_sections=['myplugin:foo', 'myplugin:bar']) + + All options must have the same section glob. + + The options are stored in an automatically-generated destination + given by:: + + <prefix><section>_<destination> + + ``<destination>`` is the original `dest + <http://docs.python.org/dev/library/argparse.html#dest>`_ of the + option. ``<section>`` is the section that it's found in. + ``<prefix>`` is automatically generated from the section glob by + replacing all consecutive characters disallowed in Python variable + names into underscores. (This can be overridden with the + constructor.) + + This group stores an additional option, the sections themselves, + in an option given by ``<prefix>sections``. + """ + + #: Regex to automatically get a destination for this option + _dest_re = re.compile(r'(\A(_|[^A-Za-z])+)|((_|[^A-Za-z0-9])+)') + + def __init__(self, *items, **kwargs): + """ + :param \*args: Child options + :type \*args: Bcfg2.Options.Option + :param prefix: The prefix to use for options generated by this + option group. By default this is generated + automatically from the config glob; see above + for details. + :type prefix: string + :param dest: The destination for the list of known sections + that match the glob. + :param dest: string + """ + OptionContainer.__init__(self, []) + self._section_glob = items[0].cf[0] + # get a default destination + self._prefix = kwargs.get("prefix", + self._dest_re.sub('_', self._section_glob)) + Option.__init__(self, dest=kwargs.get('dest', + self._prefix + "sections")) + self._options = items + + def list_options(self): + return [self] + OptionContainer.list_options(self) + + def from_config(self, cfp): + sections = [] + for section in cfp.sections(): + if fnmatch.fnmatch(section, self._section_glob): + sections.append(section) + newopts = [] + for opt_tmpl in self._options: + option = copy.deepcopy(opt_tmpl) + option.cf = (section, option.cf[1]) + option.dest = prefix + section + "_" + option.dest + newopts.append(option) + self.extend(newopts) + for parser in self.parsers: + parser.add_options(newopts) + return sections + + def add_to_parser(self, parser): + Option.add_to_parser(self, parser) + OptionContainer.add_to_parser(self, parser) diff --git a/src/lib/Bcfg2/Options/Options.py b/src/lib/Bcfg2/Options/Options.py new file mode 100644 index 000000000..18e5cc75d --- /dev/null +++ b/src/lib/Bcfg2/Options/Options.py @@ -0,0 +1,305 @@ +""" The base :class:`Bcfg2.Options.Option` object represents an +option. Unlike options in :mod:`argparse`, an Option object does not +need to be associated with an option parser; it exists on its own.""" + +import os +import copy +import Types +import fnmatch +import argparse +from Bcfg2.Compat import ConfigParser + + +__all__ = ["Option", "BooleanOption", "PathOption", "PositionalArgument"] + +#: A dict that records a mapping of argparse action name (e.g., +#: "store_true") to the argparse Action class for it. See +#: :func:`_get_action_class` +_action_map = dict() + + +def _get_action_class(action_name): + """ Given an argparse action name (e.g., "store_true"), get the + related :class:`argparse.Action` class. The mapping that stores + this information in :mod:`argparse` itself is unfortunately + private, so it's an implementation detail that we shouldn't depend + on. So we just instantiate a dummy parser, add a dummy argument, + and determine the class that way. """ + if (isinstance(action_name, type) and + issubclass(action_name, argparse.Action)): + return action_name + if action_name not in _action_map: + action = argparse.ArgumentParser().add_argument(action_name, + action=action_name) + _action_map[action_name] = action.__class__ + return _action_map[action_name] + + +class Option(object): + """ Representation of an option that can be specified on the + command line, as an environment variable, or in a config + file. Precedence is in that order; that is, an option specified on + the command line takes precendence over an option given by the + environment, which takes precedence over an option specified in + the config file. """ + + #: Keyword arguments that should not be passed on to the + #: :class:`argparse.ArgumentParser` constructor + _local_args = ['cf', 'env', 'man'] + + def __init__(self, *args, **kwargs): + """ See :meth:`argparse.ArgumentParser.add_argument` for a + full list of accepted parameters. + + In addition to supporting all arguments and keyword arguments + from :meth:`argparse.ArgumentParser.add_argument`, several + additional keyword arguments are allowed. + + :param cf: A tuple giving the section and option name that + this argument can be referenced as in the config + file. The option name may contain the wildcard + '*', in which case the value will be a dict of all + options matching the glob. (To use a wildcard in + the section, use a + :class:`Bcfg2.Options.WildcardSectionGroup`.) + :type cf: tuple + :param env: An environment variable that the value of this + option can be taken from. + :type env: string + :param man: A detailed description of the option that will be + used to populate automatically-generated manpages. + :type man: string + """ + #: The options by which this option can be called. + #: (Coincidentally, this is also the list of arguments that + #: will be passed to + #: :meth:`argparse.ArgumentParser.add_argument` when this + #: option is added to a parser.) As a result, ``args`` can be + #: tested to see if this argument can be given on the command + #: line at all, or if it is purely a config file option. + self.args = args + self._kwargs = kwargs + + #: The tuple giving the section and option name for this + #: option in the config file + self.cf = None + + #: The environment variable that this option can take its + #: value from + self.env = None + + #: A detailed description of this option that will be used in + #: man pages. + self.man = None + + #: A list of :class:`Bcfg2.Options.Parser` objects to which + #: this option has been added. (There will be more than one + #: parser if this option is added to a subparser, for + #: instance.) + self.parsers = [] + + #: A dict of :class:`Bcfg2.Options.Parser` -> + #: :class:`argparse.Action` that gives the actions that + #: resulted from adding this option to each parser that it was + #: added to. If this option cannot be specified on the + #: command line (i.e., it only takes its value from the config + #: file), then this will be empty. + self.actions = dict() + + self.type = self._kwargs.get("type") + self.help = self._kwargs.get("help") + self._default = self._kwargs.get("default") + for kwarg in self._local_args: + setattr(self, kwarg, self._kwargs.pop(kwarg, None)) + if self.args: + # cli option + self._dest = None + else: + action_cls = _get_action_class(self._kwargs.get('action', 'store')) + # determine the name of this option. use, in order, the + # 'name' kwarg; the option name; the environment variable + # name. + self._dest = None + if 'dest' in self._kwargs: + self._dest = self._kwargs.pop('dest') + elif self.cf is not None: + self._dest = self.cf[1] + elif self.env is not None: + self._dest = self.env + kwargs = copy.copy(self._kwargs) + kwargs.pop("action", None) + self.actions[None] = action_cls(self._dest, self._dest, **kwargs) + + def __repr__(self): + sources = [] + if self.args: + sources.extend(self.args) + if self.cf: + sources.append("%s.%s" % self.cf) + if self.env: + sources.append("$" + self.env) + spec = ["sources=%s" % sources, "default=%s" % self.default] + spec.append("%d parsers" % (len(self.parsers))) + return 'Option(%s: %s)' % (self.dest, ", ".join(spec)) + + def list_options(self): + """ List options contained in this option. This exists to + provide a consistent interface with + :class:`Bcfg2.Options.OptionGroup` """ + return [self] + + def finalize(self): + """ Finalize the default value for this option. This is used + with actions (such as :class:`Bcfg2.Options.ComponentAction`) + that allow you to specify a default in a different format than + its final storage format; this can be called after it has been + determined that the default will be used (i.e., the option is + not given on the command line or in the config file) to store + the appropriate default value in the appropriate format.""" + for parser, action in self.actions.items(): + if parser is not None: + if hasattr(action, "finalize"): + action.finalize(parser, parser.namespace) + + def from_config(self, cfp): + """ Get the value of this option from the given + :class:`ConfigParser.ConfigParser`. If it is not found in the + config file, the default is returned. (If there is no + default, None is returned.) + + :param cfp: The config parser to get the option value from + :type cfp: ConfigParser.ConfigParser + :returns: The default value + """ + if not self.cf: + return None + if '*' in self.cf[1]: + if cfp.has_section(self.cf[0]): + # build a list of known options in this section, and + # exclude them + exclude = set() + for parser in self.parsers: + exclude.update(o.cf[1] + for o in parser.option_list + if o.cf and o.cf[0] == self.cf[0]) + return dict([(o, cfp.get(self.cf[0], o)) + for o in fnmatch.filter(cfp.options(self.cf[0]), + self.cf[1]) + if o not in exclude]) + else: + return dict() + else: + try: + val = cfp.getboolean(*self.cf) + except ValueError: + val = cfp.get(*self.cf) + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): + return None + if self.type: + return self.type(val) + else: + return val + + def default_from_config(self, cfp): + """ Set the default value of this option from the config file + or from the environment. + + :param cfp: The config parser to get the option value from + :type cfp: ConfigParser.ConfigParser + """ + if self.env and self.env in os.environ: + self.default = os.environ[self.env] + else: + val = self.from_config(cfp) + if val is not None: + self.default = val + + def _get_default(self): + return self._default + + def _set_default(self, value): + self._default = value + for action in self.actions.values(): + action.default = value + + #: The current default value of this option + default = property(_get_default, _set_default) + + def _get_dest(self): + return self._dest + + def _set_dest(self, value): + self._dest = value + for action in self.actions.values(): + action.dest = value + + #: The namespace destination of this option (see `dest + #: <http://docs.python.org/dev/library/argparse.html#dest>`_) + dest = property(_get_dest, _set_dest) + + def add_to_parser(self, parser): + """ Add this option to the given parser. + + :param parser: The parser to add the option to. + :type parser: Bcfg2.Options.Parser + :returns: argparse.Action + """ + self.parsers.append(parser) + if self.args: + # cli option + action = parser.add_argument(*self.args, **self._kwargs) + if not self._dest: + self._dest = action.dest + if self._default: + action.default = self._default + self.actions[parser] = action + # else, config file-only option + + +class PathOption(Option): + """ Shortcut for options that expect a path argument. Uses + :meth:`Bcfg2.Options.Types.path` to transform the argument into a + canonical path. + + The type of a path option can also be overridden to return an + option file-like object. For example: + + .. code-block:: python + + options = [ + Bcfg2.Options.PathOption( + "--input", type=argparse.FileType('r'), + help="The input file")] + """ + + def __init__(self, *args, **kwargs): + kwargs.setdefault('type', Types.path) + kwargs.setdefault('metavar', '<path>') + Option.__init__(self, *args, **kwargs) + + +class BooleanOption(Option): + """ Shortcut for boolean options. The default is False, but this + can easily be overridden: + + .. code-block:: python + + options = [ + Bcfg2.Options.PathOption( + "--dwim", default=True, help="Do What I Mean")] + """ + def __init__(self, *args, **kwargs): + if 'default' in kwargs and kwargs['default']: + kwargs.setdefault('action', 'store_false') + else: + kwargs.setdefault('action', 'store_true') + kwargs.setdefault('default', False) + Option.__init__(self, *args, **kwargs) + + +class PositionalArgument(Option): + """ Shortcut for positional arguments. """ + def __init__(self, *args, **kwargs): + if 'metavar' not in kwargs: + kwargs['metavar'] = '<%s>' % args[0] + Option.__init__(self, *args, **kwargs) diff --git a/src/lib/Bcfg2/Options/Parser.py b/src/lib/Bcfg2/Options/Parser.py new file mode 100644 index 000000000..6414cf98e --- /dev/null +++ b/src/lib/Bcfg2/Options/Parser.py @@ -0,0 +1,282 @@ +""" The option parser """ + +import os +import sys +import argparse +from Bcfg2.version import __version__ +from Bcfg2.Compat import ConfigParser +from Options import Option, PathOption, BooleanOption + +__all__ = ["setup", "OptionParserException", "Parser", "get_parser"] + + +#: The repository option. This is specified here (and imported into +#: :module:`Bcfg2.Options.Common`) rather than vice-versa due to +#: circular imports. +repository = PathOption( + '-Q', '--repository', cf=('server', 'repository'), + default='var/lib/bcfg2', help="Server repository path") + + +#: A module-level :class:`argparse.Namespace` object that stores all +#: configuration for Bcfg2. +setup = argparse.Namespace(version=__version__, + name="Bcfg2", + uri='http://trac.mcs.anl.gov/projects/bcfg2') + + +class OptionParserException(Exception): + """ Base exception raised for generic option parser errors """ + + +class Parser(argparse.ArgumentParser): + """ The Bcfg2 option parser. Most interfaces should not need to + instantiate a parser, but should instead use + :func:`Bcfg2.Options.get_parser` to get the parser that already + exists.""" + + #: Option for specifying the path to the Bcfg2 config file + configfile = PathOption('-C', '--config', + help="Path to configuration file", + default="/etc/bcfg2.conf") + + #: Builtin options that apply to all commands + options = [configfile, + BooleanOption('--version', help="Print the version and exit"), + Option('-E', '--encoding', metavar='<encoding>', + default='UTF-8', help="Encoding of config files", + cf=('components', 'encoding'))] + + def __init__(self, **kwargs): + """ See :class:`argparse.ArgumentParser` for a full list of + accepted parameters. + + In addition to supporting all arguments and keyword arguments + from :class:`argparse.ArgumentParser`, several additional + keyword arguments are allowed. + + :param components: A list of components to add to the parser. + :type components: list + :param namespace: The namespace to store options in. Default + is :attr:`Bcfg2.Options.setup`. + :type namespace: argparse.Namespace + :param add_base_options: Whether or not to add the options in + :attr:`Bcfg2.Options.Parser.options` + to the parser. Setting this to False + is default for subparsers. Default is + True. + :type add_base_options: bool + """ + self._cfp = ConfigParser.ConfigParser() + components = kwargs.pop('components', []) + + #: The namespace options will be stored in. + self.namespace = kwargs.pop('namespace', setup) + add_base_options = kwargs.pop('add_base_options', True) + + if 'add_help' not in kwargs: + kwargs['add_help'] = add_base_options + argparse.ArgumentParser.__init__(self, **kwargs) + + #: Whether or not parsing has completed on all current options. + self.parsed = False + + #: The argument list that was parsed. + self.argv = None + + #: Components that have been added to the parser + self.components = [] + + #: Options that have been added to the parser + self.option_list = [] + self._defaults_set = [] + self._config_files = [] + if add_base_options: + self.add_component(self) + for component in components: + self.add_component(component) + + def add_options(self, options): + """ Add an explicit list of options to the parser. When + possible, prefer :func:`Bcfg2.Options.Parser.add_component` to + add a whole component instead.""" + self.parsed = False + for option in options: + if option not in self.option_list: + self.option_list.extend(option.list_options()) + option.add_to_parser(self) + + def add_component(self, component): + """ Add a component (and all of its options) to the + parser. """ + if component not in self.components: + self.components.append(component) + if hasattr(component, "options"): + self.add_options(getattr(component, "options")) + + def _set_defaults(self): + for opt in self.option_list: + if opt not in self._defaults_set: + opt.default_from_config(self._cfp) + self._defaults_set.append(opt) + + def _parse_config_options(self): + """ populate the namespace with default values for any options + that aren't already in the namespace (i.e., options without + CLI arguments) """ + for opt in self.option_list[:]: + if not opt.args and opt.dest not in self.namespace: + value = opt.default + if value: + for parser, action in opt.actions.items(): + if parser is None: + action(self, self.namespace, value) + else: + action(parser, parser.namespace, value) + else: + setattr(self.namespace, opt.dest, value) + + def _finalize(self): + for opt in self.option_list[:]: + opt.finalize() + + def _reset_namespace(self): + self.parsed = False + for attr in dir(self.namespace): + if (not attr.startswith("_") and + attr not in ['uri', 'version', 'name'] and + attr not in self.config_files): + delattr(self.namespace, attr) + + def add_config_file(self, dest, cfile): + """ Add a config file, which triggers a full reparse of all + options. """ + if dest not in self.config_files: + self._reset_namespace() + self._cfp.read([cfile]) + self._defaults_set = [] + self._set_defaults() + self._parse_config_options() + self.config_files.append(dest) + + def reparse(self, argv=None): + """ Reparse options after they have already been parsed. + + :param argv: The argument list to parse. By default, + :attr:`Bcfg2.Options.Parser.argv` is reused. + (I.e., the argument list that was initially + parsed.) :type argv: list + """ + self._reset_namespace() + self.parse(argv or self.argv) + + def parse(self, argv=None): + """ Parse options. + + :param argv: The argument list to parse. By default, + ``sys.argv[1:]`` is used. This is stored in + :attr:`Bcfg2.Options.Parser.argv` for reuse by + :func:`Bcfg2.Options.Parser.reparse`. :type + argv: list + """ + if argv is None: + argv = sys.argv[1:] + if self.parsed and self.argv == argv: + return self.namespace + self.argv = argv + + # phase 1: get and read config file + bootstrap_parser = argparse.ArgumentParser(add_help=False) + self.configfile.add_to_parser(bootstrap_parser) + bootstrap = bootstrap_parser.parse_known_args(args=self.argv)[0] + + # check whether the specified bcfg2.conf exists + if not os.path.exists(bootstrap.config): + print("Could not read %s" % bootstrap.config) + return 1 + self.add_config_file(self.configfile.dest, bootstrap.config) + + # phase 2: re-parse command line, loading additional + # components, until all components have been loaded. On each + # iteration, set defaults from config file/environment + # variables + remaining = self.argv + while not self.parsed: + self.parsed = True + self._set_defaults() + remaining = self.parse_known_args(args=remaining, + namespace=self.namespace)[1] + self._parse_config_options() + self._finalize() + + # phase 3: parse command line for real, with all components + # loaded and all options known + self._parse_config_options() + + # phase 4: fix up <repository> macros + repo = getattr(self.namespace, "repository", repository.default) + for attr in dir(self.namespace): + value = getattr(self.namespace, attr) + if not attr.startswith("_") and hasattr(value, "replace"): + setattr(self.namespace, attr, + value.replace("<repository>", repo, 1)) + + # phase 5: call post-parsing hooks + for component in self.components: + if hasattr(component, "options_parsed_hook"): + getattr(component, "options_parsed_hook")() + + return self.namespace + + +#: A module-level :class:`Bcfg2.Options.Parser` object that is used +#: for all parsing +_parser = Parser() + +#: Track whether or not the module-level parser has been initialized +#: yet. We track this separately because some things (e.g., modules +#: that add components on import) will use the parser before it has +#: been initialized, so we can't just set +#: :attr:`Bcfg2.Options._parser` to None and wait for +#: :func:`Bcfg2.Options.get_parser` to be called. +_parser_initialized = False + + +def get_parser(description=None, components=None, namespace=None): + """ Get an existing :class:`Bcfg2.Options.Parser` object. (One is + created at the module level when :mod:`Bcfg2.Options` is + imported.) If no arguments are given, then the existing parser is + simply fetched. + + If arguments are given, then one of two things happens: + + * If this is the first ``get_parser`` call with arguments, then + the values given are set accordingly in the parser, and it is + returned. + * If this is not the first such call, then + :class:`Bcfg2.Options.OptionParserException` is raised. + + That is, a ``get_parser`` call with options is considered to + initialize the parser that already exists, and that can only + happen once. + + :param description: Set the parser description + :type description: string + :param components: Load the given components in the parser + :type components: list + :param namespace: Use the given namespace instead of + :attr:`Bcfg2.Options.setup` + :type namespace: argparse.Namespace + :returns: Bcfg2.Options.Parser object + """ + if _parser_initialized and (description or components or namespace): + raise OptionParserException("Parser has already been initialized") + elif (description or components or namespace): + if description: + _parser.description = description + if components is not None: + for component in components: + _parser.add_component(component) + if namespace: + _parser.namespace = namespace + return _parser diff --git a/src/lib/Bcfg2/Options/Subcommands.py b/src/lib/Bcfg2/Options/Subcommands.py new file mode 100644 index 000000000..53c4e563f --- /dev/null +++ b/src/lib/Bcfg2/Options/Subcommands.py @@ -0,0 +1,237 @@ +""" Classes to make it easier to create commands with large numbers of +subcommands (e.g., bcfg2-admin, bcfg2-info). """ + +import re +import cmd +import sys +import copy +import shlex +import logging +from Bcfg2.Compat import StringIO +from OptionGroups import Subparser +from Options import PositionalArgument +from Parser import Parser, setup as master_setup + + +__all__ = ["Subcommand", "HelpCommand", "CommandRegistry", "register_commands"] + + +class Subcommand(object): + """ Base class for subcommands. This must be subclassed to create + commands. + + Specifically, you must override + :func:`Bcfg2.Options.Subcommand.run`. You may want to override: + + * The docstring, which will be used as the short help. + * :attr:`Bcfg2.Options.Subcommand.options` + * :attr:`Bcfg2.Options.Subcommand.help` + * :attr:`Bcfg2.Options.Subcommand.interactive` + * + * :func:`Bcfg2.Options.Subcommand.shutdown` + + You should not need to override + :func:`Bcfg2.Options.Subcommand.__call__` or + :func:`Bcfg2.Options.Subcommand.usage`. + + A ``Subcommand`` subclass constructor must not take any arguments. + """ + + #: Options this command takes + options = [] + + #: Longer help message + help = None + + #: Whether or not to expose this command in an interactive + #: :class:`cmd.Cmd` shell, if one is used. (``bcfg2-info`` uses + #: one, ``bcfg2-admin`` does not.) + interactive = True + + _ws_re = re.compile(r'\s+', flags=re.MULTILINE) + + def __init__(self): + self.core = None + description = "%s: %s" % (self.__class__.__name__.lower(), + self.__class__.__doc__) + + #: The :class:`Bcfg2.Options.Parser` that will be used to + #: parse options if this subcommand is called from an + #: interactive :class:`cmd.Cmd` shell. + self.parser = Parser( + prog=self.__class__.__name__.lower(), + description=description, + components=[self], + add_base_options=False, + epilog=self.help) + self._usage = None + + #: A :class:`logging.Logger` that can be used to produce + #: logging output for this command. + self.logger = logging.getLogger(self.__class__.__name__.lower()) + + def __call__(self, args=None): + """ Perform option parsing and other tasks necessary to + support running ``Subcommand`` objects as part of a + :class:`cmd.Cmd` shell. You should not need to override + ``__call__``. + + :param args: Arguments given in the interactive shell + :type args: list of strings + :returns: The return value of :func:`Bcfg2.Options.Subcommand.run` + """ + if args is not None: + self.parser.namespace = copy.copy(master_setup) + alist = shlex.split(args) + try: + setup = self.parser.parse(alist) + except SystemExit: + return sys.exc_info()[1].code + return self.run(setup) + else: + return self.run(master_setup) + + def usage(self): + """ Get the short usage message. """ + if self._usage is None: + io = StringIO() + self.parser.print_usage(file=io) + usage = self._ws_re.sub(' ', io.getvalue()).strip()[7:] + doc = self._ws_re.sub(' ', getattr(self, "__doc__")).strip() + if doc is None: + self._usage = usage + else: + self._usage = "%s - %s" % (usage, doc) + return self._usage + + def run(self, setup): + """ Run the command. + + :param setup: A namespace giving the options for this command. + This must be used instead of + :attr:`Bcfg2.Options.setup` because this command + may have been called from an interactive + :class:`cmd.Cmd` shell, and thus has its own + option parser and its own (private) namespace. + ``setup`` is guaranteed to contain all of the + options in the global + :attr:`Bcfg2.Options.setup` namespace, in + addition to any local options given to this + command from the interactive shell. + :type setup: argparse.Namespace + """ + raise NotImplementedError + + def shutdown(self): + """ Perform any necessary shtudown tasks for this command This + is called to when the program exits (*not* when this command + is finished executing). """ + pass + + +class HelpCommand(Subcommand): + """ Get help on a specific subcommand. This must be subclassed to + create the actual help command by overriding + :func:`Bcfg2.Options.HelpCommand.command_registry` and giving the + command access to a :class:`Bcfg2.Options.CommandRegistry`. """ + options = [PositionalArgument("command", nargs='?')] + + # the interactive shell has its own help + interactive = False + + def command_registry(self): + """ Return a :class:`Bcfg2.Options.CommandRegistry` class. + All commands registered with the class will be included in the + help message. """ + raise NotImplementedError + + def run(self, setup): + commands = self.command_registry() + if setup.command: + try: + commands[setup.command].parser.print_help() + return 0 + except KeyError: + print("No such command: %s" % setup.command) + for command in sorted(commands.keys()): + print(commands[command].usage()) + + +class CommandRegistry(object): + """ A ``CommandRegistry`` is used to register subcommands and + provides a single interface to run them. It's also used by + :class:`Bcfg2.Options.HelpCommand` to produce help messages for + all available commands. """ + + #: A dict of registered commands. Keys are the class names, + #: lowercased (i.e., the command names), and values are instances + #: of the command objects. + commands = dict() + + options = [] + + def runcommand(self): + """ Run the single command named in + ``Bcfg2.Options.setup.subcommand``, which is where + :class:`Bcfg2.Options.Subparser` groups store the + subcommand. """ + try: + return self.commands[master_setup.subcommand].run(master_setup) + finally: + self.shutdown() + + def shutdown(self): + """ Perform shutdown tasks. This calls the ``shutdown`` + method of all registered subcommands. """ + self.commands[master_setup.subcommand].shutdown() + + @classmethod + def register_command(cls, cmdcls): + """ Register a single command. + + :param cmdcls: The command class to register + :type cmdcls: type + :returns: An instance of ``cmdcls`` + """ + cmd_obj = cmdcls() + name = cmdcls.__name__.lower() + cls.commands[name] = cmd_obj + cls.options.append( + Subparser(*cmdcls.options, name=name, help=cmdcls.__doc__)) + if issubclass(cls, cmd.Cmd) and cmdcls.interactive: + setattr(cls, "do_%s" % name, cmd_obj) + setattr(cls, "help_%s" % name, cmd_obj.parser.print_help) + return cmd_obj + + +def register_commands(registry, candidates, parent=Subcommand): + """ Register all subcommands in ``candidates`` against the + :class:`Bcfg2.Options.CommandRegistry` subclass given in + ``registry``. A command is registered if and only if: + + * It is a subclass of the given ``parent`` (by default, + :class:`Bcfg2.Options.Subcommand`); + * It is not the parent class itself; and + * Its name does not start with an underscore. + + :param registry: The :class:`Bcfg2.Options.CommandRegistry` + subclass against which commands will be + registered. + :type registry: Bcfg2.Options.CommandRegistry + :param candidates: A list of objects that will be considered for + registration. Only objects that meet the + criteria listed above will be registered. + :type candidates: list + :param parent: Specify a parent class other than + :class:`Bcfg2.Options.Subcommand` that all + registered commands must subclass. + :type parent: type + """ + for attr in candidates: + try: + if (issubclass(attr, parent) and + attr != parent and + not attr.__name__.startswith("_")): + registry.register_command(attr) + except TypeError: + pass diff --git a/src/lib/Bcfg2/Options/Types.py b/src/lib/Bcfg2/Options/Types.py new file mode 100644 index 000000000..329c671ea --- /dev/null +++ b/src/lib/Bcfg2/Options/Types.py @@ -0,0 +1,87 @@ +""" :mod:`Bcfg2.Options` provides a number of useful types for use +with the :class:`Bcfg2.Options.Option` constructor. """ + +import os +import re +import pwd +import grp + +_COMMA_SPLIT_RE = re.compile(r'\s*,\s*') + + +def path(value): + """ A generic path. ``~`` will be expanded with + :func:`os.path.expanduser` and the absolute resulting path will be + used. This does *not* ensure that the path exists. """ + return os.path.abspath(os.path.expanduser(value)) + + +def comma_list(value): + """ Split a comma-delimited list, with optional whitespace around + the commas.""" + return _COMMA_SPLIT_RE.split(value) + + +def colon_list(value): + """ Split a colon-delimited list. Whitespace is not allowed + around the colons. """ + return value.split(':') + + +def octal(value): + """ Given an octal string, get an integer representation. """ + return int(value, 8) + + +def username(value): + """ Given a username or numeric UID, get a numeric UID. The user + must exist.""" + try: + return int(value) + except ValueError: + return int(pwd.getpwnam(value)[2]) + + +def groupname(value): + """ Given a group name or numeric GID, get a numeric GID. The + user must exist.""" + try: + return int(value) + except ValueError: + return int(grp.getgrnam(value)[2]) + + +def timeout(value): + """ Convert the value into a float or None. """ + if value is None: + return value + rv = float(value) # pass ValueError up the stack + if rv <= 0: + return None + return rv + + +_bytes_multipliers = dict(k=1, + m=2, + g=3, + t=4) +_suffixes = "".join(_bytes_multipliers.keys()).lower() +_suffixes += _suffixes.upper() +_bytes_re = re.compile(r'(?P<value>\d+)(?P<multiplier>[%s])?' % _suffixes) + + +def size(value): + """ Given a number of bytes in a human-readable format (e.g., + '512m', '2g'), get the absolute number of bytes as an integer. + """ + if value == -1: + return value + mat = _bytes_re.match(value) + if not mat: + raise ValueError("Not a valid size", value) + rvalue = int(mat.group("value")) + mult = mat.group("multiplier") + if mult: + return rvalue * (1024 ** _bytes_multipliers[mult.lower()]) + else: + return rvalue diff --git a/src/lib/Bcfg2/Options/__init__.py b/src/lib/Bcfg2/Options/__init__.py new file mode 100644 index 000000000..546068f1f --- /dev/null +++ b/src/lib/Bcfg2/Options/__init__.py @@ -0,0 +1,10 @@ +""" Bcfg2 options parsing. """ + +# pylint: disable=W0611,W0401,W0403 +import Types +from Common import * +from Parser import * +from Actions import * +from Options import * +from Subcommands import * +from OptionGroups import * |