diff --git a/doc/man/man1/clush.1 b/doc/man/man1/clush.1 index 98ef2bfa..ce73762c 100644 --- a/doc/man/man1/clush.1 +++ b/doc/man/man1/clush.1 @@ -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: diff --git a/doc/sphinx/tools/clush.rst b/doc/sphinx/tools/clush.rst index 6b282f2b..98f34db4 100644 --- a/doc/sphinx/tools/clush.rst +++ b/doc/sphinx/tools/clush.rst @@ -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 """"""""""""""""""""""" diff --git a/doc/txt/clush.txt b/doc/txt/clush.txt index 6dfa1e18..e3c102bc 100644 --- a/doc/txt/clush.txt +++ b/doc/txt/clush.txt @@ -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 diff --git a/lib/ClusterShell/CLI/Clush.py b/lib/ClusterShell/CLI/Clush.py index 41a59762..eedc7edd 100755 --- a/lib/ClusterShell/CLI/Clush.py +++ b/lib/ClusterShell/CLI/Clush.py @@ -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.""" @@ -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) diff --git a/lib/ClusterShell/CLI/Display.py b/lib/ClusterShell/CLI/Display.py index 20e744bf..5186f69a 100644 --- a/lib/ClusterShell/CLI/Display.py +++ b/lib/ClusterShell/CLI/Display.py @@ -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) diff --git a/lib/ClusterShell/CLI/OptionParser.py b/lib/ClusterShell/CLI/OptionParser.py index e47e6cea..9e4e87b1 100644 --- a/lib/ClusterShell/CLI/OptionParser.py +++ b/lib/ClusterShell/CLI/OptionParser.py @@ -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): diff --git a/tests/CLIClushTest.py b/tests/CLIClushTest.py index 7b222f8d..497fc809 100644 --- a/tests/CLIClushTest.py +++ b/tests/CLIClushTest.py @@ -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):