Skip to content

clush: add --sudo support (#234) #477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion conf/clush.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions doc/extras/vim/syntax/clushconf.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion doc/man/man1/clush.1
Original file line number Diff line number Diff line change
@@ -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
.
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion doc/man/man5/clush.conf.5
Original file line number Diff line number Diff line change
@@ -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
.
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions doc/sphinx/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <clush-sudo>` |
+-----------------+----------------------------------------------------+


.. _groups-config:
Expand Down
44 changes: 44 additions & 0 deletions doc/sphinx/tools/clush.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <clush-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 <clush-interactive>` 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 <clush-config>` 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)

Expand All @@ -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/
4 changes: 3 additions & 1 deletion doc/txt/clush.conf.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Configuration file for `clush`
------------------------------

:Author: Stephane Thiell, <[email protected]>
: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
Expand Down Expand Up @@ -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
===========
Expand Down
3 changes: 2 additions & 1 deletion doc/txt/clush.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ execute shell commands on a cluster
-----------------------------------

:Author: Stephane Thiell <[email protected]>
: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
Expand Down Expand Up @@ -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 <KEY=VALUE>, --option=<KEY=VALUE>
Expand Down
30 changes: 28 additions & 2 deletions lib/ClusterShell/CLI/Clush.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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':
Expand Down
7 changes: 6 additions & 1 deletion lib/ClusterShell/CLI/Config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
6 changes: 4 additions & 2 deletions lib/ClusterShell/CLI/Error.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions lib/ClusterShell/Worker/Pdsh.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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
31 changes: 31 additions & 0 deletions tests/CLIClushTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down