Few lessons I learned after using python fabric 1.x


Fabric is a great framework for executing code on remote & local machines. The 1.X had a pretty good documentation, but workflows and tools were not so clearly explained, after writing several scrips myself, I managed to come up with a few rules that I believe others will find to be useful.

Default function arguments

using env.* in method bodies is a bad idea, but it can be made better:

def list_root(default_path=None):

    if not default_path:
        default_path = env.sys_default_path
        # this will make function more reusable in other scripts, because
        # you can simply delete this if part and all env.* specific vars are
        # no longer a problem

    run('ls -alt {}'.format(default_path))
  • setting default_path inside an if statement makes function more reusable.
  • if you use def list_root(default_path=env.sys_default_path): it will be incompatible with paragraph [$5].

Split your code to Python modules

|- nodes.py (servers)
|- installers.py
|- services.py
`- fabfile.py (imports files above)

Your code will outgrow a single file, trust me. Think about module structure first! More info about splitting to modules is here

Use more global module constants

If modules global are not enough only then use env.*

from fabric.api import run

# we know that this will never change, so let's store it in a constant
ALL_CAPS_CAPS_LOCK_TEST = 'armstrong'

Import constant from module below:

from nasa.hax import hacked_user as ALL_CAPS_CAPS_LOCK_TEST

env.host=['nasa.org:22']
env.user = ALL_CAPS_CAPS_LOCK_TEST

def prod_env():
    env.hosts=['prod-server.com']

Differentiate environments

Use Python methods to define different environments:

from fabric.api import run

env.host=['test-server.com']

def prod_env():
    env.hosts=['prod-server.com']

def test_server():
    # we don't call env hosts inside this func body, because we want test-server
    # to be the default server because, writing: `$ fab test_server [...]` is annoying
    pass

def list_home():
    run('ls -alt $HOME')

call these methods in your terminal

$ fab prod_env list_home  # this will execute code on prod server
$ fab test_env list_home # this will execute code on test server 
$ fab list_home  # this will execute code on test server because `env.hosts` are global in fabfile.py module

Use One Execution Context Per Function

If possible, do not mix execution functions like root(), run() or local() inside one function.

Excessive usage of mixed execution methods will make your function very hairy and messy, because in order to write a generic method that you can run both locally, and remotely you will need to write many if statements also giving your function tons of default parameters when doing a subsystem call.

The optimal way is to avoid mixing subsystem functions: root(), run(), local() in Fabric method you are writing. The benefits of not mixing these functions in a method is that you can pass them as needed as a function variable:

from fabric.api import local, run, sudo
from fabric.state import env


env.user='troubled.man'
env.host=['nasa.org:22']

# bad
def which_user(run_local=True, run_remote=False, run_root=False):
    if run_local:
        local('whoami')
   elif run_remote:
       run('whoami')
   ...



# good
def which_user(caller=local):
    caller('whoami')

which_user(caller=local)  # this will print user of your local machine
which_user(caller=run)  # this will print `troubled.man`
which_user(caller=root)  # this will print `root` (because in linux sudo command temporaraly changes your user to root)

You made a function that does 3 different things by passing a single parameter. That is why you want consistency in your code execution functions.

Do Not Mix Python Versions

Fabric 1.x is written in Python 2.7, but most of the newer projects are written in Python 3.x. This means that you can't simply pip install fabric to a python 3.X environment this results in you having to change virtual or Anaconda environments when doing fabric calls, see example below:

$ source activate py27  # activating anaconda python=2.7 env
$ python manage.py runserver  # for example let's call django app writen in 3.X

  File "manage.py", line 15
    ) from exc
         ^
SyntaxError: invalid syntax

What just happened that once I activate my virtual environment python executable path changes:

$ which python
/usr/bin/python
$ source activate py27
$ which python
~/anaconda/envs/py27/bin/python

So that is the reason why Django app in example below did not start. One possible fix is to use py27 environment always. And modify fabfile.py to activate python3 environment locally when needed.

from fabric.api import local, run, sudo
from fabric.state import env


env.user='armstrong'
env.host=['nasa.org:22']  
env.hosts = env.host

env.runtime = 'source activate py36 &&'  # let's assume our django app is running, on 3.6

def prod_env():
    env.runtime = ''  # we assume default python path is 3.X on prod_env
    env.caller = run


def local_env():
    pass


def python_version(caller=local, runtime=None):

    # let's use runtime of our environment
    if not runtime:
        runtime = env.runtime

    caller('{} python -V'.format(runtime))


def agnostic_python_version(env_func=prod_env):
    """this will print whichever version is on the remote server"""

    env_func()  # calls any environment function you set
    python_version(caller=env.caller)
    """
    in this case we can use caller=env.* as
    default parameter, because we explicitly set by calling env_func
    """
$ fab local_env python_version
[localhost] local: source activate py36&& python -V
Python 3.6.3 :: Anaconda, Inc.

# now let's hack into nasa

$ fab agnostic_python_version
[herver.local] Executing task 'agnostic_python_version'
[herver.local] run:  python -V
[herver.local] out: Python 2.7.13
[herver.local] out:


Done.
Disconnecting from nasa.org... done.