Fabric: a System Administrator's Best Friend

Adrian Hannah

Issue #226, February 2013

Do you routinely make changes to more than a dozen machines at a time? Read this article to find out about a tool to make that task much easier.

I'll be honest. Even though this library is fully five years old, I hadn't heard of Fabric until about six months ago. Now I can't imagine not having it in my digital tool belt. Fabric is a Python library/tool that is designed to use SSH to execute system administration and deployment tasks on one or more remote machines. No more running the same task, machine by machine, to make one change across the board. It is a simple fire-and-forget tool that will make your life so much simpler. Not only can you run simple tasks via SSH on multiple machines, but since you're using Python code to execute items, you can combine it with any arbitrary Python code to make robust, complex, elegant applications for deployment or administration tasks.

Installation

Fabric requires Python 2.5 or later, the setuptools packaging/installation library, the ssh Python library, and SSH and its dependencies. For the most part, you won't have to worry about any of this, because Fabric can be installed easily through various package managers. The easiest, and most prolific way to install Fabric is using pip (or easy_install). On most systems, you can use your systems package manager (apt-get, install, and so on) to install it (the package either will be fabric or python-fabric). If you're feeling froggy, you can check out the git repository and hack away at the source code.

Once installed, you will have access to the fab script from the command line.

Operations

The Fabric library is composed of nine separate operations that can be used in conjunction to achieve your desired effect. Simply insert these functions into your fabfile and off you go:

  • get(remote_path, local_path=None)get allows you to pull files from the remote machine to your local machine. This is like using rsync or scp to copy a file or files from many machines. This is super effective for systematically collecting log files or backups in a central location. The remote path is the path of the file on the remote machine that you are grabbing, and the local path is the path to which you want to save the file on the local machine. If the local path is omitted, Fabric assumes you are saving the file to the working directory.

  • local(command, capture=False) — the local function allows you to take action on the local host in a similar fashion to the Python subprocess module (in fact, local is a simplistic wrapper that sits on top of the subprocess module). Simply supply the command to run and, if needed, whether you want to capture the output. If you specify capture=True, the output will be returned as a string from local, otherwise it will be output to STDOUT.

  • open_shell(command=None) — this function is mostly for debugging purposes. It opens an interactive shell on the remote end, allowing you to run any number of commands. This is particularly helpful if you are running a series of particularly complex commands and it doesn't seem to be working on some of your machines.

  • prompt(text, key=None, default='', validate=None) — in the case when you need to supply a value, but don't want to specify it on the command line for whatever reason, prompt is the ideal way to do this. I have a fabfile I use to add/remove/check the status of software on all of the servers I maintain, and I use this in the script for when I forget to specify what software I'm working on. This prompt will appear for each host you specify, so make sure you account for that!

  • put(local_path, remote_path, use_sudo=False, mirror_local_mode=False, mode=None) — this is the opposite command of get, although you are given more options when putting to a remote system than getting. The local path can be a relative or absolute file path, or it can be an actual file object. If either local_path or remote_path is left blank, the working directory will be used. If use_sudo=True is specified, Fabric will put the file in a temporary location on the remote machine, then use sudo to move it from the temporary location to the specified location. This is particularly handy when moving system files like /etc/resolv.conf or the like that can't be moved by a standard user and you have root login turned off in SSH. If you want the file mode preserved through the copy, use mirror_local_mode=True; otherwise, you can set the mode using mode.

  • reboot(wait=120)reboot does exactly what it says: reboots the remote machine. By default, reboot will wait 120 seconds before attempting to reconnect to the machine to continue executing any following commands.

  • require(*keys, **kwargs)require forces the specified keys to be present in the shared environment dict in order to continue execution. If these keys are not present, Fabric will abort. Optionally, you can specify used_for to indicate what the key is used for in this particular context.

  • run(command, shell=True, pty=True, combine_stderr=True, quiet=False, warn_only=False, stdout=None, stderr=None) — This and sudo are the two most used functions in Fabric, because they actually execute commands on the remote host (which is the whole point of Fabric). With run, you execute the specified command as the given user. run returns the output from the command as a string that can be checked for a failed, succeeded and return_code attribute. shell controls whether a shell interpreter is created for the command. If turned off, characters will not be escaped automatically in the command. Passing pty=False causes a psuedo-terminal not to be created while executing this command; this can have some benefit if the command you are running has issues interacting with the psuedo-terminal, but otherwise, it will be created by default. If you want stderr from the command to be parsable separately from stdout, use combine_stderr=False to indicate that. quiet=True will cause the command to run silently, sending no output to the screen while executing. When an error occurs in Fabric, typically the script will abort and indicate as such. You can indicate that Fabric need not abort if a particular command errors using the warn_only argument. Finally, you can redirect where the remote stderr and stdout redirect to on the local side. For instance, if you want the stderr to pipe to stdout on the local end, you could indicate that with stderr=sys.stdout.

  • sudo(command, shell=True, pty=True, combine_stderr=True, user=None, quiet=False, warn_only=False, stdout=None, stderr=None, group=None)sudo works precisely like run, except that it will elevate privileges prior to executing the command. It basically works the same is if you'd run the command using run, but prepended sudo to the front of command. sudo also takes user and group arguments, allowing you to specify which user or group to run the command as. As long as the original user has the permissions to escalate for that particular user/group and command, you are good to go.

The Basics

Now that you understand the groundwork of Fabric, you can start putting it to use. For this article, I explain how to make a simple fabfile for the purpose of installing/removing software and your machines. First, you need what is called a fabfile. The fabfile contains all of your Fabric functions. By default, it needs to be named fabfile.py and be in the working directory, but as mentioned previously, you can specify the fabfile from the command line if need be. So, open your fabfile and start it with from fabric.api import * to include all the Fabric functionality. Then define all of your functions. Let's start with installing some software:

def install(pkg=None):
   if pkg is not None:
      env["pkg"] = pkg
   elif pkg is None and env.get("pkg") is None:
      env["pkg"] = prompt("Which package? ")
   sudo('yum install -y %s' % env["pkg"])

You then can install a package via yum on all of your machines by running:

$ fab --hosts=host1,host2,host3 install

Then, you'll be prompted for the package to install only once. Alternatively, since you indicated an optional parameter of pkg, you can indicate that from the command line so you won't be prompted on execution, like this:

$ fab --hosts=host1,host2,host3 install:pkg=wormux

or:

$ fab --hosts=host1,host2,host3 install:wormux

Also note that you are prompted for the password for both SSH and sudo only once. Fabric stores this in memory and reuses it, if possible, for every other machine. Congratulations! You've just successfully created your first Fabric script. It's as simple as that!

Tips and Tricks

I've picked up some neat tricks since I've started with Fabric. First, you generally never see a Fabric command as simple as what is above. When fully automated, it looks more like this:

$ fab --skip-bad-hosts -u user -p 12345 -i ~/.ssh/id_dsa --warn-only
 ↪--hosts=host1,host2,host3,host4,host5,host6,host7,host8,host9,host10
 ↪--parallel --pool-size=20 install:pkg=wormux

Who wants to type that out every time they want to run a command? No one! That's why aliasing almost all of that is so convenient and efficient. Add the following to your .bashrc file:

alias f="fab --skip-bad-hosts -u user -p 12345 -i ~/.ssh/id_dsa
 ↪--warn-only
 ↪--hosts=host1,host2,host3,host4,host5,host6,host7,host8,host9,host10
 ↪--parallel"

Then, all you have to do each time you want to run Fabric is this:

$ f install:pkg=wormux

Even using this technique, your alias can become cumbersome if you have more than a few machines you commonly administer. A simple solution to that is to add this function to your fabfile:

def set_hosts():
   env.hosts = open('hosts', 'r').readlines()

Then, put all your hostnames in a file called hosts in the same directory as your fabfile, and modify your alias to look like this:

alias f="fab --skip-bad-hosts -u user -p 12345 -i ~/.ssh/id_dsa
 ↪--warn-only --parallel set_hosts"

This is particularly convenient if you have a variety of fabfiles that you use on different groups of machines, or in different contexts.

There are occasions when you need to execute certain commands from within a specific directory. Because each command is a discrete and non-persistent connection to the machine, this is not inherently simple. However, simply by enclosing the necessary commands in a with statement, you have a solution:

with cd("~/gitrepo"):
   run('git add --all')
   run('git commit -m "My super awesome automated 
   ↪commit script for `date`"')

More Information

There are several ways to get help with Fabric. The most effective is to use the fab-file mailing list (lists.nongnu.org/mailman/listinfo/fab-user). The developers are generally very prompt in responding. There is also a Fabric Twitter account @pyfabric (www.twitter.com/pyfabric) where Fabric news and announcements are released. You can submit and view bugs through the Fabric Github page (https://github.com/fabric/fabric/issues). Of course, you also can't discount the #fabric channel on Freenode, where you can connect with the community and get some quick answers. Finally, you always can browse the documentation hosted at www.fabfile.org.

Adrian Hannah has spent the last 15 years bashing keyboards to make computers do what he tells them. He currently works as a Senior System Administrator for a Web startup in New York City. He is a jack of all trades and a master of none. Find out more at about.me/adrianhannah.