在Fabric中将local()的输出传递给远程run()命令的最佳方式是什么?
有没有简单的方法可以把本地命令的输出直接传给远程命令(反之亦然)呢?
我一直都是把输出先保存到一个文件里,然后把文件传过去,再读取文件……但感觉应该有更简单的方法。
在一些简单的情况下,直接捕获输出并用字符串插值就可以了:
ip = local('hostname -i')
run('Script was run from ip: %s' % ip)
但是当输出需要进行转义以确保在命令行上安全,或者需要从标准输入(stdin)获取时,就有点复杂了。
如果输出是安全的,那么像 run('echo "%s" | mycmd' % ip)
这样的写法就能实现我想要的(这也让我想到了一个类似的问题:“有没有简单的方法可以对字符串进行bash转义?”),但我觉得应该有一种“正确的方法”来提供远程的标准输入。
补充:
为了更清楚地说明,当输入比较长时,简单的字符串插值可能会出现一些问题:经典的shell问题(比如输出可能包含 "; rm -rf /
),还有(在我这种情况下,更现实的是)输出可能包含引号(单引号和双引号)。
我觉得直接用 run("echo '%s' | cmd" % output.replace("'", "'\\''")
这样的方式应该可以,但可能会有一些边缘情况会被漏掉。
正如我上面提到的,这种情况似乎是fabric可以更优雅地处理的,它可以直接把字符串发送到run()的标准输入(不过也许是我被它处理其他事情的优雅方式给宠坏了 :)
3 个回答
这是对@goncalopp的回答稍微改进过的版本:
def remote_pipe(local_command, remote_command, buffer_size=1024*1024, channel_timeout=60):
'''executes a local command and a remote command (with fabric), and
sends the local's stdout to the remote's stdin'''
local_process = Popen(local_command, shell=True, stdout=PIPE)
channel = default_channel() # Fabric function
channel.set_combine_stderr(True)
channel.settimeout(channel_timeout)
channel.exec_command(remote_command)
try:
bytes_to_send = local_process.stdout.read(buffer_size)
while bytes_to_send:
channel.sendall(bytes_to_send)
bytes_to_send = local_process.stdout.read(buffer_size)
except socket.error:
# Failed to send data, let's see the return codes and received data...
local_process.kill()
local_returncode = local_process.wait()
channel.shutdown_write()
remote_output = ""
try:
bytes_received = channel.recv(buffer_size)
while bytes_received:
remote_output += bytes_received
bytes_received = channel.recv(buffer_size)
except socket.error:
pass
channel.shutdown_read()
remote_returncode = channel.recv_exit_status()
print(remote_output)
if local_returncode != 0 or remote_returncode != 0:
raise Exception("remote_pipe() failed, local return code: {0}, remote return code: {1}".format(local_returncode, remote_returncode, remote_output))
除了可读性更好之外,这个改进的地方在于:如果远程命令输出的字节数少于buffer_size
,它不会因为套接字超时而中断,并且会打印出远程命令的完整输出。
我之前做过一次这样的事情,是为了把一个(二进制)数据流发送到远程服务器。
这个方法有点儿不太正规,因为它深入到了fabric和paramiko的通道里,可能会有一些未经过测试的特殊情况,但大体上看是能完成任务的。
def remote_pipe(local_command, remote_command, buf_size=1024*1024):
'''executes a local command and a remote command (with fabric), and
sends the local's stdout to the remote's stdin'''
local_p= subprocess.Popen(local_command, shell=True, stdout=subprocess.PIPE)
channel= default_channel() #fabric function
channel.set_combine_stderr(True)
channel.settimeout(2)
channel.exec_command( remote_command )
try:
read_bytes= local_p.stdout.read(buf_size)
while read_bytes:
channel.sendall(read_bytes)
read_bytes= local_p.stdout.read(buf_size)
except socket.error:
local_p.kill()
#fail to send data, let's see the return codes and received data...
local_ret= local_p.wait()
received= channel.recv(buf_size)
channel.shutdown_write()
channel.shutdown_read()
remote_ret= channel.recv_exit_status()
if local_ret!=0 or remote_ret!=0:
raise Exception("remote_pipe failed. Local retcode: {0} Remote retcode: {1} output: {2}".format(local_ret, remote_ret, received))
如果有人想要贡献一些修改,这个内容是btrfs-send-snapshot的一部分。
你可以使用fexpect这个工具,它是我为fabric开发的一个扩展,来发送远程的标准输入。这个工具也可以发送文件,但它把这个过程隐藏在一个接口后面。虽然这样做比较方便,但你还是需要处理一些特殊字符的问题。