Skip to content

Commit 0ec671b

Browse files
committed
clush: add --sudo support (#234)
Add support for sudo password prompt and forwarding over stdin to a predefined sudo command. The sudo password is then sent over stdin to the target nodes that will run the actual command prefixed by the sudo command. Add sudo_command option to clush.conf. Closes #234.
1 parent 556cab3 commit 0ec671b

File tree

13 files changed

+136
-14
lines changed

13 files changed

+136
-14
lines changed

conf/clush.conf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ verbosity: 1
2626
#scp_path: /usr/bin/sshpass -f /root/remotepasswordfile /usr/bin/scp
2727
#ssh_options: -oBatchMode=no -oStrictHostKeyChecking=no
2828

29-
29+
# sudo command used for --sudo
30+
#sudo_command: /usr/bin/sudo -S -p "''" -k

doc/extras/vim/syntax/clushconf.vim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ syn match clushHeader "\[\w\+\]"
1919
syn keyword clushKeys fanout command_timeout connect_timeout color fd_max history_size node_count maxrc verbosity
2020
syn keyword clushKeys ssh_user ssh_path ssh_options
2121
syn keyword clushKeys rsh_path rcp_path rcp_options
22+
syn keyword clushKeys sudo_command
2223

2324
" Define the default highlighting.
2425
" For version 5.7 and earlier: only when not done already

doc/man/man1/clush.1

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.\" Man page generated from reStructuredText.
22
.
3-
.TH CLUSH 1 "2021-11-03" "1.8.4" "ClusterShell User Manual"
3+
.TH CLUSH 1 "2022-06-29" "1.8.4" "ClusterShell User Manual"
44
.SH NAME
55
clush \- execute shell commands on a cluster
66
.
@@ -162,6 +162,9 @@ optional \fBgroups.conf\fP(5) group source to use
162162
.B \-n\fP,\fB \-\-nostdin
163163
do not watch for possible input from stdin; this should be used when \fBclush\fP is run in the background (or in scripts).
164164
.TP
165+
.B \-\-sudo
166+
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).
167+
.TP
165168
.BI \-\-groupsconf\fB= FILE
166169
use alternate config file for groups.conf(5)
167170
.TP

doc/man/man5/clush.conf.5

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.\" Man page generated from reStructuredText.
22
.
3-
.TH CLUSH.CONF 5 "2021-11-03" "1.8.4" "ClusterShell User Manual"
3+
.TH CLUSH.CONF 5 "2022-06-29" "1.8.4" "ClusterShell User Manual"
44
.SH NAME
55
clush.conf \- Configuration file for clush
66
.
@@ -139,6 +139,9 @@ Same a rsh_path for rcp command. (Default is \fIrcp\fP)
139139
.TP
140140
.B rsh_options
141141
Set additional options to pass to the underlying rsh/rcp command.
142+
.TP
143+
.B sudo_command
144+
sudo command for use with \fI\-\-sudo\fP
142145
.UNINDENT
143146
.SH EXAMPLES
144147
.sp

doc/sphinx/config.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ The following table describes available *clush* config file settings.
101101
| rsh_options | Set additional options to pass to the underlying |
102102
| | rsh/rcp command. |
103103
+-----------------+----------------------------------------------------+
104+
| sudo_command | *sudo(8)* command for use with |
105+
| | :ref:`--sudo <clush-sudo>` |
106+
+-----------------+----------------------------------------------------+
104107

105108

106109
.. _groups-config:

doc/sphinx/tools/clush.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,48 @@ By default, ClusterShell supports the following worker identifiers:
638638
Worker modules distributed outside of ClusterShell are also supported by
639639
specifying the case-sensitive full Python module name of a worker module.
640640

641+
.. _clush-sudo:
642+
643+
Support for sudo
644+
""""""""""""""""
645+
646+
Since version 1.9, *clush* has support for `Sudo`_ password forwarding over
647+
stdin. This may be useful in an environment that only allows sysadmins
648+
to perform interactive *sudo* work with password.
649+
650+
.. warning:: In this section, it is assumed that *sudo* always requires a
651+
password for the user on the target nodes. If *sudo* does NOT require
652+
any password (i.e. **NOPASSWD** is specified in your sudoers file), you
653+
do not need any extra options to run your *sudo* commands with *clush*.
654+
655+
Run *clush* with ``--sudo`` to **enable a password prompt** to type your *sudo*
656+
password, then *sudo* (well, ``sudo_command`` – see below) will be used to run
657+
your commands on the target nodes. The password is broadcasted to all target
658+
nodes over *ssh(1)* (or via your :ref:`favorite worker <clush-worker>`) and
659+
as such, must be the same on all target nodes. It is not stored on disk at
660+
any time and only kept in memory during the duration of the *clush* command.
661+
Thus, the password will be prompted every time you run *clush*. When you
662+
start *clush* in :ref:`interactive mode <clush-interactive>` along with
663+
``--sudo``, you can run multiple commands in that mode without having to type
664+
your password every time.
665+
666+
When ``--sudo`` is used, *clush* will run *sudo* for you on each target node,
667+
so your command itself should NOT start with ``sudo``. The actual *sudo*
668+
command used by *clush* can be changed in :ref:`clush.conf <clush-config>` or
669+
in command line using ``-O sudo_command="..."``. The configured
670+
``sudo_command`` must be able to read a password on stdin followed by a new
671+
line (which is what ``sudo -S`` does).
672+
673+
Usage example::
674+
675+
$ clush -w n[1-2]c[01-02] --sudo -b id
676+
Password:
677+
---------------
678+
n[1-2]c[01-02] (4)
679+
---------------
680+
uid=0(root) gid=0(root) groups=0(root)
681+
682+
641683
.. [#] LLNL parallel remote shell utility
642684
(https://computing.llnl.gov/linux/pdsh.html)
643685
@@ -649,3 +691,5 @@ specifying the case-sensitive full Python module name of a worker module.
649691
.. _ticket: https://github.com/cea-hpc/clustershell/issues/new
650692

651693
.. _this paper: https://www.kernel.org/doc/ols/2012/ols2012-thiell.pdf
694+
695+
.. _Sudo: https://www.sudo.ws/

doc/txt/clush.conf.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Configuration file for `clush`
77
------------------------------
88

99
:Author: Stephane Thiell, <[email protected]>
10-
:Date: 2021-11-03
10+
:Date: 2022-06-29
1111
:Copyright: GNU Lesser General Public License version 2.1 or later (LGPLv2.1+)
1212
:Version: 1.8.4
1313
:Manual section: 5
@@ -98,6 +98,8 @@ rcp_path
9898
Same a rsh_path for rcp command. (Default is `rcp`)
9999
rsh_options
100100
Set additional options to pass to the underlying rsh/rcp command.
101+
sudo_command
102+
sudo command for use with `--sudo`
101103

102104
EXAMPLES
103105
===========

doc/txt/clush.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ execute shell commands on a cluster
77
-----------------------------------
88

99
:Author: Stephane Thiell <[email protected]>
10-
:Date: 2021-11-03
10+
:Date: 2022-06-29
1111
:Copyright: GNU Lesser General Public License version 2.1 or later (LGPLv2.1+)
1212
:Version: 1.8.4
1313
:Manual section: 1
@@ -131,6 +131,7 @@ OPTIONS
131131
-s GROUPSOURCE, --groupsource=GROUPSOURCE
132132
optional ``groups.conf``\(5) group source to use
133133
-n, --nostdin do not watch for possible input from stdin; this should be used when ``clush`` is run in the background (or in scripts).
134+
--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).
134135
--groupsconf=FILE use alternate config file for groups.conf(5)
135136
--conf=FILE use alternate config file for clush.conf(5)
136137
-O <KEY=VALUE>, --option=<KEY=VALUE>

lib/ClusterShell/CLI/Clush.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@
3333

3434
from __future__ import print_function
3535

36+
import getpass
3637
import logging
3738
import os
3839
from os.path import abspath, dirname, exists, isdir, join
3940
import random
4041
import resource
42+
import shlex
4143
import signal
4244
import sys
4345
import time
@@ -608,6 +610,9 @@ def ttyloop(task, nodeset, timeout, display, remote, trytree):
608610
continue
609611
if readline_avail:
610612
readline.write_history_file(get_history_file())
613+
if task.default("USER_sudo_command"):
614+
sudo_cmdl = shlex.split(task.default("USER_sudo_command"))
615+
cmd = "%s %s" % (' '.join(sudo_cmdl), cmd)
611616
run_command(task, cmd, ns, timeout, display, remote, trytree)
612617
return rc
613618

@@ -675,13 +680,18 @@ def run_command(task, cmd, ns, timeout, display, remote, trytree):
675680
# this is the simpler but faster output handler
676681
handler = DirectOutputHandler(display)
677682

678-
stdin = task.default("USER_stdin_worker")
683+
stdin = task.default("USER_stdin_worker") # stdin forwarding?
684+
sudo_passwd = task.default("USER_sudo_passwd") # --sudo?
679685
worker = task.shell(cmd, nodes=ns, handler=handler, timeout=timeout,
680-
remote=remote, tree=trytree, stdin=stdin)
686+
remote=remote, tree=trytree, stdin=stdin or sudo_passwd)
681687
if ns is None:
682688
worker.set_key('LOCAL')
689+
if sudo_passwd:
690+
worker.write(sudo_passwd.encode() + b'\n')
683691
if stdin:
684692
bind_stdin(worker, display)
693+
if sudo_passwd and not stdin:
694+
worker.set_write_eof() # we only enabled stdin to send the sudo password
685695
task.resume()
686696

687697
def run_copy(task, sources, dest, ns, timeout, preserve_flag, display):
@@ -743,6 +753,10 @@ def set_fdlimit(fd_max, display):
743753
msgfmt = 'Warning: Failed to set max open files limit to %d (%s)'
744754
display.vprint_err(VERB_VERB, msgfmt % (rlim_max, exc))
745755

756+
def ask_pass():
757+
"""Prompt for password (--sudo)"""
758+
return getpass.getpass()
759+
746760
def clush_exit(status, task=None):
747761
"""Exit script, flushing stdio buffers and stopping ClusterShell task."""
748762
if task:
@@ -797,6 +811,8 @@ def main():
797811

798812
parser.add_option("-n", "--nostdin", action="store_true", dest="nostdin",
799813
help="don't watch for possible input from stdin")
814+
parser.add_option("--sudo", action="store_true", dest="sudo",
815+
help="enable sudo password prompt")
800816

801817
parser.install_groupsconf_option()
802818
parser.install_clush_config_options()
@@ -976,6 +992,16 @@ def main():
976992
task.set_info("debug", config.verbosity >= VERB_DEBUG)
977993
task.set_info("fanout", config.fanout)
978994

995+
if options.sudo:
996+
# keep sudo_command for interactive mode ttyloop()
997+
task.set_default("USER_sudo_command", config.sudo_command)
998+
sudo_cmdl = shlex.split(config.sudo_command)
999+
display.vprint(VERB_DEBUG, "sudo command prefix: %s" % sudo_cmdl)
1000+
# prefix actual command with sudo command
1001+
args = sudo_cmdl + args
1002+
# prompt for sudo password
1003+
task.set_default("USER_sudo_passwd", ask_pass())
1004+
9791005
if options.worker:
9801006
try:
9811007
if options.remote == 'no':

lib/ClusterShell/CLI/Config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ class ClushConfig(configparser.ConfigParser, object):
5757
"verbosity": "%d" % VERB_STD,
5858
"node_count": "yes",
5959
"maxrc": "no",
60-
"fd_max": "8192"}
60+
"fd_max": "8192",
61+
"sudo_command": 'sudo -S -p "\'\'" -k'}
6162

6263
def __init__(self, options, filename=None):
6364
"""Initialize ClushConfig object from corresponding
@@ -225,3 +226,7 @@ def fd_max(self):
225226
"""max number of open files (soft rlimit)"""
226227
return self.getint("Main", "fd_max")
227228

229+
@property
230+
def sudo_command(self):
231+
"""sudo_command value as a string (optional)"""
232+
return self._get_optional("Main", "sudo_command")

lib/ClusterShell/CLI/Error.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@
6666
IOError,
6767
OSError,
6868
KeyboardInterrupt,
69+
ValueError,
6970
WorkerError)
7071

7172
LOGGER = logging.getLogger(__name__)
7273

73-
def handle_generic_error(excobj, prog=os.path.basename(sys.argv[0])):
74+
def handle_generic_error(excobj):
7475
"""handle error given `excobj' generic script exception"""
76+
prog = os.path.basename(sys.argv[0])
7577
try:
7678
raise excobj
7779
except EngineNotSupportedError as exc:
@@ -99,7 +101,7 @@ def handle_generic_error(excobj, prog=os.path.basename(sys.argv[0])):
99101
print("%s: TREE MODE: %s" % (prog, exc), file=sys.stderr)
100102
except configparser.Error as exc:
101103
print("%s: %s" % (prog, exc), file=sys.stderr)
102-
except (TypeError, WorkerError) as exc:
104+
except (TypeError, ValueError, WorkerError) as exc:
103105
print("%s: %s" % (prog, exc), file=sys.stderr)
104106
except (IOError, OSError) as exc: # see PEP 3151
105107
if exc.errno == errno.EPIPE:

lib/ClusterShell/Worker/Pdsh.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ def write(self, buf):
254254
"""
255255
Write data to process. Not supported with Pdsh worker.
256256
"""
257-
raise EngineClientNotSupportedError("writing is not supported by pdsh "
258-
"worker")
257+
raise EngineClientNotSupportedError("writing to stdin is not "
258+
"supported by pdsh worker")
259259

260260
def set_write_eof(self):
261261
"""
@@ -264,7 +264,7 @@ def set_write_eof(self):
264264
265265
Not supported by PDSH Worker.
266266
"""
267-
raise EngineClientNotSupportedError("writing is not supported by pdsh "
268-
"worker")
267+
raise EngineClientNotSupportedError("writing to stdin is not "
268+
"supported by pdsh worker")
269269

270270
WORKER_CLASS = WorkerPdsh

tests/CLIClushTest.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,37 @@ def test_040_stdin_eof(self):
618618
finally:
619619
delattr(ClusterShell.CLI.Clush, '_f_user_interaction')
620620

621+
def test_041_sudo(self):
622+
"""test clush --sudo"""
623+
def ask_pass_mock():
624+
return "passok"
625+
ask_pass_save = ClusterShell.CLI.Clush.ask_pass
626+
ClusterShell.CLI.Clush.ask_pass = ask_pass_mock
627+
try:
628+
s = "%s: passok\n" % HOSTNAME
629+
expected = s.encode()
630+
# test 'sudo -S' password forwarding using 'exec' command instead
631+
self._clush_t(["--sudo", "-O", "sudo_command=exec", "-w", HOSTNAME, "cat"],
632+
None, expected)
633+
self._clush_t(["--sudo","-O", "sudo_command=exec", "--nostdin", "-w", HOSTNAME, "cat"],
634+
None, expected)
635+
self._clush_t(["--sudo","-O", "sudo_command=exec", "--nostdin", "-w", HOSTNAME, "cat"],
636+
b"test\n", expected)
637+
# test sudo password forwarding followed by stdin stream
638+
s = "%s: test stdin\n" % HOSTNAME
639+
expected += s.encode()
640+
self._clush_t(["-O", "sudo_command=exec", "-w", HOSTNAME, "--sudo", "cat"],
641+
b"test stdin\n", expected)
642+
# write to stdin is not supported by pdsh worker
643+
self.assertRaises(EngineClientNotSupportedError, self._clush_t,
644+
["--sudo", "-O", "sudo_command=exec", "-w", HOSTNAME, "-R", "pdsh", "cat"],
645+
b"test stdin", expected, 1)
646+
self.assertRaises(EngineClientNotSupportedError, self._clush_t,
647+
["--nostdin", "--sudo", "-O", "sudo_command=exec", "-w", HOSTNAME, "-R", "pdsh", "cat"],
648+
b"test stdin", expected, 1)
649+
finally:
650+
ClusterShell.CLI.Clush.ask_pass = ask_pass_save
651+
621652

622653
class CLIClushTest_B_StdinFailure(unittest.TestCase):
623654
"""Unit test class for testing CLI/Clush.py and stdin failure"""

0 commit comments

Comments
 (0)