Blob Blame History Raw
From 90b6b709e9f4002376b656b155d00d85382f1828 Mon Sep 17 00:00:00 2001
From: Pavel Moravec <pmoravec@redhat.com>
Date: Mon, 29 Mar 2021 16:23:01 +0200
Subject: [PATCH] [report] add --cmd-timeout option

Add --cmd-timeout option to configure command timeout. Plugin-specific
option of the same name (i.e. -k logs.cmd-timeout=60) can control the
timeout per plugin.

Option defaults and global/plugin-specific option preference follows the
--plugin-timeout rules.

Resolves: #2466

Signed-off-by: Pavel Moravec <pmoravec@redhat.com>
Signed-off-by: Jake Hunsaker <jhunsake@redhat.com>
---
 man/en/sos-report.1            | 18 +++++++++-
 sos/collector/__init__.py      |  3 ++
 sos/collector/sosnode.py       |  5 +++
 sos/options.py                 |  3 +-
 sos/report/__init__.py         |  5 ++-
 sos/report/plugins/__init__.py | 63 ++++++++++++++++++++++++----------
 6 files changed, 76 insertions(+), 21 deletions(-)

diff --git a/man/en/sos-report.1 b/man/en/sos-report.1
index 81005959..51cf3436 100644
--- a/man/en/sos-report.1
+++ b/man/en/sos-report.1
@@ -17,6 +17,7 @@ sosreport \- Collect and package diagnostic and support data
           [--label label] [--case-id id]\fR
           [--threads threads]\fR
           [--plugin-timeout TIMEOUT]\fR
+          [--cmd-timeout TIMEOUT]\fR
           [-s|--sysroot SYSROOT]\fR
           [-c|--chroot {auto|always|never}\fR
           [--tmp-dir directory]\fR
@@ -247,7 +248,7 @@ Specify a timeout in seconds to allow each plugin to run for. A value of 0
 means no timeout will be set. A value of -1 is used to indicate the default
 timeout of 300 seconds.
 
-Note that this options sets the timeout for all plugins. If you want to set
+Note that this option sets the timeout for all plugins. If you want to set
 a timeout for a specific plugin, use the 'timeout' plugin option available to
 all plugins - e.g. '-k logs.timeout=600'.
 
@@ -255,6 +256,21 @@ The plugin-specific timeout option will override this option. For example, using
 \'--plugin-timeout=60 -k logs.timeout=600\' will set a timeout of 600 seconds for
 the logs plugin and 60 seconds for all other enabled plugins.
 .TP
+.B \--cmd-timeout TIMEOUT
+Specify a timeout limit in seconds for a command execution. Same defaults logic
+from --plugin-timeout applies here.
+
+This option sets the command timeout for all plugins. If you want to set a cmd
+timeout for a specific plugin, use the 'cmd-timeout' plugin option available to
+all plugins - e.g. '-k logs.cmd-timeout=600'.
+
+Again, the same plugin/global precedence logic as for --plugin-timeout applies
+here.
+
+Note that setting --cmd-timeout (or -k logs.cmd-timeout) high should be followed
+by increasing the --plugin-timeout equivalent, otherwise the plugin can easily
+timeout on slow commands execution.
+.TP
 .B \--case-id NUMBER
 Specify a case identifier to associate with the archive.
 Identifiers may include alphanumeric characters, commas and periods ('.').
diff --git a/sos/collector/__init__.py b/sos/collector/__init__.py
index 406c8f35..1ae73508 100644
--- a/sos/collector/__init__.py
+++ b/sos/collector/__init__.py
@@ -82,6 +82,7 @@ class SoSCollector(SoSComponent):
         'password_per_node': False,
         'plugin_options': [],
         'plugin_timeout': None,
+        'cmd_timeout': None,
         'preset': '',
         'save_group': '',
         'since': '',
@@ -276,6 +277,8 @@ class SoSCollector(SoSComponent):
                              help='Do not collect env vars in sosreports')
         sos_grp.add_argument('--plugin-timeout', type=int, default=None,
                              help='Set the global plugin timeout value')
+        sos_grp.add_argument('--cmd-timeout', type=int, default=None,
+                             help='Set the global command timeout value')
         sos_grp.add_argument('--since', default=None,
                              help=('Escapes archived files older than date. '
                                    'This will also affect --all-logs. '
diff --git a/sos/collector/sosnode.py b/sos/collector/sosnode.py
index a1679655..dbbee12e 100644
--- a/sos/collector/sosnode.py
+++ b/sos/collector/sosnode.py
@@ -664,6 +664,11 @@ class SosNode():
                     '--skip-files=%s' % (quote(self.opts.skip_files))
                 )
 
+        if self.check_sos_version('4.2'):
+            if self.opts.cmd_timeout:
+                sos_opts.append('--cmd-timeout=%s'
+                                % quote(str(self.opts.cmd_timeout)))
+
         sos_cmd = sos_cmd.replace(
             'sosreport',
             os.path.join(self.host.sos_bin_path, self.sos_bin)
diff --git a/sos/options.py b/sos/options.py
index b82a7d36..1eda55d6 100644
--- a/sos/options.py
+++ b/sos/options.py
@@ -283,7 +283,8 @@ class SoSOptions():
             if name in ("add_preset", "del_preset", "desc", "note"):
                 return False
             # Exception list for options that still need to be reported when 0
-            if name in ['log_size', 'plugin_timeout'] and value == 0:
+            if name in ['log_size', 'plugin_timeout', 'cmd_timeout'] \
+               and value == 0:
                 return True
             return has_value(name, value)
 
diff --git a/sos/report/__init__.py b/sos/report/__init__.py
index 25478ba7..945d0fc1 100644
--- a/sos/report/__init__.py
+++ b/sos/report/__init__.py
@@ -107,6 +107,7 @@ class SoSReport(SoSComponent):
         'only_plugins': [],
         'preset': 'auto',
         'plugin_timeout': 300,
+        'cmd_timeout': 300,
         'profiles': [],
         'since': None,
         'verify': False,
@@ -266,6 +267,8 @@ class SoSReport(SoSComponent):
                                 help="A preset identifier", default="auto")
         report_grp.add_argument("--plugin-timeout", default=None,
                                 help="set a timeout for all plugins")
+        report_grp.add_argument("--cmd-timeout", default=None,
+                                help="set a command timeout for all plugins")
         report_grp.add_argument("-p", "--profile", "--profiles",
                                 action="extend", dest="profiles", type=str,
                                 default=[],
@@ -709,7 +712,7 @@ class SoSReport(SoSComponent):
 
             self.ui_log.info(_("The following plugin options are available:"))
             for (plug, plugname, optname, optparm) in self.all_options:
-                if optname in ('timeout', 'postproc'):
+                if optname in ('timeout', 'postproc', 'cmd-timeout'):
                     continue
                 # format option value based on its type (int or bool)
                 if type(optparm["enabled"]) == bool:
diff --git a/sos/report/plugins/__init__.py b/sos/report/plugins/__init__.py
index 02625eb1..779119af 100644
--- a/sos/report/plugins/__init__.py
+++ b/sos/report/plugins/__init__.py
@@ -472,6 +472,9 @@ class Plugin(object):
     _default_plug_opts = [
         ('timeout', 'Timeout in seconds for plugin. The default value (-1) ' +
             'defers to the general plugin timeout, 300 seconds', 'fast', -1),
+        ('cmd-timeout', 'Timeout in seconds for a command execution. The ' +
+            'default value (-1) defers to the general cmd timeout, 300 ' +
+            'seconds', 'fast', -1),
         ('postproc', 'Enable post-processing collected plugin data', 'fast',
          True)
     ]
@@ -532,16 +535,15 @@ class Plugin(object):
         self.manifest.add_list('commands', [])
         self.manifest.add_list('files', [])
 
-    @property
-    def timeout(self):
-        """Returns either the default plugin timeout value, the value as
-        provided on the commandline via -k plugin.timeout=value, or the value
-        of the global --plugin-timeout option.
+    def timeout_from_options(self, optname, plugoptname, default_timeout):
+        """Returns either the default [plugin|cmd] timeout value, the value as
+        provided on the commandline via -k plugin.[|cmd-]timeout=value, or the
+        value of the global --[plugin|cmd]-timeout option.
         """
         _timeout = None
         try:
-            opt_timeout = self.get_option('plugin_timeout')
-            own_timeout = int(self.get_option('timeout'))
+            opt_timeout = self.get_option(optname)
+            own_timeout = int(self.get_option(plugoptname))
             if opt_timeout is None:
                 _timeout = own_timeout
             elif opt_timeout is not None and own_timeout == -1:
@@ -551,10 +553,30 @@ class Plugin(object):
             else:
                 return None
         except ValueError:
-            return self.plugin_timeout  # Default to known safe value
+            return default_timeout  # Default to known safe value
         if _timeout is not None and _timeout > -1:
             return _timeout
-        return self.plugin_timeout
+        return default_timeout
+
+    @property
+    def timeout(self):
+        """Returns either the default plugin timeout value, the value as
+        provided on the commandline via -k plugin.timeout=value, or the value
+        of the global --plugin-timeout option.
+        """
+        _timeout = self.timeout_from_options('plugin_timeout', 'timeout',
+                                             self.plugin_timeout)
+        return _timeout
+
+    @property
+    def cmdtimeout(self):
+        """Returns either the default command timeout value, the value as
+        provided on the commandline via -k plugin.cmd-timeout=value, or the
+        value of the global --cmd-timeout option.
+        """
+        _cmdtimeout = self.timeout_from_options('cmd_timeout', 'cmd-timeout',
+                                                self.cmd_timeout)
+        return _cmdtimeout
 
     def set_timeout_hit(self):
         self._timeout_hit = True
@@ -1235,8 +1257,8 @@ class Plugin(object):
         """
 
         global_options = (
-            'all_logs', 'allow_system_changes', 'log_size', 'plugin_timeout',
-            'since', 'verify'
+            'all_logs', 'allow_system_changes', 'cmd_timeout', 'log_size',
+            'plugin_timeout', 'since', 'verify'
         )
 
         if optionname in global_options:
@@ -1505,7 +1527,7 @@ class Plugin(object):
                     'tags': _spec_tags
                 })
 
-    def add_blockdev_cmd(self, cmds, devices='block', timeout=300,
+    def add_blockdev_cmd(self, cmds, devices='block', timeout=None,
                          sizelimit=None, chroot=True, runat=None, env=None,
                          binary=False, prepend_path=None, whitelist=[],
                          blacklist=[], tags=[]):
@@ -1569,7 +1591,7 @@ class Plugin(object):
                              whitelist=whitelist, blacklist=blacklist,
                              tags=_dev_tags)
 
-    def _add_device_cmd(self, cmds, devices, timeout=300, sizelimit=None,
+    def _add_device_cmd(self, cmds, devices, timeout=None, sizelimit=None,
                         chroot=True, runat=None, env=None, binary=False,
                         prepend_path=None, whitelist=[], blacklist=[],
                         tags=[]):
@@ -1627,7 +1649,7 @@ class Plugin(object):
                                  changes=soscmd.changes)
 
     def add_cmd_output(self, cmds, suggest_filename=None,
-                       root_symlink=None, timeout=cmd_timeout, stderr=True,
+                       root_symlink=None, timeout=None, stderr=True,
                        chroot=True, runat=None, env=None, binary=False,
                        sizelimit=None, pred=None, subdir=None,
                        changes=False, foreground=False, tags=[]):
@@ -1849,7 +1871,7 @@ class Plugin(object):
         self._log_debug("added string ...'%s' as '%s'" % (summary, filename))
 
     def _collect_cmd_output(self, cmd, suggest_filename=None,
-                            root_symlink=False, timeout=cmd_timeout,
+                            root_symlink=False, timeout=None,
                             stderr=True, chroot=True, runat=None, env=None,
                             binary=False, sizelimit=None, subdir=None,
                             changes=False, foreground=False, tags=[]):
@@ -1883,6 +1905,8 @@ class Plugin(object):
         if self._timeout_hit:
             return
 
+        if timeout is None:
+            timeout = self.cmdtimeout
         _tags = []
 
         if isinstance(tags, str):
@@ -1975,7 +1999,7 @@ class Plugin(object):
         return result
 
     def collect_cmd_output(self, cmd, suggest_filename=None,
-                           root_symlink=False, timeout=cmd_timeout,
+                           root_symlink=False, timeout=None,
                            stderr=True, chroot=True, runat=None, env=None,
                            binary=False, sizelimit=None, pred=None,
                            subdir=None, tags=[]):
@@ -2044,7 +2068,7 @@ class Plugin(object):
             tags=tags
         )
 
-    def exec_cmd(self, cmd, timeout=cmd_timeout, stderr=True, chroot=True,
+    def exec_cmd(self, cmd, timeout=None, stderr=True, chroot=True,
                  runat=None, env=None, binary=False, pred=None,
                  foreground=False, container=False, quotecmd=False):
         """Execute a command right now and return the output and status, but
@@ -2095,6 +2119,9 @@ class Plugin(object):
         if not self.test_predicate(cmd=True, pred=pred):
             return _default
 
+        if timeout is None:
+            timeout = self.cmdtimeout
+
         if chroot or self.commons['cmdlineopts'].chroot == 'always':
             root = self.sysroot
         else:
@@ -2331,7 +2358,7 @@ class Plugin(object):
 
     def add_journal(self, units=None, boot=None, since=None, until=None,
                     lines=None, allfields=False, output=None,
-                    timeout=cmd_timeout, identifier=None, catalog=None,
+                    timeout=None, identifier=None, catalog=None,
                     sizelimit=None, pred=None, tags=[]):
         """Collect journald logs from one of more units.
 
-- 
2.26.3