From 13890528fd960c7e1bf0b33b7b20bc9d74f07020 Mon Sep 17 00:00:00 2001 From: Arif Ali Date: Mon, 20 May 2024 20:59:19 +0100 Subject: [PATCH] [global] Add the ability to run command as user Signed-off-by: Arif Ali --- sos/report/plugins/__init__.py | 39 +++++++++++++++++++++++++--------- sos/utilities.py | 16 +++++++++++++- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/sos/report/plugins/__init__.py b/sos/report/plugins/__init__.py index a7978b5814..92aada268a 100644 --- a/sos/report/plugins/__init__.py +++ b/sos/report/plugins/__init__.py @@ -2003,7 +2003,10 @@ def _add_cmd_output(self, **kwargs): return if self.test_predicate(cmd=True, pred=pred): self.collect_cmds.append(soscmd) - self._log_info(f"added cmd output '{soscmd.cmd}'") + user = "" + if hasattr(soscmd, "runas"): + user = f", as the {soscmd.runas} user" + self._log_info(f"added cmd output '{soscmd.cmd}'{user}") else: self.log_skipped_cmd(soscmd.cmd, pred, changes=soscmd.changes) @@ -2013,7 +2016,7 @@ def add_cmd_output(self, cmds, suggest_filename=None, sizelimit=None, pred=None, subdir=None, changes=False, foreground=False, tags=[], priority=10, cmd_as_tag=False, container=None, - to_file=False): + to_file=False, runas=None): """Run a program or a list of programs and collect the output Output will be limited to `sizelimit`, collecting the last X amount @@ -2086,6 +2089,9 @@ def add_cmd_output(self, cmds, suggest_filename=None, :param to_file: Should command output be written directly to a new file rather than stored in memory? :type to_file: ``bool`` + + :param runas: Run the `cmd` as the `runas` user + :type runas: ``str`` """ if isinstance(cmds, str): cmds = [cmds] @@ -2113,7 +2119,8 @@ def add_cmd_output(self, cmds, suggest_filename=None, pred=pred, subdir=subdir, tags=tags, changes=changes, foreground=foreground, priority=priority, cmd_as_tag=cmd_as_tag, - to_file=to_file, container_cmd=container_cmd) + to_file=to_file, container_cmd=container_cmd, + runas=runas) def add_cmd_tags(self, tagdict): """Retroactively add tags to any commands that have been run by this @@ -2275,7 +2282,7 @@ def _collect_cmd_output(self, cmd, suggest_filename=None, binary=False, sizelimit=None, subdir=None, changes=False, foreground=False, tags=[], priority=10, cmd_as_tag=False, to_file=False, - container_cmd=False): + container_cmd=False, runas=None): """Execute a command and save the output to a file for inclusion in the report. @@ -2303,6 +2310,7 @@ def _collect_cmd_output(self, cmd, suggest_filename=None, :param cmd_as_tag: Format command string to tag :param to_file: Write output directly to file instead of saving in memory + :param runas: Run the `cmd` as the `runas` user :returns: dict containing status, output, and filename in the archive for the executed cmd @@ -2353,7 +2361,7 @@ def _collect_cmd_output(self, cmd, suggest_filename=None, cmd, timeout=timeout, stderr=stderr, chroot=root, chdir=runat, env=_env, binary=binary, sizelimit=sizelimit, poller=self.check_timeout, foreground=foreground, - to_file=out_file + to_file=out_file, runas=runas ) end = time() @@ -2451,7 +2459,7 @@ def collect_cmd_output(self, cmd, suggest_filename=None, stderr=True, chroot=True, runat=None, env=None, binary=False, sizelimit=None, pred=None, changes=False, foreground=False, subdir=None, - tags=[]): + tags=[], runas=None): """Execute a command and save the output to a file for inclusion in the report, then return the results for further use by the plugin @@ -2500,6 +2508,9 @@ def collect_cmd_output(self, cmd, suggest_filename=None, :param tags: Add tags in the archive manifest :type tags: ``str`` or a ``list`` of strings + :param runas: Run the `cmd` as the `runas` user + :type runas: ``str`` + :returns: `cmd` exit status, output, and the filepath within the archive output was saved to :rtype: ``dict`` @@ -2516,12 +2527,13 @@ def collect_cmd_output(self, cmd, suggest_filename=None, cmd, suggest_filename=suggest_filename, root_symlink=root_symlink, timeout=timeout, stderr=stderr, chroot=chroot, runat=runat, env=env, binary=binary, sizelimit=sizelimit, foreground=foreground, - subdir=subdir, tags=tags + subdir=subdir, tags=tags, runas=runas ) 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): + foreground=False, container=False, quotecmd=False, + runas=None): """Execute a command right now and return the output and status, but do not save the output within the archive. @@ -2563,6 +2575,9 @@ def exec_cmd(self, cmd, timeout=None, stderr=True, chroot=True, :param quotecmd: Whether the cmd should be quoted. :type quotecmd: ``bool`` + :param runas: Run the `cmd` as the `runas` user + :type runas: ``str`` + :returns: Command exit status and output :rtype: ``dict`` """ @@ -2593,7 +2608,8 @@ def exec_cmd(self, cmd, timeout=None, stderr=True, chroot=True, return sos_get_command_output(cmd, timeout=timeout, chroot=root, chdir=runat, binary=binary, env=_env, - foreground=foreground, stderr=stderr) + foreground=foreground, stderr=stderr, + runas=runas) def _add_container_file_to_manifest(self, container, path, arcpath, tags): """Adds a file collection to the manifest for a particular container @@ -3079,7 +3095,10 @@ def _collect_cmds(self): self.collect_cmds.sort(key=lambda x: x.priority) for soscmd in self.collect_cmds: self._log_debug("unpacked command: " + soscmd.__str__()) - self._log_info(f"collecting output of '{soscmd.cmd}'") + user = "" + if hasattr(soscmd, "runas"): + user = f", as the {soscmd.runas} user" + self._log_info(f"collecting output of '{soscmd.cmd}'{user}") self._collect_cmd_output(**soscmd.__dict__) def _collect_tailed_files(self): diff --git a/sos/utilities.py b/sos/utilities.py index c0a93ada9a..535e3d336c 100644 --- a/sos/utilities.py +++ b/sos/utilities.py @@ -7,6 +7,7 @@ # See the LICENSE file in the source distribution for further information. import os +import pwd import re import inspect from subprocess import Popen, PIPE, STDOUT @@ -218,7 +219,7 @@ def is_executable(command, sysroot=None): def sos_get_command_output(command, timeout=TIMEOUT_DEFAULT, stderr=False, chroot=None, chdir=None, env=None, foreground=False, binary=False, sizelimit=None, poller=None, - to_file=False): + to_file=False, runas=None): # pylint: disable=too-many-locals,too-many-branches """Execute a command and return a dictionary of status and output, optionally changing root or current working directory before @@ -232,6 +233,10 @@ def _child_prep_fn(): os.chroot(chroot) if (chdir): os.chdir(chdir) + if runas: + os.setgid(pwd.getpwnam(runas).pw_gid) + os.setuid(pwd.getpwnam(runas).pw_uid) + os.chdir(pwd.getpwnam(runas).pw_dir) def _check_poller(proc): if poller() or proc.poll() == 124: @@ -239,6 +244,15 @@ def _check_poller(proc): raise SoSTimeoutError time.sleep(0.01) + if runas: + pwd_user = pwd.getpwnam(runas) + env.update({ + 'HOME': pwd_user.pw_dir, + 'LOGNAME': runas, + 'PWD': pwd_user.pw_dir, + 'USER': runas + }) + cmd_env = os.environ.copy() # ensure consistent locale for collected command output cmd_env['LC_ALL'] = 'C.UTF-8'