Skip to content

Add same arguments '--outdir=OUTDIR' and '--errdir=ERRDIR' as pssh #470

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 8 commits into from
Aug 12, 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
6 changes: 6 additions & 0 deletions doc/man/man1/clush.1
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ return the largest of command return codes
.TP
.B \-\-diff
show diff between common outputs (find the best reference output by focusing on largest nodeset and also smaller command return code)
.TP
.BI \-\-outdir\fB= OUTDIR
output directory for stdout files (OPTIONAL)
.TP
.BI \-\-errdir\fB= ERRDIR
output directory for sterr files (OPTIONAL)
.UNINDENT
.TP
.B File copying:
Expand Down
14 changes: 14 additions & 0 deletions doc/sphinx/tools/clush.rst
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,20 @@ these criteria:
#. largest nodeset with the same output result
#. otherwise the first nodeset is taken (ordered (1) by name and (2) lowest range indexes)

Saving output in files
""""""""""""""""""""""

To save the standard output (stdout) and/or error (stderr) of all remote
commands to local files identified with the node name in a given directory,
use the options ``--outdir`` and/or ``--errdir``. Any directory that
doesn't exist will be automatically created. These options provide a
similar functionality as *pssh(1)*.

For example, to save all logs from *journalctl(1)* in a local directory
``/tmp/run1/stdout``, you could use::

$ clush -w node[40-42] --outdir=/tmp/run1/stdout/ journalctl >/dev/null

Standard input bindings
"""""""""""""""""""""""

Expand Down
2 changes: 2 additions & 0 deletions doc/txt/clush.txt
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ Output behaviour:
-S, --maxrc return the largest of command return codes
--color=WHENCOLOR ``clush`` can use NO_COLOR, CLICOLOR and CLICOLOR_FORCE environment variables. NO_COLOR takes precedence over CLICOLOR_FORCE which takes precedence over CLICOLOR. When ``--color`` option is used these environment variables are not taken into account. ``--color`` tells whether to use ANSI colors to surround node or nodeset prefix/header with escape sequences to display them in color on the terminal. *WHENCOLOR* is ``never``, ``always`` or ``auto`` (which use color if standard output/error refer to a terminal). Colors are set to [34m (blue foreground text) for stdout and [31m (red foreground text) for stderr, and cannot be modified.
--diff show diff between common outputs (find the best reference output by focusing on largest nodeset and also smaller command return code)
--outdir=OUTDIR output directory for stdout files (OPTIONAL)
--errdir=ERRDIR output directory for sterr files (OPTIONAL)

File copying:
-c, --copy copy local file or directory to remote nodes
Expand Down
38 changes: 38 additions & 0 deletions lib/ClusterShell/CLI/Clush.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,38 @@ def ev_close(self, worker, timedout):
(self._prog, nodeset))
self.update_prompt(worker)

class DirectOutputDirHandler(DirectOutputHandler):
"""Direct output files event handler class. pssh style"""
def __init__(self, display, ns, prog=None):
DirectOutputHandler.__init__(self, display, prog)
self._ns = ns
self._outfiles = {}
self._errfiles = {}
if display.outdir:
for n in self._ns:
self._outfiles[n] = open(join(display.outdir, n), mode="w")
if display.errdir:
for n in self._ns:
self._errfiles[n] = open(join(display.errdir, n), mode="w")

def ev_read(self, worker, node, sname, msg):
DirectOutputHandler.ev_read(self, worker, node, sname, msg)
if sname == worker.SNAME_STDOUT:
if self._display.outdir:
self._outfiles[node].write("{}\n".format(msg.decode()))
elif sname == worker.SNAME_STDERR:
if self._display.errdir:
self._errfiles[node].write("{}\n".format(msg.decode()))

def ev_close(self, worker, timedout):
DirectOutputHandler.ev_close(self, worker, timedout)
if self._display.outdir:
for v in self._outfiles.values():
v.close()
if self._display.errdir:
for v in self._errfiles.values():
v.close()

class DirectProgressOutputHandler(DirectOutputHandler):
"""Direct output event handler class with progress support."""

Expand Down Expand Up @@ -676,6 +708,12 @@ def run_command(task, cmd, ns, timeout, display, remote, trytree):
elif display.progress and display.verbosity > VERB_QUIET:
handler = DirectProgressOutputHandler(display)
handler.runtimer_init(task, len(ns))
elif (display.outdir or display.errdir) and ns is not None:
if display.outdir and not exists(display.outdir):
os.makedirs(display.outdir)
if display.errdir and not exists(display.errdir):
os.makedirs(display.errdir)
handler = DirectOutputDirHandler(display, ns)
else:
# this is the simpler but faster output handler
handler = DirectOutputHandler(display)
Expand Down
2 changes: 2 additions & 0 deletions lib/ClusterShell/CLI/Display.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ def __init__(self, options, config=None, color=None):
self.regroup = options.regroup
self.groupsource = options.groupsource
self.noprefix = options.groupbase
self.outdir = options.outdir
self.errdir = options.errdir
# display may change when 'max return code' option is set
self.maxrc = getattr(options, 'maxrc', False)

Expand Down
2 changes: 2 additions & 0 deletions lib/ClusterShell/CLI/OptionParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ def install_display_options(self,
"colors (never, always or auto)")
optgrp.add_option("--diff", action="store_true", dest="diff",
help="show diff between gathered outputs")
optgrp.add_option("--outdir", action="store", dest="outdir", help="output directory for stdout files (OPTIONAL)")
optgrp.add_option("--errdir", action="store", dest="errdir", help="output directory for sterr files (OPTIONAL)")
self.add_option_group(optgrp)

def _copy_callback(self, option, opt_str, value, parser):
Expand Down
39 changes: 39 additions & 0 deletions tests/CLIClushTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,45 @@ def ask_pass_mock():
b"test stdin", expected, 1)
finally:
ClusterShell.CLI.Clush.ask_pass = ask_pass_save

def test_042_outdir_errdir(self):
"""test clush --outdir and --errdir"""
odir = make_temp_dir()
edir = make_temp_dir()
tofilepath = os.path.join(odir, HOSTNAME)
tefilepath = os.path.join(edir, HOSTNAME)
try:
self._clush_t(["-w", HOSTNAME, "--outdir", odir, "echo", "ok"],
None, self.output_ok)
self.assertTrue(os.path.isfile(tofilepath))
with open(tofilepath, "r") as f:
self.assertEqual(f.read(), "ok\n")
finally:
os.unlink(tofilepath)
try:
self._clush_t(["-w", HOSTNAME, "--errdir", edir, "echo", "ok", ">&2"],
None, None, 0, self.output_ok)
self.assertTrue(os.path.isfile(tefilepath))
with open(tefilepath, "r") as f:
self.assertEqual(f.read(), "ok\n")
finally:
os.unlink(tefilepath)
try:
serr = "%s: err\n" % HOSTNAME
self._clush_t(["-w", HOSTNAME, "--outdir", odir, "--errdir", edir,
"echo", "ok", ";", "echo", "err", ">&2"], None,
self.output_ok, 0, serr.encode())
self.assertTrue(os.path.isfile(tofilepath))
self.assertTrue(os.path.isfile(tefilepath))
with open(tofilepath, "r") as f:
self.assertEqual(f.read(), "ok\n")
with open(tefilepath, "r") as f:
self.assertEqual(f.read(), "err\n")
finally:
os.unlink(tofilepath)
os.unlink(tefilepath)
os.rmdir(odir)
os.rmdir(edir)


class CLIClushTest_B_StdinFailure(unittest.TestCase):
Expand Down