Fixtures for ansible, testinfra and molecule


License
Apache-2.0
Install
pip install testaid==0.15.1

Documentation

testaid fixtures for ansible, molecule and testinfra

About

With the pytest plugin testinfra you can write unit tests in python to test your servers configured by the management tool ansible. testinfra is the default verifier of the molecule testing environment.

The pytest plugin testaid provides helper functions and fixtures to facilitate the use of testinfra. It helps to not only unit test your ansible roles but to integration and system test your whole ansible project.

testinfra wraps cli calls to the ansible executable. testaid uses the ansible python api to run ansible playbooks.

Outline

License

Apache License 2.0

Quick start

Install the testaid plugin using pip:

$ pip install testaid

Tests

Run unit tests (pytest) and system tests (molecule test) by invoking tox:

$ tox

Example

Have a look at debian system test directory for an example of a molecule project using ansible, testinfra and testaid. The molecule project doubles as as a system test (golden master) for the testaid plugin.

Boilerplate

As a boilerplate for testinfra tests it is enough to do:

import testaid

testinfra_hosts = testaid.hosts()

Fixture testpass

You can access gopass secrets by using the testpass fixture:

def test_mytest(host, testpass):

    my_password = testpass('my_project/my_password')

Fixture testvars

Arguably the most useful feature of the testaid plugin is the testvars fixture. The fixture resolves and exposes ansible variables as a python dict:

def test_mytest(host, testvars):

    my_variable = testvars['my_variable']

testvars runs a playbook against the molecule host using the ansible python api.

testvars creates a symbolic link to the roles directory of your ansible project in the ephemeral playbook environment which molecule sets up. It then runs a playbook with gather_facts:true and a debug task to get the ansible variables and the ansible facts of the play and host.

testvars uses the ansible VariableManager so the usual ansible variable precedence rules apply. Internally, the fixture uses the ansible debug module to resolve templates. Thus, it can resolve any kind of template that the debug module can resolve including jinja2 code and calls to lookup plugins.

extra vars

The TESTVARS_EXTRA_VARS environment variable can be set in molecule.yml. It can contain dirpaths or filepaths relative to the MOLECULE_SCENARIO_DIRECTORY separated by colons:

verifier:
  name: testinfra
  env:
    TESTVARS_EXTRA_VARS: "../../vars:../../extra_vars/extra_vars.yml"

The vars files will be included in moleculebook playbooks by adding the paths to vars_files (and not by adding include_vars tasks).

roles

Which roles are included is determined in this order:

  • List of roles separated by colon specified in the TESTVARS_ROLES_WHITELIST environment variable
  • List of roles specified in playbook speciied in molecule.yml
  • List of roles specified in default playbook playbook.yml
  • All roles in roles directory in project directory

Roles blacklisted in TESTVARS_ROLES_BLACKLIST won't be included.

options

testvars is a session scope fixture so its configuration is done in molecule.yml by using pytest command line options. You can add a couple of options in the options dictionary of the verifier section:

verifier:
  name: testinfra
  options:
    testvars-no-gather-facts: true

By default, testvars runs a playbook to gather ansible variables and facts. It then runs a playbook to resolve the variables.

You can change the default behaviour with these options:

  • testvars-no-gather-facts
    Run playbook to gather variables with gather_facts: false. You won't be able to access ansible_facts but your tests will run faster.
  • testvars-no-gather-molecule
    Do not resolve molecule variables. You probably won't need these variables but it won't take much time to resolve them, either.
  • testvars-no-extra-vars
    Do not add extra variables specified in TESTVARS_EXTRA_VARS. Ignores the environment variable.

caching

Hopefully the testvars fixture allows fast test-driven development. It has session scope so variables are collected and resolved only once per testrun as pytest caches the result. If this is still too slow for you then you can enable the pytest cache plugin in molecule.yml:

verifier:
  name: testinfra
  options:
    p: cacheprovider

You should use the testaid boilerplate code to be able to run pytest directly. Otherwise testinfra will complain about missing environment variables.

Remember to clear the cache when you add or change an ansible variable:

pytest --cache-clear; molecule verify

The cache will use the molecule ephemeral directory as the cache key which is unique for each molecule instance. When using the boilerplate you can inspect the cache by running:

pytest --cache-show

Ansible Python API

The testaid plugin provides four main pytest fixtures (and a couple of command line, environment variables and helper fixtures):

  • testpass - exposes the ansible passwordstore plugin
  • testvars - resolves and exposes ansible vars and facts
  • moleculebook - api to run playbooks against a molecule host
  • moleculeplay - api to leverage the ansible python api

The testvars and testpass fixtures use the moleculebook fixture which in turn uses the moleculeplay fixture. moleculeplay makes low-level calls to the ansible python api and uses the moleculeenv fixture to handle the sysadmin tasks of setting the right symlinks. moleculeplay and moleculeenv will probably not be very useful on their own but moleculebook might be handy in those situations where you know you shouldn't implement a hackaround. ;-)

Here is how you could run an ansible playbook programmatically from a test (or even better: from a fixture) using dependency injection.

def test_testaid_moleculebook(host, moleculebook):
    playbook = moleculebook.get()
    args = dict(path='/tmp/moleculebook_did_this', state='touch')
    task_touch = dict(action=dict(module='file', args=args))
    playbook['tasks'].append(task_touch)
    moleculebook.set(playbook)
    moleculebook.run()
    assert host.file('/tmp/moleculebook_did_this').exists