off the stack

Piping hosts to Fabric

2012-09-11 | fabric, python

based on fabric version 0.9.1 and python 2.6

Fabric is a command-line tool and library which can be used to streamline various system administration and application deployment tasks providing facilities to run commands locally and remotely (through SSH).

Why

For remote commands, you can specify the hosts to run the commands on in various ways. They may be added to the env.hosts right within the "fabfile" or may be given using the -H HOSTS, --hosts=HOSTS command-line options. You can even use tasks to populate your env.hosts in the way you want it. Refer to the documentation for some examples.

I am currently using fabric mostly for ad-hoc tasks on remote systems, such as placing files, adjusting configuration and restarting services. Most of the time I am using it like this:

fab -H host1,host2,host3 example_task

Recently, I needed to execute some task on several hosts given as a file though. The file consisted of several lines, with a hostname on each line I was used to using the -H option and came up with something like this:

fab -H `cat hosts | perl -pe 's/\n/,/' | perl -pe 's/(.*),$/$1/'` example_task

This reads the file "hosts", joins the lines using a comma (as required by -H) and removes the eventually trailing comma. The output is then used to call fab with a list of hosts.

While this works, it looks ugly and could become unwieldy in case the list of hosts needs extra preprocessing/filtering IMHO. I would rather rewrite it to something like this:

cat hosts | fab read_hosts example_task

How

What basically needs to be done is read the lines from STDIN and use them to populate the "env.hosts" list. Furthermore I want this logic to reside within a fabric task in order to keep the possibility to use other ways of populating the hosts-list too.

The corresponding code would be something like this:

import sys
from fabric.api import env, run

def read_hosts():
    """
    Reads hosts from sys.stdin line by line, expecting one host per line.
    """
    env.hosts = [line.strip() for line in sys.stdin.readlines()]

def example_task():
    """
    Example task which executes the uptime command remotely.
    """
    run("uptime")

Looking at the read_hosts() function, it is actually quite simple to add support for reading from STDIN to a fabfile. From here it should be easy to add more processing and filtering if appropriate such as ignoring empty and commented lines:

def read_hosts_filtered():
    """
    Reads hosts from sys.stdin line by line, expecting one host per line and
    ignoring empty or commented lines.
    """
    env.hosts = []
    for line in sys.stdin.readlines():
        host = line.strip()
        if host and not host.startswith("#"):
            env.hosts.append(host)

Another example reading CSV files with the fields host and role:

import csv

def read_hosts_csv(**kwargs):
    """
    Reads hosts from sys.stdin as CSV, passing any additional parameters to the
    csv.reader() function.
    The CSV is expected to have two columns, one for the hostname and one for
    the role which are used to populate the fabric roledefs.
    """
    reader = csv.reader(sys.stdin, **kwargs)
    for row in reader:
        host, role = row
        env.roledefs[role] = env.roledefs.get(role, []) + [host]

Which then can be used to read a standard CSV file like this:

cat hosts_csv | fab read_hosts_csv example_task:role=web

The read_hosts_csv() task supports different CSV dialects too due to the **kwargs being passed to the csv.reader() function:

$ cat hosts_csv_colon
localhost:db
localhost:web
localhost:cache
127.0.0.1:web
$ cat hosts_csv_colon | fab read_hosts_csv:delimiter=":" example_task:role=web

Further error checking and validation is left as an exercise for the reader.

Similarly it would be easy to support different formats too, such as JSON or XML. Just implement a matching reader function which translates the input to hosts or other fabric settings. Then you could for example read the configuration from a remote feed using curl and pipe it to fabric. Or you could use a command-line database client to retrieve host information.