Qoyyuum
2 supporters
FabFile Tutorial

FabFile Tutorial

Feb 26, 2024

Originally, this was written and published to the ReadTheDocs site but for additional visibility, I've also decided to copy and paste it here. Source code and raw markdown files are also available on my Github repo: https://github.com/Qoyyuum/fabfile-tutorial

How to Fabric

This is but a small side tutorial on how to use fabric 3.0.0 in Python 3.

As always, please read the official docs.

Getting Started

Pre-requisites

In order to fully understand how useful and powerful Fabric is, build a VPS (or a Linux VM) and set up a user account with credentials.

We can quickly spin one up with Vagrant (install Vagrant if you havn't already) on our local machine. For this tutorial, we will be using VirtualBox.

Get a VM

We can use any Vagrant file. We just need a dummy VM to play around. The following terminal command will initialise a Vagrantfile with setup and installed for a simple Postgresql DB VM.

# Init a Vagrantfile template
$ vagrant init benfante/pgbox --box-version 1.0.0

# Download and start the VM
$ vagrant up

Once its up, get the vagrant ssh config into a file to ssh with.

$ vagrant ssh-config > my-vagrant-ssh-config

# Contents of my-vagrant-ssh-config
$ cat my-vagrant-ssh-config

Host default
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile C:/Users/yourusername/.vagrant.d/boxes/wagtail-VAGRANTSLASH-buster64/1.1.0/virtualbox/vagrant_private_key
  IdentitiesOnly yes
  LogLevel FATAL

NB: If this is done on Windows, writing the vagrant ssh-config to a file will set its encoding to UTF-16 LE or UTF-8 with BOM. If there's mingw or cygwin or equivalent on the Windows machine, run dos2unix my-vagrant-ssh-config to change it to UTF-8. Or copy and paste the content into a new file with the UTF-8 encoding works too.

Then test ssh with it to the VM (assuming its name is default as per the ssh config file)

$ ssh -F my-vagrant-ssh-config default

Or alternatively, set up a quick VPS on DigitalOcean. If you don't have a DigitalOcean account yet, sign up here and get $200 credit to play around with. Cheapest VPS is $4 per month.

Using Fabric

As usual, install fabric (at time of this writing, version 3.0.0) to your local machine. Highly recommended to set it up in a virtual environment. I personally prefer pipenv.

# Enter pipenv shell
$ python -m pipenv shell

# Install fabric after pipenv is installed and activated
$ pip install fabric

# Verify with `pip list`
$ pip list

# Or by test importing fabric in Python IDLE/REPL
Python 3.10.5 (tags/v3.10.5:f377153, Jun  6 2022, 16:14:13) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import fabric
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'fabric'] # We can see 'fabric'

Installing fabric also provides us the fab cli. Typing fab --help in a terminal will show us what options are available to us:

$ fab --help
Usage: fab [--core-opts] task1 [--task1-opts] ... taskN [--taskN-opts]

Core options:

  --complete                         Print tab-completion candidates for given parse remainder.
  --hide=STRING                      Set default value of run()'s 'hide' kwarg.
  --no-dedupe                        Disable task deduplication.
  --print-completion-script=STRING   Print the tab-completion script for your preferred shell (bash|zsh|fish).
  --prompt-for-login-password        Request an upfront SSH-auth password prompt.
  --prompt-for-passphrase            Request an upfront SSH key passphrase prompt.
  --prompt-for-sudo-password         Prompt user at start of session for the sudo.password config value.
  --write-pyc                        Enable creation of .pyc files.
  -c STRING, --collection=STRING     Specify collection name to load.
  -d, --debug                        Enable debug output.
  -D INT, --list-depth=INT           When listing tasks, only show the first INT levels.
  -e, --echo                         Echo executed commands before running.
  -f STRING, --config=STRING         Runtime configuration file to use.
  -F STRING, --list-format=STRING    Change the display format used when listing tasks. Should be one of: flat (default), nested, json.
  -h [STRING], --help[=STRING]       Show core or per-task help and exit.
  -H STRING, --hosts=STRING          Comma-separated host name(s) to execute tasks against.
  -i, --identity                     Path to runtime SSH identity (key) file. May be given multiple times.
  -l [STRING], --list[=STRING]       List available tasks, optionally limited to a namespace.
  -p, --pty                          Use a pty when executing shell commands.
  -r STRING, --search-root=STRING    Change root directory used for finding task modules.
  -R, --dry                          Echo commands instead of running.
  -S STRING, --ssh-config=STRING     Path to runtime SSH config file.
  -t INT, --connect-timeout=INT      Specifies default connection timeout, in seconds.
  -T INT, --command-timeout=INT      Specify a global command execution timeout, in seconds.
  -V, --version                      Show version and exit.
  -w, --warn-only                    Warn, instead of failing, when shell commands fail.

The fabfile

Fabric works by storing the logic and tasks in a specific file, aptly named, fabfile.py. A quick simple example of setting up a task in fabfile.py:

from fabric import task

@task
def getuname(context):
    "Get server's uname"
    context.run('uname -a')

The above script creates a very simple task of running uname -a in the server. The fab cli will pick this up as one of the available task in a list.

$ fab --list

Available tasks:

  getuname   Get server's uname

If we are doing this via the Vagrant route, we can run this task and supply the saved ssh-config to it like so:

$ fab -H default -S my-vagrant-ssh-config getuname

Linux buster 4.19.0-8-amd64 #1 SMP Debian 4.19.98-1 (2020-01-26) x86_64 GNU/Linux

Likewise if we did it with an actual VPS, we have to supply our ssh config to it. By default, fab will use our actual ~/.ssh/config if it exists and matches on the host name. Assuming that the VPS is tied to a username and is authenticated with a private key and these details are already in the ssh config, then the following command will work.

$ fab -H <VPS IP or Hostname> getuname

If the private key has a passphrase, add the --prompt-for-passphrase to the fab command.

$ fab -H <VPS IP or Hostname> --prompt-for-passphrase getuname

If no keys and uses a standard login password (albeit definitely not a recommended setup), add the --prompt-for-login-password to the fab command.

$ fab -H <VPS IP or Hostname> --prompt-for-login-password getuname

The option -H is short for --hosts and for each host should also be included in the ssh-config for easily identifying which needs authentication. Example:

$ fab -H app1,app2,db1,db2,git,redis,log getuname

The above command will execute the task getuname to all of those hosts from app1 to log, assuming those are valid hosts to ssh into and exists in the ssh-config file. Equivalently, we could store this in a bash or powershell script so we don't have to rerun the task to which hosts each time.

Assuming I have a file mycustomfab_script.ps1, it would contain the following:

fab -H app1 readerrorlog
fab -H db1 searchemptycolumns

Automation Examples

Here are some examples and ideas of what you can do to automate your system administration tasks.

1. Find Missing Data and Update in Database

Using a real world working example that I was working on, received a ticket that the system had missing data. After formulating what the SQL statement is, I could easily VPN, ssh to the DB and run that SQL script. Or, I could just VPN and run the fab task to search, filter, list, and update if I wanted to.

Example fabfile.py:

from fabric import task

@task
def searchmissingdata(context):
  "Find and list missing data"
  sql = "select * from some_table where column is null;"
  psql = f"psql -d app_core -c '{sql}'"
  context.run(psql)

@task
def updateonemissingdata(context):
  "Update One Missing Data"
  row_id = input("What is the row_id to update?\n")
  new_value = input(f"What's the new value to update in {row_id}?\n")
  sql = f"update from some_table set column = '{new_value}' where row_id = {row_id};"
  psql = f"psql -d app_core -c '{sql}'"
  context.run(psql)

Then in fab command:

> fab -H db1 searchmissingdata

Do some cross reference with a different system (sometimes its in Excel because backups), gets the value and use that to update the missing data.

> fab -H db1 updateonemissingdata

And when prompted for the inputs for each row and what value to update, it handles it for us.

2. Find Errors in Logs

Often times, we would find odd errors not accounted for in system design or something unexpected happen in one service that the system started malfunctioning. Service checks are already in place but they can only give us an indication or alert of the malfunction but not enough to help diagnose or fix the problem faster before it gets worse. So the workflow usually goes: ssh to app server -> open different log files -> find error message -> traceback to root cause -> fix or remove problematic cause -> restart service? -> done.

So a fabfile might look like:

from fabric import task

@task
def checkapplog(context):
  "Checks app log for ERROR"
  app_log_command = "grep ERROR /var/log/messages"
  context.sudo(app_log_command)

Notice that this uses sudo as part of the context connection. So in a fab cli, this should look like:

> fab -H app1 --prompt-for-sudo-password checkapplog

Enjoy this post?

Buy Qoyyuum a coffee

More from Qoyyuum