Blob Blame History Raw
From cb3d265849771f7e53b0587196930328005414e0 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 19 Jan 2015 18:54:09 +0000
Subject: [PATCH 01/38] [sosreport] add --sysroot option

Add a --sysroot=SYSROOT option to specify that the root file system to
be inspected is mounted at SYSROOT.

This allows basic support for container environments where sos is
running in a container and inspecting the containing host and its
environment ('superspection').

For this to work currently the following conditions must be met:

- sos is sufficiently privileged to read and search relevant file
  system paths within SYSROOT

- sos must share the PID and network namespace of the target host

- binaries called by sos must be present and executable in the
  SYSROOT inherited by sos. If PATH includes paths inside SYSROOT
  appropriate values must be set for LD_LIBRARY_PATH to allow
  shared executables to be linked.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/sosreport.py | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/sos/sosreport.py b/sos/sosreport.py
index 9a0cf6c..47eddc9 100644
--- a/sos/sosreport.py
+++ b/sos/sosreport.py
@@ -529,6 +529,17 @@ class SoSOptions(object):
         self._report = value
 
     @property
+    def sysroot(self):
+        if self._options is not None:
+            return self._options.sysroot
+        return self._sysroot
+
+    @sysroot.setter
+    def sysroot(self, value):
+        self._check_options_initialized()
+        self._sysroot = value
+
+    @property
     def compression_type(self):
         if self._options is not None:
             return self._options.compression_type
@@ -615,6 +626,9 @@ class SoSOptions(object):
         parser.add_option("--no-report", action="store_true",
                           dest="report",
                           help="Disable HTML/XML reporting", default=False)
+        parser.add_option("-s", "--sysroot", action="store", dest="sysroot",
+                          help="system root directory path (default='/')",
+                          default="/")
         parser.add_option("-z", "--compression-type", dest="compression_type",
                           help="compression technology to use [auto, "
                                "gzip, bzip2, xz] (default=auto)",
-- 
1.8.3.1


From 3eed62e132f67930bb1cf5c9eaa5927083011043 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Fri, 23 Jan 2015 15:24:00 +0000
Subject: [PATCH 02/38] [plugins] propagate sysroot to Plugin via commons

Although plugins should generally be unaware that they are being
run with an alternate sysroot the generic plugin IO code must
peform the appropriate path prefixing when sysroot is not '/'.

Propagate sysroot to plugin classes via the commons dictionary.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/sosreport.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/sos/sosreport.py b/sos/sosreport.py
index 47eddc9..580b5bd 100644
--- a/sos/sosreport.py
+++ b/sos/sosreport.py
@@ -651,6 +651,7 @@ class SoSReport(object):
         self.archive = None
         self.tempfile_util = None
         self._args = args
+        self.sysroot = "/"
 
         try:
             import signal
@@ -681,6 +682,10 @@ class SoSReport(object):
         self.tempfile_util = TempFileUtil(self.tmpdir)
         self._set_directories()
 
+        # set alternate system root directory
+        if self.opts.sysroot:
+            self.sysroot = self.opts.sysroot
+
     def print_header(self):
         self.ui_log.info("\n%s\n" % _("sosreport (version %s)" %
                          (__version__,)))
@@ -693,6 +698,7 @@ class SoSReport(object):
             'tmpdir': self.tmpdir,
             'soslog': self.soslog,
             'policy': self.policy,
+            'sysroot': self.sysroot,
             'verbosity': self.opts.verbosity,
             'xmlreport': self.xml_report,
             'cmdlineopts': self.opts,
-- 
1.8.3.1


From b1f3b3373e8ef3e94238760a3e7e78d95c564260 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Fri, 23 Jan 2015 23:17:34 +0000
Subject: [PATCH 03/38] [plugins] prefix target paths with self.sysroot

Prefix copyspecs with self.sysroot when using an alternate root
path. Prefixes are applied before expanding copyspecs and the
prefixed paths are stored as the 'srcpath' attribute in the
archive. Destination paths in the report archive do not include
the prefix.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py | 50 +++++++++++++++++++++++++++++++++++--------
 tests/option_tests.py   |  3 ++-
 tests/plugin_tests.py   | 57 ++++++++++++++++++++++++++++++++-----------------
 3 files changed, 81 insertions(+), 29 deletions(-)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index 413ee73..790338b 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -101,6 +101,7 @@ class Plugin(object):
     files = ()
     archive = None
     profiles = ()
+    sysroot = '/'
 
     def __init__(self, commons):
         if not getattr(self, "option_list", False):
@@ -117,6 +118,7 @@ class Plugin(object):
         self.copy_paths = set()
         self.copy_strings = []
         self.collect_cmds = []
+        self.sysroot = commons['sysroot']
 
         self.soslog = self.commons['soslog'] if 'soslog' in self.commons \
             else logging.getLogger('sos')
@@ -154,6 +156,19 @@ class Plugin(object):
     def policy(self):
         return self.commons["policy"]
 
+    def join_sysroot(self, path):
+        if path[0] == os.sep:
+            path = path[1:]
+        return os.path.join(self.sysroot, path)
+
+    def strip_sysroot(self, path):
+        if path.startswith(self.sysroot):
+            return path[len(self.sysroot):]
+        return path
+
+    def use_sysroot(self):
+        return self.sysroot != os.path.abspath(os.sep)
+
     def is_installed(self, package_name):
         '''Is the package $package_name installed?'''
         return self.policy().pkg_by_name(package_name) is not None
@@ -207,6 +222,7 @@ class Plugin(object):
         '''
         try:
             path = self._get_dest_for_srcpath(srcpath)
+            self._log_debug("substituting scrpath '%s'" % srcpath)
             self._log_debug("substituting '%s' for '%s' in '%s'"
                             % (subst, regexp, path))
             if not path:
@@ -257,8 +273,9 @@ class Plugin(object):
         self._log_debug("copying link '%s' pointing to '%s' with isdir=%s"
                         % (srcpath, linkdest, os.path.isdir(absdest)))
 
+        dstpath = self.strip_sysroot(srcpath)
         # use the relative target path in the tarball
-        self.archive.add_link(reldest, srcpath)
+        self.archive.add_link(reldest, dstpath)
 
         if os.path.isdir(absdest):
             self._log_debug("link '%s' is a directory, skipping..." % linkdest)
@@ -277,7 +294,7 @@ class Plugin(object):
         self._do_copy_path(absdest)
 
         self.copied_files.append({'srcpath': srcpath,
-                                  'dstpath': srcpath,
+                                  'dstpath': dstpath,
                                   'symlink': "yes",
                                   'pointsto': linkdest})
 
@@ -288,6 +305,8 @@ class Plugin(object):
             self._do_copy_path(os.path.join(srcpath, afile), dest=None)
 
     def _get_dest_for_srcpath(self, srcpath):
+        if self.use_sysroot():
+            srcpath = self.join_sysroot(srcpath)
         for copied in self.copied_files:
             if srcpath == copied["srcpath"]:
                 return copied["dstpath"]
@@ -315,6 +334,9 @@ class Plugin(object):
         if not dest:
             dest = srcpath
 
+        if self.use_sysroot():
+            dest = self.strip_sysroot(dest)
+
         try:
             st = os.lstat(srcpath)
         except (OSError, IOError):
@@ -333,7 +355,7 @@ class Plugin(object):
         if not (stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode)):
             ntype = _node_type(st)
             self._log_debug("creating %s node at archive:'%s'"
-                            % (ntype, srcpath))
+                            % (ntype, dest))
             self._copy_node(srcpath, st)
             return
 
@@ -347,9 +369,11 @@ class Plugin(object):
         else:
             self.archive.add_file(srcpath, dest)
 
-        self.copied_files.append({'srcpath': srcpath,
-                                  'dstpath': dest,
-                                  'symlink': "no"})
+        self.copied_files.append({
+            'srcpath': srcpath,
+            'dstpath': dest,
+            'symlink': "no"
+        })
 
     def add_forbidden_path(self, forbiddenPath):
         """Specify a path to not copy, even if it's part of a copy_specs[]
@@ -416,6 +440,9 @@ class Plugin(object):
         except Exception:
             return default
 
+    def _add_copy_paths(self, copy_paths):
+        self.copy_paths.update(copy_paths)
+
     def add_copy_spec_limit(self, copyspec, sizelimit=None, tailit=True):
         """Add a file or glob but limit it to sizelimit megabytes. If fname is
         a single file the file will be tailed to meet sizelimit. If the first
@@ -424,10 +451,13 @@ class Plugin(object):
         if not (copyspec and len(copyspec)):
             return False
 
+        if self.use_sysroot():
+            copyspec = self.join_sysroot(copyspec)
         files = glob.glob(copyspec)
         files.sort()
         if len(files) == 0:
             return
+
         current_size = 0
         limit_reached = False
         sizelimit *= 1024 * 1024  # in MB
@@ -438,7 +468,7 @@ class Plugin(object):
             if sizelimit and current_size > sizelimit:
                 limit_reached = True
                 break
-            self.add_copy_spec(_file)
+            self._add_copy_paths([_file])
 
         if limit_reached and tailit:
             file_name = _file
@@ -459,12 +489,14 @@ class Plugin(object):
         if isinstance(copyspecs, six.string_types):
             copyspecs = [copyspecs]
         for copyspec in copyspecs:
+            if self.use_sysroot():
+                copyspec = self.join_sysroot(copyspec)
             if not (copyspec and len(copyspec)):
                 self._log_warn("added null or empty copy spec")
                 return False
             copy_paths = self._expand_copy_spec(copyspec)
-            self.copy_paths.update(copy_paths)
-            self._log_info("added copyspec '%s'" % copyspec)
+            self._add_copy_paths(copy_paths)
+            self._log_info("added copyspec '%s'" % copy_paths)
 
     def get_command_output(self, prog, timeout=300, runat=None, stderr=True):
         result = sos_get_command_output(prog, timeout=timeout, runat=runat,
diff --git a/tests/option_tests.py b/tests/option_tests.py
index fe37ccf..e8a26e2 100644
--- a/tests/option_tests.py
+++ b/tests/option_tests.py
@@ -8,10 +8,11 @@ class GlobalOptionTest(unittest.TestCase):
 
     def setUp(self):
         self.commons = {
+            'sysroot': '/',
             'global_plugin_options': {
                 'test_option': 'foobar',
                 'baz': None,
-                'empty_global': True,
+                'empty_global': True
             },
         }
         self.plugin = Plugin(self.commons)
diff --git a/tests/plugin_tests.py b/tests/plugin_tests.py
index e30ded5..14d3b49 100644
--- a/tests/plugin_tests.py
+++ b/tests/plugin_tests.py
@@ -127,50 +127,53 @@ class PluginToolTests(unittest.TestCase):
 
 class PluginTests(unittest.TestCase):
 
+    sysroot = os.getcwd()
+
     def setUp(self):
         self.mp = MockPlugin({
-            'cmdlineopts': MockOptions()
+            'cmdlineopts': MockOptions(),
+            'sysroot': self.sysroot
         })
         self.mp.archive = MockArchive()
 
     def test_plugin_default_name(self):
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         self.assertEquals(p.name(), "mockplugin")
 
     def test_plugin_set_name(self):
-        p = NamedMockPlugin({})
+        p = NamedMockPlugin({'sysroot': self.sysroot})
         self.assertEquals(p.name(), "testing")
 
     def test_plugin_no_descrip(self):
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         self.assertEquals(p.get_description(), "<no description available>")
 
     def test_plugin_no_descrip(self):
-        p = NamedMockPlugin({})
+        p = NamedMockPlugin({'sysroot': self.sysroot})
         self.assertEquals(p.get_description(), "This plugin has a description.")
 
     def test_set_plugin_option(self):
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         p.set_option("opt", "testing")
         self.assertEquals(p.get_option("opt"), "testing")
 
     def test_set_nonexistant_plugin_option(self):
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         self.assertFalse(p.set_option("badopt", "testing"))
 
     def test_get_nonexistant_plugin_option(self):
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         self.assertEquals(p.get_option("badopt"), 0)
 
     def test_get_unset_plugin_option(self):
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         self.assertEquals(p.get_option("opt"), 0)
 
     def test_get_unset_plugin_option_with_default(self):
         # this shows that even when we pass in a default to get,
         # we'll get the option's default as set in the plugin
         # this might not be what we really want
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         self.assertEquals(p.get_option("opt", True), True)
 
     def test_get_unset_plugin_option_with_default_not_none(self):
@@ -178,20 +181,20 @@ class PluginTests(unittest.TestCase):
         # if the plugin default is not None
         # we'll get the option's default as set in the plugin
         # this might not be what we really want
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         self.assertEquals(p.get_option("opt2", True), False)
 
     def test_get_option_as_list_plugin_option(self):
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         p.set_option("opt", "one,two,three")
         self.assertEquals(p.get_option_as_list("opt"), ['one', 'two', 'three'])
 
     def test_get_option_as_list_plugin_option_default(self):
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         self.assertEquals(p.get_option_as_list("opt", default=[]), [])
 
     def test_get_option_as_list_plugin_option_not_list(self):
-        p = MockPlugin({})
+        p = MockPlugin({'sysroot': self.sysroot})
         p.set_option("opt", "testing")
         self.assertEquals(p.get_option_as_list("opt"), ['testing'])
 
@@ -205,7 +208,8 @@ class PluginTests(unittest.TestCase):
 
     def test_copy_dir_forbidden_path(self):
         p = ForbiddenMockPlugin({
-            'cmdlineopts': MockOptions()
+            'cmdlineopts': MockOptions(),
+            'sysroot': self.sysroot
         })
         p.archive = MockArchive()
         p.setup()
@@ -219,12 +223,18 @@ class AddCopySpecTests(unittest.TestCase):
 
     def setUp(self):
         self.mp = MockPlugin({
-            'cmdlineopts': MockOptions()
+            'cmdlineopts': MockOptions(),
+            'sysroot': os.getcwd()
         })
         self.mp.archive = MockArchive()
 
     def assert_expect_paths(self):
-        self.assertEquals(self.mp.copy_paths, self.expect_paths)
+        def pathmunge(path):
+            if path[0] == '/':
+                path = path[1:]
+            return os.path.join(self.mp.sysroot, path)
+        expected_paths = set(map(pathmunge, self.expect_paths))
+        self.assertEquals(self.mp.copy_paths, expected_paths)
         
     # add_copy_spec()
 
@@ -242,6 +252,7 @@ class AddCopySpecTests(unittest.TestCase):
     # add_copy_spec_limit()
 
     def test_single_file_over_limit(self):
+        self.mp.sysroot = '/'
         fn = create_file(2) # create 2MB file, consider a context manager
         self.mp.add_copy_spec_limit(fn, 1)
         content, fname = self.mp.copy_strings[0]
@@ -252,10 +263,12 @@ class AddCopySpecTests(unittest.TestCase):
         os.unlink(fn)
 
     def test_bad_filename(self):
+        self.mp.sysroot = '/'
         self.assertFalse(self.mp.add_copy_spec_limit('', 1))
         self.assertFalse(self.mp.add_copy_spec_limit(None, 1))
 
     def test_glob_file_over_limit(self):
+        self.mp.sysroot = '/'
         # assume these are in /tmp
         fn = create_file(2)
         fn2 = create_file(2)
@@ -271,7 +284,10 @@ class AddCopySpecTests(unittest.TestCase):
 class CheckEnabledTests(unittest.TestCase):
 
     def setUp(self):
-        self.mp = EnablerPlugin({'policy': sos.policies.load()})
+        self.mp = EnablerPlugin({
+            'policy': sos.policies.load(),
+            'sysroot': os.getcwd()
+        })
 
     def test_checks_for_file(self):
         f = j("tail_test.txt")
@@ -296,7 +312,8 @@ class RegexSubTests(unittest.TestCase):
 
     def setUp(self):
         self.mp = MockPlugin({
-            'cmdlineopts': MockOptions()
+            'cmdlineopts': MockOptions(),
+            'sysroot': os.getcwd()
         })
         self.mp.archive = MockArchive()
 
@@ -310,6 +327,8 @@ class RegexSubTests(unittest.TestCase):
         self.assertEquals(0, replacements)
 
     def test_replacements(self):
+        # test uses absolute paths
+        self.mp.sysroot = '/'
         self.mp.add_copy_spec(j("tail_test.txt"))
         self.mp.collect()
         replacements = self.mp.do_file_sub(j("tail_test.txt"), r"(tail)", "foobar")
-- 
1.8.3.1


From c4957d8aa4ea35f879639726267043f6bb46cc7c Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Sat, 24 Jan 2015 00:35:09 +0000
Subject: [PATCH 04/38] [docs] add -s/--sysroot to sosreport.1

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 man/en/sosreport.1 | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/man/en/sosreport.1 b/man/en/sosreport.1
index c2b60d8..b0a86f2 100644
--- a/man/en/sosreport.1
+++ b/man/en/sosreport.1
@@ -12,6 +12,7 @@ sosreport \- Collect and package diagnostic and support data
           [--no-report] [--config-file conf]\fR
           [--batch] [--build] [--debug]\fR
           [--name name] [--case-id id] [--ticket-number nr]
+          [-s|--sysroot]\fR
           [--tmp-dir directory]\fR
           [-p|--profile profile-name]\fR
           [--list-profiles]\fR
@@ -72,6 +73,10 @@ Disable HTML/XML report writing.
 .B \--config-file CONFIG
 Specify alternate configuration file.
 .TP
+.B \-s, \--sysroot SYSROOT
+Specify an alternate root file system path. Useful for collecting
+reports from containers and images.
+.TP
 .B \--tmp-dir DIRECTORY
 Specify alternate temporary directory to copy data as well as the
 compressed report.
-- 
1.8.3.1


From 4a0a3f9607006d402713320fc31780fb54556e6a Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Sun, 25 Jan 2015 14:20:10 +0000
Subject: [PATCH 05/38] [utilities] add chroot support to
 sos_get_command_output()

Allow callers of sos_get_command_output() to specify a path to
chroot into before executing command.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py  |  2 +-
 sos/utilities.py         | 27 ++++++++++++++++-----------
 tests/utilities_tests.py |  5 +++++
 3 files changed, 22 insertions(+), 12 deletions(-)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index 790338b..137e1a1 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -499,7 +499,7 @@ class Plugin(object):
             self._log_info("added copyspec '%s'" % copy_paths)
 
     def get_command_output(self, prog, timeout=300, runat=None, stderr=True):
-        result = sos_get_command_output(prog, timeout=timeout, runat=runat,
+        result = sos_get_command_output(prog, timeout=timeout, chdir=runat,
                                         stderr=stderr)
         if result['status'] == 124:
             self._log_warn("command '%s' timed out after %ds"
diff --git a/sos/utilities.py b/sos/utilities.py
index dfe6128..a82ac7c 100644
--- a/sos/utilities.py
+++ b/sos/utilities.py
@@ -120,15 +120,20 @@ def is_executable(command):
     return any(os.access(path, os.X_OK) for path in candidates)
 
 
-def sos_get_command_output(command, timeout=300, runat=None, stderr=True):
-    """Execute a command through the system shell. First checks to see if the
-    requested command is executable. Returns (returncode, stdout, 0)"""
-    def _child_chdir():
-        if(runat):
-            try:
-                os.chdir(runat)
-            except:
-                self.log_error("failed to chdir to '%s'" % runat)
+def sos_get_command_output(command, timeout=300, stderr=False,
+                           chroot=None, chdir=None):
+    """Execute a command and return a dictionary of status and output,
+    optionally changing root or current working directory before
+    executing command.
+    """
+    # Change root or cwd for child only. Exceptions in the prexec_fn
+    # closure are caught in the parent (chroot and chdir are bound from
+    # the enclosing scope).
+    def _child_prep_fn():
+        if (chroot):
+                os.chroot(chroot)
+        if (chdir):
+                os.chdir(chdir)
 
     cmd_env = os.environ
     # ensure consistent locale for collected command output
@@ -145,7 +150,7 @@ def sos_get_command_output(command, timeout=300, runat=None, stderr=True):
         p = Popen(args, shell=False, stdout=PIPE,
                   stderr=STDOUT if stderr else PIPE,
                   bufsize=-1, env=cmd_env, close_fds=True,
-                  preexec_fn=_child_chdir)
+                  preexec_fn=_child_prep_fn)
     except OSError as e:
         if e.errno == errno.ENOENT:
             return {'status': 127, 'output': ""}
@@ -185,7 +190,7 @@ def shell_out(cmd, timeout=30, runat=None):
     """Shell out to an external command and return the output or the empty
     string in case of error.
     """
-    return sos_get_command_output(cmd, timeout=timeout, runat=runat)['output']
+    return sos_get_command_output(cmd, timeout=timeout, chdir=runat)['output']
 
 
 class ImporterHelper(object):
diff --git a/tests/utilities_tests.py b/tests/utilities_tests.py
index 607056e..9327b1f 100644
--- a/tests/utilities_tests.py
+++ b/tests/utilities_tests.py
@@ -68,6 +68,11 @@ class ExecutableTest(unittest.TestCase):
         self.assertEquals(result['status'], 127)
         self.assertEquals(result['output'], "")
 
+    def test_output_chdir(self):
+        result = sos_get_command_output("/usr/bin/pwd", chdir=TEST_DIR)
+        self.assertEquals(result['status'], 0)
+        self.assertEquals(result['output'].strip(), TEST_DIR)
+
     def test_shell_out(self):
         path = os.path.join(TEST_DIR, 'test_exe.py')
         self.assertEquals("executed\n", shell_out(path))
-- 
1.8.3.1


From 9a87cb3415a7a9587828ee40d689439949def1be Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Sun, 25 Jan 2015 14:30:13 +0000
Subject: [PATCH 06/38] [sosreport] add --chroot option

Add a --chroot option to sosreport to control command chrooting.

The option takes one of three values:

  * auto   - Allow callers of the API to control chroot behaviour
  * always - Always chroot external commands to --sysroot
  * never  - Never chroot external commands

This is a fairly low-level option and may not be exposed to the
user in a final release; for now it will allow tests in container
environments to control the chrooting behaviour used for a run.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/sosreport.py         | 19 +++++++++++++++++++
 tests/utilities_tests.py |  4 +++-
 2 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/sos/sosreport.py b/sos/sosreport.py
index 580b5bd..d9abcb8 100644
--- a/sos/sosreport.py
+++ b/sos/sosreport.py
@@ -540,6 +540,21 @@ class SoSOptions(object):
         self._sysroot = value
 
     @property
+    def chroot(self):
+        if self._options is not None:
+            return self._options.chroot
+        return self._chroot
+
+    @chroot.setter
+    def chroot(self, value):
+        self._check_options_initialized()
+        if value not in ["auto", "always", "never"]:
+            msg = "SoSOptions.chroot '%s' is not a valid chroot mode: "
+            msg += "('auto', 'always', 'never')"
+            raise ValueError(msg % value)
+        self._chroot = value
+
+    @property
     def compression_type(self):
         if self._options is not None:
             return self._options.compression_type
@@ -630,6 +645,10 @@ class SoSOptions(object):
         parser.add_option("-s", "--sysroot", action="store", dest="sysroot",
                           help="system root directory path (default='/')",
                           default="/")
+        parser.add_option("-c", "--chroot", action="store", dest="chroot",
+                          help="chroot executed commands to SYSROOT "
+                               "[auto, always, never] (default=auto)",
+                               default="auto")
         parser.add_option("-z", "--compression-type", dest="compression_type",
                           help="compression technology to use [auto, "
                                "gzip, bzip2, xz] (default=auto)",
diff --git a/tests/utilities_tests.py b/tests/utilities_tests.py
index 9327b1f..c464692 100644
--- a/tests/utilities_tests.py
+++ b/tests/utilities_tests.py
@@ -69,7 +69,9 @@ class ExecutableTest(unittest.TestCase):
         self.assertEquals(result['output'], "")
 
     def test_output_chdir(self):
-        result = sos_get_command_output("/usr/bin/pwd", chdir=TEST_DIR)
+        cmd = "/bin/bash -c 'echo $PWD'"
+        result = sos_get_command_output(cmd, chdir=TEST_DIR)
+        print(result)
         self.assertEquals(result['status'], 0)
         self.assertEquals(result['output'].strip(), TEST_DIR)
 
-- 
1.8.3.1


From 0d060dc3aa5e90373e7bb55f9310b4cf9db0dad4 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Sun, 25 Jan 2015 15:04:29 +0000
Subject: [PATCH 07/38] [plugins] implement --chroot for command callouts

When --chroot=always is given chroot all commands to SYSROOT.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index 137e1a1..10fdae5 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -499,8 +499,12 @@ class Plugin(object):
             self._log_info("added copyspec '%s'" % copy_paths)
 
     def get_command_output(self, prog, timeout=300, runat=None, stderr=True):
-        result = sos_get_command_output(prog, timeout=timeout, chdir=runat,
-                                        stderr=stderr)
+        if self.commons['cmdlineopts'].chroot == 'always':
+            root = self.sysroot
+        else:
+            root = None
+        result = sos_get_command_output(prog, timeout=timeout, stderr=stderr
+                                        chroot=root, chdir=runat)
         if result['status'] == 124:
             self._log_warn("command '%s' timed out after %ds"
                            % (prog, timeout))
-- 
1.8.3.1


From f6f7934c7d3e7f6cb41879fc0625b06d0468af4e Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Sun, 25 Jan 2015 19:32:04 +0000
Subject: [PATCH 08/38] [plugin] fix chrooted symlink handling

_copy_symlink() needs to strip_sysroot(), not join_sysroot(), on
a link target before handing it to _do_copy_path().

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index 10fdae5..9d04939 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -294,7 +294,7 @@ class Plugin(object):
         # to absolute paths to pass to _do_copy_path.
         self._log_debug("normalized link target '%s' as '%s'"
                         % (linkdest, absdest))
-        self._do_copy_path(absdest)
+        self._do_copy_path(self.strip_sysroot(absdest))
 
         self.copied_files.append({'srcpath': srcpath,
                                   'dstpath': dstpath,
-- 
1.8.3.1


From b16fbf3911c6256674e072cff6fa706050861993 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Sun, 25 Jan 2015 21:54:19 +0000
Subject: [PATCH 09/38] [sosreport] check for valid CHROOT values

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/sosreport.py | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

diff --git a/sos/sosreport.py b/sos/sosreport.py
index d9abcb8..a0b89e7 100644
--- a/sos/sosreport.py
+++ b/sos/sosreport.py
@@ -225,6 +225,10 @@ class XmlReport(object):
         outf.close()
 
 
+# valid modes for --chroot
+chroot_modes = ["auto", "always", "never"]
+
+
 class SoSOptions(object):
     _list_plugins = False
     _noplugins = []
@@ -548,7 +552,7 @@ class SoSOptions(object):
     @chroot.setter
     def chroot(self, value):
         self._check_options_initialized()
-        if value not in ["auto", "always", "never"]:
+        if value not in chroot_modes:
             msg = "SoSOptions.chroot '%s' is not a valid chroot mode: "
             msg += "('auto', 'always', 'never')"
             raise ValueError(msg % value)
@@ -705,6 +709,14 @@ class SoSReport(object):
         if self.opts.sysroot:
             self.sysroot = self.opts.sysroot
 
+        self._setup_logging()
+
+        if self.opts.chroot not in chroot_modes:
+            self.soslog.error("invalid chroot mode: %s" % self.opts.chroot)
+            logging.shutdown()
+            self.tempfile_util.clean()
+            self._exit(1)
+
     def print_header(self):
         self.ui_log.info("\n%s\n" % _("sosreport (version %s)" %
                          (__version__,)))
@@ -1205,7 +1217,6 @@ class SoSReport(object):
                     self.ui_log.error(" %s while setting up plugins"
                                       % e.strerror)
                     self.ui_log.error("")
-                    self._exit(1)
                 if self.raise_plugins:
                     raise
                 self._log_plugin_exception(plugname, "setup")
@@ -1455,7 +1466,6 @@ class SoSReport(object):
 
     def execute(self):
         try:
-            self._setup_logging()
             self.policy.set_commons(self.get_commons())
             self.print_header()
             self.load_plugins()
-- 
1.8.3.1


From f06efd6fa7bbb0c81ce0461d4eaeed225d6f04a2 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Sun, 25 Jan 2015 23:03:08 +0000
Subject: [PATCH 10/38] [plugins] add chroot parameter to callout APIs

Expose sos_get_command_output()'s chroot support to plugins via
add_cmd_output(), get_command_output(), call_ext_prog() and
related Plugin methods.

'chroot' is a boolean indicating whether the command should run
in the chroot (True) or in the host namespace (False).

Has no effect when Plugin.use_sysroot() is False.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py | 50 ++++++++++++++++++++++++++++---------------------
 1 file changed, 29 insertions(+), 21 deletions(-)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index 9d04939..c1b659d 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -498,12 +498,13 @@ class Plugin(object):
             self._add_copy_paths(copy_paths)
             self._log_info("added copyspec '%s'" % copy_paths)
 
-    def get_command_output(self, prog, timeout=300, runat=None, stderr=True):
-        if self.commons['cmdlineopts'].chroot == 'always':
+    def get_command_output(self, prog, timeout=300, stderr=True,
+                           chroot=True, runat=None):
+        if chroot or self.commons['cmdlineopts'].chroot == 'always':
             root = self.sysroot
         else:
             root = None
-        result = sos_get_command_output(prog, timeout=timeout, stderr=stderr
+        result = sos_get_command_output(prog, timeout=timeout, stderr=stderr,
                                         chroot=root, chdir=runat)
         if result['status'] == 124:
             self._log_warn("command '%s' timed out after %ds"
@@ -513,12 +514,13 @@ class Plugin(object):
             self._log_debug("could not run '%s': command not found" % prog)
         return result
 
-    def call_ext_prog(self, prog, timeout=300, runat=None, stderr=True):
+    def call_ext_prog(self, prog, timeout=300, stderr=True,
+                      chroot=True, runat=None):
         """Execute a command independantly of the output gathering part of
         sosreport.
         """
-        return self.get_command_output(prog, timeout=timeout, runat=runat,
-                                       stderr=True)
+        return self.get_command_output(prog, timeout=timeout, stderr=stderr,
+                                       chroot=chroot, runat=runat)
 
     def check_ext_prog(self, prog):
         """Execute a command independently of the output gathering part of
@@ -528,8 +530,8 @@ class Plugin(object):
         return self.call_ext_prog(prog)['status'] == 0
 
     def add_cmd_output(self, cmds, suggest_filename=None,
-                       root_symlink=None, timeout=300, runat=None,
-                       stderr=True):
+                       root_symlink=None, timeout=300, stderr=True,
+                       chroot=True, runat=None):
         """Run a program or a list of programs and collect the output"""
         if isinstance(cmds, six.string_types):
             cmds = [cmds]
@@ -537,9 +539,10 @@ class Plugin(object):
             self._log_warn("ambiguous filename or symlink for command list")
         for cmd in cmds:
             cmdt = (
-                cmd, suggest_filename, root_symlink, timeout, runat, stderr
+                cmd, suggest_filename, root_symlink, timeout, stderr,
+                chroot, runat
             )
-            _tuplefmt = "('%s', '%s', '%s', %s, '%s', '%s')"
+            _tuplefmt = "('%s', '%s', '%s', %s, '%s', '%s', '%s')"
             _logstr = "packed command tuple: " + _tuplefmt
             self._log_debug(_logstr % cmdt)
             self.collect_cmds.append(cmdt)
@@ -594,14 +597,14 @@ class Plugin(object):
         self._log_debug("added string '%s' as '%s'" % (content, filename))
 
     def get_cmd_output_now(self, exe, suggest_filename=None,
-                           root_symlink=False, timeout=300,
-                           runat=None, stderr=True):
+                           root_symlink=False, timeout=300, stderr=True,
+                           chroot=True, runat=None):
         """Execute a command and save the output to a file for inclusion in the
         report.
         """
         start = time()
-        result = self.get_command_output(exe, timeout=timeout, runat=runat,
-                                         stderr=stderr)
+        result = self.get_command_output(exe, timeout=timeout, stderr=stderr,
+                                         chroot=chroot, runat=runat)
         # 126 means 'found but not executable'
         if result['status'] == 126 or result['status'] == 127:
             return None
@@ -650,15 +653,20 @@ class Plugin(object):
 
     def _collect_cmd_output(self):
         for progs in zip(self.collect_cmds):
-            (prog, suggest_filename, root_symlink, timeout, runat, stderr
-             ) = progs[0]
-            self._log_debug("unpacked command tuple: "
-                            + "('%s', '%s', '%s', %s, '%s', %s)" % progs[0])
+            (
+                prog,
+                suggest_filename, root_symlink,
+                timeout,
+                stderr,
+                chroot, runat
+            ) = progs[0]
+            self._log_debug("unpacked command tuple: " +
+                            "('%s', '%s', '%s', %s, '%s', '%s', '%s')" %
+                            progs[0])
             self._log_info("collecting output of '%s'" % prog)
             self.get_cmd_output_now(prog, suggest_filename=suggest_filename,
-                                    root_symlink=root_symlink,
-                                    timeout=timeout, runat=runat,
-                                    stderr=stderr)
+                                    root_symlink=root_symlink, timeout=timeout,
+                                    chroot=chroot, runat=runat)
 
     def _collect_strings(self):
         for string, file_name in self.copy_strings:
-- 
1.8.3.1


From 3390d070b2945715a15f74462c511df2b2941ef5 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Sun, 25 Jan 2015 23:08:37 +0000
Subject: [PATCH 11/38] [plugin] add tmp_in_sysroot() method

Add a method that plugins can test to determine whether the
archive's temporary directory is inside sysroot. This is always
true when sysroot is '/'. When sysroot is a subdirectory of root
the temporary directory may be inaccessible from the chroot
namespace. Plugins can test this method to determine where to
write output.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index c1b659d..49f62bf 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -169,6 +169,10 @@ class Plugin(object):
     def use_sysroot(self):
         return self.sysroot != os.path.abspath(os.sep)
 
+    def tmp_in_sysroot(self):
+        paths = [self.sysroot, self.archive.get_tmp_dir()]
+        return os.path.commonprefix(paths) == self.sysroot
+
     def is_installed(self, package_name):
         '''Is the package $package_name installed?'''
         return self.policy().pkg_by_name(package_name) is not None
-- 
1.8.3.1


From 5ae1b392d1f081bcb43e91a572342d6f02e4728d Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Sun, 25 Jan 2015 23:27:51 +0000
Subject: [PATCH 12/38] [plugin] enforce forbidden paths when --sysroot is set

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index 49f62bf..fd1acb5 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -317,6 +317,8 @@ class Plugin(object):
         return None
 
     def _is_forbidden_path(self, path):
+        if self.use_sysroot():
+            path = self.join_sysroot(path)
         return _path_in_path_list(path, self.forbidden_paths)
 
     def _copy_node(self, path, st):
@@ -379,13 +381,15 @@ class Plugin(object):
             'symlink': "no"
         })
 
-    def add_forbidden_path(self, forbiddenPath):
+    def add_forbidden_path(self, forbidden):
         """Specify a path to not copy, even if it's part of a copy_specs[]
         entry.
         """
+        if self.use_sysroot():
+            forbidden = self.join_sysroot(forbidden)
         # Glob case handling is such that a valid non-glob is a reduced glob
-        for filespec in glob.glob(forbiddenPath):
-            self.forbidden_paths.append(filespec)
+        for path in glob.glob(forbidden):
+            self.forbidden_paths.append(path)
 
     def get_all_options(self):
         """return a list of all options selected"""
-- 
1.8.3.1


From e18d25a0e0c10a2702893f7bae2530dc2a41a394 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 00:00:08 +0000
Subject: [PATCH 13/38] [cluster] handle crm_report with --sysroot

Don't attempt to run crm_report in the chroot if tmp is not a
subdirectory of sysroot.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/cluster.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/sos/plugins/cluster.py b/sos/plugins/cluster.py
index ea5dbae..f52f154 100644
--- a/sos/plugins/cluster.py
+++ b/sos/plugins/cluster.py
@@ -120,8 +120,9 @@ class Cluster(Plugin, RedHatPlugin):
             self._log_warn("scrubbing of crm passwords has been disabled:")
             self._log_warn("data collected by crm_report may contain"
                            " sensitive values.")
-        self.add_cmd_output('crm_report %s -S -d --dest %s --from "%s"'
-                            % (crm_scrub, crm_dest, crm_from))
+        self.add_cmd_output('crm_report %s -S -d --dest %s --from "%s"' %
+                            (crm_scrub, crm_dest, crm_from),
+                            chroot=self.tmp_in_sysroot())
 
     def do_lockdump(self):
         if self._mount_debug():
-- 
1.8.3.1


From 2ca9c74454699ba6ecad21d6b0c0809333d729aa Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 00:02:35 +0000
Subject: [PATCH 14/38] [dmraid] don't chroot if tmp is not inside sysroot

To dump metadata dmraid needs to chdir to the temporary archive
directory. Don't attempt to chroot into sysroot if the temporary
directory is not a subdirectory of it.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/dmraid.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/sos/plugins/dmraid.py b/sos/plugins/dmraid.py
index b7c0b42..87381a0 100644
--- a/sos/plugins/dmraid.py
+++ b/sos/plugins/dmraid.py
@@ -39,6 +39,7 @@ class Dmraid(Plugin, RedHatPlugin, DebianPlugin, UbuntuPlugin):
             self.add_cmd_output("dmraid -%s" % (opt,))
         if self.get_option("metadata"):
             metadata_path = self.get_cmd_output_path("metadata")
-            self.add_cmd_output("dmraid -rD", runat=metadata_path)
+            self.add_cmd_output("dmraid -rD", runat=metadata_path,
+                                chroot=self.tmp_in_sysroot())
 
 # vim: et ts=4 sw=4
-- 
1.8.3.1


From 4ae09ee0ed25d771cc6cc8a013837ed4c647b3ed Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 00:04:15 +0000
Subject: [PATCH 15/38] [foreman] don't chroot if tmp is not inside sysroot

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/foreman.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/sos/plugins/foreman.py b/sos/plugins/foreman.py
index 9d1cbad..363b9d6 100644
--- a/sos/plugins/foreman.py
+++ b/sos/plugins/foreman.py
@@ -27,7 +27,9 @@ class Foreman(Plugin, RedHatPlugin):
 
     def setup(self):
         cmd = "foreman-debug"
+
         path = self.get_cmd_output_path(name="foreman-debug")
-        self.add_cmd_output("%s -g -q -a -d %s" % (cmd, path))
+        self.add_cmd_output("%s -g -q -a -d %s" % (cmd, path),
+                            chroot=self.tmp_in_sysroot())
 
 # vim: et ts=4 sw=4
-- 
1.8.3.1


From 78761114ea9cc1b33233db2186a5ff762e1ac2f2 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 00:07:12 +0000
Subject: [PATCH 16/38] [libvirt] use join_sysroot() before calling
 os.path.exists

The libvirt plugin tests for the presence of files. Use
join_sysroot() to ensure the correct path is tested.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/libvirt.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sos/plugins/libvirt.py b/sos/plugins/libvirt.py
index aaf862b..295c8eb 100644
--- a/sos/plugins/libvirt.py
+++ b/sos/plugins/libvirt.py
@@ -56,7 +56,7 @@ class Libvirt(Plugin, RedHatPlugin, UbuntuPlugin, DebianPlugin):
         else:
             self.add_copy_spec("/var/log/libvirt")
 
-        if os.path.exists(libvirt_keytab):
+        if os.path.exists(self.join_sysroot(libvirt_keytab)):
             self.add_cmd_output("klist -ket %s" % libvirt_keytab)
 
         self.add_cmd_output("ls -lR /var/lib/libvirt/qemu")
-- 
1.8.3.1


From b34ea0c6ea5449bbd2f4f9624e1644dc01b07e9d Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 00:09:11 +0000
Subject: [PATCH 17/38] [logs] fix do_regex_find_all() use for --sysroot

The logs plugin searches syslog configuration files. When using
--sysroot the plugin needs to use join_sysroot() to open the
correct path.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/logs.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/sos/plugins/logs.py b/sos/plugins/logs.py
index 2f86acc..7957898 100644
--- a/sos/plugins/logs.py
+++ b/sos/plugins/logs.py
@@ -38,12 +38,12 @@ class Logs(Plugin):
         ])
 
         if self.get_option('all_logs'):
-            logs = self.do_regex_find_all("^\S+\s+(-?\/.*$)\s+",
-                                          "/etc/syslog.conf")
+            syslog_conf = self.join_sysroot("/etc/syslog.conf")
+            logs = self.do_regex_find_all("^\S+\s+(-?\/.*$)\s+", syslog_conf)
             if self.is_installed("rsyslog") \
                     or os.path.exists("/etc/rsyslog.conf"):
                 logs += self.do_regex_find_all("^\S+\s+(-?\/.*$)\s+",
-                                               "/etc/rsyslog.conf")
+                                               rsyslog_conf)
             for i in logs:
                 if i.startswith("-"):
                     i = i[1:]
@@ -74,7 +74,7 @@ class RedHatLogs(Logs, RedHatPlugin):
         messages = "/var/log/messages"
         self.add_copy_spec_limit("/var/log/secure*", sizelimit=self.limit)
         self.add_copy_spec_limit(messages + "*", sizelimit=self.limit)
-        # collect five days worth of logs by default if the system is
+        # collect three days worth of logs by default if the system is
         # configured to use the journal and not /var/log/messages
         if not os.path.exists(messages) and self.is_installed("systemd"):
             try:
-- 
1.8.3.1


From 534eb7fe732ec366292ec582ec2891ef0648ffcb Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 00:11:15 +0000
Subject: [PATCH 18/38] [lvm2] don't chroot if tmp is not inside sysroot

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/lvm2.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sos/plugins/lvm2.py b/sos/plugins/lvm2.py
index dd10fa7..c626231 100644
--- a/sos/plugins/lvm2.py
+++ b/sos/plugins/lvm2.py
@@ -37,7 +37,7 @@ class Lvm2(Plugin, RedHatPlugin, DebianPlugin, UbuntuPlugin):
             lvmdump_opts = "-a -m"
         cmd = lvmdump_cmd % (lvmdump_opts,
                              self.get_cmd_output_path(name="lvmdump"))
-        self.add_cmd_output(cmd)
+        self.add_cmd_output(cmd, chroot=self.tmp_in_sysroot())
 
     def setup(self):
         # use locking_type 0 (no locks) when running LVM2 commands,
-- 
1.8.3.1


From f4218ff5bab93908ca3d0804c9d837bdfa57f654 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 00:37:32 +0000
Subject: [PATCH 19/38] [docs] add --chroot to sosreport.1

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 man/en/sosreport.1 | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/man/en/sosreport.1 b/man/en/sosreport.1
index b0a86f2..f36f845 100644
--- a/man/en/sosreport.1
+++ b/man/en/sosreport.1
@@ -13,6 +13,7 @@ sosreport \- Collect and package diagnostic and support data
           [--batch] [--build] [--debug]\fR
           [--name name] [--case-id id] [--ticket-number nr]
           [-s|--sysroot]\fR
+          [-c|--chroot {auto|always|never}\fR
           [--tmp-dir directory]\fR
           [-p|--profile profile-name]\fR
           [--list-profiles]\fR
@@ -77,6 +78,13 @@ Specify alternate configuration file.
 Specify an alternate root file system path. Useful for collecting
 reports from containers and images.
 .TP
+.B \-c, \--chroot {auto|always|never}
+Set the chroot mode. When \--sysroot is used commands default to
+executing with SYSROOT as the root directory (unless disabled by
+a specific plugin). This can be overriden by setting \--chroot to
+"always" (alwyas chroot) or "never" (always run in the host
+namespace).
+.TP
 .B \--tmp-dir DIRECTORY
 Specify alternate temporary directory to copy data as well as the
 compressed report.
-- 
1.8.3.1


From fb1b4a8b6793611ed91f43f6d3553351c704f50f Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 14:57:01 +0000
Subject: [PATCH 20/38] [plugin] handle ELOOP in _copy_dir()

A problem with systemd's management of the binfmt_misc automount
point in Atomic environments causes attempts to access this path to
fail with ELOOP:

  >>> import os
  >>> os.listdir("/host/proc/sys/fs/binfmt_misc/")
  Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
  OSError: [Errno 2] No such file or directory: '/host/proc/sys/fs/binfmt_misc/'

For reasons that are not yet clear this causes the entire sos
process to immediately terminate.

For now avoid the problem by enclosing the problem os.listdir in
a try/except block that explicitly handles the one errno value
implicated here.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index fd1acb5..b206470 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -27,6 +27,7 @@ import stat
 from time import time
 import logging
 import fnmatch
+import errno
 
 # PYCOMPAT
 import six
@@ -303,10 +304,17 @@ class Plugin(object):
                                   'pointsto': linkdest})
 
     def _copy_dir(self, srcpath):
-        for afile in os.listdir(srcpath):
-            self._log_debug("recursively adding '%s' from '%s'"
-                            % (afile, srcpath))
-            self._do_copy_path(os.path.join(srcpath, afile), dest=None)
+        try:
+            for afile in os.listdir(srcpath):
+                self._log_debug("recursively adding '%s' from '%s'"
+                                % (afile, srcpath))
+                self._do_copy_path(os.path.join(srcpath, afile), dest=None)
+        except OSError as e:
+            if e.errno == errno.ELOOP:
+                msg = "Too many levels of symbolic links copying"
+                self._log_error("_copy_dir: %s '%s'" % (msg, srcpath))
+                return
+            raise e
 
     def _get_dest_for_srcpath(self, srcpath):
         if self.use_sysroot():
-- 
1.8.3.1


From 2c1af0457b668acb99129ed1f6dedcf7cdaa0eea Mon Sep 17 00:00:00 2001
From: Neependra Khare <nkhare@redhat.com>
Date: Mon, 26 Jan 2015 15:04:07 +0000
Subject: [PATCH 21/38] [kubernetes] new plugin

Add a plugin for Kubernetes support.

Signed-off-by: Neependra Khare <nkhare@redhat.com>
Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/kubernetes.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)
 create mode 100644 sos/plugins/kubernetes.py

diff --git a/sos/plugins/kubernetes.py b/sos/plugins/kubernetes.py
new file mode 100644
index 0000000..af3f3a6
--- /dev/null
+++ b/sos/plugins/kubernetes.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2014 Red Hat, Inc. Neependra Khare <nkhare@redhat.com>
+# Copyright (C) 2014 Red Hat, Inc. Bryn M. Reeves <bmr@redhat.com>
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+from sos.plugins import Plugin, RedHatPlugin
+
+
+class kubernetes(Plugin, RedHatPlugin):
+
+    """Kubernetes plugin
+    """
+
+    def setup(self):
+        self.add_copy_spec("/etc/kubernetes")
+        self.add_copy_spec("/etc/etcd")
+        self.add_copy_spec("/var/run/flannel")
+
+        # Kubernetes master info
+        self.add_cmd_output("kubectl version")
+        self.add_cmd_output("kubectl get -o json pods")
+        self.add_cmd_output("kubectl get -o json minions")
+        self.add_cmd_output("kubectl get -o json replicationController")
+        self.add_cmd_output("kubectl get -o json events")
+        self.add_cmd_output("journalctl -r -u kubelet")
+
+        # etcd
+        self.add_cmd_output("curl http://127.0.0.1:4001/version")
+        self.add_cmd_output("curl http://127.0.0.1:4001/v2/members")
+        self.add_cmd_output("curl http://127.0.0.1:4001/v2/stats/leader")
+        self.add_cmd_output("curl http://127.0.0.1:4001/v2/stats/self")
+        self.add_cmd_output("curl http://127.0.0.1:4001/v2/stats/store")
+
+
+# vim: et ts=5 sw=4
-- 
1.8.3.1


From 550dda69c6c5d527498ad928a29c466fad4e250e Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 17:32:35 +0000
Subject: [PATCH 22/38] [docs] fix documentation of --sysroot parameter

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 man/en/sosreport.1 | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/man/en/sosreport.1 b/man/en/sosreport.1
index f36f845..eac9047 100644
--- a/man/en/sosreport.1
+++ b/man/en/sosreport.1
@@ -12,7 +12,7 @@ sosreport \- Collect and package diagnostic and support data
           [--no-report] [--config-file conf]\fR
           [--batch] [--build] [--debug]\fR
           [--name name] [--case-id id] [--ticket-number nr]
-          [-s|--sysroot]\fR
+          [-s|--sysroot SYSROOT]\fR
           [-c|--chroot {auto|always|never}\fR
           [--tmp-dir directory]\fR
           [-p|--profile profile-name]\fR
-- 
1.8.3.1


From d7cf8535a3403fe6050e0905bef2b4429e595664 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 15:32:03 -0500
Subject: [PATCH 23/38] [utilities] add chroot support to shell_out()

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/utilities.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/sos/utilities.py b/sos/utilities.py
index a82ac7c..6475619 100644
--- a/sos/utilities.py
+++ b/sos/utilities.py
@@ -186,11 +186,12 @@ def import_module(module_fqname, superclasses=None):
     return modules
 
 
-def shell_out(cmd, timeout=30, runat=None):
+def shell_out(cmd, timeout=30, chroot=None, runat=None):
     """Shell out to an external command and return the output or the empty
     string in case of error.
     """
-    return sos_get_command_output(cmd, timeout=timeout, chdir=runat)['output']
+    return sos_get_command_output(cmd, timeout=timeout,
+                                  chroot=chroot, chdir=runat)['output']
 
 
 class ImporterHelper(object):
-- 
1.8.3.1


From a1a1fd6cfcdb62d7af7744bb5710a2c7d5b4262a Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 15:33:18 -0500
Subject: [PATCH 24/38] [policies] make PackageManager and Policy sysroot-aware

Add methods to Policy to get the host root file system path and
to test if sos is running in a container and allow Policy classes
to pass a chroot path into the PackageManager constructor in order
to obtain package data from the chroot.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/policies/__init__.py | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

diff --git a/sos/policies/__init__.py b/sos/policies/__init__.py
index 4657614..cea4c09 100644
--- a/sos/policies/__init__.py
+++ b/sos/policies/__init__.py
@@ -57,11 +57,14 @@ class PackageManager(object):
 
     query_command = None
     timeout = 30
+    chroot = None
 
-    def __init__(self, query_command=None):
+    def __init__(self, query_command=None, chroot=None):
         self.packages = {}
         if query_command:
             self.query_command = query_command
+        if chroot:
+            self.chroot = chroot
 
     def all_pkgs_by_name(self, name):
         """
@@ -93,7 +96,11 @@ class PackageManager(object):
                           version': 'major.minor.version'}}
         """
         if self.query_command:
-            pkg_list = shell_out(self.query_command, self.timeout).splitlines()
+            cmd = self.query_command
+            pkg_list = shell_out(
+                cmd, timeout=self.timeout, chroot=self.chroot
+            ).splitlines()
+
             for pkg in pkg_list:
                 if '|' not in pkg:
                     continue
@@ -145,6 +152,9 @@ No changes will be made to system configuration.
     vendor_text = ""
     PATH = ""
 
+    _in_container = False
+    _host_sysroot = '/'
+
     def __init__(self):
         """Subclasses that choose to override this initializer should call
         super() to ensure that they get the required platform bits attached.
@@ -180,6 +190,14 @@ No changes will be made to system configuration.
         """
         return False
 
+    def in_container(self):
+        """ Returns True if sos is running inside a container environment.
+        """
+        return self._in_container
+
+    def host_sysroot(self):
+        return self._host_sysroot
+
     def dist_version(self):
         """
         Return the OS version
-- 
1.8.3.1


From 63805ed15d63ddfebb06cd03f96f310bbf60d3b2 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 15:36:40 -0500
Subject: [PATCH 25/38] [policies] add container support to Red Hat policy

Check for the presence of container-specific environment variables
and set _host_sysroot if present:

  container_uuid=UUID
  HOST=/path

If both a container environment variable and HOST are present run
the PackageManager query command in a chroot.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/policies/redhat.py | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)

diff --git a/sos/policies/redhat.py b/sos/policies/redhat.py
index d2f8db0..f1db8ac 100644
--- a/sos/policies/redhat.py
+++ b/sos/policies/redhat.py
@@ -38,13 +38,17 @@ class RedHatPolicy(LinuxPolicy):
     vendor = "Red Hat"
     vendor_url = "http://www.redhat.com/"
     _tmp_dir = "/var/tmp"
+    _rpmq_cmd = 'rpm -qa --queryformat "%{NAME}|%{VERSION}\\n"'
+    _in_container = False
+    _host_sysroot = '/'
 
     def __init__(self):
         super(RedHatPolicy, self).__init__()
         self.report_name = ""
         self.ticket_number = ""
-        self.package_manager = PackageManager(
-            'rpm -qa --queryformat "%{NAME}|%{VERSION}\\n"')
+        # need to set _host_sysroot before PackageManager()
+        sysroot = self._container_init()
+        self.package_manager = PackageManager(self._rpmq_cmd, chroot=sysroot)
         self.valid_subclasses = [RedHatPlugin]
 
         pkgs = self.package_manager.all_pkgs()
@@ -70,6 +74,17 @@ class RedHatPolicy(LinuxPolicy):
         Fedora, RHEL or other Red Hat distribution or False otherwise."""
         return False
 
+    def _container_init(self):
+        """Check if sos is running in a container and if a host sysroot
+        has been passed in the environment.
+        """
+        if ENV_CONTAINER_UUID in os.environ:
+            self._in_container = True
+        if ENV_HOST_SYSROOT in os.environ:
+            self._host_sysroot = os.environ[ENV_HOST_SYSROOT]
+        use_sysroot = self._in_container and self._host_sysroot != '/'
+        return self._host_sysroot if use_sysroot else None
+
     def runlevel_by_service(self, name):
         from subprocess import Popen, PIPE
         ret = []
@@ -100,6 +115,10 @@ class RedHatPolicy(LinuxPolicy):
     def get_local_name(self):
         return self.host_name()
 
+# Container environment variables on Red Hat systems.
+ENV_CONTAINER_UUID = 'container_uuid'
+ENV_HOST_SYSROOT = 'HOST'
+
 
 class RHELPolicy(RedHatPolicy):
     distro = "Red Hat Enterprise Linux"
-- 
1.8.3.1


From f8a1746a871e560e548d21f1fc68067d452140a0 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 15:39:16 -0500
Subject: [PATCH 26/38] [sosreport] set SYSROOT by policy

If --sysroot is not given on the command line and
Policy.in_container() is True set sysroot automatically if
Policy.get_host_sysroot() is not '/'.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/sosreport.py | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/sos/sosreport.py b/sos/sosreport.py
index a0b89e7..835d828 100644
--- a/sos/sosreport.py
+++ b/sos/sosreport.py
@@ -648,7 +648,7 @@ class SoSOptions(object):
                           help="Disable HTML/XML reporting", default=False)
         parser.add_option("-s", "--sysroot", action="store", dest="sysroot",
                           help="system root directory path (default='/')",
-                          default="/")
+                          default=None)
         parser.add_option("-c", "--chroot", action="store", dest="chroot",
                           help="chroot executed commands to SYSROOT "
                                "[auto, always, never] (default=auto)",
@@ -705,11 +705,18 @@ class SoSReport(object):
         self.tempfile_util = TempFileUtil(self.tmpdir)
         self._set_directories()
 
+        self._setup_logging()
+
+        msg = "default"
+        host_sysroot = self.policy.host_sysroot()
         # set alternate system root directory
         if self.opts.sysroot:
+            msg = "cmdline"
             self.sysroot = self.opts.sysroot
-
-        self._setup_logging()
+        elif self.policy.in_container() and host_sysroot != os.sep:
+            msg = "policy"
+            self.sysroot = host_sysroot
+        self.soslog.debug("set sysroot to '%s' (%s)" % (self.sysroot, msg))
 
         if self.opts.chroot not in chroot_modes:
             self.soslog.error("invalid chroot mode: %s" % self.opts.chroot)
-- 
1.8.3.1


From fac57721ae10dafec909c4fac408f42ebe23d4ac Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 16:14:01 -0500
Subject: [PATCH 27/38] [firewalld] work around command hangs in container
 environments

Add a 10s timeout to firewalld-cmd execution to avoid long dbus
timeouts in docker containers.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/firewalld.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/sos/plugins/firewalld.py b/sos/plugins/firewalld.py
index 98d011b..8eeb2b6 100644
--- a/sos/plugins/firewalld.py
+++ b/sos/plugins/firewalld.py
@@ -35,9 +35,11 @@ class FirewallD(Plugin, RedHatPlugin):
             "/etc/sysconfig/firewalld"
         ])
 
+        # use a 10s timeout to workaround dbus problems in
+        # docker containers.
         self.add_cmd_output([
             "firewall-cmd --list-all-zones",
             "firewall-cmd --permanent --list-all-zones"
-        ])
+        ], timeout=10)
 
 # vim: et ts=4 sw=4
-- 
1.8.3.1


From 6a239ddeb5de9a04e7e7081ea6425d89dddda3f5 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Mon, 26 Jan 2015 17:13:24 -0500
Subject: [PATCH 28/38] [policies] pass --sysroot down to policy classes

Policies that don't auto-detect a container environment with a
host file system need to pass the value of --sysroot down to the
PackageManager class in order to obtain package details from the
chroot environment.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/policies/__init__.py | 11 ++++++-----
 sos/policies/debian.py   |  4 ++--
 sos/policies/redhat.py   | 18 +++++++++++-------
 sos/policies/ubuntu.py   |  4 ++--
 sos/sosreport.py         |  2 +-
 5 files changed, 22 insertions(+), 17 deletions(-)

diff --git a/sos/policies/__init__.py b/sos/policies/__init__.py
index cea4c09..a403bb9 100644
--- a/sos/policies/__init__.py
+++ b/sos/policies/__init__.py
@@ -28,7 +28,7 @@ def import_policy(name):
         return None
 
 
-def load(cache={}):
+def load(cache={}, sysroot=None):
     if 'policy' in cache:
         return cache.get('policy')
 
@@ -37,7 +37,7 @@ def load(cache={}):
     for module in helper.get_modules():
         for policy in import_policy(module):
             if policy.check():
-                cache['policy'] = policy()
+                cache['policy'] = policy(sysroot=sysroot)
 
     if 'policy' not in cache:
         cache['policy'] = GenericPolicy()
@@ -155,7 +155,7 @@ No changes will be made to system configuration.
     _in_container = False
     _host_sysroot = '/'
 
-    def __init__(self):
+    def __init__(self, sysroot=None):
         """Subclasses that choose to override this initializer should call
         super() to ensure that they get the required platform bits attached.
         super(SubClass, self).__init__(). Policies that require runtime
@@ -167,6 +167,7 @@ No changes will be made to system configuration.
         self.package_manager = PackageManager()
         self._valid_subclasses = []
         self.set_exec_path()
+        self._host_sysroot = sysroot
 
     def get_valid_subclasses(self):
         return [IndependentPlugin] + self._valid_subclasses
@@ -372,8 +373,8 @@ class LinuxPolicy(Policy):
     vendor = "None"
     PATH = "/bin:/sbin:/usr/bin:/usr/sbin"
 
-    def __init__(self):
-        super(LinuxPolicy, self).__init__()
+    def __init__(self, sysroot=None):
+        super(LinuxPolicy, self).__init__(sysroot=sysroot)
 
     def get_preferred_hash_algorithm(self):
         checksum = "md5"
diff --git a/sos/policies/debian.py b/sos/policies/debian.py
index acb5b85..e56e546 100644
--- a/sos/policies/debian.py
+++ b/sos/policies/debian.py
@@ -16,8 +16,8 @@ class DebianPolicy(LinuxPolicy):
     PATH = "/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games" \
            + ":/usr/local/sbin:/usr/local/bin"
 
-    def __init__(self):
-        super(DebianPolicy, self).__init__()
+    def __init__(self, sysroot=None):
+        super(DebianPolicy, self).__init__(sysroot=sysroot)
         self.report_name = ""
         self.ticket_number = ""
         self.package_manager = PackageManager(
diff --git a/sos/policies/redhat.py b/sos/policies/redhat.py
index f1db8ac..f20359d 100644
--- a/sos/policies/redhat.py
+++ b/sos/policies/redhat.py
@@ -42,12 +42,16 @@ class RedHatPolicy(LinuxPolicy):
     _in_container = False
     _host_sysroot = '/'
 
-    def __init__(self):
-        super(RedHatPolicy, self).__init__()
+    def __init__(self, sysroot=None):
+        super(RedHatPolicy, self).__init__(sysroot=sysroot)
         self.report_name = ""
         self.ticket_number = ""
         # need to set _host_sysroot before PackageManager()
-        sysroot = self._container_init()
+        if sysroot:
+            self._container_init()
+            self._host_sysroot = sysroot
+        else:
+            sysroot = self._container_init()
         self.package_manager = PackageManager(self._rpmq_cmd, chroot=sysroot)
         self.valid_subclasses = [RedHatPlugin]
 
@@ -145,8 +149,8 @@ No changes will be made to system configuration.
 %(vendor_text)s
 """)
 
-    def __init__(self):
-        super(RHELPolicy, self).__init__()
+    def __init__(self, sysroot=None):
+        super(RHELPolicy, self).__init__(sysroot=sysroot)
 
     @classmethod
     def check(self):
@@ -192,8 +196,8 @@ class FedoraPolicy(RedHatPolicy):
     vendor = "the Fedora Project"
     vendor_url = "https://fedoraproject.org/"
 
-    def __init__(self):
-        super(FedoraPolicy, self).__init__()
+    def __init__(self, sysroot=None):
+        super(FedoraPolicy, self).__init__(sysroot=sysroot)
 
     @classmethod
     def check(self):
diff --git a/sos/policies/ubuntu.py b/sos/policies/ubuntu.py
index 0dd2ea2..f236421 100644
--- a/sos/policies/ubuntu.py
+++ b/sos/policies/ubuntu.py
@@ -9,8 +9,8 @@ class UbuntuPolicy(DebianPolicy):
     vendor = "Ubuntu"
     vendor_url = "http://www.ubuntu.com/"
 
-    def __init__(self):
-        super(UbuntuPolicy, self).__init__()
+    def __init__(self, sysroot=None):
+        super(UbuntuPolicy, self).__init__(sysroot=sysroot)
         self.valid_subclasses = [UbuntuPlugin, DebianPlugin]
 
     @classmethod
diff --git a/sos/sosreport.py b/sos/sosreport.py
index 835d828..ad13a2d 100644
--- a/sos/sosreport.py
+++ b/sos/sosreport.py
@@ -687,7 +687,7 @@ class SoSReport(object):
         self._read_config()
 
         try:
-            self.policy = sos.policies.load()
+            self.policy = sos.policies.load(sysroot=self.opts.sysroot)
         except KeyboardInterrupt:
             self._exit(0)
 
-- 
1.8.3.1


From d34bf4982f8a627901f6538be3b9e52cc30fe91b Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Tue, 27 Jan 2015 11:40:06 +0000
Subject: [PATCH 29/38] [sosoptions] ensure '_sysroot' and '_chroot' are
 initialised

Make sure the sysroot and chroot members of the SoSOptions object
are initialised to prevent exceptions when these are not set on
the command line:

  sosreport
  Traceback (most recent call last):
    File "/usr/sbin/sosreport", line 25, in <module>
      main(sys.argv[1:])
    File "/usr/lib/python2.7/site-packages/sos/sosreport.py", line 1490, in main
      sos = SoSReport(args)
    File "/usr/lib/python2.7/site-packages/sos/sosreport.py", line 673, in __init__
      self.policy = sos.policies.load(sysroot=self.opts.sysroot)
    File "/usr/lib/python2.7/site-packages/sos/policies/__init__.py", line 40, in load
      cache['policy'] = policy(sysroot=sysroot)
    File "/usr/lib/python2.7/site-packages/sos/policies/redhat.py", line 192, in __init__
      super(FedoraPolicy, self).__init__(sysroot=sysroot)
    File "/usr/lib/python2.7/site-packages/sos/policies/redhat.py", line 58, in __init__
      if self.package_manager.all_pkgs()['filesystem']['version'][0] == '3':
    File "/usr/lib/python2.7/site-packages/sos/policies/__init__.py", line 116, in all_pkgs
      self.packages = self.get_pkg_list()
    File "/usr/lib/python2.7/site-packages/sos/policies/__init__.py", line 99, in get_pkg_list
      pkg_list = shell_out(cmd, chroot=self.chroot).splitlines()
    File "/usr/lib/python2.7/site-packages/sos/utilities.py", line 191, in shell_out
      return sos_get_command_output(cmd, chroot=chroot, chdir=runat)['output']
    File "/usr/lib/python2.7/site-packages/sos/utilities.py", line 156, in sos_get_command_output
      raise e
  OSError: [Errno 1] Operation not permitted: '/'

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/sosreport.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/sos/sosreport.py b/sos/sosreport.py
index ad13a2d..27c756f 100644
--- a/sos/sosreport.py
+++ b/sos/sosreport.py
@@ -251,6 +251,8 @@ class SoSOptions(object):
     _config_file = ""
     _tmp_dir = ""
     _report = True
+    _sysroot = None
+    _chroot = 'auto'
     _compression_type = 'auto'
 
     _options = None
-- 
1.8.3.1


From c0858c2c87f246283f6c59b7bf7f64f7dea73a82 Mon Sep 17 00:00:00 2001
From: Neependra Khare <nkhare@redhat.com>
Date: Tue, 27 Jan 2015 15:54:23 +0000
Subject: [PATCH 30/38] [etcd] split etcd functionality from kubernetes into
 new plugin

Signed-off-by: Neependra Khare <nkhare@redhat.com>
Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/etcd.py       | 36 ++++++++++++++++++++++++++++++++++++
 sos/plugins/kubernetes.py |  8 --------
 2 files changed, 36 insertions(+), 8 deletions(-)
 create mode 100644 sos/plugins/etcd.py

diff --git a/sos/plugins/etcd.py b/sos/plugins/etcd.py
new file mode 100644
index 0000000..69edca0
--- /dev/null
+++ b/sos/plugins/etcd.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2015 Red Hat, Inc. Neependra Khare <nkhare@redhat.com>
+# Copyright (C) 2015 Red Hat, Inc. Bryn M. Reeves <bmr@redhat.com>
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+from sos.plugins import Plugin, RedHatPlugin
+
+
+class etcd(Plugin, RedHatPlugin):
+
+    """etcd plugin
+    """
+
+    def setup(self):
+        self.add_copy_spec("/etc/etcd")
+
+        self.add_cmd_output("curl http://localhost:4001/version")
+        self.add_cmd_output("curl http://localhost:4001/v2/members")
+        self.add_cmd_output("curl http://localhost:4001/v2/stats/leader")
+        self.add_cmd_output("curl http://localhost:4001/v2/stats/self")
+        self.add_cmd_output("curl http://localhost:4001/v2/stats/store")
+        self.add_cmd_output("ls -lR /var/lib/etcd/")
+
+
+# vim: et ts=5 sw=4
diff --git a/sos/plugins/kubernetes.py b/sos/plugins/kubernetes.py
index af3f3a6..289d784 100644
--- a/sos/plugins/kubernetes.py
+++ b/sos/plugins/kubernetes.py
@@ -24,7 +24,6 @@ class kubernetes(Plugin, RedHatPlugin):
 
     def setup(self):
         self.add_copy_spec("/etc/kubernetes")
-        self.add_copy_spec("/etc/etcd")
         self.add_copy_spec("/var/run/flannel")
 
         # Kubernetes master info
@@ -35,12 +34,5 @@ class kubernetes(Plugin, RedHatPlugin):
         self.add_cmd_output("kubectl get -o json events")
         self.add_cmd_output("journalctl -r -u kubelet")
 
-        # etcd
-        self.add_cmd_output("curl http://127.0.0.1:4001/version")
-        self.add_cmd_output("curl http://127.0.0.1:4001/v2/members")
-        self.add_cmd_output("curl http://127.0.0.1:4001/v2/stats/leader")
-        self.add_cmd_output("curl http://127.0.0.1:4001/v2/stats/self")
-        self.add_cmd_output("curl http://127.0.0.1:4001/v2/stats/store")
-
 
 # vim: et ts=5 sw=4
-- 
1.8.3.1


From 2ddc706c7219d0b891304fcb066dea865f8516b5 Mon Sep 17 00:00:00 2001
From: Neependra Khare <nkhare@redhat.com>
Date: Tue, 27 Jan 2015 15:58:32 +0000
Subject: [PATCH 31/38] [kubernetes] add services and pod logs collection

Signed-off-by: Neependra Khare <nkhare@redhat.com>
Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/kubernetes.py | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/sos/plugins/kubernetes.py b/sos/plugins/kubernetes.py
index 289d784..9c2df5e 100644
--- a/sos/plugins/kubernetes.py
+++ b/sos/plugins/kubernetes.py
@@ -22,6 +22,8 @@ class kubernetes(Plugin, RedHatPlugin):
     """Kubernetes plugin
     """
 
+    option_list = [("podslog", "capture logs for pods", 'slow', False)]
+
     def setup(self):
         self.add_copy_spec("/etc/kubernetes")
         self.add_copy_spec("/var/run/flannel")
@@ -30,9 +32,19 @@ class kubernetes(Plugin, RedHatPlugin):
         self.add_cmd_output("kubectl version")
         self.add_cmd_output("kubectl get -o json pods")
         self.add_cmd_output("kubectl get -o json minions")
+        self.add_cmd_output("kubectl get -o json services")
         self.add_cmd_output("kubectl get -o json replicationController")
         self.add_cmd_output("kubectl get -o json events")
         self.add_cmd_output("journalctl -r -u kubelet")
 
+        if self.get_option('podslog'):
+            result = self.get_command_output("kubectl get pods")
+            if result['status'] == 0:
+                for line in result['output'].splitlines()[1:]:
+                    pod_name = line.split(" ")[0]
+                    self.add_cmd_output([
+                        "{0} log {1}".format("kubectl", pod_name)
+                    ])
+
 
 # vim: et ts=5 sw=4
-- 
1.8.3.1


From 259897b60b0e4ee00585a8d73521fa4e291eb8da Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Tue, 27 Jan 2015 12:54:31 -0500
Subject: [PATCH 32/38] [policies/redhat] automatically set tmp_dir in
 containers

Now that policies have the infrastructure to detect that they
are running in a container and to use the HOST environment
variable if present enable automatic setting of self._tmp_dir in
appriate container environments.

This causes reports to be automatically written to $HOST/var/tmp
when $HOST is set while still allowing users to override the tmp
path manually by specificying --tmp-dir=PATH.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/policies/redhat.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/sos/policies/redhat.py b/sos/policies/redhat.py
index f20359d..3cf40b3 100644
--- a/sos/policies/redhat.py
+++ b/sos/policies/redhat.py
@@ -79,14 +79,17 @@ class RedHatPolicy(LinuxPolicy):
         return False
 
     def _container_init(self):
-        """Check if sos is running in a container and if a host sysroot
-        has been passed in the environment.
+        """Check if sos is running in a container and perform container
+        specific initialisation based on ENV_HOST_SYSROOT.
         """
         if ENV_CONTAINER_UUID in os.environ:
             self._in_container = True
         if ENV_HOST_SYSROOT in os.environ:
             self._host_sysroot = os.environ[ENV_HOST_SYSROOT]
         use_sysroot = self._in_container and self._host_sysroot != '/'
+        if use_sysroot:
+            host_tmp_dir = os.path.abspath(self._host_sysroot + self._tmp_dir)
+            self._tmp_dir = host_tmp_dir
         return self._host_sysroot if use_sysroot else None
 
     def runlevel_by_service(self, name):
-- 
1.8.3.1


From b880c6cb4668815b97841e6532450144a5f825f1 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Tue, 27 Jan 2015 13:53:34 -0500
Subject: [PATCH 33/38] [policies/redhat] add Red Hat Atomic Host policy

Add a new policy for the Red Hat Atomic Host.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/policies/redhat.py | 39 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 38 insertions(+), 1 deletion(-)

diff --git a/sos/policies/redhat.py b/sos/policies/redhat.py
index 3cf40b3..9decd0a 100644
--- a/sos/policies/redhat.py
+++ b/sos/policies/redhat.py
@@ -37,6 +37,7 @@ class RedHatPolicy(LinuxPolicy):
     distro = "Red Hat"
     vendor = "Red Hat"
     vendor_url = "http://www.redhat.com/"
+    _redhat_release = '/etc/redhat-release'
     _tmp_dir = "/var/tmp"
     _rpmq_cmd = 'rpm -qa --queryformat "%{NAME}|%{VERSION}\\n"'
     _in_container = False
@@ -159,7 +160,7 @@ No changes will be made to system configuration.
     def check(self):
         """This method checks to see if we are running on RHEL. It returns True
         or False."""
-        return (os.path.isfile('/etc/redhat-release')
+        return (os.path.isfile(self._redhat_release)
                 and not os.path.isfile('/etc/fedora-release'))
 
     def dist_version(self):
@@ -193,6 +194,42 @@ No changes will be made to system configuration.
         return self.rhn_username() or self.host_name()
 
 
+class RedHatAtomicPolicy(RHELPolicy):
+    distro = "Red Hat Atomic Host"
+    msg = _("""\
+This command will collect diagnostic and configuration \
+information from this %(distro)s system.
+
+An archive containing the collected information will be \
+generated in %(tmpdir)s and may be provided to a %(vendor)s \
+support representative.
+
+Any information provided to %(vendor)s will be treated in \
+accordance with the published support policies at:\n
+  %(vendor_url)s
+
+The generated archive may contain data considered sensitive \
+and its content should be reviewed by the originating \
+organization before being passed to any third party.
+%(vendor_text)s
+""")
+
+    @classmethod
+    def check(self):
+        atomic = False
+        if ENV_HOST_SYSROOT not in os.environ:
+            return atomic
+        host_release = os.environ[ENV_HOST_SYSROOT] + self._redhat_release
+        if not os.path.exists(host_release):
+            return False
+        try:
+            for line in open(host_release, "r").read().splitlines():
+                atomic |= 'Atomic' in line
+        except:
+            pass
+        return atomic
+
+
 class FedoraPolicy(RedHatPolicy):
 
     distro = "Fedora"
-- 
1.8.3.1


From cd0d5c1f06a69c115e674f87254cc7e056c9891a Mon Sep 17 00:00:00 2001
From: Jeremy Eder <jeder@redhat.com>
Date: Wed, 28 Jan 2015 12:54:14 +0000
Subject: [PATCH 34/38] [docker] add 'docker' to the package list for Red Hat
 distros

The docker package is named 'docker-io' in Fedora and 'docker'
in RHEL and other downstream products. Add the 'docker' name to
the package list in RedHatDocker to ensure the plugin runs.

Signed-off-by: Jeremy Eder <jeder@redhat.com>
Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/docker.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sos/plugins/docker.py b/sos/plugins/docker.py
index 3e5bd4a..c5ea8a0 100644
--- a/sos/plugins/docker.py
+++ b/sos/plugins/docker.py
@@ -54,7 +54,7 @@ class Docker(Plugin):
 
 class RedHatDocker(Docker, RedHatPlugin):
 
-    packages = ('docker-io',)
+    packages = ('docker', 'docker-io')
 
     def setup(self):
         super(RedHatDocker, self).setup()
-- 
1.8.3.1


From d4ae63afbf211a7234b7605bef037d827263bf70 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Wed, 28 Jan 2015 22:54:48 +0000
Subject: [PATCH 35/38] [plugins] automatically re-try chroot'ed commands in
 the host

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py | 12 +++++++++++-
 sos/utilities.py        |  5 +----
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index b206470..7fa7ac6 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -520,13 +520,23 @@ class Plugin(object):
             root = self.sysroot
         else:
             root = None
+
         result = sos_get_command_output(prog, timeout=timeout, stderr=stderr,
                                         chroot=root, chdir=runat)
+
         if result['status'] == 124:
             self._log_warn("command '%s' timed out after %ds"
                            % (prog, timeout))
-        # 126 means 'found but not executable'
+
+        # command not found or not runnable
         if result['status'] == 126 or result['status'] == 127:
+            # automatically retry chroot'ed commands in the host namespace
+            if chroot and self.commons['cmdlineopts'].chroot != 'always':
+                self._log_info("command '%s' not found in %s - "
+                               "re-trying in host root"
+                               % (prog.split()[0], root))
+                return self.get_command_output(prog, timeout=timeout,
+                                               chroot=False, runat=runat)
             self._log_debug("could not run '%s': command not found" % prog)
         return result
 
diff --git a/sos/utilities.py b/sos/utilities.py
index 6475619..d3a1048 100644
--- a/sos/utilities.py
+++ b/sos/utilities.py
@@ -151,16 +151,13 @@ def sos_get_command_output(command, timeout=300, stderr=False,
                   stderr=STDOUT if stderr else PIPE,
                   bufsize=-1, env=cmd_env, close_fds=True,
                   preexec_fn=_child_prep_fn)
+        stdout, stderr = p.communicate()
     except OSError as e:
         if e.errno == errno.ENOENT:
             return {'status': 127, 'output': ""}
         else:
             raise e
 
-    stdout, stderr = p.communicate()
-
-    # Required hack while we still pass shell=True to Popen; a Popen
-    # call with shell=False for a non-existant binary will raise OSError.
     if p.returncode == 126 or p.returncode == 127:
         stdout = six.binary_type(b"")
 
-- 
1.8.3.1


From 6fe240a31c01d63957ce548f4415ca55859dc071 Mon Sep 17 00:00:00 2001
From: Neependra Khare <nkhare@redhat.com>
Date: Thu, 29 Jan 2015 18:23:53 +0000
Subject: [PATCH 36/38] [kubernetes] add journal output for kube services

Add journalctl output for the following kubernetes units:

  kube-apiserver
  kube-controller-manager
  kube-scheduler
  kube-proxy

Signed-off-by: Neependra Khare <nkhare@redhat.com>
Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/kubernetes.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/sos/plugins/kubernetes.py b/sos/plugins/kubernetes.py
index 9c2df5e..38faeb2 100644
--- a/sos/plugins/kubernetes.py
+++ b/sos/plugins/kubernetes.py
@@ -36,6 +36,10 @@ class kubernetes(Plugin, RedHatPlugin):
         self.add_cmd_output("kubectl get -o json replicationController")
         self.add_cmd_output("kubectl get -o json events")
         self.add_cmd_output("journalctl -r -u kubelet")
+        self.add_cmd_output("journalctl -r -u kube-apiserver")
+        self.add_cmd_output("journalctl -r -u kube-controller-manager")
+        self.add_cmd_output("journalctl -r -u kube-scheduler")
+        self.add_cmd_output("journalctl -r -u kube-proxy")
 
         if self.get_option('podslog'):
             result = self.get_command_output("kubectl get pods")
-- 
1.8.3.1


From 8d69d74a5be6dac29fad2ab5d7206a0485969924 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Thu, 29 Jan 2015 20:30:31 +0000
Subject: [PATCH 37/38] [plugins] do not strip SYSROOT when copying link
 targets

The abspath() call in _copy_symlink returns a host-relative path
(when SYSROOT is not '/'). Pass this directly to _do_copy_path()
without stripping the SYSROOT path component.

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index 7fa7ac6..a7c0146 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -265,7 +265,8 @@ class Plugin(object):
         # the target stored in the original symlink
         linkdest = os.readlink(srcpath)
         dest = os.path.join(os.path.dirname(srcpath), linkdest)
-        # absolute path to the link target
+        # Absolute path to the link target. If SYSROOT != '/' this path
+        # is relative to the host root file system.
         absdest = os.path.normpath(dest)
         # adjust the target used inside the report to always be relative
         if os.path.isabs(linkdest):
-- 
1.8.3.1


From 76c04a773f1927359d264f59e4fbcc9363424882 Mon Sep 17 00:00:00 2001
From: "Bryn M. Reeves" <bmr@redhat.com>
Date: Thu, 29 Jan 2015 22:00:10 +0000
Subject: [PATCH 38/38] [plugins] trim leading '../' from links when sysroot is
 set

When SYSROOT is not '/' relative symlinks need to be trimmed to
remove the extra leading '../' returned by
abspath('/sysroot/...').

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
---
 sos/plugins/__init__.py | 3 +++
 sos/sosreport.py        | 1 +
 2 files changed, 4 insertions(+)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index a7c0146..a06c0b1 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -271,6 +271,9 @@ class Plugin(object):
         # adjust the target used inside the report to always be relative
         if os.path.isabs(linkdest):
             reldest = os.path.relpath(linkdest, os.path.dirname(srcpath))
+            # trim leading /sysroot
+            if self.use_sysroot():
+                reldest = reldest[len(os.sep + os.pardir):]
             self._log_debug("made link target '%s' relative as '%s'"
                             % (linkdest, reldest))
         else:
diff --git a/sos/sosreport.py b/sos/sosreport.py
index 27c756f..567e3df 100644
--- a/sos/sosreport.py
+++ b/sos/sosreport.py
@@ -1226,6 +1226,7 @@ class SoSReport(object):
                     self.ui_log.error(" %s while setting up plugins"
                                       % e.strerror)
                     self.ui_log.error("")
+                    self._exit(1)
                 if self.raise_plugins:
                     raise
                 self._log_plugin_exception(plugname, "setup")
-- 
1.8.3.1

From bb6f546603d8e3199bda0bfe0f9b23f1da1cb8c9 Mon Sep 17 00:00:00 2001
From: Pavel Moravec <pmoravec@redhat.com>
Date: Mon, 13 Jul 2015 15:03:37 +0200
Subject: [PATCH] [plugin] pass stderr through _collect_cmd_output

Commit f06efd6 removed passing stderr in _collect_cmd_output to
get_cmd_output_now. That prevents passing stderr=False in several
scenarios.

This fix adds the argument to be passed back.

Resolves: #600

Signed-off-by: Pavel Moravec <pmoravec@redhat.com>
---
 sos/plugins/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sos/plugins/__init__.py b/sos/plugins/__init__.py
index a06c0b1..aed7496 100644
--- a/sos/plugins/__init__.py
+++ b/sos/plugins/__init__.py
@@ -696,7 +696,7 @@ class Plugin(object):
             self._log_info("collecting output of '%s'" % prog)
             self.get_cmd_output_now(prog, suggest_filename=suggest_filename,
                                     root_symlink=root_symlink, timeout=timeout,
-                                    chroot=chroot, runat=runat)
+                                    stderr=stderr, chroot=chroot, runat=runat)
 
     def _collect_strings(self):
         for string, file_name in self.copy_strings:
-- 
1.8.3.1