Paramikoを使ってシェルスクリプトをリモートホストに流しこむ

SSHClient.exec_commandを使って一つずつコマンドを実行することはできるのですが、既存のシェルスクリプトを流しこみたいときどうしたらいいんだろうと試行錯誤しました。

TLDR

明示的にバッファをフラッシュするためにシェルスクリプトの終わりにexit\nを付ける。

作業ログ

import getpass
import paramiko

REMOTE_HOST = "YOUR_REMOTE_HOST"

c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
pkey = paramiko.RSAKey.from_private_key_file('/home/ubuntu/.ssh/id_rsa', getpass.getpass())
c.connect(REMOTE_HOST, username='ubuntu', pkey=pkey)

# こういう複数行のやつを流しこみたい
cmd = '''
/bin/ls /
/bin/touch /tmp/foo
echo 'foo
bar
baz' >> /tmp/foo
cat /tmp/foo
'''

まずbashを起動して標準入力で流しこむ方法を試しました。

stdin, stdout, stderr = c.exec_command('bash -')
stdin.write(cmd)
print(stdout.read())

しかし応答せず。 どこかでブロックしているのかと考え次にselectの方法を試しました。

import socket
import select
chan = c.invoke_shell()
chan.setblocking(False)

chan.send(cmd)
while True:
    r, _, _ = select.select([chan], [], [])
    if chan in r:
        try:
            x = chan.recv(1024)
            if len(x) == 0:
                break
            print(x)
        except socket.timeout:
            pass

これでも応答せず。ブロックしているというわけではなさそうなことが分かりました。 じゃあ何だろう、バッファかなと。 ローカルの送信側のバッファは明示的にフラッシュすればいいのですが、リモートホストのバッファをフラッシュする方法が分からず。調べてみたらシェルスクリプトを終了させれば自動的にフラッシュすると記述があったので、スクリプトを修正してみました。

cmd = '''
/bin/ls /
/bin/touch /tmp/foo
echo 'foo
bar
baz' >> /tmp/foo
cat /tmp/foo
exit
'''

動いた。なるほど。

まとめ

  • 同期非同期の問題ではなかった。
  • やりたいことが終わったらプロセスを落とす。シェルかソケットのバッファだろうがプロセス落とすときにはフラッシュされる。