From 86706ca24fa43ca10d5e002e6a4be0af6cc3e291 Mon Sep 17 00:00:00 2001 From: Edwin Eefting Date: Thu, 27 Jan 2022 11:16:19 +0100 Subject: [PATCH] script mode wip --- tests/test_executenode.py | 8 ++++ zfs_autobackup/ExecuteNode.py | 80 +++++++++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/tests/test_executenode.py b/tests/test_executenode.py index 100ac7c..270ab95 100644 --- a/tests/test_executenode.py +++ b/tests/test_executenode.py @@ -157,7 +157,15 @@ class TestExecuteNode(unittest2.TestCase): self.assertEqual(nodeb.run(cmd=["cat", ExecuteNode.PIPE, "pwd"], cwd="/tmp/space test"), ["/tmp/space test"]) + def test_script(self): + def stdout_handler(line): + print("handle: " + line) + + nodea=ExecuteNode(debug_output=True, ssh_to="localhost") + + cmd_pipe=nodea.script(lines=["echo line1", "echo line 2"]) + cmd_pipe.execute(stdout_handler) diff --git a/zfs_autobackup/ExecuteNode.py b/zfs_autobackup/ExecuteNode.py index 823fc49..a487692 100644 --- a/zfs_autobackup/ExecuteNode.py +++ b/zfs_autobackup/ExecuteNode.py @@ -92,17 +92,18 @@ class ExecuteNode(LogStub): return_stderr=False, pipe=False, return_all=False, cwd=None): """run a command on the node , checks output and parses/handle output and returns it + Takes care of proper quoting/escaping/ssh and logging of stdout/err/exit codes. + Either uses a local shell (sh -c) or remote shell (ssh) to execute the command. Therefore the command can have stuff like actual pipes in it, if you dont want to use pipe=True to pipe stuff. :param cmd: the actual command, should be a list, where the first item is the command and the rest are parameters. use ExecuteNode.PIPE to add an unescaped | (if you want to use system piping instead of python piping) - :param pipe: return CmdPipe instead of executing it. + :param pipe: return CmdPipe instead of executing it. (pipe this into anoter run() command via inp=...) :param inp: Can be None, a string or a CmdPipe that was previously returned. :param tab_split: split tabbed files in output into a list - :param valid_exitcodes: list of valid exit codes for this command (checks exit code of both sides of a pipe) - Use [] to accept all exit codes. Default [0] + :param valid_exitcodes: list of valid exit codes for this command. Use [] to accept all exit codes. Default [0] :param readonly: make this True if the command doesn't make any changes and is safe to execute in testmode :param hide_errors: don't show stderr output as error, instead show it as debugging output (use to hide expected errors) :param return_stderr: return both stdout and stderr as a tuple. (normally only returns stdout) @@ -176,3 +177,76 @@ class ExecuteNode(LogStub): return output_lines, error_lines else: return output_lines + + def script(self, lines, inp=None, valid_exitcodes=None, readonly=False, hide_errors=False): + """Run a multiline script on the node. + + This is much more low level than run() and allows for finer grained control. + + Either uses a local shell (sh -c) or remote shell (ssh) to execute the command. + It will always return a CmdPipe that you should call execute on, or pipe to another script. (via inp=...) + You need to do your own escaping/quoting. + It will do logging of stderr and exit codes, but you should + specify your stdout handler when calling CmdPipe.execute. + Also specify the optional stderr/exit code handlers if you need them. + Handlers are called for each line. + It wont collect lines internally like run() does, so streams of data can be of unlimited size. + + :param lines: list of lines of the actual script. + :param inp: Can be None, a string or a CmdPipe that was previously returned. + :param readonly: make this True if the command doesn't make any changes and is safe to execute in testmode + :param valid_exitcodes: list of valid exit codes for this command. Use [] to accept all exit codes. Default [0] + :param hide_errors: don't show stderr output as error, instead show it as debugging output (use to hide expected errors) + + """ + + + # create new pipe? + if not isinstance(inp, CmdPipe): + cmd_pipe = CmdPipe(self.readonly, inp) + else: + # add stuff to existing pipe + cmd_pipe = inp + + + def stderr_handler(line): + self._parse_stderr(line, hide_errors) + + # exit code hanlder + if valid_exitcodes is None: + valid_exitcodes = [0] + + def exit_handler(exit_code): + if self.debug_output: + self.debug("EXIT > {}".format(exit_code)) + + if (valid_exitcodes != []) and (exit_code not in valid_exitcodes): + self.error("Script returned exit code {} (valid codes: {})".format(cmd_item, exit_code, valid_exitcodes)) + return False + + return True + + #build command + cmd=[] + + #add remote shell + if not self.is_local(): + #note: dont escape this part (executed directly without shell) + cmd.append("ssh") + + if self.ssh_config is not None: + cmd.append(["-F", self.ssh_config]) + + cmd.append(self.ssh_to) + + # convert to script + cmd.append("\n".join(lines)) + + # add shell command and handlers to pipe + cmd_item=CmdItem(cmd=cmd, readonly=readonly, stderr_handler=stderr_handler, exit_handler=exit_handler, shell=self.is_local()) + cmd_pipe.add(cmd_item) + + self.debug("SCRIPT > {}".format(cmd_pipe)) + + return cmd_pipe +