A couple of fairly significant benefits:
- Pushy does more than just SSH, so Fabric could conceivably be made to support additional transports by using Pushy (which uses Paramiko), rather than using Paramiko directly.
- Pushy provides access to the entire Python standard library, which is largely platform independent. So you could do things like determine the operating system name without using "uname", which is a *NIX thing. That's a trivialisation, but you get the idea I'm sure.
- Pushy absolutely requires Python on the remote system (as well as SSH, of course, but Fabric requires that anyway.) So requiring Pushy would mean that Fabric would be restricted to working only with remote machines that have Python installed. Probably a safe bet in general, but not ideal.
How about using Pushy if Python is available, and just failing gracefully if it doesn't? This turns out to be really easy to do, since Fabric and Pushy both use Paramiko. So I wrote a Fabric "operation" to import a remote Python module. Under the covers, this piggy-backs a Pushy connection over the existing Paramiko connection created by Fabric. I'll bring this to the attention of the Fabric developers, but I thought I'd just paste it here for now.
First an example of how one might use the "remote_import" operation. Pass it a module name, and you'll get back a reference to the remote module. You can then use the module as you would use the module as if you had done a plain old "import ".
fabfile.py
from fabric_pushy import remote_import def get_platform(): platform = remote_import("platform") print platform.platform()
You just execute your fabfile as per usual, and the "remote_import" operation will create a Pushy connection to each host, import the remote Python interpreter's standard platform module, and call its platform method to determine its platform name. Easy like Sunday morning...
$ fab -H localhost get_platform [localhost] Executing task 'get_platform' Linux-2.6.35-27-generic-i686-with-Ubuntu-10.10-maverick Done. Disconnecting from localhost... done.
fabric_pushy.py
from fabric.state import env, default_channel from fabric.network import needs_host import pushy import pushy.transport from pushy.transport.ssh import WrappedChannelFile class FabricPopen(pushy.transport.BaseTransport): """ Pushy transport for Fabric, piggy-backing the Paramiko SSH connection managed by Fabric. """ def __init__(self, command, address): pushy.transport.BaseTransport.__init__(self, address) # Join arguments into a string args = command for i in range(len(args)): if " " in args[i]: args[i] = "'%s'" % args[i] command = " ".join(args) self.__channel = default_channel() self.__channel.exec_command(command) self.stdin = WrappedChannelFile(self.__channel.makefile("wb"), 1) self.stdout = WrappedChannelFile(self.__channel.makefile("rb"), 0) self.stderr = self.__channel.makefile_stderr("rb") def __del__(self): self.close() def close(self): if hasattr(self, "stdin"): self.stdin.close() self.stdout.close() self.stderr.close() self.__channel.close() # Add a "fabric" transport", which piggy-backs the existing SSH connection, but # otherwise operates the same way as the built-in Paramiko transport. class pseudo_module: Popen = FabricPopen pushy.transports["fabric"] = pseudo_module ############################################################################### # Pushy connection cache connections = {} @needs_host def remote_import(name, python="python"): """ A Fabric operation for importing and returning a reference to a remote Python package. """ if (env.host_string, python) in connections: conn = connections[(env.host_string, python)] else: conn = pushy.connect("fabric:", python=python) connections[(env.host_string, python)] = conn m = getattr(conn.modules, name) if "." in name: for p in name.split(".")[1:]: m = getattr(m, p) return m
Hi Andrew,
ReplyDeleteThis pushy/fab thing is pretty sweet; I'm
experimenting with it now but I get this error
in traceback... any idea? I'm running python 2.7
on localhost and python 2.6 remote, perhaps this is causing "versionitis"?
Thanks for your contribution. :)
$ ./tabdev.py get_platform
Traceback (most recent call last):
File "/usr/lib/pymodules/python2.7/fabric/main.py", line 551, in main
commands[name](*args, **kwargs)
File "/home/jkauth/Development/Ricoh/build_engineering/bin/tabdev.py", line 56, in get_platform
platform = remote_import("platform")
File "/usr/lib/pymodules/python2.7/fabric/network.py", line 303, in host_prompting_wrapper
return func(*args, **kwargs)
File "/home/jkauth/Development/Ricoh/build_engineering/bin/fabric_pushy.py", line 64, in remote_import
conn = pushy.connect("fabric:", python=python)
File "/usr/local/lib/python2.7/dist-packages/pushy-0.5.1-py2.7.egg/pushy/client.py", line 583, in connect
return PushyClient(target, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/pushy-0.5.1-py2.7.egg/pushy/client.py", line 352, in __init__
self.server = transport.Popen(command, **kwargs)
File "/home/jkauth/Development/Ricoh/build_engineering/bin/fabric_pushy.py", line 28, in __init__
self.__channel.exec_command(command)
File "/usr/lib/python2.7/dist-packages/paramiko/channel.py", line 213, in exec_command
self._wait_for_event()
File "/usr/lib/python2.7/dist-packages/paramiko/channel.py", line 1084, in _wait_for_event
raise e
EOFError
Disconnecting from root@10.159.151.55... done.
Hi there,
ReplyDeleteThanks for the comments. Sorry for the delay in responding, I didn't have comment notification set up properly.
I can't tell a great deal from the traceback unfortunately. I've just verified that I can connect from a Python 2.7.2 to a Python 2.6.7 interpreter, using Fabric 1.3.1 and Pushy 0.5.1.
It appears that you're using an older version of Fabric, as I can see it's using Paramiko, as opposed to the newer 'ssh' replacement package. The only thing I can really suggest for now is to try upgrading Fabric.
Please contact me at axwalk@gmail.com if you'd like to discuss this further.
thanks Andrew, I'll try that :)
ReplyDelete