Wednesday, September 6, 2017

Testing Ansible roles with Molecule - tutorial

Molecule provides a clean way of testing Ansible roles which you write. Molecule can spin a docker container and run your Ansible role on it and report back the test results. In this blog I'm going to demonstrate a simple use case of Molecule to test a simple Ansible role on a Docker container.

Prerequisites

Install the following on your workstation. I'm using Mac and the below installation commands represent Mac
  • Docker 
    • brew install docker
    • Start docker service on Mac
  • Ansible
    • pip install ansible
  • Molecule
    • pip install molecule

Sample code

The sample code used for this tutorial are hosted on GitHub at https://github.com/siddeshbg/molecule_tutorial

Here are the files involved
  1. molecule.yml
  2. playbook.yml
  3. roles/base/tasks/main.yml
  4. tests/test_default.py

How it works?

The unit test cases are written using Python based TestInfra. You don't need to install it separately, since molecule package include this.

Our goal is to test an Ansible role. In this example we want to test the role ...

cat roles/base/tasks/main.yml
---
- name: Install Aptitude
apt:
name: aptitude
state: present
 This is a simple role where, we want to install the package "aptitude" on the desired machine.

Now the role is ready and we want to use molecule to test it. Molecule can launch a Vagrant machine, or Docker machine or AWS machine or something else to test it. But in this example we will configure molecule to launch Docker.

We can init molecule by calling "molecule init --provider docker", which creates basic configuration files. I have hosted selectively few files on GitHub, which are generated by this command.

The "molecule.yml" is the main configuration file. In the "molecule.yml" file we insist Molecule to launch Docker

cat molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
docker:
containers:
- name: ansible-siddesh
image: ubuntu
image_version: latest
privileged: true
verifier:
name: testinfra
Here we insist molecule to use Docker as driver, use Docker image "ubuntu:latest" and create container out of this image by the name "ansible-siddesh". Also we are using the default verifier Testinfra.

The "playbook.yml" file is the default Ansible playbook, molecule will look for. In case if we have named our playbook differently, then we can specify that in the "molecule.yml" file as
---
ansible:
  playbook: myplaybook.yml
Let's look the content of our "playbook.yml"

cat playbook.yml
---
- hosts: all
roles:
- role: base
This is a simple playbook, which basically calls the role "base", we want to test.

Next we need to write our Testinfra based test case

cat tests/test_default.py
import testinfra.utils.ansible_runner

testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
'.molecule/ansible_inventory').get_hosts('all')

def test_hosts_file(File):
    f = File('/etc/hosts')

    assert f.exists
    assert f.user == 'root'
    assert f.group == 'root'

def test_packages(Package):
    assert Package("aptitude").is_installed
We have defined 2 tests here. One is to check whether "/etc/hosts" file exists and owned by root and another test is to ensure that aptitude package is installed.

Now it's time to run the tests with molecule, by running
$ molecule test
--> Destroying instances...
--> Checking playbook's syntax...
playbook: playbook.yml
--> Creating instances...
--> Creating Ansible compatible image of ubuntu:latest ...
Creating container ansible-siddesh with base image ubuntu:latest...
Container created.
--> Starting Ansible Run...
PLAY [all] *********************************************************************
TASK [Gathering Facts] *********************************************************
ok: [ansible-siddesh]
TASK [base : Install Aptitude] *************************************************
The following additional packages will be installed:
 0 upgraded, 38 newly installed, 0 to remove and 26 not upgraded.
changed: [ansible-siddesh]
PLAY RECAP *********************************************************************
ansible-siddesh            : ok=2    changed=1    unreachable=0    failed=0

--> Idempotence test in progress (can take a few minutes)...
--> Starting Ansible Run...
Idempotence test passed.
--> Executing ansible-lint...
--> Executing flake8 on *.py files found in tests/...
--> Executing testinfra tests found in tests/...
============================= test session starts ==============================
platform darwin -- Python 2.7.13, pytest-3.1.3, py-1.4.34, pluggy-0.4.0
rootdir: /Users/siddesh.gurusiddappa/work/github/molecule_tutorial, inifile:
plugins: testinfra-1.5.5
collected 2 itemss

tests/test_default.py ..
=============================== warnings summary ===============================
None
  Module already imported so can not be re-written: testinfra
-- Docs: http://doc.pytest.org/en/latest/warnings.html
===================== 2 passed, 1 warnings in 0.66 seconds =====================
--> Destroying instances...
Stopping container ansible-siddesh...
Removed container ansible-siddesh.

The command "molecule test" has done lot many things

  • It checked the playbook syntax
  • It created the docker container using the image "molecule_local/ubuntu:latest"
    • Alternatively if you just want the docker instances to be created without doing anything else, you can run the command "molecule create"
  • It ran our Ansible role
    • Alternatively you can run "molecule converge", which will create docker container and run Ansible role on it.
  • Next it tests whether role is idempotent (repeated calling will produce same result)
  • Next it runs our tests. As you can see, both of our tests passed.
    • Alternatively you can run "molecule verify" after running "molecule converge" to run tests
  • Next it destroys the container 
    • You can run "molecule destroy"
I like to use molecule while developing Ansible roles. Basically it provides me Docker container to test my role. I start by writing a role and then run "molecule converge", which creates a docker container with the role executed and then I login to container using "docker exec -it container-id /bin/bash" and validate my role execution manually.