开发者

Best way to pipe the output of a local() to the stdin of a remote run() command in Fabric?

Is there a simple way to pipe output from local commands to remote ones (and vice versa)?

I've always just piped to a file, moved the file over, and then read it...but it seems like there could be an easier way.

For simpler situations, just capturing the output and using string interpolation works:

ip = local('hostname -i')
run('Script was run from ip开发者_C百科: %s' % ip)

But when the output either needs escaping to be safe on the command line and/or needs to come from stdin it is a bit trickier.

If the output is bash-safe, then something like run('echo "%s" | mycmd' % ip) would do what I'm looking for (which I guess implies that an equivalent question would be "is there a simple way to bash-escape strings?"), but it seems like there should be a "right way" to provide a remote stdin.

Edit:

To clarify with long-ish inputs there a number of potential problems with simple string interpollation: classic shell problems (eg. the output could contain "; rm -rf /) but also (and more realistically, in my case) the output can contain quotes (both single and double).

I think just doing run("echo '%s' | cmd" % output.replace("'", "'\\''") should work, but there may be edge cases that misses.

As I mentioned above, this seems like the type of thing that fabric could handle more elegantly for me by directly sending a string to the run()'s stdin (though perhaps I've just been spoiled by it handling everything else so elegantly :)


You could send the remote stdin with fexpect, my fabric extension. this also sends a file, but hides it behind an api. You would still have to do the escaping though.


I've done this once in order to send a (binary) stream to a remote server.

It's a bit hackish, as it digs deep into fabric and paramiko's channels, and there may be untested edge cases, but it mostly seems to do the job

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))

In case anyone feels like contributing modifications, this is part of btrfs-send-snapshot


This is a slightly improved version of @goncalopp's answer:

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))

Apart from readability, the improvement is that it does not abort with a socket timeout in case the remote command outputs less than buffer_size bytes, and that it prints the complete output of the remote command.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜