在Fabric中将local()的输出传递给远程run()命令的最佳方式是什么?

4 投票
3 回答
2644 浏览
提问于 2025-04-16 12:25

有没有简单的方法可以把本地命令的输出直接传给远程命令(反之亦然)呢?

我一直都是把输出先保存到一个文件里,然后把文件传过去,再读取文件……但感觉应该有更简单的方法。

在一些简单的情况下,直接捕获输出并用字符串插值就可以了:

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 个回答

0

这是对@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,它不会因为套接字超时而中断,并且会打印出远程命令的完整输出。

0

我之前做过一次这样的事情,是为了把一个(二进制)数据流发送到远程服务器。

这个方法有点儿不太正规,因为它深入到了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的一部分。

0

你可以使用fexpect这个工具,它是我为fabric开发的一个扩展,来发送远程的标准输入。这个工具也可以发送文件,但它把这个过程隐藏在一个接口后面。虽然这样做比较方便,但你还是需要处理一些特殊字符的问题。

撰写回答