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