Migrating from Fabric 1 to Fabric 2

October 10, 2019 

Fabric is a Python library for executing shell commands remotely over SSH in serial or parallel mode. I used Fabric 1 for years and it was - and still is - an excellent tool. While Fabric's use-cases overlap somewhat with those of Ansible, the difference is that Fabric is not state-based. In other words, in Fabric you don't tell that "file <a> should have contents <n> and mode <m>". Instead, you just run commands on the target host which hopefully result in the end state you're looking for. Or maybe you're not even interested in end-state per se, but just need to run some commands. Both approaches have their use-cases. Fabric and Ansible are quite powerful as orchestration tools: they allow running task <a> on server <x>, then task <b> on server <y>. For enforcing infrastructure to be in a certain state we prefer Puppet and Terraform over Ansible.

The pain point with Fabric has, in my opinion, always been the documentation. I recently had to implement an old Fabric 1 task in Fabric 2, so I'll document some of the caveats here before I forget about them. There may be other ways to do the same, but these are the ones that I found and which did the trick. For a list of differences between the Fabric versions have a look at the official upgrade documentation.

Running commands in a certain directory

Fabric 1 had context managers which allowed to change the "context" of a command. Context managers seem to have been ported over to Fabric 2. In Fabric 1 I modified the context primarily to run commands in the correct directory:

@task
def mytask():
    with cd("/tmp"):
        run("somecommand")

In Fabric 2 I could not - quickly at least - figure out how to set the directory a command should run in using context managers. However, the following contraption seemed to work for "run":

@task
def mytask(c):
    mydir = "/tmp"
    cd = "cd %s && " % (mydir)
    c.run(cd + "mycommand")

For "sudo" the above approach does not work, possible because "cd" is an Bash alias. Additionally I believe that in the above case the resulting command line created by Fabric ends up being "sudo cd /tmp && mycommand", which meant that "mycommand" is not run in /tmp, but in the current directory. In any case wrapping the whole thing into "sh -c" did work:

c.sudo("sh -c '" + cd + "mycommand'")

Managing sudo password prompts

In Fabric 1 you'd typically just use

sudo("mycommand")

and Fabric would figure out what needed to be done. In parallel execution mode you had to pass the password on the command-line, but in serial mode it prompted you for it.

In Fabric 2 there are at least two ways to pass in sudo passwords to tasks that need them. The first one is this:

c.run("sudo mycommand", pty=True)

This enables Fabric to prompt the user for the sudo password. What it does not do is cache the password, so if you have several of these commands you'll have to type the password several times. If you just want to run commands as root this is annoying, but it is definitely beneficial if you have a task that runs commands as several different users with different passwords.

The second approach is to prompt for the sudo password before starting the Fabric run:

$ fab --prompt-for-sudo-password -Hserver.example.org mytask
Desired 'sudo.password' config value:

As you can see, you can also set "sudo.password" in the configuration to get rid of these prompts. In both cases Fabric handles passing the password to each sudo invocation.

Selecting which hosts the command would run on

In Fabric 1 you had several options to define which host the commands run on. When using Fabric 1 like a library you could do

@task
@runs_once
def remove_node(node):
     execute(remove_node_from_system_a, node, hosts=['system-a.example.org'])

def remove_node_from_system_a():
     sudo("run command on " + node)

Or you could set the "env.hosts" parameter explicitly:

env.hosts=['system-a.example.org']

Or you could use a task decorator:

@task
@hosts('system-a.example.org')
def mytask():
    ....

In Fabric 2 things look slightly different. The task decorators are laid out differently:

@task(hosts=['system-a.example.org')
def mytask(c):
    ....

A simplistic command-line example is the same in Fabric 1 and 2:

$ fab -Hsystem-a.example.org mytask

Make sure you check the "fab" command-line options in Fabric 2, though: the options are very different from those in Fabric 1.

Samuli Seppänen
Samuli Seppänen
Author archive
menucross-circle