Yesterday[5] I left when the problem got really interesting: how to transfer code to another machine and execute it there. I already advanced part of the solution: use pickle to convert something returned by ast.parse() into something transferable. Let's see how hard it really is:

import paramiko
import ast
import pickle

p= ast.parse ('print ("yes!")')
pick= pickle.dumps (p)
c= paramiko.SSHClient()
c.load_host_keys ('/home/mdione/.ssh/known_hosts')
c.connect ('localhost', allow_agent=False, password='foobarbaz')
(i, o, e)= c.exec_command ('''python -c "import pickle
from ast import Print, Module, Str
import sys
c= pickle.load (sys.stdin)
code= compile (c, 'remote', 'exec')
exec (code)"''')
i.write (pick)
o.readline ()

This happily prints 'yes!' on the last line.

There are a lot of caveats in this code. First, this doesn't work on Python3, only because there's no official/working port of paramiko for that version. Jan N. Schulze a.k.a. nischu7 has made a port, which looks quite active (last commit from around a month ago), but I tried it with Python 3.3 and didn't work out of the box. Furthermore, even when pickle's doc says that it automatically detects the format of the stream, which means that technically I could pickle something in Python2 and unpickle it back in Python3, the same does not happen with the ast module. Hence, I'm also using Python2 in the remote[2]. This implies that I will have to check if the reconstruction works and if the reconstructed code actually compile()'s. But I already knew that.

Second, this assumes that you have the remote machine already in the known_hosts file. Third, I'm importing things from ast specifically for reconstructing the parsed code (ast.dump (p) returns "Module(body=[Print(dest=None, values=[Str(s='yes!')], nl=True)])"). I hadn't checked yet, but somehow from ast import * is not enough. Last, the transfered code is simple enough, makes no references to local or remote variables (for whichever definition or local and remote; I will have to be consistent in the future when using those words), nor references other modules, present or not in the remote machine (there, remote is the machine mentioned in the parameter of ssh()[3][4])[1]. But this is a promising step.

Another thing to notice is that the code is sent via stdin. This might cause trouble with script expecting things that way, let's see:

import paramiko
import ast
import pickle

p= ast.parse ('foo= raw_input (); print (foo)')
pick= pickle.dumps (p)
c= paramiko.SSHClient()
c.load_host_keys ('/home/mdione/.ssh/known_hosts')
c.connect ('localhost', allow_agent=False, password='foobarbaz')

command= '''python -c "import pickle
from ast import Print, Module, Str, Assign, Name, Call, Load, Store, dump
import sys
c= pickle.loads (sys.stdin.read (%d))
code= compile (c, 'remote', 'exec')
exec (code)"''' % len (pick)

(i, o, e)= c.exec_command (command)
i.write (pick)
i.write ('bar!\n')
o.readline ()

This works, but only after someone tells you that you should use raw_input() instead of input(), which triggers the realization that you're reading Python3's doc but using Python2. Damn you, paramiko!

So, in conclusion, This technique starts to show promise. The good thing about it is that it barely requires any setup. Future developments could include a ssh client cache. The next step is to get the variables in the remote machine and gluing it with the previous developments.


[1] Another caveat: that one is definitely not my password :)

[2] Clearly this sentence was written before this other one[3].

[4] There, I just invented bodynotes :)

[5] I have again been bitten by a post that takes days, if not months, to publish.


python ayrton