summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2')
-rw-r--r--src/lib/Bcfg2/Options.py1328
-rw-r--r--src/lib/Bcfg2/Options/Actions.py164
-rw-r--r--src/lib/Bcfg2/Options/Common.py135
-rw-r--r--src/lib/Bcfg2/Options/OptionGroups.py209
-rw-r--r--src/lib/Bcfg2/Options/Options.py305
-rw-r--r--src/lib/Bcfg2/Options/Parser.py282
-rw-r--r--src/lib/Bcfg2/Options/Subcommands.py237
-rw-r--r--src/lib/Bcfg2/Options/Types.py87
-rw-r--r--src/lib/Bcfg2/Options/__init__.py10
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 *