diff --git a/conf/clush.conf b/conf/clush.conf index 753d38e0..dd24571e 100644 --- a/conf/clush.conf +++ b/conf/clush.conf @@ -26,4 +26,5 @@ verbosity: 1 #scp_path: /usr/bin/sshpass -f /root/remotepasswordfile /usr/bin/scp #ssh_options: -oBatchMode=no -oStrictHostKeyChecking=no - +# sudo command used for --sudo +#sudo_command: /usr/bin/sudo -S -p "''" -k diff --git a/doc/extras/vim/syntax/clushconf.vim b/doc/extras/vim/syntax/clushconf.vim index 3e157169..e31d6139 100644 --- a/doc/extras/vim/syntax/clushconf.vim +++ b/doc/extras/vim/syntax/clushconf.vim @@ -19,6 +19,7 @@ syn match clushHeader "\[\w\+\]" syn keyword clushKeys fanout command_timeout connect_timeout color fd_max history_size node_count maxrc verbosity syn keyword clushKeys ssh_user ssh_path ssh_options syn keyword clushKeys rsh_path rcp_path rcp_options +syn keyword clushKeys sudo_command " Define the default highlighting. " For version 5.7 and earlier: only when not done already diff --git a/doc/man/man1/clush.1 b/doc/man/man1/clush.1 index ea100e22..98ef2bfa 100644 --- a/doc/man/man1/clush.1 +++ b/doc/man/man1/clush.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH CLUSH 1 "2021-11-03" "1.8.4" "ClusterShell User Manual" +.TH CLUSH 1 "2022-06-29" "1.8.4" "ClusterShell User Manual" .SH NAME clush \- execute shell commands on a cluster . @@ -162,6 +162,9 @@ optional \fBgroups.conf\fP(5) group source to use .B \-n\fP,\fB \-\-nostdin do not watch for possible input from stdin; this should be used when \fBclush\fP is run in the background (or in scripts). .TP +.B \-\-sudo +enable sudo password prompt: a prompt will ask for your sudo password and sudo will be used to run your commands on the target nodes. The password must be the same on all target nodes. The actual sudo command used by \fBclush\fP can be changed in \fBclush.conf\fP(5) or in command line using \fB\-O sudo_command="..."\fP\&. The configured \fBsudo_command\fP must be able to read a password on stdin followed by a new line (which is what \fBsudo \-S\fP does). +.TP .BI \-\-groupsconf\fB= FILE use alternate config file for groups.conf(5) .TP diff --git a/doc/man/man5/clush.conf.5 b/doc/man/man5/clush.conf.5 index 2d01df24..d068c0b2 100644 --- a/doc/man/man5/clush.conf.5 +++ b/doc/man/man5/clush.conf.5 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH CLUSH.CONF 5 "2021-11-03" "1.8.4" "ClusterShell User Manual" +.TH CLUSH.CONF 5 "2022-06-29" "1.8.4" "ClusterShell User Manual" .SH NAME clush.conf \- Configuration file for clush . @@ -139,6 +139,9 @@ Same a rsh_path for rcp command. (Default is \fIrcp\fP) .TP .B rsh_options Set additional options to pass to the underlying rsh/rcp command. +.TP +.B sudo_command +sudo command for use with \fI\-\-sudo\fP .UNINDENT .SH EXAMPLES .sp diff --git a/doc/sphinx/config.rst b/doc/sphinx/config.rst index 1320d48f..421c091b 100644 --- a/doc/sphinx/config.rst +++ b/doc/sphinx/config.rst @@ -101,6 +101,9 @@ The following table describes available *clush* config file settings. | rsh_options | Set additional options to pass to the underlying | | | rsh/rcp command. | +-----------------+----------------------------------------------------+ +| sudo_command | *sudo(8)* command for use with | +| | :ref:`--sudo ` | ++-----------------+----------------------------------------------------+ .. _groups-config: diff --git a/doc/sphinx/tools/clush.rst b/doc/sphinx/tools/clush.rst index e90791a8..6b282f2b 100644 --- a/doc/sphinx/tools/clush.rst +++ b/doc/sphinx/tools/clush.rst @@ -638,6 +638,48 @@ By default, ClusterShell supports the following worker identifiers: Worker modules distributed outside of ClusterShell are also supported by specifying the case-sensitive full Python module name of a worker module. +.. _clush-sudo: + +Support for sudo +"""""""""""""""" + +Since version 1.9, *clush* has support for `Sudo`_ password forwarding over +stdin. This may be useful in an environment that only allows sysadmins +to perform interactive *sudo* work with password. + +.. warning:: In this section, it is assumed that *sudo* always requires a + password for the user on the target nodes. If *sudo* does NOT require + any password (i.e. **NOPASSWD** is specified in your sudoers file), you + do not need any extra options to run your *sudo* commands with *clush*. + +Run *clush* with ``--sudo`` to **enable a password prompt** to type your *sudo* +password, then *sudo* (well, ``sudo_command`` – see below) will be used to run +your commands on the target nodes. The password is broadcasted to all target +nodes over *ssh(1)* (or via your :ref:`favorite worker `) and +as such, must be the same on all target nodes. It is not stored on disk at +any time and only kept in memory during the duration of the *clush* command. +Thus, the password will be prompted every time you run *clush*. When you +start *clush* in :ref:`interactive mode ` along with +``--sudo``, you can run multiple commands in that mode without having to type +your password every time. + +When ``--sudo`` is used, *clush* will run *sudo* for you on each target node, +so your command itself should NOT start with ``sudo``. The actual *sudo* +command used by *clush* can be changed in :ref:`clush.conf ` or +in command line using ``-O sudo_command="..."``. The configured +``sudo_command`` must be able to read a password on stdin followed by a new +line (which is what ``sudo -S`` does). + +Usage example:: + + $ clush -w n[1-2]c[01-02] --sudo -b id + Password: + --------------- + n[1-2]c[01-02] (4) + --------------- + uid=0(root) gid=0(root) groups=0(root) + + .. [#] LLNL parallel remote shell utility (https://computing.llnl.gov/linux/pdsh.html) @@ -649,3 +691,5 @@ specifying the case-sensitive full Python module name of a worker module. .. _ticket: https://github.com/cea-hpc/clustershell/issues/new .. _this paper: https://www.kernel.org/doc/ols/2012/ols2012-thiell.pdf + +.. _Sudo: https://www.sudo.ws/ diff --git a/doc/txt/clush.conf.txt b/doc/txt/clush.conf.txt index 34b928fe..46bc5702 100644 --- a/doc/txt/clush.conf.txt +++ b/doc/txt/clush.conf.txt @@ -7,7 +7,7 @@ Configuration file for `clush` ------------------------------ :Author: Stephane Thiell, -:Date: 2021-11-03 +:Date: 2022-06-29 :Copyright: GNU Lesser General Public License version 2.1 or later (LGPLv2.1+) :Version: 1.8.4 :Manual section: 5 @@ -98,6 +98,8 @@ rcp_path Same a rsh_path for rcp command. (Default is `rcp`) rsh_options Set additional options to pass to the underlying rsh/rcp command. +sudo_command + sudo command for use with `--sudo` EXAMPLES =========== diff --git a/doc/txt/clush.txt b/doc/txt/clush.txt index 61aa8465..3c4528b3 100644 --- a/doc/txt/clush.txt +++ b/doc/txt/clush.txt @@ -7,7 +7,7 @@ execute shell commands on a cluster ----------------------------------- :Author: Stephane Thiell -:Date: 2021-11-03 +:Date: 2022-06-29 :Copyright: GNU Lesser General Public License version 2.1 or later (LGPLv2.1+) :Version: 1.8.4 :Manual section: 1 @@ -131,6 +131,7 @@ OPTIONS -s GROUPSOURCE, --groupsource=GROUPSOURCE optional ``groups.conf``\(5) group source to use -n, --nostdin do not watch for possible input from stdin; this should be used when ``clush`` is run in the background (or in scripts). +--sudo enable sudo password prompt: a prompt will ask for your sudo password and sudo will be used to run your commands on the target nodes. The password must be the same on all target nodes. The actual sudo command used by ``clush`` can be changed in ``clush.conf``\(5) or in command line using ``-O sudo_command="..."``. The configured ``sudo_command`` must be able to read a password on stdin followed by a new line (which is what ``sudo -S`` does). --groupsconf=FILE use alternate config file for groups.conf(5) --conf=FILE use alternate config file for clush.conf(5) -O , --option= diff --git a/lib/ClusterShell/CLI/Clush.py b/lib/ClusterShell/CLI/Clush.py index f408ab6b..41a59762 100755 --- a/lib/ClusterShell/CLI/Clush.py +++ b/lib/ClusterShell/CLI/Clush.py @@ -33,11 +33,13 @@ from __future__ import print_function +import getpass import logging import os from os.path import abspath, dirname, exists, isdir, join import random import resource +import shlex import signal import sys import time @@ -608,6 +610,9 @@ def ttyloop(task, nodeset, timeout, display, remote, trytree): continue if readline_avail: readline.write_history_file(get_history_file()) + if task.default("USER_sudo_command"): + sudo_cmdl = shlex.split(task.default("USER_sudo_command")) + cmd = "%s %s" % (' '.join(sudo_cmdl), cmd) run_command(task, cmd, ns, timeout, display, remote, trytree) return rc @@ -675,13 +680,18 @@ def run_command(task, cmd, ns, timeout, display, remote, trytree): # this is the simpler but faster output handler handler = DirectOutputHandler(display) - stdin = task.default("USER_stdin_worker") + stdin = task.default("USER_stdin_worker") # stdin forwarding? + sudo_passwd = task.default("USER_sudo_passwd") # --sudo? worker = task.shell(cmd, nodes=ns, handler=handler, timeout=timeout, - remote=remote, tree=trytree, stdin=stdin) + remote=remote, tree=trytree, stdin=stdin or sudo_passwd) if ns is None: worker.set_key('LOCAL') + if sudo_passwd: + worker.write(sudo_passwd.encode() + b'\n') if stdin: bind_stdin(worker, display) + if sudo_passwd and not stdin: + worker.set_write_eof() # we only enabled stdin to send the sudo password task.resume() def run_copy(task, sources, dest, ns, timeout, preserve_flag, display): @@ -743,6 +753,10 @@ def set_fdlimit(fd_max, display): msgfmt = 'Warning: Failed to set max open files limit to %d (%s)' display.vprint_err(VERB_VERB, msgfmt % (rlim_max, exc)) +def ask_pass(): + """Prompt for password (--sudo)""" + return getpass.getpass() + def clush_exit(status, task=None): """Exit script, flushing stdio buffers and stopping ClusterShell task.""" if task: @@ -797,6 +811,8 @@ def main(): parser.add_option("-n", "--nostdin", action="store_true", dest="nostdin", help="don't watch for possible input from stdin") + parser.add_option("--sudo", action="store_true", dest="sudo", + help="enable sudo password prompt") parser.install_groupsconf_option() parser.install_clush_config_options() @@ -976,6 +992,16 @@ def main(): task.set_info("debug", config.verbosity >= VERB_DEBUG) task.set_info("fanout", config.fanout) + if options.sudo: + # keep sudo_command for interactive mode ttyloop() + task.set_default("USER_sudo_command", config.sudo_command) + sudo_cmdl = shlex.split(config.sudo_command) + display.vprint(VERB_DEBUG, "sudo command prefix: %s" % sudo_cmdl) + # prefix actual command with sudo command + args = sudo_cmdl + args + # prompt for sudo password + task.set_default("USER_sudo_passwd", ask_pass()) + if options.worker: try: if options.remote == 'no': diff --git a/lib/ClusterShell/CLI/Config.py b/lib/ClusterShell/CLI/Config.py index 46715ae5..3625a49c 100644 --- a/lib/ClusterShell/CLI/Config.py +++ b/lib/ClusterShell/CLI/Config.py @@ -57,7 +57,8 @@ class ClushConfig(configparser.ConfigParser, object): "verbosity": "%d" % VERB_STD, "node_count": "yes", "maxrc": "no", - "fd_max": "8192"} + "fd_max": "8192", + "sudo_command": 'sudo -S -p "\'\'" -k'} def __init__(self, options, filename=None): """Initialize ClushConfig object from corresponding @@ -225,3 +226,7 @@ def fd_max(self): """max number of open files (soft rlimit)""" return self.getint("Main", "fd_max") + @property + def sudo_command(self): + """sudo_command value as a string (optional)""" + return self._get_optional("Main", "sudo_command") diff --git a/lib/ClusterShell/CLI/Error.py b/lib/ClusterShell/CLI/Error.py index 08aab8fe..6704732f 100644 --- a/lib/ClusterShell/CLI/Error.py +++ b/lib/ClusterShell/CLI/Error.py @@ -66,12 +66,14 @@ IOError, OSError, KeyboardInterrupt, + ValueError, WorkerError) LOGGER = logging.getLogger(__name__) -def handle_generic_error(excobj, prog=os.path.basename(sys.argv[0])): +def handle_generic_error(excobj): """handle error given `excobj' generic script exception""" + prog = os.path.basename(sys.argv[0]) try: raise excobj except EngineNotSupportedError as exc: @@ -99,7 +101,7 @@ def handle_generic_error(excobj, prog=os.path.basename(sys.argv[0])): print("%s: TREE MODE: %s" % (prog, exc), file=sys.stderr) except configparser.Error as exc: print("%s: %s" % (prog, exc), file=sys.stderr) - except (TypeError, WorkerError) as exc: + except (TypeError, ValueError, WorkerError) as exc: print("%s: %s" % (prog, exc), file=sys.stderr) except (IOError, OSError) as exc: # see PEP 3151 if exc.errno == errno.EPIPE: diff --git a/lib/ClusterShell/Worker/Pdsh.py b/lib/ClusterShell/Worker/Pdsh.py index dafde8cd..2b358499 100644 --- a/lib/ClusterShell/Worker/Pdsh.py +++ b/lib/ClusterShell/Worker/Pdsh.py @@ -254,8 +254,8 @@ def write(self, buf): """ Write data to process. Not supported with Pdsh worker. """ - raise EngineClientNotSupportedError("writing is not supported by pdsh " - "worker") + raise EngineClientNotSupportedError("writing to stdin is not " + "supported by pdsh worker") def set_write_eof(self): """ @@ -264,7 +264,7 @@ def set_write_eof(self): Not supported by PDSH Worker. """ - raise EngineClientNotSupportedError("writing is not supported by pdsh " - "worker") + raise EngineClientNotSupportedError("writing to stdin is not " + "supported by pdsh worker") WORKER_CLASS = WorkerPdsh diff --git a/tests/CLIClushTest.py b/tests/CLIClushTest.py index c987d1cb..7b222f8d 100644 --- a/tests/CLIClushTest.py +++ b/tests/CLIClushTest.py @@ -618,6 +618,37 @@ def test_040_stdin_eof(self): finally: delattr(ClusterShell.CLI.Clush, '_f_user_interaction') + def test_041_sudo(self): + """test clush --sudo""" + def ask_pass_mock(): + return "passok" + ask_pass_save = ClusterShell.CLI.Clush.ask_pass + ClusterShell.CLI.Clush.ask_pass = ask_pass_mock + try: + s = "%s: passok\n" % HOSTNAME + expected = s.encode() + # test 'sudo -S' password forwarding using 'exec' command instead + self._clush_t(["--sudo", "-O", "sudo_command=exec", "-w", HOSTNAME, "cat"], + None, expected) + self._clush_t(["--sudo","-O", "sudo_command=exec", "--nostdin", "-w", HOSTNAME, "cat"], + None, expected) + self._clush_t(["--sudo","-O", "sudo_command=exec", "--nostdin", "-w", HOSTNAME, "cat"], + b"test\n", expected) + # test sudo password forwarding followed by stdin stream + s = "%s: test stdin\n" % HOSTNAME + expected += s.encode() + self._clush_t(["-O", "sudo_command=exec", "-w", HOSTNAME, "--sudo", "cat"], + b"test stdin\n", expected) + # write to stdin is not supported by pdsh worker + self.assertRaises(EngineClientNotSupportedError, self._clush_t, + ["--sudo", "-O", "sudo_command=exec", "-w", HOSTNAME, "-R", "pdsh", "cat"], + b"test stdin", expected, 1) + self.assertRaises(EngineClientNotSupportedError, self._clush_t, + ["--nostdin", "--sudo", "-O", "sudo_command=exec", "-w", HOSTNAME, "-R", "pdsh", "cat"], + b"test stdin", expected, 1) + finally: + ClusterShell.CLI.Clush.ask_pass = ask_pass_save + class CLIClushTest_B_StdinFailure(unittest.TestCase): """Unit test class for testing CLI/Clush.py and stdin failure"""