Ansible Roles for Python Developers

I have been using ansible for managing my development configuration for a few months now, and I’m finally getting around to releasing two of the roles I’ve created in a form (possibly) useful to other folks.

Why Ansible?

Ansible is an tool for automating system tasks like deploying packages and configuring servers. In my case, I use it to set up cloud servers and vagrant VMs with the packages and tools I need to be productive at work. The ansible input files I have let me use almost any version of an Ubuntu base image on a cloud provider to launch a disposable development environment quickly and reliably. I no longer forget to copy my editor’s configuration files over, and I don’t have to remember which packages contain the tools I regularly want to use. I can also use the ansible input files as documentation when I can’t remember how I set something up and I want to explain it to someone else.

Installing Ansible

Ansible is usually run on a local machine to change the configuration of a remote machine. I configure ansible in a virtualenv on my desktop machine, and then use it to control VMs created locally with Vagrant or in a remote OpenStack cloud.

$ virtualenv ansible
$ source ansible/bin/activate
$ pip install ansible

Configuring Ansible

Ansible needs a configuration file to tell it where to find the server definitions. You can also use this file to control various search paths and other behaviors. I use the default naming, so I have an ansible.cfg in my working directory. It contains:

[defaults]
host_key_checking = False
roles_path = roles
hostfile = hosts

Ansible manages the definition of “inventory” of available servers separately from the “roles” those servers have. This allows you to re-purpose servers or automatically create cloud instances and then deploy software to them without knowing their IPs in advance.

The roles_path variable in the configuration file is a search path for roles that might be installed globally or in a shared directory. In order to install the roles under the current directory, I specify a relative path to a ./roles directory.

The hostfile variable in the configuration tells ansible where to find your servers. Advanced users can group multiple servers that should have the same configuration, use wildcards to apply changes to servers with similar names, etc. For this example I’m just working with a single Vagrant VM, so my hosts file contains:

[vagrant]
192.168.30.102

[vagrant:vars]
ansible_ssh_user=vagrant
ansible_ssh_private_key_file=/Users/dhellmann/.vagrant.d/insecure_private_key

The first stanza defines a group of servers called “vagrant” with a single member, identified by its IP address. The second stanza sets some variables to tell ansible how to ssh into the server. The user in the image in the VM does not match my user, so I need to specify a name, and then I tell ansible to use vagrant’s ssh key file when connecting as that user.

Installing Roles

I have two public roles I use for configuring my Python settings. The python-dev role installs several versions of the Python interpreter, including PyPy. The devpi role installs a devpi server and configures pip to use it so repeated installations of the same packages go more quickly.

You can check out the roles from github, or use ansible-galaxy to install them:

$ mkdir roles
$ ansible-galaxy install dhellmann.python-dev
- downloading role 'python-dev', owned by dhellmann
- downloading role from https://github.com/dhellmann/ansible-python-dev/archive/1.0.0.tar.gz
- extracting dhellmann.python-dev to roles/dhellmann.python-dev
- dhellmann.python-dev was installed successfully
$ ansible-galaxy install dhellmann.devpi
- downloading role 'devpi', owned by dhellmann
- downloading role from https://github.com/dhellmann/ansible-devpi/archive/1.0.0.tar.gz
- extracting dhellmann.devpi to roles/dhellmann.devpi
- dhellmann.devpi was installed successfully

Applying the Roles

Ansible’s top-level input file is called a “playbook”. The playbook is where you link the inventory to the roles, and provide any extra configuration settings based on your customization needs.

The playbook to deploy both of the python developer roles to my Vagrant VM is in a file called ansible.yml that contains:

---
- hosts: vagrant
  roles:
    - dhellmann.python-dev
    - dhellmann.devpi

The hosts specifier tells ansible to use the “vagrant” group of servers and the roles list contains the names of the roles.

Running Ansible

Assuming the Vagrant VM is running, I can tell ansible to run the playbook with:

$ ansible-playbook ./ansible.yml

PLAY [vagrant] ****************************************************************

GATHERING FACTS ***************************************************************
ok: [192.168.30.102]

TASK: [dhellmann.python-dev | Install tools for adding PPA repositories] ******
changed: [192.168.30.102] => (item=python-software-properties,software-properties-common)

TASK: [dhellmann.python-dev | Install Python dependencies] ********************
changed: [192.168.30.102] => (item=libffi-dev,gcc)

TASK: [dhellmann.python-dev | Add Python 2.6 repository] **********************
changed: [192.168.30.102]

TASK: [dhellmann.python-dev | Remove system package for pip] ******************
ok: [192.168.30.102]

TASK: [dhellmann.python-dev | Download ez_setup.py] ***************************
changed: [192.168.30.102]

TASK: [dhellmann.python-dev | Download get-pip.py] ****************************
changed: [192.168.30.102]

TASK: [dhellmann.python-dev | Install python] *********************************
changed: [192.168.30.102] => (item={'version': '2.6'})
changed: [192.168.30.102] => (item={'version': '3.3'})
changed: [192.168.30.102] => (item={'version': '3.4'})
changed: [192.168.30.102] => (item={'version': '2.7'})

TASK: [dhellmann.python-dev | Install python-dev] *****************************
changed: [192.168.30.102] => (item={'version': '2.6'})
changed: [192.168.30.102] => (item={'version': '3.3'})
changed: [192.168.30.102] => (item={'version': '3.4'})
ok: [192.168.30.102] => (item={'version': '2.7'})

TASK: [dhellmann.python-dev | Install easy_install from source] ***************
changed: [192.168.30.102] => (item={'version': '2.6'})
changed: [192.168.30.102] => (item={'version': '3.3'})
changed: [192.168.30.102] => (item={'version': '3.4'})
changed: [192.168.30.102] => (item={'version': '2.7'})

TASK: [dhellmann.python-dev | Install pip from source] ************************
changed: [192.168.30.102] => (item={'version': '2.6'})
changed: [192.168.30.102] => (item={'version': '3.3'})
changed: [192.168.30.102] => (item={'version': '3.4'})
changed: [192.168.30.102] => (item={'version': '2.7'})

TASK: [dhellmann.python-dev | Install wheel] **********************************
changed: [192.168.30.102] => (item={'version': '2.6'})
changed: [192.168.30.102] => (item={'version': '3.3'})
changed: [192.168.30.102] => (item={'version': '3.4'})
changed: [192.168.30.102] => (item={'version': '2.7'})

TASK: [dhellmann.python-dev | Add PyPy PPA repository] ************************
changed: [192.168.30.102]

TASK: [dhellmann.python-dev | Install PyPy] ***********************************
changed: [192.168.30.102] => (item=pypy,pypy-dev,python-cffi)

TASK: [dhellmann.python-dev | Install Python tools] ***************************
changed: [192.168.30.102] => (item=tox)
ok: [192.168.30.102] => (item=virtualenv)
changed: [192.168.30.102] => (item=virtualenvwrapper)
ok: [192.168.30.102] => (item=wheel)

TASK: [dhellmann.devpi | Create devpi virtualenv] *****************************
changed: [192.168.30.102]

TASK: [dhellmann.devpi | Install packages] ************************************
changed: [192.168.30.102] => (item=devpi)
changed: [192.168.30.102] => (item=wheel)

TASK: [dhellmann.devpi | Install monit] ***************************************
changed: [192.168.30.102]

TASK: [dhellmann.devpi | Make sure monit is running] **************************
ok: [192.168.30.102]

TASK: [dhellmann.devpi | Configure monit to run devpi] ************************
changed: [192.168.30.102]

TASK: [dhellmann.devpi | Create ~/.pip/wheelhouse] ****************************
changed: [192.168.30.102]

TASK: [dhellmann.devpi | ~/.pip.conf] *****************************************
changed: [192.168.30.102]

NOTIFIED: [dhellmann.devpi | restart monit] ***********************************
changed: [192.168.30.102]

PLAY RECAP ********************************************************************
192.168.30.102             : ok=23   changed=20   unreachable=0    failed=0

Then I can login to the VM and build some wheels:

$ vagrant ssh
Welcome to Ubuntu 12.04 LTS (GNU/Linux 3.2.0-24-generic x86_64)

 * Documentation:  https://help.ubuntu.com/

  System information as of Sat Mar  7 16:46:39 EST 2015

  System load:  0.56              Users logged in:     0
  Usage of /:   25.6% of 6.98GB   IP address for eth0: 10.0.2.15
  Memory usage: 36%               IP address for eth1: 192.168.27.102
  Swap usage:   0%                IP address for eth2: 192.168.30.102
  Processes:    71

  Graph this data and manage this system at https://landscape.canonical.com/

Last login: Sat Mar  7 16:43:39 2015 from 192.168.30.1
vagrant@hubert-linuxvm:~$ pip wheel ansible
Collecting ansible
  Downloading http://localhost:3141/root/pypi/+f/e0b/bf484e9fa49cd/ansible-1.8.4.tar.gz (758kB)
    100% |################################| 761kB 13.8MB/s
Collecting paramiko (from ansible)
  Downloading http://localhost:3141/root/pypi/+f/ea8/ad8f18a432797/paramiko-1.15.2-py2.py3-none-any.whl (165kB)
    100% |################################| 167kB 11.2MB/s
  Saved ./.pip/wheelhouse/paramiko-1.15.2-py2.py3-none-any.whl
Collecting jinja2 (from ansible)
  Downloading http://localhost:3141/root/pypi/+f/b9d/ffd2f3b43d673/Jinja2-2.7.3.tar.gz (378kB)
    100% |################################| 380kB 17.0MB/s
Collecting PyYAML (from ansible)
  Downloading http://localhost:3141/root/pypi/+f/89c/bc92cda979042/PyYAML-3.11.zip (371kB)
    100% |################################| 372kB 16.3MB/s
Collecting setuptools (from ansible)
  Downloading http://localhost:3141/root/pypi/+f/caa/7e1c8b83f4134/setuptools-14.0-py2.py3-none-any.whl (501kB)
    100% |################################| 503kB 11.9MB/s
  Saved ./.pip/wheelhouse/setuptools-14.0-py2.py3-none-any.whl
Collecting pycrypto>=2.6 (from ansible)
  Downloading http://localhost:3141/root/pypi/+f/55a/61a054aa66812/pycrypto-2.6.1.tar.gz (446kB)
    100% |################################| 446kB 10.9MB/s
Collecting ecdsa>=0.11 (from paramiko->ansible)
  Downloading http://localhost:3141/root/pypi/+f/a32/5d50195d04959/ecdsa-0.13-py2.py3-none-any.whl (86kB)
    100% |################################| 90kB 11.1MB/s
  Saved ./.pip/wheelhouse/ecdsa-0.13-py2.py3-none-any.whl
Collecting markupsafe (from jinja2->ansible)
  Downloading http://localhost:3141/root/pypi/+f/f5a/b3deee4c37cd6/MarkupSafe-0.23.tar.gz
Skipping paramiko, due to already being wheel.
Skipping setuptools, due to already being wheel.
Skipping ecdsa, due to already being wheel.
Building wheels for collected packages: ansible, jinja2, PyYAML, pycrypto, markupsafe
  Running setup.py bdist_wheel for ansible
  Destination directory: /home/vagrant/.pip/wheelhouse
  Running setup.py bdist_wheel for jinja2
  Destination directory: /home/vagrant/.pip/wheelhouse
  Running setup.py bdist_wheel for PyYAML
  Destination directory: /home/vagrant/.pip/wheelhouse
  Running setup.py bdist_wheel for pycrypto
  Destination directory: /home/vagrant/.pip/wheelhouse
  Running setup.py bdist_wheel for markupsafe
  Destination directory: /home/vagrant/.pip/wheelhouse
Successfully built ansible jinja2 PyYAML pycrypto markupsafe

And now the wheels are available in the local wheelhouse created by the devpi role:

vagrant@hubert-linuxvm:~$ source /usr/local/bin/virtualenvwrapper.sh
vagrant@hubert-linuxvm:~$ mkvirtualenv ansible
New python executable in ansible/bin/python2.7
Also creating executable in ansible/bin/python
Installing setuptools, pip...done.
(ansible)vagrant@hubert-linuxvm:~$ pip install ansible
Collecting ansible
Collecting paramiko (from ansible)
Requirement already satisfied (use --upgrade to upgrade): setuptools in ./.virtualenvs/ansible/lib/python2.7/site-packages (from ansible)
Collecting PyYAML (from ansible)
Collecting pycrypto>=2.6 (from ansible)
Collecting jinja2 (from ansible)
Collecting ecdsa>=0.11 (from paramiko->ansible)
Collecting markupsafe (from jinja2->ansible)
Installing collected packages: markupsafe, ecdsa, jinja2, pycrypto, PyYAML, paramiko, ansible
Successfully installed PyYAML-3.11 ansible-1.8.4 ecdsa-0.13 jinja2-2.7.3 markupsafe-0.23 paramiko-1.15.2 pycrypto-2.6.1

Passing Variables to Roles

The python-dev role supports a variable to specify which Python versions you want installed. By default it includes 2.6, 2.7, 3.3, and 3.4 because those are the versions where I commonly want to run tests for various packages I maintain. If you wanted only 2.7 and 3.4, you could change the playbook to override that list of versions by adding the python_dev_versions variable to the role specification:

---
- hosts: vagrant
  roles:
    - role: dhellmann.python-dev
      python_dev_versions:
        - version: "3.4"
        - version: "2.7"
    - dhellmann.devpi

Running ansible again will show no updates needed, but only the two versions listed in the playbook are mentioned.

$ ansible-playbook ./ansible.yml

PLAY [vagrant] ****************************************************************

GATHERING FACTS ***************************************************************
ok: [192.168.30.102]

TASK: [dhellmann.python-dev | Install tools for adding PPA repositories] ******
ok: [192.168.30.102] => (item=python-software-properties,software-properties-common)

TASK: [dhellmann.python-dev | Install Python dependencies] ********************
ok: [192.168.30.102] => (item=libffi-dev,gcc)

TASK: [dhellmann.python-dev | Add Python 2.6 repository] **********************
ok: [192.168.30.102]

TASK: [dhellmann.python-dev | Remove system package for pip] ******************
ok: [192.168.30.102]

TASK: [dhellmann.python-dev | Download ez_setup.py] ***************************
ok: [192.168.30.102]

TASK: [dhellmann.python-dev | Download get-pip.py] ****************************
ok: [192.168.30.102]

TASK: [dhellmann.python-dev | Install python] *********************************
ok: [192.168.30.102] => (item={'version': '3.4'})
ok: [192.168.30.102] => (item={'version': '2.7'})

TASK: [dhellmann.python-dev | Install python-dev] *****************************
ok: [192.168.30.102] => (item={'version': '3.4'})
ok: [192.168.30.102] => (item={'version': '2.7'})

TASK: [dhellmann.python-dev | Install easy_install from source] ***************
ok: [192.168.30.102] => (item={'version': '3.4'})
ok: [192.168.30.102] => (item={'version': '2.7'})

TASK: [dhellmann.python-dev | Install pip from source] ************************
ok: [192.168.30.102] => (item={'version': '3.4'})
ok: [192.168.30.102] => (item={'version': '2.7'})

TASK: [dhellmann.python-dev | Install wheel] **********************************
ok: [192.168.30.102] => (item={'version': '3.4'})
ok: [192.168.30.102] => (item={'version': '2.7'})

TASK: [dhellmann.python-dev | Add PyPy PPA repository] ************************
ok: [192.168.30.102]

TASK: [dhellmann.python-dev | Install PyPy] ***********************************
ok: [192.168.30.102] => (item=pypy,pypy-dev,python-cffi)

TASK: [dhellmann.python-dev | Install Python tools] ***************************
ok: [192.168.30.102] => (item=tox)
ok: [192.168.30.102] => (item=virtualenv)
ok: [192.168.30.102] => (item=virtualenvwrapper)
ok: [192.168.30.102] => (item=wheel)

TASK: [dhellmann.devpi | Create devpi virtualenv] *****************************
ok: [192.168.30.102]

TASK: [dhellmann.devpi | Install packages] ************************************
ok: [192.168.30.102] => (item=devpi)
ok: [192.168.30.102] => (item=wheel)

TASK: [dhellmann.devpi | Install monit] ***************************************
ok: [192.168.30.102]

TASK: [dhellmann.devpi | Make sure monit is running] **************************
ok: [192.168.30.102]

TASK: [dhellmann.devpi | Configure monit to run devpi] ************************
ok: [192.168.30.102]

TASK: [dhellmann.devpi | Create ~/.pip/wheelhouse] ****************************
ok: [192.168.30.102]

TASK: [dhellmann.devpi | ~/.pip.conf] *****************************************
ok: [192.168.30.102]

PLAY RECAP ********************************************************************
192.168.30.102             : ok=22   changed=0    unreachable=0    failed=0

Future Work

This version of the python-dev role only works for Ubuntu, and I would like to make it work on Fedora as well (patches welcome). I may also add some of the system packages needed to pip install Python libraries like lxml, since those are frequently not installed by default.

The devpi role uses the meta-package devpi, which was apparently deprecated as part of the most recent release. I need to update that to install the server packages separately.