Skip navigation.
Home
That which cannot be rendered in binary is by definition a delusion
 

Writing BASH scripts in Python with Subprocess and Fabric

Supbrocess

The most fundamental way to execute BASH from Python is the subprocess module. 

suck_it_world.py

import subprocess

result = subprocess.call(['echo', 'suck it world'])
print result

result

	suck it world 
0

not so bad; but what if we care about the output of a bash command?

probe.py

import subprocess

child = subprocess.Popen('ls -la', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
child_out, child_err = child.communicate()

print 'out', child_out.split("\n")
print 'err', child_err

result

	out ['total 16', 'drwxr-xr-x 4 dave staff 136 Sep 16 11:00 .',
'drwxr-xr-x 22 dave staff 748 Sep 16 10:57 ..', 
'-rw-r--r-- 1 dave staff 215 Sep 16 11:00 probe.py', 
'-rw-r--r-- 1 dave staff 113 Sep 16 10:56 suck_it_world.py', '']
 err 

Two things to note about bash and subprocess:

  1. with Popen, you pass the comand as a single string; with call, the arguments are passed as an array. 
  2. if you are using call, clump quoted strings as a single element. i.e., ['echo', 'this is a space seperated string'], not ['echo', 'this', 'is'...]

Fabric: spamming SSH servers

Fabric lets you run bash processs on other servers. It is best to have SSH key files for your target servers. The power of fabric is in a fabfile, a super library that has all the methods you want to run remotely. Say you wanted to explore the home contents of a bunch of remote users. This is the kind of fabfile you would write: 

fabfile.py

from fabric.api import *

def init():
    key_user =  "root"
    env.user = key_user
    env.key_filename = ['/fabkeys/server_one.dev', '/fabkeys/server_two.dev']

def look_in():
    run('ls -la')

Note that Fabric is not just a library - it is a command line program. In fact its a little tricky to use Fabric from within Python scripts.

To call the above fabfile, assuming you're in the same dirctory as the fabfile.py file, you execute the following from the command line:

fab -H root@server_one,root@server_two init look_in

Result

	[root@server_one] Executing task 'init'
[root@server_two] Executing task 'init'
[root@server_one] Executing task 'look_in'
[root@server_one] run: ls -la
[root@server_one] out: total 8748
[root@server_one] out: drwxr-x---  7 root root     4096 Aug  3 17:31 .
[root@server_one] out: drwxr-xr-x 24 root root     4096 Jul 26 12:08 ..
[root@server_one] out: -rw-------  1 root root    14046 Sep 15 18:06 .bash_history
[root@server_one] out: -rw-r--r--  1 root root       24 Jan  6  2007 .bash_logout
[root@server_one] out: -rw-r--r--  1 root root      201 Jul 22 00:20 .bash_profile
...
[root@server_one] out: -rw-r--r--  1 root root      129 Jan  6  2007 .tcshrc
[root@server_one] out: drwxrwxrwx 17   20 games    4096 Nov 11  2010 ImageMagick-6.5.0-10
[root@server_one] out: -rw-r--r--  1 root root  8796079 Nov 11  2010 ImageMagick-6.5.0-10.tar.bz2
[root@server_one] out: -rw-r--r--  1 root root    19954 Jul  9  2010 install.log
[root@server_one] out: -rw-r--r--  1 root root        0 Jul  9  2010 install.log.syslog
[root@server_one] out: -rw-r--r--  1 root root      251 Jul  9  2010 upgrade.log
[root@server_one] out: -rw-r--r--  1 root root        0 Jul  9  2010 upgrade.log.syslog
[root@server_one] out: 
[root@server_two] Executing task 'look_in'
[root@server_two] run: ls -la
[root@server_two] out: total 8752
[root@server_two] out: drwxr-x---  7 root root     4096 Aug 17 18:48 .
[root@server_two] out: drwxr-xr-x 24 root root     4096 Aug  3 15:49 ..
[root@server_two] out: -rw-------  1 root root    13805 Sep 14 20:14 .bash_history
[root@server_two] out: -rw-r--r--  1 root root       24 Jan  6  2007 .bash_logout
[root@server_two] out: -rw-r--r--  1 root root      201 Jul 22 00:20 .bash_profile
[root@server_two] out: -rw-r--r--  1 root root      176 Jan  6  2007 .bashrc
[root@server_two] out: -rw-r--r--  1 root root      100 Jan  6  2007 .cshrc
...
[root@server_two] out: -rw-r--r--  1 root root    19954 Jul  9  2010 install.log
[root@server_two] out: -rw-r--r--  1 root root        0 Jul  9  2010 install.log.syslog
[root@server_two] out: -rw-r--r--  1 root root       70 Aug 17 18:48 stall.rb
[root@server_two] out: -rw-r--r--  1 root root      251 Jul  9  2010 upgrade.log
[root@server_two] out: -rw-r--r--  1 root root        0 Jul  9  2010 upgrade.log.syslog
[root@server_two] out: 

Done.
Disconnecting from root@server_one... done.
Disconnecting from root@server_two... done.

Things to note:

  1. The methods to be called are called in the order they are passed in, and they are applied to each server in the -H list, one by one; i.e., if you call "fab -H FooServer,BarServer,ZedServer AlphaMethod BetaMethod GammaMethod" then AlphaMethod is applied ot all three servers before the first BetaMethod is called. 
  2. Server names are comma seperated, with no spaces allowed. Methods are space seperated. 

Getting a little trickier with Fabric

Fabric allows for parametrized method arguments. Say, for instance, that you want to look inside the '/etc' directory across all the target servers. A little surgery on the fabfile:

from fabric.api import *

def init():
    key_user =  "root"
    env.user = key_user
    env.key_filename = [ "/opt/deploy/keys/tyger.dev"]

def look_in(path_to_look_in):
    run("cd '%s'; ls -la" % (path_to_look_in))
[root@gecko examples]# 

Something else to note - I originally tried putting the cd and the ls on two different runs as in

run("cd '%s'" % (path_to_look_in))
    run(ls -la")

However, and I guess I should have seen this coming but, these runs executed in different SSH sessions and therefore, I got a listing of the home directory again.

Moving along: executing

fab -H root@server_one,root@server_two init look_in:/etc

returns a listing of the /etc directory.

Given the simplicity of Fabric, you can do a lot of administration without a lot of deep Python experience.

Further tricks with Fabric

There are two straightforward reasons why you would want to tweak with the settings in Fabric:

  1. You want to run a series of functions, most of them over the same servers, but a few of them on just one server.
  2. You want to ignore any irrelevant errors thrown by BASH.

The latter came up for me when I wanted to run "killall -9 ruby". If there are no processes to kill it throws an error (really?). But I don't want that error to be a show stopper.

There are a bunch of ways to change your "hit list" of servers. the Fabric Documentation covers them as well as I could - have a look. But here I will highlight the use of the settings construct which has options for both of the above scenarios. 

Say you wanted to inspect a series of servers, then kill ruby processes on a single server. Also you got tired of typing server names, so you wanted to embed that list in the script. Here's the modifed fabfile.py:

from fabric.api import *

env.hosts=['server_one', 'server_two']
env.user='root'

def init():
    key_user =  "root"
    env.user = key_user
    env.key_filename = [ "/opt/deploy/keys/tyger.dev"]

def look_in(path_to_look_in):
    run("cd '%s'; ls -la" % (path_to_look_in))

runs = 0
def kill_all_ruby():
	global runs
	if runs == 0:
		runs += 1
		with settings(host_string='mongodb-dev',warn_only=True):
			run('killall -9 ruby')	

Note that there is a bit of hackery in this script that I do to further limit the scope of executions. Because fab by definition loops each of the tasks you pass it, the kill_all_ruby() task will be executes several times - once for each host. To ensure it only executes once, I use a "runs" variable to track how many times it executes - to once. Also, with the host_string parameter, ony a single host can be targeted. 

Another, and more "fabric'y" way of doing this is with a task that switches the env.hosts value. Its important to note the following:

  1. You cannot modify the env.hosts AND execute a run FROM THE SAME DEF. they must be seperated.
  2. Any changes to env.hosts in this manner are persistent. Changing the host list in one def changes the host target list for all subsequent defs. 
from fabric.api import *

env.hosts=['server_one', 'server_two', 'server_three']
env.user='root'

def init():
    key_user =  "root"
    env.user = key_user
    env.key_filename = [ "/opt/deploy/keys/tyger.dev"]

def look_in(path_to_look_in):
    run("cd '%s'; ls -la" % (path_to_look_in))

def kill_all_ruby():
	with settings(warn_only=True):
	    run('killall -9 ruby')
		
def point_to_mongo():
	env.hosts = ['server_mongo']

shows what is needed for def-based env.host switching. If called like this:

fab init look_in:/root point_to_mongo kill_all_ruby look_in:/root

(note the absence of -H) you will see the following flow:

  • look in root of server_one
  • look in root of server_two
  • look in root of server three
  • kill ruby on server_mongo
  • look in root of server_mongo

In Closing

I hope this flyover of the Fabric system is useful to your script writing efforts. 

Post new comment

  • Allowed HTML tags: <a> <p> <span><small> <div> <h1> <h2> <h3> <h4> <h5> <h6> <img> <map> <area> <hr> <br> <br /> <ul> <ol> <li> <dl> <dt> <dd> <table> <tr> <td> <em> <b> <u> <i> <strong> <font> <del> <ins> <sub> <sup> <quote> <blockquote> <pre> <address> <code> <cite> <embed> <object> <param> <strike> <caption>
  • Lines and paragraphs break automatically.
  • Web page addresses and e-mail addresses turn into links automatically.

More information about formatting options