diff options
author | Pavel Moravec <pmoravec@redhat.com> | 2019-01-07 13:20:04 +0100 |
---|---|---|
committer | Bryn M. Reeves <bmr@redhat.com> | 2019-03-20 11:57:11 +0000 |
commit | aeade1c831eb700c7fab3ae5aa3e1d6ef9f0c483 (patch) | |
tree | 191c5ac5bb6f802dbd9cc49f321527f209252e7b | |
parent | a725a319b50057495b6335df2891334aa9b18069 (diff) | |
download | sos-aeade1c831eb700c7fab3ae5aa3e1d6ef9f0c483.tar.gz |
[sosreport] support all command line options in /etc/sos.conf
- SoSOptions class changes:
- from_file method to load load options from file
- encapsulation of particular options into __init__ method
- updated merge logic to replace non-default values only
- sosreport.py changes:
- _parse_args (renamed to _get_parser) newly returns just the
ArgumentParser object as parsing itself is done elsewhere
- SoSOptions are built in ordering:
- defaults from parser (with emtpy cmdline) loaded
- cmdline options replace defaults
- config file known even now
- config.file options replace defaults until already set by
cmdline (i.e. until SoSOptions marked them as nondefault)
- presets can be known even now
- options from given preset replace defaults, until the options
are already updated from cmdline or config.file
- some extra logging/formatting added
- some extra comments added
Fixes: #855
Resolves: #1530
Signed-off-by: Pavel Moravec <pmoravec@redhat.com>
Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
-rw-r--r-- | sos/__init__.py | 167 | ||||
-rw-r--r-- | sos/sosreport.py | 99 |
2 files changed, 154 insertions, 112 deletions
diff --git a/sos/__init__.py b/sos/__init__.py index cc795ab2..491d801f 100644 --- a/sos/__init__.py +++ b/sos/__init__.py @@ -18,6 +18,13 @@ gettext to internationalize messages. import gettext import six +from argparse import ArgumentParser + +if six.PY3: + from configparser import ConfigParser, ParsingError, Error +else: + from ConfigParser import ConfigParser, ParsingError, Error + __version__ = "3.6" gettext_dir = "/usr/share/locale" @@ -71,55 +78,34 @@ def _is_seq(val): class SoSOptions(object): - add_preset = "" - alloptions = False - all_logs = False - batch = False - build = False - case_id = "" - chroot = _arg_defaults["chroot"] - compression_type = _arg_defaults["compression_type"] - config_file = "" - debug = False - del_preset = "" - desc = "" - enableplugins = [] - encrypt_key = None - encrypt_pass = None - experimental = False - label = "" - list_plugins = False - list_presets = False - list_profiles = False - log_size = _arg_defaults["log_size"] - noplugins = [] - noreport = False - note = "" - onlyplugins = [] - plugin_timeout = None - plugopts = [] - preset = "" - profiles = [] - quiet = False - sysroot = None - threads = 4 - tmp_dir = "" - verbosity = 0 - verify = False - - def _merge_opt(self, opt, src, replace): + + def _merge_opt(self, opt, src, is_default): + def _unset(val): + return (val == "" or val is None) + if hasattr(src, opt): - value = getattr(src, opt) - if replace or not _is_seq(value): + newvalue = getattr(src, opt) + oldvalue = getattr(self, opt) + # overwrite value iff: + # - we replace unset option by a real value + # - new default is set, or + # - non-sequential variable keeps its default value + if (_unset(oldvalue) and not _unset(newvalue)) or \ + is_default or \ + ((opt not in self._nondefault) and (not _is_seq(newvalue))): # Overwrite atomic values - setattr(self, opt, getattr(src, opt)) - else: + setattr(self, opt, newvalue) + if is_default: + self._nondefault.discard(opt) + else: + self._nondefault.add(opt) + elif _is_seq(newvalue): # Concatenate sequence types - setattr(self, opt, getattr(self, opt) + getattr(src, opt)) + setattr(self, opt, newvalue + oldvalue) - def _merge_opts(self, src, replace): + def _merge_opts(self, src, is_default): for arg in _arg_names: - self._merge_opt(arg, src, replace) + self._merge_opt(arg, src, is_default) def __str(self, quote=False, sep=" ", prefix="", suffix=""): """Format a SoSOptions object as a human or machine readable string. @@ -168,6 +154,42 @@ class SoSOptions(object): :param *kwargs: a list of ``SoSOptions`` keyword args. :returns: the new ``SoSOptions`` object. """ + self.add_preset = "" + self.alloptions = False + self.all_logs = False + self.batch = False + self.build = False + self.case_id = "" + self.chroot = _arg_defaults["chroot"] + self.compression_type = _arg_defaults["compression_type"] + self.config_file = "" + self.debug = False + self.del_preset = "" + self.desc = "" + self.enableplugins = [] + self.encrypt_key = None + self.encrypt_pass = None + self.experimental = False + self.label = "" + self.list_plugins = False + self.list_presets = False + self.list_profiles = False + self.log_size = _arg_defaults["log_size"] + self.noplugins = [] + self.noreport = False + self.note = "" + self.onlyplugins = [] + self.plugin_timeout = None + self.plugopts = [] + self.preset = _arg_defaults["preset"] + self.profiles = [] + self.quiet = False + self.sysroot = None + self.threads = 4 + self.tmp_dir = "" + self.verbosity = _arg_defaults["verbosity"] + self.verify = False + self._nondefault = set() for arg in kwargs.keys(): if arg not in _arg_names: raise ValueError("Unknown SoSOptions attribute: %s" % arg) @@ -186,28 +208,61 @@ class SoSOptions(object): opts._merge_opts(args, True) return opts - def merge(self, src, replace=False): + @classmethod + def from_file(cls, argparser, config_file, is_default=True): + opts = SoSOptions() + config = ConfigParser() + try: + try: + with open(config_file) as f: + config.readfp(f) + except (ParsingError, Error) as e: + raise exit('Failed to parse configuration ' + 'file %s' % config_file) + except (OSError, IOError) as e: + raise exit('Unable to read configuration file %s ' + ': %s' % (config_file, e.args[1])) + + if config.has_section("general"): + optlist = [] + for opt, val in config.items("general"): + # assume just long options are specified so prefix them by "--" + optlist.append("--" + opt + "=" + val) + opts._merge_opts(argparser.parse_args(optlist), is_default) + + if config.has_option("plugins", "disable"): + opts.noplugins = [] + opts.noplugins.extend([plugin.strip() for plugin in + config.get("plugins", "disable").split(',')]) + + if config.has_option("plugins", "enable"): + opts.enableplugins = [] + opts.enableplugins.extend( + [plugin.strip() for plugin in + config.get("plugins", "enable").split(',')]) + + if config.has_section("tunables"): + opts.plugopts = [] + for opt, val in config.items("tunables"): + if not opt.split('.')[0] in opts.noplugins: + opts.plugopts.append(opt + "=" + val) + + return opts + + def merge(self, src, skip_default=True): """Merge another set of ``SoSOptions`` into this object. Merge two ``SoSOptions`` objects by setting unset or default values to their value in the ``src`` object. :param src: the ``SoSOptions`` object to copy from - :param replace: ``True`` if non-default values should be - overwritten. + :param is_default: ``True`` if new default values are to be set. """ for arg in _arg_names: if not hasattr(src, arg): continue - if _is_seq(getattr(self, arg)): - self._merge_opt(arg, src, replace) - continue - if arg in _arg_defaults.keys(): - if replace or getattr(self, arg) == _arg_defaults[arg]: - self._merge_opt(arg, src, replace) - else: - if replace or not getattr(self, arg): - self._merge_opt(arg, src, replace) + if getattr(src, arg) is not None or not skip_default: + self._merge_opt(arg, src, False) def dict(self): """Return this ``SoSOptions`` option values as a dictionary of diff --git a/sos/sosreport.py b/sos/sosreport.py index 47fcc0cd..c5f406c2 100644 --- a/sos/sosreport.py +++ b/sos/sosreport.py @@ -45,11 +45,6 @@ import six from six.moves import zip, input from six import print_ -if six.PY3: - from configparser import ConfigParser, ParsingError, Error -else: - from ConfigParser import ConfigParser, ParsingError, Error - # file system errors that should terminate a run fatal_fs_errors = (errno.ENOSPC, errno.EROFS) @@ -201,8 +196,8 @@ class XmlReport(object): chroot_modes = ["auto", "always", "never"] -def _parse_args(args): - """ Parse command line options and arguments""" +def _get_parser(): + """ Build ArgumentParser content""" usage_string = ("%(prog)s [options]\n\n" "Some examples:\n\n" @@ -235,8 +230,8 @@ def _parse_args(args): help="chroot executed commands to SYSROOT " "[auto, always, never] (default=auto)", default=_arg_defaults["chroot"]) - parser.add_argument("--config-file", action="store", - dest="config_file", + parser.add_argument("--config-file", type=str, action="store", + dest="config_file", default="/etc/sos.conf", help="specify alternate configuration file") parser.add_argument("--debug", action="store_true", dest="debug", help="enable interactive debugging using the " @@ -319,6 +314,7 @@ def _parse_args(args): preset_grp.add_argument("--del-preset", type=str, action="store", help="Delete the named command line preset") + # Group to make tarball encryption (via GPG/password) exclusive encrypt_grp = parser.add_mutually_exclusive_group() encrypt_grp.add_argument("--encrypt-key", help="Encrypt the final archive using a GPG " @@ -326,7 +322,7 @@ def _parse_args(args): encrypt_grp.add_argument("--encrypt-pass", help="Encrypt the final archive using a password") - return parser.parse_args(args) + return parser class SoSReport(object): @@ -352,30 +348,47 @@ class SoSReport(object): except Exception: pass # not available in java, but we don't care - cmd_args = _parse_args(args) - self.opts = SoSOptions.from_args(cmd_args) + # load default options and store them in self.opts + parser = _get_parser() + self.opts = SoSOptions().from_args(parser.parse_args([])) + + # remove default options now, such that by processing cmdline options + # we know what exact options were provided there and should not be + # overwritten any time further + # then merge these options on top of self.opts + # this approach is required since: + # - we process the more priority options first (cmdline, then config + # file, then presets) - required to know cfgfile or preset + # - we have to apply lower prio options only on top of non-default + for option in parser._actions: + if option.default != '==SUPPRESS==': + option.default = None + cmd_opts = SoSOptions().from_args(parser.parse_args(args)) + self.opts.merge(cmd_opts) + + # load options from config.file and merge them to self.opts + self.fileopts = SoSOptions().from_file(parser, self.opts.config_file) + self.opts.merge(self.fileopts) self._set_debug() - self._read_config() + # load preset and options from it - first, identify policy for that try: self.policy = sos.policies.load(sysroot=self.opts.sysroot) except KeyboardInterrupt: self._exit(0) - self._is_root = self.policy.is_root() # user specified command line preset - if cmd_args.preset != _arg_defaults["preset"]: - self.preset = self.policy.find_preset(cmd_args.preset) + if self.opts.preset != _arg_defaults["preset"]: + self.preset = self.policy.find_preset(self.opts.preset) if not self.preset: - sys.stderr.write("Unknown preset: '%s'\n" % cmd_args.preset) + sys.stderr.write("Unknown preset: '%s'\n" % self.opts.preset) self.preset = self.policy.probe_preset() self.opts.list_presets = True - # --preset=auto if not self.preset: self.preset = self.policy.probe_preset() - + # now merge preset options to self.opts self.opts.merge(self.preset.opts) # system temporary directory to use @@ -432,7 +445,6 @@ class SoSReport(object): 'verbosity': self.opts.verbosity, 'xmlreport': self.xml_report, 'cmdlineopts': self.opts, - 'config': self.config, } def get_temp_file(self): @@ -515,24 +527,6 @@ class SoSReport(object): if plugname and func: self._log_plugin_exception(plugname, func) - def _read_config(self): - self.config = ConfigParser() - if self.opts.config_file: - config_file = self.opts.config_file - else: - config_file = '/etc/sos.conf' - - try: - try: - with open(config_file) as f: - self.config.readfp(f) - except (ParsingError, Error) as e: - raise exit('Failed to parse configuration ' - 'file %s' % config_file) - except (OSError, IOError) as e: - raise exit('Unable to read configuration file %s ' - ': %s' % (config_file, e.args[1])) - def _setup_logging(self): # main soslog self.soslog = logging.getLogger('sos') @@ -585,13 +579,6 @@ class SoSReport(object): self.archive.add_file(self.sos_ui_log_file, dest=os.path.join('sos_logs', 'ui.log')) - def _get_disabled_plugins(self): - disabled = [] - if self.config.has_option("plugins", "disable"): - disabled = [plugin.strip() for plugin in - self.config.get("plugins", "disable").split(',')] - return disabled - def _is_in_profile(self, plugin_class): onlyplugins = self.opts.onlyplugins if not len(self.opts.profiles): @@ -603,8 +590,7 @@ class SoSReport(object): return any([p in self.opts.profiles for p in plugin_class.profiles]) def _is_skipped(self, plugin_name): - return (plugin_name in self.opts.noplugins or - plugin_name in self._get_disabled_plugins()) + return (plugin_name in self.opts.noplugins) def _is_inactive(self, plugin_name, pluginClass): return (not pluginClass(self.get_commons()).check_enabled() and @@ -727,13 +713,6 @@ class SoSReport(object): parms["enabled"] = True def _set_tunables(self): - if self.config.has_section("tunables"): - if not self.opts.plugopts: - self.opts.plugopts = [] - - for opt, val in self.config.items("tunables"): - if not opt.split('.')[0] in self._get_disabled_plugins(): - self.opts.plugopts.append(opt + "=" + val) if self.opts.plugopts: opts = {} for opt in self.opts.plugopts: @@ -774,8 +753,12 @@ class SoSReport(object): self._exit(1) del opts[plugname] for plugname in opts.keys(): - self.soslog.error('unable to set option for disabled or ' - 'non-existing plugin (%s)' % (plugname)) + self.soslog.error('WARNING: unable to set option for disabled ' + 'or non-existing plugin (%s)' % (plugname)) + # in case we printed warnings above, visually intend them from + # subsequent header text + if opts.keys(): + self.soslog.error('') def _check_for_unknown_plugins(self): import itertools @@ -1020,6 +1003,10 @@ class SoSReport(object): msg = "[%s:%s] executing 'sosreport %s'" self.soslog.info(msg % (__name__, "setup", " ".join(self._args))) + msg = "[%s:%s] loaded options from config file: %s'" + self.soslog.info(msg % (__name__, "setup", + " ".join(self.fileopts.to_args()))) + # Log active preset defaults preset_args = self.preset.opts.to_args() msg = ("[%s:%s] using '%s' preset defaults (%s)" % |