forked from third-party-mirrors/zfs_autobackup
run everything in either local shell (shell=true), or remote shell (ssh). this it to allow external shell piping
This commit is contained in:
parent
521d1078bd
commit
086cfe570b
@ -26,9 +26,9 @@ class TestExecuteNode(unittest2.TestCase):
|
|||||||
with self.subTest("multiline tabsplit"):
|
with self.subTest("multiline tabsplit"):
|
||||||
self.assertEqual(node.run(["echo","l1c1\tl1c2\nl2c1\tl2c2"], tab_split=True), [['l1c1', 'l1c2'], ['l2c1', 'l2c2']])
|
self.assertEqual(node.run(["echo","l1c1\tl1c2\nl2c1\tl2c2"], tab_split=True), [['l1c1', 'l1c2'], ['l2c1', 'l2c2']])
|
||||||
|
|
||||||
#escaping test (shouldnt be a problem locally, single quotes can be a problem remote via ssh)
|
#escaping test
|
||||||
with self.subTest("escape test"):
|
with self.subTest("escape test"):
|
||||||
s="><`'\"@&$()$bla\\/.*!#test _+-={}[]|"
|
s="><`'\"@&$()$bla\\/.* !#test _+-={}[]| ${bla} $bla"
|
||||||
self.assertEqual(node.run(["echo",s]), [s])
|
self.assertEqual(node.run(["echo",s]), [s])
|
||||||
|
|
||||||
#return std err as well, trigger stderr by listing something non existing
|
#return std err as well, trigger stderr by listing something non existing
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
|
import shlex
|
||||||
|
|
||||||
class CmdPipe:
|
class CmdPipe:
|
||||||
"""a pipe of one or more commands. also takes care of utf-8 encoding/decoding and line based parsing"""
|
"""a pipe of one or more commands. also takes care of utf-8 encoding/decoding and line based parsing"""
|
||||||
@ -17,31 +18,32 @@ class CmdPipe:
|
|||||||
self.readonly = readonly
|
self.readonly = readonly
|
||||||
self._should_execute = True
|
self._should_execute = True
|
||||||
|
|
||||||
def add(self, cmd, readonly=False, stderr_handler=None, exit_handler=None):
|
def add(self, cmd, readonly=False, stderr_handler=None, exit_handler=None, shell=False):
|
||||||
"""adds a command to pipe"""
|
"""adds a command to pipe"""
|
||||||
|
|
||||||
self.items.append({
|
self.items.append({
|
||||||
'cmd': cmd,
|
'cmd': cmd,
|
||||||
'stderr_handler': stderr_handler,
|
'stderr_handler': stderr_handler,
|
||||||
'exit_handler': exit_handler
|
'exit_handler': exit_handler,
|
||||||
|
'shell': shell
|
||||||
})
|
})
|
||||||
|
|
||||||
if not readonly and self.readonly:
|
if not readonly and self.readonly:
|
||||||
self._should_execute = False
|
self._should_execute = False
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""transform into oneliner for debugging and testing """
|
"""transform into oneliner for debugging and testing. this should generate a copy-pastable string for in a console """
|
||||||
|
|
||||||
#just one command?
|
|
||||||
if len(self.items)==1:
|
|
||||||
return " ".join(self.items[0]['cmd'])
|
|
||||||
|
|
||||||
#an actual pipe
|
|
||||||
ret = ""
|
ret = ""
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if ret:
|
if ret:
|
||||||
ret = ret + " | "
|
ret = ret + " | "
|
||||||
|
if item['shell']:
|
||||||
|
#its already copy pastable for a shell:
|
||||||
ret = ret + "(" + " ".join(item['cmd']) + ")"
|
ret = ret + "(" + " ".join(item['cmd']) + ")"
|
||||||
|
else:
|
||||||
|
#make it copy-pastable, will make a mess of quotes sometimes, but is correct
|
||||||
|
ret = ret + "(" + shlex.join(item['cmd']) + ")"
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@ -69,7 +71,7 @@ class CmdPipe:
|
|||||||
encoded_cmd.append(arg.encode('utf-8'))
|
encoded_cmd.append(arg.encode('utf-8'))
|
||||||
|
|
||||||
item['process'] = subprocess.Popen(encoded_cmd, env=os.environ, stdout=subprocess.PIPE, stdin=stdin,
|
item['process'] = subprocess.Popen(encoded_cmd, env=os.environ, stdout=subprocess.PIPE, stdin=stdin,
|
||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE, shell=item['shell'])
|
||||||
|
|
||||||
selectors.append(item['process'].stderr)
|
selectors.append(item['process'].stderr)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import shlex
|
||||||
from zfs_autobackup.CmdPipe import CmdPipe
|
from zfs_autobackup.CmdPipe import CmdPipe
|
||||||
from zfs_autobackup.LogStub import LogStub
|
from zfs_autobackup.LogStub import LogStub
|
||||||
|
|
||||||
@ -48,30 +48,23 @@ class ExecuteNode(LogStub):
|
|||||||
# else:
|
# else:
|
||||||
# self.error("STDERR|> " + line.rstrip())
|
# self.error("STDERR|> " + line.rstrip())
|
||||||
|
|
||||||
def _remote_cmd(self, cmd):
|
def _shell_cmd(self, cmd):
|
||||||
"""transforms cmd in correct form for remote over ssh, if needed"""
|
"""prefix specified ssh shell to command and escape shell characters"""
|
||||||
|
|
||||||
# use ssh?
|
ret=[]
|
||||||
if self.ssh_to is not None:
|
|
||||||
encoded_cmd = []
|
#add remote shell
|
||||||
encoded_cmd.append("ssh")
|
if not self.is_local():
|
||||||
|
ret=["ssh"]
|
||||||
|
|
||||||
if self.ssh_config is not None:
|
if self.ssh_config is not None:
|
||||||
encoded_cmd.extend(["-F", self.ssh_config])
|
ret.extend(["-F", self.ssh_config])
|
||||||
|
|
||||||
encoded_cmd.append(self.ssh_to)
|
ret.append(self.ssh_to)
|
||||||
|
|
||||||
for arg in cmd:
|
ret.append(shlex.join(cmd))
|
||||||
# add single quotes for remote commands to support spaces and other weird stuff (remote commands are
|
|
||||||
# executed in a shell) and escape existing single quotes (bash needs ' to end the quoted string,
|
|
||||||
# then a \' for the actual quote and then another ' to start a new quoted string) (and then python
|
|
||||||
# needs the double \ to get a single \)
|
|
||||||
encoded_cmd.append(("'" + arg.replace("'", "'\\''") + "'"))
|
|
||||||
|
|
||||||
return encoded_cmd
|
|
||||||
else:
|
|
||||||
return(cmd)
|
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
def is_local(self):
|
def is_local(self):
|
||||||
return self.ssh_to is None
|
return self.ssh_to is None
|
||||||
@ -81,6 +74,8 @@ class ExecuteNode(LogStub):
|
|||||||
return_stderr=False, pipe=False):
|
return_stderr=False, pipe=False):
|
||||||
"""run a command on the node , checks output and parses/handle output and returns it
|
"""run a command on the node , checks output and parses/handle output and returns it
|
||||||
|
|
||||||
|
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
|
:param cmd: the actual command, should be a list, where the first item is the command
|
||||||
and the rest are parameters.
|
and the rest are parameters.
|
||||||
:param pipe: return CmdPipe instead of executing it.
|
:param pipe: return CmdPipe instead of executing it.
|
||||||
@ -121,9 +116,8 @@ class ExecuteNode(LogStub):
|
|||||||
if (valid_exitcodes != []) and (exit_code not in valid_exitcodes):
|
if (valid_exitcodes != []) and (exit_code not in valid_exitcodes):
|
||||||
raise (ExecuteError("Command '{}' returned exit code {} (valid codes: {})".format(" ".join(cmd), exit_code, valid_exitcodes)))
|
raise (ExecuteError("Command '{}' returned exit code {} (valid codes: {})".format(" ".join(cmd), exit_code, valid_exitcodes)))
|
||||||
|
|
||||||
# add command to pipe
|
#add shell command and handlers to pipe
|
||||||
encoded_cmd = self._remote_cmd(cmd)
|
p.add(cmd=self._shell_cmd(cmd), readonly=readonly, stderr_handler=stderr_handler, exit_handler=exit_handler, shell=self.is_local())
|
||||||
p.add(cmd=encoded_cmd, readonly=readonly, stderr_handler=stderr_handler, exit_handler=exit_handler)
|
|
||||||
|
|
||||||
# return pipe instead of executing?
|
# return pipe instead of executing?
|
||||||
if pipe:
|
if pipe:
|
||||||
|
@ -557,16 +557,16 @@ class ZfsDataset:
|
|||||||
cmd.append(self.name)
|
cmd.append(self.name)
|
||||||
|
|
||||||
# #add custom output pipes?
|
# #add custom output pipes?
|
||||||
#local so do our own piping
|
# #local so do our own piping
|
||||||
if self.zfs_node.is_local():
|
# if self.zfs_node.is_local():
|
||||||
output_pipe = self.zfs_node.run(cmd, pipe=True, readonly=True)
|
# output_pipe = self.zfs_node.run(cmd, pipe=True, readonly=True)
|
||||||
for pipe_cmd in output_pipes:
|
# for pipe_cmd in output_pipes:
|
||||||
output_pipe=self.zfs_node.run(pipe_cmd.split(" "), inp=output_pipe, pipe=True, readonly=False)
|
# output_pipe=self.zfs_node.run(pipe_cmd.split(" "), inp=output_pipe, pipe=True, readonly=False)
|
||||||
#remote, so add with actual | and let remote shell handle it
|
# #remote, so add with actual | and let remote shell handle it
|
||||||
else:
|
# else:
|
||||||
for pipe_cmd in output_pipes:
|
# for pipe_cmd in output_pipes:
|
||||||
cmd.append("|")
|
# cmd.append("|")
|
||||||
cmd.extend(pipe_cmd.split(" "))
|
# cmd.extend(pipe_cmd.split(" "))
|
||||||
output_pipe = self.zfs_node.run(cmd, pipe=True, readonly=True)
|
output_pipe = self.zfs_node.run(cmd, pipe=True, readonly=True)
|
||||||
|
|
||||||
return output_pipe
|
return output_pipe
|
||||||
|
Loading…
x
Reference in New Issue
Block a user