Skip to main content

Plugin for py.test to allow running ansible

Project description

pytest-ansible
==============

|Build Status| |Coverage Status| |Version| |Downloads| |License|
|Supported Python Versions|

This repository contains a plugin for ``py.test`` which adds several
fixtures for running ``ansible`` modules, or inspecting
``ansible_facts``. While one can simply call out to ``ansible`` using
the ``subprocess`` module, having to parse stdout to determine the
outcome of the operation is unpleasant and prone to error. With
``pytest-ansible``, modules return JSON data which you can inspect and
act on, much like with an ansible
`playbook <http://docs.ansible.com/playbooks.html>`__.

Installation
------------

Install this plugin using ``pip``

.. code:: bash

pip install pytest-ansible

Usage
-----

Once installed, the following ``py.test`` command-line parameters are
available:

.. code:: bash

py.test \
[--ansible-inventory <path_to_inventory>] \
[--ansible-host-pattern <host-pattern>] \
[--ansible-connection <plugin>] \
[--ansible-user <username>] \
[--ansible-sudo] \
[--ansible-sudo-user <username>]

The following fixtures are available:

Fixture ``ansible_module``
~~~~~~~~~~~~~~~~~~~~~~~~~~

The ``ansible_module`` fixture allows tests and fixtures to call ansible
`modules <http://docs.ansible.com/modules.html>`__.

A very basic example demonstrating the ansible ```ping``
module <http://docs.ansible.com/ping_module.html>`__:

.. code:: python

def test_ping(ansible_module):
ansible_module.ping()

The above example doesn't do any validation/inspection of the return
value. A more likely use case will involve inspecting the return value.
The ``ansible_module`` fixture returns a JSON data describing the
ansible module result. The format of the JSON data depends on the
``--ansible-inventory`` used, and the `ansible
module <http://docs.ansible.com/modules_by_category.html>`__.

The following example demonstrates inspecting the module result.

.. code:: python

def test_ping(ansible_module):
contacted = ansible_module.ping()
for (host, result) in contacted.items():
assert 'ping' in result, \
"Failure on host:%s" % host
assert result['ping'] == 'pong', \
"Unexpected ping response: %s" % result['ping']

A more involved example of updating the sshd configuration, and
restarting the service.

.. code:: python

def test_sshd_config(ansible_module):

# update sshd MaxSessions
contacted = ansible_module.lineinfile(
dest="/etc/ssh/sshd_config",
regexp="^#?MaxSessions .*",
line="MaxSessions 150")
)

# assert desired outcome
for (host, result) in contacted.items():
assert 'failed' not in result, result['msg']
assert 'changed' in result

# restart sshd
contacted = ansible_module.service(
name="sshd",
state="restarted"
)

# assert successful restart
for (host, result) in contacted.items():
assert 'changed' in result and result['changed']
assert result['name'] == 'sshd'

# do other stuff ...

Fixture ``ansible_facts``
~~~~~~~~~~~~~~~~~~~~~~~~~

The ``ansible_facts`` fixture returns a JSON structure representing the
system facts for the associated inventory. Sample fact data is available
in the `ansible
documentation <http://docs.ansible.com/playbooks_variables.html#information-discovered-from-systems-facts>`__.

Note, this fixture is provided for convenience and could easily be
called using ``ansible_module.setup()``.

A systems facts can be useful when deciding whether to skip a test ...

.. code:: python

def test_something_with_amazon_ec2(ansible_facts):
for (host, facts) in ansible_facts.items():
if 'ec2.internal' != facts['ansible_domain']:
pytest.skip("This test only applies to ec2 instances")

Alternatively, you could inspect ``ec2_facts`` for greater granularity
...

.. code:: python


def test_terminate_us_east_1_instances(ansible_module):

for (host, ec2_facts) in ansible_module.ec2_facts().items():
if ec2_facts['ansible_ec2_placement_region'].startswith('us-east'):
'''do some testing'''

Parameterizing with ``pytest.mark.ansible``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Perhaps the ``--ansible-inventory=<inventory>`` includes many systems,
but you only wish to interact with a subset. The ``pytest.mark.ansible``
marker can be used to modify the ``pytest-ansible`` command-line
parameters for a single test.

For example, to interact with the local system, you would adjust the
``host_pattern`` and ``connection`` parameters.

.. code:: python

@pytest.mark.ansible(host_pattern='local,', connection='local')
def test_copy_local(ansible_module):

# create a file with random data
contacted = ansible_module.copy(
dest='/etc/motd',
content='PyTest is amazing!',
owner='root',
group='root',
mode='0644',
)

# assert only a single host was contacted
assert len(contacted) == 1, \
"Unexpected number of hosts contacted (%d != %d)" % \
(1, len(contacted))

assert 'local' in contacted

# assert the copy module reported changes
assert 'changed' in contacted['local']
assert contacted['local']['changed']

Note, the parameters provided by ``pytest.mark.ansible`` will apply to
all class methods.

.. code:: python

@pytest.mark.ansible(host_pattern='local,', connection='local')
class Test_Local(object):
def test_install(self, ansible_module):
'''do some testing'''
def test_template(self, ansible_module):
'''do some testing'''
def test_service(self, ansible_module):
'''do some testing'''

Exception handling
~~~~~~~~~~~~~~~~~~

If ``ansible`` is unable to connect to any inventory, an exception will
be raised.

.. code:: python

@pytest.mark.ansible(inventory='unreachable.example.com,')
def test_shutdown(ansible_module):

# attempt to ping a host that is down (or doesn't exist)
pytest.raises(pytest_ansible.AnsibleHostUnreachable):
ansible_module.ping()

Sometimes, only a single host is unreachable, and others will have
properly returned data. The following demonstrates how to catch the
exception, and inspect the results.

.. code:: python

@pytest.mark.ansible(inventory='good:bad')
def test_inventory_unreachable(ansible_module):
exc_info = pytest.raises(pytest_ansible.AnsibleHostUnreachable, ansible_module.ping)
(contacted, dark) = exc_info.value.results

# inspect the JSON result...
for (host, result) in contacted.items():
assert result['ping'] == 'pong'

for (host, result) in dark.items():
assert result['failed'] == True

Release History
---------------

1.3.0 (pending)
~~~~~~~~~~~~~~~

- Add support for ansible-2.0

1.2.5 (2015-04-20)
~~~~~~~~~~~~~~~~~~

- Only validate --ansible-\* parameters when using pytest-ansible
fixture
- Include --ansible-user when running module

1.2.4 (2015-03-18)
~~~~~~~~~~~~~~~~~~

- Add ansible-1.9 privilege escalation support

1.2.3 (2015-03-03)
~~~~~~~~~~~~~~~~~~

- Resolve setuptools import failure by migrating from a module to a
package

1.2.2 (2015-03-03)
~~~~~~~~~~~~~~~~~~

- Removed py module dependency
- Add HISTORY.md

1.2.1 (2015-03-02)
~~~~~~~~~~~~~~~~~~

- Use pandoc to convert existing markdown into pypi friendly rst

1.2 (2015-03-02)
~~~~~~~~~~~~~~~~

- Add ``ansible_host`` and ``ansible_group`` parametrized fixture
- Add cls level fixtures for users needing scope=class fixtures
- Updated examples to match new fixture return value
- Alter fixture return data to more closely align with ansible
- Raise ``AnsibleHostUnreachable`` whenever hosts are ... unreachable
- Set contacted and dark hosts in ConnectionError

1.1 (2015-02-16)
~~~~~~~~~~~~~~~~

- Initial release

.. |Build Status| image:: https://img.shields.io/travis/jlaska/pytest-ansible.svg
:target: https://travis-ci.org/jlaska/pytest-ansible
.. |Coverage Status| image:: https://img.shields.io/coveralls/jlaska/pytest-ansible.svg
:target: https://coveralls.io/r/jlaska/pytest-ansible
.. |Version| image:: https://img.shields.io/pypi/v/pytest-ansible.svg
:target: https://pypi.python.org/pypi/pytest-ansible/
.. |Downloads| image:: https://img.shields.io/pypi/dm/pytest-ansible.svg
:target: https://pypi.python.org/pypi/pytest-ansible/
.. |License| image:: https://img.shields.io/pypi/l/pytest-ansible.svg
:target: https://pypi.python.org/pypi/pytest-ansible/
.. |Supported Python Versions| image:: https://img.shields.io/pypi/pyversions/pytest-ansible.svg
:target: https://pypi.python.org/pypi/pytest-ansible/
pytest-ansible
==============

|Build Status| |Coverage Status| |Version| |Downloads| |License|
|Supported Python Versions|

This repository contains a plugin for ``py.test`` which adds several
fixtures for running ``ansible`` modules, or inspecting
``ansible_facts``. While one can simply call out to ``ansible`` using
the ``subprocess`` module, having to parse stdout to determine the
outcome of the operation is unpleasant and prone to error. With
``pytest-ansible``, modules return JSON data which you can inspect and
act on, much like with an ansible
`playbook <http://docs.ansible.com/playbooks.html>`__.

Installation
------------

Install this plugin using ``pip``

.. code:: bash

pip install pytest-ansible

Usage
-----

Once installed, the following ``py.test`` command-line parameters are
available:

.. code:: bash

py.test \
[--ansible-inventory <path_to_inventory>] \
[--ansible-host-pattern <host-pattern>] \
[--ansible-connection <plugin>] \
[--ansible-user <username>] \
[--ansible-sudo] \
[--ansible-sudo-user <username>]

The following fixtures are available:

Fixture ``ansible_module``
~~~~~~~~~~~~~~~~~~~~~~~~~~

The ``ansible_module`` fixture allows tests and fixtures to call ansible
`modules <http://docs.ansible.com/modules.html>`__.

A very basic example demonstrating the ansible ```ping``
module <http://docs.ansible.com/ping_module.html>`__:

.. code:: python

def test_ping(ansible_module):
ansible_module.ping()

The above example doesn't do any validation/inspection of the return
value. A more likely use case will involve inspecting the return value.
The ``ansible_module`` fixture returns a JSON data describing the
ansible module result. The format of the JSON data depends on the
``--ansible-inventory`` used, and the `ansible
module <http://docs.ansible.com/modules_by_category.html>`__.

The following example demonstrates inspecting the module result.

.. code:: python

def test_ping(ansible_module):
contacted = ansible_module.ping()
for (host, result) in contacted.items():
assert 'ping' in result, \
"Failure on host:%s" % host
assert result['ping'] == 'pong', \
"Unexpected ping response: %s" % result['ping']

A more involved example of updating the sshd configuration, and
restarting the service.

.. code:: python

def test_sshd_config(ansible_module):

# update sshd MaxSessions
contacted = ansible_module.lineinfile(
dest="/etc/ssh/sshd_config",
regexp="^#?MaxSessions .*",
line="MaxSessions 150")
)

# assert desired outcome
for (host, result) in contacted.items():
assert 'failed' not in result, result['msg']
assert 'changed' in result

# restart sshd
contacted = ansible_module.service(
name="sshd",
state="restarted"
)

# assert successful restart
for (host, result) in contacted.items():
assert 'changed' in result and result['changed']
assert result['name'] == 'sshd'

# do other stuff ...

Fixture ``ansible_facts``
~~~~~~~~~~~~~~~~~~~~~~~~~

The ``ansible_facts`` fixture returns a JSON structure representing the
system facts for the associated inventory. Sample fact data is available
in the `ansible
documentation <http://docs.ansible.com/playbooks_variables.html#information-discovered-from-systems-facts>`__.

Note, this fixture is provided for convenience and could easily be
called using ``ansible_module.setup()``.

A systems facts can be useful when deciding whether to skip a test ...

.. code:: python

def test_something_with_amazon_ec2(ansible_facts):
for (host, facts) in ansible_facts.items():
if 'ec2.internal' != facts['ansible_domain']:
pytest.skip("This test only applies to ec2 instances")

Alternatively, you could inspect ``ec2_facts`` for greater granularity
...

.. code:: python


def test_terminate_us_east_1_instances(ansible_module):

for (host, ec2_facts) in ansible_module.ec2_facts().items():
if ec2_facts['ansible_ec2_placement_region'].startswith('us-east'):
'''do some testing'''

Parameterizing with ``pytest.mark.ansible``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Perhaps the ``--ansible-inventory=<inventory>`` includes many systems,
but you only wish to interact with a subset. The ``pytest.mark.ansible``
marker can be used to modify the ``pytest-ansible`` command-line
parameters for a single test.

For example, to interact with the local system, you would adjust the
``host_pattern`` and ``connection`` parameters.

.. code:: python

@pytest.mark.ansible(host_pattern='local,', connection='local')
def test_copy_local(ansible_module):

# create a file with random data
contacted = ansible_module.copy(
dest='/etc/motd',
content='PyTest is amazing!',
owner='root',
group='root',
mode='0644',
)

# assert only a single host was contacted
assert len(contacted) == 1, \
"Unexpected number of hosts contacted (%d != %d)" % \
(1, len(contacted))

assert 'local' in contacted

# assert the copy module reported changes
assert 'changed' in contacted['local']
assert contacted['local']['changed']

Note, the parameters provided by ``pytest.mark.ansible`` will apply to
all class methods.

.. code:: python

@pytest.mark.ansible(host_pattern='local,', connection='local')
class Test_Local(object):
def test_install(self, ansible_module):
'''do some testing'''
def test_template(self, ansible_module):
'''do some testing'''
def test_service(self, ansible_module):
'''do some testing'''

Exception handling
~~~~~~~~~~~~~~~~~~

If ``ansible`` is unable to connect to any inventory, an exception will
be raised.

.. code:: python

@pytest.mark.ansible(inventory='unreachable.example.com,')
def test_shutdown(ansible_module):

# attempt to ping a host that is down (or doesn't exist)
pytest.raises(pytest_ansible.AnsibleHostUnreachable):
ansible_module.ping()

Sometimes, only a single host is unreachable, and others will have
properly returned data. The following demonstrates how to catch the
exception, and inspect the results.

.. code:: python

@pytest.mark.ansible(inventory='good:bad')
def test_inventory_unreachable(ansible_module):
exc_info = pytest.raises(pytest_ansible.AnsibleHostUnreachable, ansible_module.ping)
(contacted, dark) = exc_info.value.results

# inspect the JSON result...
for (host, result) in contacted.items():
assert result['ping'] == 'pong'

for (host, result) in dark.items():
assert result['failed'] == True

Release History
---------------

1.3.0 (2016-01-20)
~~~~~~~~~~~~~~~~~~

- Add support for ansible-2.0

1.2.5 (2015-04-20)
~~~~~~~~~~~~~~~~~~

- Only validate --ansible-\* parameters when using pytest-ansible
fixture
- Include --ansible-user when running module

1.2.4 (2015-03-18)
~~~~~~~~~~~~~~~~~~

- Add ansible-1.9 privilege escalation support

1.2.3 (2015-03-03)
~~~~~~~~~~~~~~~~~~

- Resolve setuptools import failure by migrating from a module to a
package

1.2.2 (2015-03-03)
~~~~~~~~~~~~~~~~~~

- Removed py module dependency
- Add HISTORY.md

1.2.1 (2015-03-02)
~~~~~~~~~~~~~~~~~~

- Use pandoc to convert existing markdown into pypi friendly rst

1.2 (2015-03-02)
~~~~~~~~~~~~~~~~

- Add ``ansible_host`` and ``ansible_group`` parametrized fixture
- Add cls level fixtures for users needing scope=class fixtures
- Updated examples to match new fixture return value
- Alter fixture return data to more closely align with ansible
- Raise ``AnsibleHostUnreachable`` whenever hosts are ... unreachable
- Set contacted and dark hosts in ConnectionError

1.1 (2015-02-16)
~~~~~~~~~~~~~~~~

- Initial release

.. |Build Status| image:: https://img.shields.io/travis/jlaska/pytest-ansible.svg
:target: https://travis-ci.org/jlaska/pytest-ansible
.. |Coverage Status| image:: https://img.shields.io/coveralls/jlaska/pytest-ansible.svg
:target: https://coveralls.io/r/jlaska/pytest-ansible
.. |Version| image:: https://img.shields.io/pypi/v/pytest-ansible.svg
:target: https://pypi.python.org/pypi/pytest-ansible/
.. |Downloads| image:: https://img.shields.io/pypi/dm/pytest-ansible.svg
:target: https://pypi.python.org/pypi/pytest-ansible/
.. |License| image:: https://img.shields.io/pypi/l/pytest-ansible.svg
:target: https://pypi.python.org/pypi/pytest-ansible/
.. |Supported Python Versions| image:: https://img.shields.io/pypi/pyversions/pytest-ansible.svg
:target: https://pypi.python.org/pypi/pytest-ansible/

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest-ansible-1.3.0.tar.gz (10.4 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page