Coder Social home page Coder Social logo

ansible / pytest-ansible Goto Github PK

View Code? Open in Web Editor NEW
175.0 35.0 63.0 732 KB

A pytest plugin that enables the use of ansible in tests, enables the use of pytest as a collection unit test runner, and exposes molecule scenarios through a pytest fixture.

Home Page: https://ansible.readthedocs.io/projects/pytest-ansible/

License: MIT License

Python 100.00%
ansible-dev-tools

pytest-ansible's Introduction

pytest-ansible

Build Status Version License Supported Python Versions

The pytest-ansible plugin is designed to provide seamless integration between pytest and Ansible, allowing you to efficiently run and test Ansible-related tasks and scenarios within your pytest test suite. This plugin enhances the testing workflow by offering three distinct pieces of functionality:

  1. Unit Testing for Ansible Collections: This feature aids in running unit tests for Ansible collections using pytest. It allows you to validate the behavior of your Ansible modules and roles in isolation, ensuring that each component functions as expected.

  2. Molecule Scenario Integration: The plugin assists in running Molecule scenarios using pytest. This integration streamlines the testing of Ansible roles and playbooks across different environments, making it easier to identify and fix issues across diverse setups.

  3. Ansible Integration for Pytest Tests: With this functionality, you can seamlessly use Ansible from within your pytest tests. This opens up possibilities to interact with Ansible components and perform tasks like provisioning resources, testing configurations, and more, all while leveraging the power and flexibility of pytest.

Supported Ansible

Pytest Ansible will only support versions of python and ansible-core which are under active upstream support which currently translates to:

  • Python 3.10 or newer
  • Ansible-core 2.14 or newer

Installation

Install this plugin using pip:

pip install pytest-ansible

Getting Started

Unit Testing for Ansible Collections

The pytest-ansible-units plugin allows ansible collection's unit tests to be run with only pytest. It offers a focused approach to testing individual Ansible modules. With this plugin, you can write and execute unit tests specifically for Ansible modules, ensuring the accuracy and reliability of your module code. This is particularly useful for verifying the correctness of module behavior in isolation.

To use pytest-ansible-units, follow these steps:

  1. Install the plugin using pip:
pip install pytest-ansible
  1. Ensure you have Python 3.10 or greater, ansible-core, and pyyaml installed.

  2. Depending on your preferred directory structure, you can clone collections into the appropriate paths.

    • Collection Tree Approach: The preferred approach is to clone the collections being developed into it's proper collection tree path. This eliminates the need for any symlinks and other collections being developed can be cloned into the same tree structure.

      git clone <repo> collections/ansible_collections/<namespace>/<name>
      

      Note: Run pytest in the root of the collection directory, adjacent to the collection's galaxy.yml file.

    • Shallow Tree Approach:

      git clone <repo>
      

      Notes:

      • Run pytest in the root of the collection directory, adjacent to the collection's galaxy.yml file.
      • A collections directory will be created in the repository directory, and collection content will be linked into it.
  3. Execute the unit tests using pytest: pytest tests

Help

The following may be added to the collections' pyproject.toml file to limit warnings and set the default path for the collection's tests

[tool.pytest.ini_options]
testpaths = [
    "tests",
]
filterwarnings = [
    'ignore:AnsibleCollectionFinder has already been configured',
]

Information from the galaxy.yml file is used to build the collections directory structure and link the contents. The galaxy.yml file should reflect the correct collection namespace and name.

One way to detect issues without running the tests is to run:

pytest --collect-only

The follow errors may be seen:

E   ModuleNotFoundError: No module named 'ansible_collections'
  • Check the galaxy.yml file for an accurate namespace and name
  • Ensure pytest is being run from the collection's root directory, adjacent to the galaxy.yml
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules

Molecule Scenario Integration

This functionality assists in running Molecule scenarios using pytest. It enables pytest discovery of all molecule.yml files inside the codebase and runs them as pytest tests. It allows you to include Molecule scenarios as part of your pytest test suite, allowing you to thoroughly test your Ansible roles and playbooks across different scenarios and environments.

Running molecule scenarios using pytest

Molecule scenarios can be tested using 2 different methods if molecule is installed.

Recommended:

Add a test_integration.py file to the tests/integration directory of the ansible collection:

"""Tests for molecule scenarios."""
from __future__ import absolute_import, division, print_function

from pytest_ansible.molecule import MoleculeScenario


def test_integration(molecule_scenario: MoleculeScenario) -> None:
    """Run molecule for each scenario.

    :param molecule_scenario: The molecule scenario object
    """
    proc = molecule_scenario.test()
    assert proc.returncode == 0

The molecule_scenario fixture provides parameterized molecule scenarios discovered in the collection's extensions/molecule directory, as well as other directories within the collection.

molecule test -s <scenario> will be run for each scenario and a completed subprocess returned from the test() call.

Legacy:

Run molecule with the --molecule command line parameter to inject each molecule directory found in the current working directory. Each scenario will be injected as an external test in the the tests available for pytest. Due to the nature of this approach, the molecule scenarios are not represented as python tests and may not show in the IDE's pytest test tree.

To run Molecule scenarios using pytest, follow these steps:

  1. Install the pytest-ansible plugin using pip:
pip install pytest-ansible
  1. Execute pytest to run Molecule scenarios: pytest

Ansible Integration for Pytest Tests

The ansible_module, ansible_adhoc, localhost, and ansible_facts fixtures are provided to help you integrate Ansible functionalities into your pytest tests. These fixtures allow you to interact with Ansible modules, run commands on localhost, fetch Ansible facts, and more.

Fixtures and helpers for use in tests

Here's a quick overview of the available fixtures:

  • ansible_module: Allows you to call Ansible modules directly within your test functions.
  • ansible_adhoc: Provides a function to initialize a HostManager object to work with Ansible inventory.
  • localhost: A convenience fixture for running Ansible modules that typically run on the local machine.
  • ansible_facts: Returns a JSON structure representing system facts for the associated inventory.

Usage

Once installed, the following pytest command-line parameters are available:

pytest \
    [--inventory <path_to_inventory>] \
    [--extra-inventory <path_to_extra_inventory>] \
    [--host-pattern <host-pattern>] \
    [--connection <plugin>] \
    [--module-path <path_to_modules] \
    [--user <username>] \
    [--become] \
    [--become-user <username>] \
    [--become-method <method>] \
    [--ask-become-pass] \
    [--limit <limit>] \
    [--ansible-unit-inject-only] \
    [--molecule] \
    [--molecule-unavailable-driver] \
    [--skip-no-git-change] \
    [--check]

Inventory

Using ansible first starts with defining your inventory. This can be done in several ways, but to start, we'll use the ansible_adhoc fixture.

def test_my_inventory(ansible_adhoc):
    hosts = ansible_adhoc()

In the example above, the hosts variable is an instance of the HostManager class and describes your ansible inventory. For this to work, you'll need to tell ansible where to find your inventory. Inventory can be anything supported by ansible, which includes an INI file or an executable script that returns properly formatted JSON. For example,

pytest --inventory my_inventory.ini --host-pattern all

or

pytest --inventory path/to/my/script.py --host-pattern webservers

or

pytest --inventory one.example.com,two.example.com --host-pattern all

In the above examples, the inventory provided at runtime will be used in all tests that use the ansible_adhoc fixture. A more realistic scenario may involve using different inventory files (or host patterns) with different tests. To accomplish this, the fixture ansible_adhoc allows you to customize the inventory parameters. Read on for more detail on using the ansible_adhoc fixture.

Extra Inventory

Using ansible first starts with defining your extra inventory. This feature was added in version 2.3.0, and is intended to allow the user to work with two different inventories. This can be done in several ways, but to start, we'll use the ansible_adhoc fixture.

For example,

pytest --inventory my_inventory.ini --extra-inventory my_second_inventory.ini --host-pattern host_in_second_inventory

Fixture ansible_adhoc

The ansible_adhoc fixture returns a function used to initialize a HostManager object. The ansible_adhoc fixture will default to parameters supplied to the pytest command-line, but also allows one to provide keyword arguments used to initialize the inventory.

The example below demonstrates basic usage with options supplied at run-time to pytest.

def test_all_the_pings(ansible_adhoc):
    ansible_adhoc().all.ping()

The following example demonstrates available keyword arguments when creating a HostManager object.

def test_uptime(ansible_adhoc):
    # take down the database
    ansible_adhoc(inventory='db1.example.com,', user='ec2-user',
        become=True, become_user='root').all.command('reboot')

The HostManager object returned by the ansible_adhoc() function provides numerous ways of calling ansible modules against some, or all, of the inventory. The following demonstrates sample usage.

def test_host_manager(ansible_adhoc):
    hosts = ansible_adhoc()

    # __getitem__
    hosts['all'].ping()
    hosts['localhost'].ping()

    # __getattr__
    hosts.all.ping()
    hosts.localhost.ping()

    # Supports [ansible host patterns](http://docs.ansible.com/ansible/latest/intro_patterns.html)
    hosts['webservers:!phoenix'].ping()  # all webservers that are not in phoenix
    hosts[0].ping()
    hosts[0:2].ping()

    assert 'one.example.com' in hosts

    assert hasattr(hosts, 'two.example.com')

    for a_host in hosts:
        a_host.ping()

Fixture localhost

The localhost fixture is a convenience fixture that surfaces a ModuleDispatcher instance for ansible host running pytest. This is convenient when using ansible modules that typically run on the local machine, such as cloud modules (ec2, gce etc...).

def test_do_something_cloudy(localhost, ansible_adhoc):
    """Deploy an ec2 instance using multiple fixtures."""
    params = dict(
        key_name='some_key',
        instance_type='t2.micro',
        image='ami-123456',
        wait=True,
        group='webservers',
        count=1,
        vpc_subnet_id='subnet-29e63245',
        assign_public_ip=True,
    )

    # Deploy an ec2 instance from localhost using the `ansible_adhoc` fixture
    ansible_adhoc(inventory='localhost,', connection='local').localhost.ec2(**params)

    # Deploy an ec2 instance from localhost using the `localhost` fixture
    localhost.ec2(**params)

Fixture ansible_module

The ansible_module fixture allows tests and fixtures to call ansible modules. Unlike the ansible_adhoc fixture, this fixture only uses the options supplied to pytest at run time.

A very basic example demonstrating the ansible ping module:

def test_ping(ansible_module):
    ansible_module.ping()

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

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.

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 ...

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

Additionally, since facts are just ansible modules, you could inspect the contents of the ec2_facts module for greater granularity ...

def test_terminate_us_east_1_instances(ansible_adhoc):

    for facts in ansible_adhoc().all.ec2_facts():
        if facts['ansible_ec2_placement_region'].startswith('us-east'):
            '''do some testing'''

Parameterize 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. Please note, the fixture ansible_adhoc is the prefer mechanism for interacting with ansible inventory within tests.

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

@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.

@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'''

Inspecting results

When using the ansible_adhoc, localhost or ansible_module fixtures, the object returned will be an instance of class AdHocResult. The AdHocResult class can be inspected as follows:

def test_adhoc_result(ansible_adhoc):
    contacted = ansible_adhoc(inventory=my_inventory).command("date")

    # As a dictionary
    for (host, result) in contacted.items():
        assert result.is_successful, "Failed on host %s" % host
    for result in contacted.values():
        assert result.is_successful
    for host in contacted.keys():
        assert host in ['localhost', 'one.example.com']

    assert contacted.localhost.is_successful

    # As a list
    assert len(contacted) > 0
    assert 'localhost' in contacted

    # As an iterator
    for result in contacted:
        assert result.is_successful

    # With __getattr__
    assert contacted.localhost.is_successful

    # Or __getitem__
    assert contacted['localhost'].is_successful

Using the AdHocResult object provides ways to conveniently access results for different hosts involved in the ansible adhoc command. Once the specific host result is found, you may inspect the result of the ansible adhoc command on that use by way of the ModuleResult interface. The ModuleResult class represents the dictionary returned by the ansible module for a particular host. The contents of the dictionary depend on the module called.

The ModuleResult interface provides some convenient properties to determine the success of the module call. Examples are included below.

def test_module_result(localhost):
    contacted = localhost.command("find /tmp")

    assert contacted.localhost.is_successful
    assert contacted.localhost.is_ok
    assert contacted.localhost.is_changed
    assert not contacted.localhost.is_failed

    contacted = localhost.shell("exit 1")
    assert contacted.localhost.is_failed
    assert not contacted.localhost.is_successful

The contents of the JSON returned by an ansible module differs from module to module. For guidance, consult the documentation and examples for the specific ansible module.

Exception handling

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

@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.

@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

Communication

Refer to the Communication section of the documentation to find out how to get in touch with us.

You can also find more information in the Ansible communication guide.

Contributing

Contributions are very welcome. Tests can be run with tox, please ensure the coverage at least stays the same before you submit a pull request.

Code of Conduct

Please see the Ansible Community Code of Conduct.

License

Distributed under the terms of the MIT license, "pytest-ansible" is free and open source software

Issues

If you encounter any problems, please file an issue along with a detailed description.

pytest-ansible's People

Contributors

abadger avatar alancoding avatar alisonlhart avatar audgirka avatar cfsnm avatar cidrblock avatar cipherboy avatar cristianonicolai avatar dbarrosop avatar dependabot[bot] avatar gundalow avatar jlaska avatar kdelee avatar m4rt1nch avatar oranod avatar pguermo avatar pre-commit-ci[bot] avatar priejos avatar ruchip16 avatar shanemcd avatar spredzy avatar ssbarnea avatar thenets avatar tjni avatar vgramer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pytest-ansible's Issues

run module with its own variable

I'd like to run following task, how can i achieve using pytest?

- name: Gather information from LLDP
  lldp:
  vars:
    ansible_shell_type: docker
    ansible_python_interpreter: docker exec -i lldp python

ModuleNotFoundError when a function being tested is importing from another module

Here is how the directories are structured.

playbooks/
    module_utils/
        __init__.py
        my_custom_module.py
        my_other_custom_module_util .py
tests/
    __init__.py
    test_module_utils/
        test_my_custom_module.py

And in my_custom_module.py it is importing from another file in module_utils likeso:

from ansible.module_utils.my_other_custom_module_util import some_func

When running pytest it returns an error
ModuleNotFoundError: No module named 'ansible.module_utils.my_other_custom_module_util'

Any ideas how to fix this?

pytest-ansible==2.2.2 fails with not reachable logs

Earlier i was seeing this Issues/28 and then I updated my pytest-ansible and ansible ,

- But Now I am facing below issue.

  • My pytest execution hangs for really long time and fails saying host unreachable.
  • My host is reachable without any password/ key. { ssh root@IP }
  • Is there any setting I can do to avoid infinite connection check. This test passed once for me but the time of execution increased alot (+45mins)

Command:

  • py.test --ansible-inventory hosts --user root --ansible-host-pattern master pytest/sanity/test_role_users_kra.py -q -s --junitxml role_user_creation_junit.xml -qsvv --debug

.

.
.
plugins: metadata-1.10.0, logger-0.3.0, html-1.22.1, autochecklog-0.2.0, ansible-2.2.2, ansible-playbook-0.3.0
collected 2 items
pytest/sanity/test_role_users_kra.py INFO - ------------------------------------------------------
INFO:Test:------------------------------------------------------
INFO - Func test_setup_kra_role_users in file: /root/pki-pytest-ansible/pytest/sanity/test_role_users_kra.py
INFO:Test:Func test_setup_kra_role_users in file: /root/pki-pytest-ansible/pytest/sanity/test_role_users_kra.py
Loading callback plugin unnamed of type old, v1.0 from /usr/lib/python2.7/site-packages/pytest_ansible/module_dispatcher/v28.py
META: ran handlers
<10.0.151.209> ESTABLISH SSH CONNECTION FOR USER: root
<10.0.151.209> SSH: ansible.cfg set ssh_args: (-C)(-o)(ControlMaster=auto)(-o)(ControlPersist=60s)
<10.0.151.209> SSH: ansible_password/ansible_ssh_password not set: (-o)(KbdInteractiveAuthentication=no)(-o)(PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey)(-o)(PasswordAuthentication=no)
<10.0.151.209> SSH: ANSIBLE_REMOTE_USER/remote_user/ansible_user/user/-u set: (-o)(User="root")
.
.
.
.
.
.

<10.0.151.209> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o 'User="root"' -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/af01c08f47 10.0.151.209 '/bin/sh -c '"'"'chmod u+x /root/.ansible/tmp/ansible-tmp-1596797381.57-70538618513074/ /root/.ansible/tmp/ansible-tmp-1596797381.57-70538618513074/AnsiballZ_command.py && sleep 0'"'"''
<10.0.151.209> (0, '', 'OpenSSH_7.7p1, OpenSSL 1.1.0h-fips 27 Mar 2018\r\ndebug1: Reading configuration data /root/.ssh/config\r\ndebug1: /root/.ssh/config line 1: Applying options for *\r\ndebug1: Reading configuration data /etc/ssh/ssh_config\r\ndebug3: /etc/ssh/ssh_config line 52: Including file /etc/ssh/ssh_config.d/05-redhat.conf depth 0\r\ndebug1: Reading configuration data /etc/ssh/ssh_config.d/05-redhat.conf\r\ndebug3: /etc/ssh/ssh_config.d/05-redhat.conf line 2: Including file /etc/crypto-policies/back-ends/openssh.config depth 1\r\ndebug1: Reading configuration data /etc/crypto-policies/back-ends/openssh.config\r\ndebug3: gss kex names ok: [gss-gex-sha1-,gss-group14-sha1-]\r\ndebug3: kex names ok: [[email protected],ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1]\r\ndebug1: /etc/ssh/ssh_config.d/05-redhat.conf line 8: Applying options for *\r\ndebug2: resolve_canonicalize: hostname 10.0.151.209 is address\r\ndebug1: auto-mux: Trying existing master\r\ndebug2: fd 3 setting O_NONBLOCK\r\ndebug2: mux_client_hello_exchange: master version 4\r\ndebug3: mux_client_forwards: request forwardings: 0 local, 0 remote\r\ndebug3: mux_client_request_session: entering\r\ndebug3: mux_client_request_alive: entering\r\ndebug3: mux_client_request_alive: done pid = 620\r\ndebug3: mux_client_request_session: session request sent\r\ndebug1: mux_client_request_session: master session id: 2\r\ndebug3: mux_client_read_packet: read header failed: Broken pipe\r\ndebug2: Received exit status from master 0\r\n')
<10.0.151.209> ESTABLISH SSH CONNECTION FOR USER: root
<10.0.151.209> SSH: ansible.cfg set ssh_args: (-C)(-o)(ControlMaster=auto)(-o)(ControlPersist=60s)
<10.0.151.209> SSH: ansible_password/ansible_ssh_password not set: (-o)(KbdInteractiveAuthentication=no)(-o)(PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey)(-o)(PasswordAuthentication=no)
<10.0.151.209> SSH: ANSIBLE_REMOTE_USER/remote_user/ansible_user/user/-u set: (-o)(User="root")
<10.0.151.209> SSH: ANSIBLE_TIMEOUT/timeout set: (-o)(ConnectTimeout=10)
<10.0.151.209> SSH: PlayContext set ssh_common_args: ()
<10.0.151.209> SSH: PlayContext set ssh_extra_args: ()
<10.0.151.209> SSH: found only ControlPersist; added ControlPath: (-o)(ControlPath=/root/.ansible/cp/af01c08f47)
<10.0.151.209> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o 'User="root"' -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/af01c08f47 -tt 10.0.151.209 '/bin/sh -c '"'"'/usr/bin/python /root/.ansible/tmp/ansible-tmp-1596797381.57-70538618513074/AnsiballZ_command.py && sleep 0'"'"''

Versions used:
ansible==2.8.0
pytest==3.6.3
pytest-ansible==2.2.2
pytest-ansible-playbook==0.3.0
pytest-autochecklog==0.2.0
pytest-html==1.22.1
pytest-logger==0.3.0
pytest-metadata==1.10.0

Todo: Clarify error handling in `__getattr__` method

The current implementation of the __getattr__ method lacks clarity in the error handling logic when the requested module is not found. The existing TODO comment raises a question about the appropriate approach for handling this scenario.

Code Snippet:

def __getattr__(self, name):
    """Run the ansible module matching the provided `name`.

    Raise `AnsibleModuleError` when no such module exists.
    """
    if not self.has_module(name):
        # TODO: should we just raise an AttributeError, or a more
        # suitable error handling strategy?
    self.options["module_name"] = name
    return self._run

Proposed Solution:

To enhance code readability and provide a consistent error handling strategy, let's decide between two possible solutions:

  1. Raise AttributeError Exception: Modify the code to raise a AttributeError exception when the requested module is not found. This aligns with the standard behavior in Python when accessing nonexistent attributes.
if not self.has_module(name):
    raise AttributeError(f"Module {name} not found.")
  1. Keep AnsibleModuleError: Since the function states to raise AnsibleModuleError exception, AttributeError exception gets eliminated
if not self.has_module(name):
    raise AnsibleModuleError(
        f"The module {name} was not found in configured module paths."
    )

Issue invoking molecule via pytest

Hi community,

Environment details:

ansible                    9.0.1
ansible-compat             4.1.10
ansible-core               2.16.0
ansible-lint               6.22.1
ansible-lint-junit         0.17.8
ansible-rulebook           1.0.4
ansible-runner             2.3.4
molecule                   6.0.3
molecule-plugins           23.5.0
pytest                     7.4.4
pytest-aiohttp             1.0.5
pytest-ansible             24.1.0
pytest-asyncio             0.23.3
pytest-html                4.1.1
pytest-json                0.4.0
pytest-lazy-fixture        0.6.3
pytest-metadata            3.0.0
pytest-mock                3.12.0
pytest-xdist               3.5.0

I'm not able to execute molecule from pytest via pytest-ansible via the console. The call I make is pytest --molecule tests/integration/molecule/scenario1

This ends up in the following error:

tests/integration/molecule/scenario1/molecule.yml::test0 FAILED

======================================================= FAILURES =======================================================
___________________________________________________ use_case: test0 ____________________________________________________
Error code 2 returned by: /path/to/python/bin/python3.11 -m molecule test0 -s scenario1

In particular. the test0 seems to be the issue as molecule expects to be invoked via molecule test, the test0 seems to be the pytest identifier.

The "issue" could be either fixed here where the test scenarios are named testx (e.g. test0) or here where molecule is invoked with self.name resulting in /path/to/python/bin/python3.11 -m molecule test0 -s scenario1

Molecule Integration: Missing option to pass base config

Hey,

we currently leverage molecule via pytest and we would like to leverage the feature to make the integration tests visible in our IDE. Therefore, we leverage this snippet from the docu:

"""Tests for molecule scenarios."""
from __future__ import absolute_import, division, print_function

from pytest_ansible.molecule import MoleculeScenario


def test_integration(molecule_scenario: MoleculeScenario) -> None:
    """Run molecule for each scenario.

    :param molecule_scenario: The molecule scenario object
    """
    proc = molecule_scenario.test()
    assert proc.returncode == 0

Since we heavily rely on a molecule base config, I was wondering if there is a way to pass a molecule base config (similar to what you can achieve if you run pytest from the command line pytest --molecule --molecule_base_config=base.yml -s tests/integration/molecule/thetest)?! I believe this is not possible today, but would appreciate this feature for our adoption.

Thanks for your feedback.

ping not working

Hi,
Was trying out this plugin , my ansible command works but my test fails with Error 'AnsibleHostUnreachable: Host unreachable'

ansible -i hosts all -m ping -u <user_name> works

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']

The above always fails , help appreciated

Todo: Validate the warning message

The TODO comment is asking to validate a specific warning message, but the code following it is not written correctly. The code inside the if False: block will never be executed, and therefore, the warning message validation will never occur.

Would raise a PR soon for the issue.

Error in retrieve host_pattern attribute

These two integration tests from tower-qa repository are failing when upgrading from Tower 4.0.0 to Tower 4.0.1. This error is caused to the mismatch between the inventory groups (4.0.0 -> tower, 4.0.1 -> automationcontroller):

  • tests.api.job_templates.test_job_template_callbacks.TestJobTemplateCallbacks.test_provision_with_provided_variables_needed_to_start
  • tests.api.job_templates.test_job_template_callbacks.TestJobTemplateCallbacks.test_provision_job_template_with_limit[regular_tower_instance]

The root cause is here:

Error Message

failed on setup with "AttributeError: type HostManager has no attribute 'tower[0]'"

Trace

Traceback (most recent call last):
File "/home/ec2-user/venvs/venv/lib64/python3.8/site-packages/pytest_ansible/fixtures.py", line 20, in ansible_module
return getattr(host_mgr, host_mgr.options['host_pattern'])
File "/home/ec2-user/venvs/venv/lib64/python3.8/site-packages/pytest_ansible/host_manager/init.py", line 68, in getattr
raise AttributeError("type HostManager has no attribute '%s'" % attr)
AttributeError: type HostManager has no attribute 'tower[0]'

We will need to change the code to, depending of the tower version, search for tower[0] or automationcontroller[0] attribute

ImportError: cannot import name VariableManager

Could you please merge it from current branch to master.I need to run this code in 2.4 and it's breaking once i migrate from pytest-ansible 2.3 to 2.4.

Error snip

Traceback (most recent call last):
  File "/bin/py.test", line 11, in <module>
    sys.exit(main())
  File "/usr/lib/python2.7/site-packages/_pytest/config.py", line 49, in main
    config = _prepareconfig(args, plugins)
  File "/usr/lib/python2.7/site-packages/_pytest/config.py", line 168, in _prepareconfig
    pluginmanager=pluginmanager, args=args)
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 745, in __call__
    return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 339, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 334, in <lambda>
    _MultiCall(methods, kwargs, hook.spec_opts).execute()
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 613, in execute
    return _wrapped_call(hook_impl.function(*args), self.execute)
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 250, in _wrapped_call
    wrap_controller.send(call_outcome)
  File "/usr/lib/python2.7/site-packages/_pytest/helpconfig.py", line 68, in pytest_cmdline_parse
    config = outcome.get_result()
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 280, in get_result
    _reraise(*ex)  # noqa
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 265, in __init__
    self.result = func()
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 614, in execute
    res = hook_impl.function(*args)
  File "/usr/lib/python2.7/site-packages/_pytest/config.py", line 957, in pytest_cmdline_parse
    self.parse(args)
  File "/usr/lib/python2.7/site-packages/_pytest/config.py", line 1121, in parse
    self._preparse(args, addopts=addopts)
  File "/usr/lib/python2.7/site-packages/_pytest/config.py", line 1084, in _preparse
    self.pluginmanager.load_setuptools_entrypoints('pytest11')
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 510, in load_setuptools_entrypoints
    plugin = ep.load()
  File "/usr/lib/python2.7/site-packages/pkg_resources/__init__.py", line 2341, in load
    return self.resolve()
  File "/usr/lib/python2.7/site-packages/pkg_resources/__init__.py", line 2347, in resolve
    module = __import__(self.module_name, fromlist=['__name__'], level=0)
  File "/usr/lib/python2.7/site-packages/_pytest/assertion/rewrite.py", line 212, in load_module
    py.builtin.exec_(co, mod.__dict__)
  File "/usr/lib/python2.7/site-packages/py/_builtin.py", line 221, in exec_
    exec2(obj, globals, locals)
  File "<string>", line 7, in exec2
  File "/usr/lib/python2.7/site-packages/pytest_ansible/plugin.py", line 21, in <module>
    from ansible.vars import VariableManager
ImportError: cannot import name VariableManager
/opt/pki-tests on  banner-automation! ⌚ 12:14:12
$ ls
ansible  backup-jobs  brew  ci  config  cs-tet-install  dogtag  etc  host  jobs  misc  python  restraint  rfc.sh  scripts  single_instance
/opt/pki-tests on  banner-automation! ⌚ 12:14:30
$ py.test dogtag/pytest/cli/banner/test_banner_automation.py --ansible-inventory host -s -v -k test_banner_show_banner_is_installed             
Traceback (most recent call last):
  File "/bin/py.test", line 11, in <module>
    sys.exit(main())
  File "/usr/lib/python2.7/site-packages/_pytest/config.py", line 49, in main
    config = _prepareconfig(args, plugins)
  File "/usr/lib/python2.7/site-packages/_pytest/config.py", line 168, in _prepareconfig
    pluginmanager=pluginmanager, args=args)
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 745, in __call__
    return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 339, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 334, in <lambda>
    _MultiCall(methods, kwargs, hook.spec_opts).execute()
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 613, in execute
    return _wrapped_call(hook_impl.function(*args), self.execute)
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 250, in _wrapped_call
    wrap_controller.send(call_outcome)
  File "/usr/lib/python2.7/site-packages/_pytest/helpconfig.py", line 68, in pytest_cmdline_parse
    config = outcome.get_result()
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 280, in get_result
    _reraise(*ex)  # noqa
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 265, in __init__
    self.result = func()
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 614, in execute
    res = hook_impl.function(*args)
  File "/usr/lib/python2.7/site-packages/_pytest/config.py", line 957, in pytest_cmdline_parse
    self.parse(args)
  File "/usr/lib/python2.7/site-packages/_pytest/config.py", line 1121, in parse
    self._preparse(args, addopts=addopts)
  File "/usr/lib/python2.7/site-packages/_pytest/config.py", line 1084, in _preparse
    self.pluginmanager.load_setuptools_entrypoints('pytest11')
  File "/usr/lib/python2.7/site-packages/_pytest/vendored_packages/pluggy.py", line 510, in load_setuptools_entrypoints
    plugin = ep.load()
  File "/usr/lib/python2.7/site-packages/pkg_resources/__init__.py", line 2341, in load
    return self.resolve()
  File "/usr/lib/python2.7/site-packages/pkg_resources/__init__.py", line 2347, in resolve
    module = __import__(self.module_name, fromlist=['__name__'], level=0)
  File "/usr/lib/python2.7/site-packages/_pytest/assertion/rewrite.py", line 212, in load_module
    py.builtin.exec_(co, mod.__dict__)
  File "/usr/lib/python2.7/site-packages/py/_builtin.py", line 221, in exec_
    exec2(obj, globals, locals)
  File "<string>", line 7, in exec2
  File "/usr/lib/python2.7/site-packages/pytest_ansible/plugin.py", line 21, in <module>
    from ansible.vars import VariableManager
ImportError: cannot import name VariableManager

Even no extra-inventory is specified, extra_inventory_manager will still be initialized.

In the plugin.py, the option --extra-inventory is defined as following, the extra-inventory will always be there.
"extra_inventory" in self.options will always be true.

This cause even no extra-inventory is specified, the extra_inventory_manager will defined, and the it will cause the ansible command run twice.

group.addoption(
"--extra-inventory",
"--ansible-extra-inventory",
action="store",
dest="ansible_extra_inventory",
default=None,
metavar="ANSIBLE_EXTRA_INVENTORY",
help="ansible extra inventory file URI (default: %(default)s)",
)

class HostManagerV29(BaseHostManager):
"""Fixme."""

def __init__(self, *args, **kwargs) -> None:
    """Fixme."""
    super().__init__(*args, **kwargs)
    self._dispatcher = ModuleDispatcherV29

def initialize_inventory(self):
    self.options["loader"] = DataLoader()
    self.options["inventory_manager"] = InventoryManager(
        loader=self.options["loader"],
        sources=self.options["inventory"],
    )
    self.options["variable_manager"] = VariableManager(
        loader=self.options["loader"],
        inventory=self.options["inventory_manager"],
    )
    **if "extra_inventory" in self.options:**
        self.options["extra_loader"] = DataLoader()
        self.options["extra_inventory_manager"] = InventoryManager(
            loader=self.options["extra_loader"],
            sources=self.options["extra_inventory"],
        )
        self.options["extra_variable_manager"] = VariableManager(
            loader=self.options["extra_loader"],
            inventory=self.options["extra_inventory_manager"],
        )

class ModuleDispatcherV29(ModuleDispatcherV2):
......
if "extra_inventory_manager" in self.options:
tqm_extra = None
try:
tqm_extra = TaskQueueManager(**kwargs_extra)
tqm_extra.run(play_extra)
finally:
if tqm_extra:
tqm_extra.cleanup()

ansible singleton display.verbosity is always overwritten in module dispatcher

In plugin.py, there is logic to get the singleton object ansible.utils.display::Display() and set its verbosity value according to pytest config option.

def pytest_configure(config):
    """Validate --ansible-* parameters."""
    log.debug("pytest_configure() called")

    config.addinivalue_line("markers", "ansible(**kwargs): Ansible integration")

    # Enable connection debugging
    if config.option.verbose > 0:
        if hasattr(ansible.utils, 'VERBOSITY'):
            ansible.utils.VERBOSITY = int(config.option.verbose)
        else:
            from ansible.utils.display import Display
            display = Display()
            display.verbosity = int(config.option.verbose)

    assert config.pluginmanager.register(PyTestAnsiblePlugin(config), "ansible")

However, this verbosity is always overwritten in module dispatcher. Take module_dispatcher/v28.py as an example. Below lines will always set display.verbosity to 5 under the hood.

        # Pass along cli options
        args = ['pytest-ansible', '-vvvvv', self.options['host_pattern']]
        for argument in ('connection', 'user', 'become', 'become_method', 'become_user', 'module_path'):
            arg_value = self.options.get(argument)
            argument = argument.replace('_', '-')

            if arg_value in (None, False):
                continue

            if arg_value is True:
                args.append('--{0}'.format(argument))
            else:
                args.append('--{0}={1}'.format(argument, arg_value))

        # Use Ansible's own adhoc cli to parse the fake command line we created and then save it
        # into Ansible's global context
        adhoc = AdHocCLI(args)
        adhoc.parse()

The fake command line has "-vvvvv" hardcoded. When adhoc.parse() is executed, display.verbosity will be set to 5.

The consequence is that all the ansible connection debug logs are always output.

Is there a way to get around this except patching the code to get rid of the hard coded "-vvvvv"?

False positive for plugin invocation

If a fixture or param like ansible_result is used, it is assumed to be a fixture from this plugin.

We should be checking a real list of fixture names from the plugin and note in the README that they are reserved names in use by the plugin.

https://github.com/ansible-community/pytest-ansible/blob/150dfe781adfd0de5f9e0f450892c8ebe674f2d9/src/pytest_ansible/plugin.py#L235

Found with the AWS collection: https://github.com/ansible-collections/amazon.aws/blob/b53f7d727509cc36de45d98ad0dd403a001d7a73/tests/unit/plugins/modules/test_lambda_layer.py#L124

Not able to see `ansible_adhoc` fixture in the fixture list.

Hello,

My use case is same as mentioned in #5, here I'm not able to see ansible_adhoc fixture.

My current version of Ansible is 2.3.2.0 if I upgrade that then I'm facing the same issue #14.

Versions:
ansible 2.3.2.0
pytest-3.3.1
pytest-ansible-1.3.1
pytest-ansible-playbook-0.3.0

------------------------------------------------------------------------- fixtures defined from pytest_ansible.plugin -------------------------------------------------------------------------
ansible_module_cls
    Return AnsibleV1Module instance with class scope.
ansible_module
    Return AnsibleV1Module instance with function scope.
ansible_facts_cls
    Return ansible_facts dictionary
ansible_facts
    Return ansible_facts dictionary

------------------------------------------------------------------------ fixtures defined from pytest_ansible_playbook ------------------------------------------------------------------------
ansible_playbook
    Pytest fixture which runs given ansible playbook. When ansible returns
    nonzero return code, the test case which uses this fixture is not
    executed and ends in ``ERROR`` state.

Todo: Refactor Extra Inventory Groups in `plugin.py`

The current implementation in plugin.py involves direct interaction with hosts.options["inventory_manager"] to list groups and extra groups. So, the code can be refactored to encapsulate the group listing logic within the hosts object and use descriptive variable names. Additionally, the parametrization logic can be enhanced for better readability and understanding.

[DEPRECATION WARNING]: ansible.constants.BECOME_METHODS is deprecated

I am using Ansible 2.8.1. I get this error when I install pytest-ansible and run py.test:

[DEPRECATION WARNING]: ansible.constants.BECOME_METHODS is deprecated, please use ansible.plugins.loader.become_loader. This list is statically defined and may not include all become methods. This feature will be removed in
version 2.10. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.

I can suppress deprecation warnings to hide this, but would rather not. This is not ideal.

It appears that no longer supporting BECOME_METHODS could break compatibility with old versions of Ansible. Maybe that's ok. Need some guidance. If I knew what I had to do, I'd make a PR.

Can't run module with ansible 2.4.3.0

python2.7/site-packages/pytest_ansible/module_dispatcher/init.py:32: in getattr
if not self.has_module(name):

self = <pytest_ansible.module_dispatcher.v2.ModuleDispatcherV2 object at 0x90a7cd0>
name = 'ovirt_cluster_facts'

def has_module(self, name):
return ansible.plugins.module_loader.has_plugin(name)
E AttributeError: 'module' object has no attribute 'module_loader'

there should be:
return ansible.plugins.loader.module_loader.has_plugin(name)

check mode supported?

Hello,
running a module in check mode doesn't seem to be supported, is that right?

Thanks!
David

The `is_failed` property of failed result of some ansible modules is False

I have a simple script with content like below:

test_failed.py:

import pprint

def test_failed1(localhost):
    result = localhost.copy(src='not_exist', dest='/tmp/not_exist')

    pprint.pprint('is_failed={}'.format(result['localhost'].is_failed))
    pprint.pprint(result['localhost'])

This script called the ansible copy module with some arguments that will make it fail.

$ pytest test_failed.py --capture no
================================== test session starts ==================================
platform linux2 -- Python 2.7.12, pytest-4.6.5, py-1.8.1, pluggy-0.13.1
ansible: 2.8.7
rootdir: /var/johnar
plugins: repeat-0.8.0, xdist-1.28.0, forked-1.1.3, ansible-2.2.2
collected 1 item                                                                        

test_failed.py Loading callback plugin unnamed of type old, v1.0 from /usr/local/lib/python2.7/dist-packages/pytest_ansible/module_dispatcher/v28.py
META: ran handlers
<localhost> ESTABLISH LOCAL CONNECTION FOR USER: johnar
<localhost> EXEC /bin/sh -c 'echo ~johnar && sleep 0'
<localhost> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /var/johnar/.ansible/tmp/ansible-tmp-1592465619.4-38779564525538 `" && echo ansible-tmp-1592465619.4-38779564525538="` echo /var/johnar/.ansible/tmp/ansible-tmp-1592465619.4-38779564525538 `" ) && sleep 0'
looking for "not_exist" at "/var/johnar/files/not_exist"
looking for "not_exist" at "/var/johnar/not_exist"
looking for "not_exist" at "./files/not_exist"
looking for "not_exist" at "./not_exist"
'is_failed=False'
{'_ansible_no_log': False,
 'changed': False,
 'exception': u'Traceback (most recent call last):\n  File "/usr/local/lib/python2.7/dist-packages/ansible/plugins/action/copy.py", line 464, in run\n    source = self._find_needle(\'files\', source)\n  File "/usr/local/lib/python2.7/dist-packages/ansible/plugins/action/__init__.py", line 1168, in _find_needle\n    return self._loader.path_dwim_relative_stack(path_stack, dirname, needle)\n  File "/usr/local/lib/python2.7/dist-packages/ansible/parsing/dataloader.py", line 319, in path_dwim_relative_stack\n    raise AnsibleFileNotFound(file_name=source, paths=[to_text(p) for p in search])\nAnsibleFileNotFound: Could not find or access \'not_exist\'\nSearched in:\n\t/var/johnar/files/not_exist\n\t/var/johnar/not_exist\n\t./files/not_exist\n\t./not_exist on the Ansible Controller.\nIf you are using a module and expect the file to exist on the remote, see the remote_src option\n',
 'invocation': {'dest': u'/tmp/not_exist',
                'module_args': {'dest': u'/tmp/not_exist',
                                'src': u'not_exist'},
                'src': u'not_exist'},
 'msg': u"Could not find or access 'not_exist'\nSearched in:\n\t/var/johnar/files/not_exist\n\t/var/johnar/not_exist\n\t./files/not_exist\n\t./not_exist on the Ansible Controller.\nIf you are using a module and expect the file to exist on the remote, see the remote_src option"}
.

=============================== 1 passed in 0.88 seconds ================================

In the log, we can see that the module failed with exception. However, the is_failed property of the test result is False. This does not make sense.

The reason is that pytest-ansible uses ResultAccumulator as callback to get the ansible module execution result. However, when ansible calls the ResultAccumulator::v2_runner_on_failed callback function, the 'failed' and 'skipped' keys are removed from the task_result. Without this 'failed' key in the result dictionary, calling results.py::ModuleResult.is_failed will always get 'False' unless the result dictionary has key 'rc' and its value is none-zero.

Ansible called the callback defined in ResultAccumulator here:
https://github.com/ansible/ansible/blob/stable-2.8/lib/ansible/plugins/strategy/__init__.py#L523

Then before passing the task_result to the callback, ansible called the clean_copy() method of TaskResult:
https://github.com/ansible/ansible/blob/stable-2.8/lib/ansible/executor/task_queue_manager.py#L325

The clean_copy() method of TaskResult removed keys 'failed' and 'skipped' from the result._result dictionary:
https://github.com/ansible/ansible/blob/stable-2.8/lib/ansible/executor/task_result.py#L146

IMO, the ResultAccumulator.v2_runner_on_failed method should add back the 'failed' key to the result dictionary. And the v2_runner_on_ok method should not be same as the v2_runner_on_failed method.

Molecule base config not considered when collecting tests

Hi all,

Environment Details:

ansible                    9.0.1
ansible-compat             4.1.10
ansible-core               2.16.0
ansible-lint               6.22.1
ansible-lint-junit         0.17.8
ansible-rulebook           1.0.4
ansible-runner             2.3.4
molecule                   6.0.3
molecule-plugins           23.5.0
pytest                     7.4.4
pytest-aiohttp             1.0.5
pytest-ansible             24.1.0
pytest-asyncio             0.23.3
pytest-html                4.1.1
pytest-json                0.4.0
pytest-lazy-fixture        0.6.3
pytest-metadata            3.0.0
pytest-mock                3.12.0
pytest-xdist               3.5.0

We rely on a base config for molecule that is located in <COLLECTION_DIR>/.config/molecule/config.yml. When pytest runs the test discovery the base config file is not considered / processed. As a consequence, markers such as the marker for the driver is not applied unless it is maintained in each scenario's molecule.yml file.

cd <COLLECTION_DIR>
pytest --collect-only --molecule tests/integration/molecule/

INFO:pytest_ansible.units:Looking for collection info in /home/$USER/.ansible/collections/ansible_collections/NAMESPACE/COLLECTIONNAME/galaxy.yml
INFO:pytest_ansible.units:Collection namespace: NAMESPACE
INFO:pytest_ansible.units:Collection name: COLLECTIONNAME
INFO:pytest_ansible.units:In collection tree
INFO:pytest_ansible.units:Additional Collections Paths: [PosixPath('/home/$USER/.ansible/collections')]
INFO:pytest_ansible.units:Setting ANSIBLE_COLLECTIONS_PATH to /home/$USER/.ansible/collections:/home/$USER/.ansible/collections
=========================================================================================================================================== test session starts ===========================================================================================================================================
platform linux -- Python 3.11.7, pytest-7.4.4, pluggy-1.3.0 -- /path/to/python/bin/python3.11
cachedir: .pytest_cache
metadata: {'Python': '3.11.7', 'Platform': 'Linux-6.5.0-1009-oem-x86_64-with-glibc2.35', 'Packages': {'pytest': '7.4.4', 'pluggy': '1.3.0'}, 'Plugins': {'asyncio': '0.23.3', 'metadata': '3.0.0', 'html': '4.1.1', 'lazy-fixture': '0.6.3', 'anyio': '4.2.0', 'mock': '3.12.0', 'xdist': '3.5.0', 'json': '0.4.0', 'ansible': '24.1.0', 'aiohttp': '1.0.5'}, 'GIT_BRANCH': 'feature-153-xyz'}
ansible: 2.16.0
rootdir: /home/$USER/.ansible/collections/ansible_collections/NAMESPACE/COLLECTIONNAME
configfile: pyproject.toml
plugins: asyncio-0.23.3, metadata-3.0.0, html-4.1.1, lazy-fixture-0.6.3, anyio-4.2.0, mock-3.12.0, xdist-3.5.0, json-0.4.0, ansible-24.1.0, aiohttp-1.0.5
asyncio: mode=Mode.STRICT
collected 36 items                                                                                                                                                                                                                                                                                        

tests/integration/molecule/scenario1/molecule.yml
  test0[no_driver]
tests/integration/molecule/scenario2/molecule.yml
  test1[no_driver]
tests/integration/molecule/scenario3/molecule.yml
  test2[no_driver]
...

How to access host variables?

Is there an easy way to access the host variables of hosts returned by ansible_adhoc?

Some custom variables are defined in our inventories and I need those to perform my tests. Those hosts are not Linux machines so I cannot use ansible_adhoc.setup() or ansible_facts.

I see that ansible_adhoc()[inventory_hostname].options has inventory_manager and variable_manager items, but I don't see how to get a specific host's variables.

Any idea?

Todo: Configuration Option for `COLLECTIONS_PATH` in `units.py`

The current implementation of the inject function involves hard-coded paths for discovering collections. To provide users with greater flexibility and control over collection paths, the code should be enhanced to incorporate a configuration option (COLLECTIONS_PATH) that users can set to specify their preferred paths.

Refer the TODO here

Breaks with ansible devel/ansible 2.8+

I get this traceback:

Traceback (most recent call last):                                                                                                                                                                                
  File "/home/elijah/sfw/ansible/py3-tower-qa/tests/api/inventories/test_group.py", line 85, in root_variation     
    content="""# --inventory-id %s %s""" % (inventory.id, request.param['inventory'])                                                                                                                             
  File "/home/elijah/envs/py36towerqa/lib64/python3.6/site-packages/pytest_ansible/module_dispatcher/v24.py", line 81, in _run
    parser = CLI.base_parser(                                                                                                
AttributeError: type object 'CLI' has no attribute 'base_parser'   

I'm guessing ansible/ansible@7e92ff8
impacted this?

@abadger if you have any input on what proper use should be in these two places, or how to be compatible with older versions as well as the new way, it would be appreciated!

parser = CLI.base_parser(

Maybe we need a module_dispatcher/v28.py

coverage doesn't work with pytest-ansible

Hello,
have you tried to use pytest-ansible with pytest-cov? I just realized that pytest-cov is failing to notice that code is being covered by the tests.

Regards,
David

Todo: Integrating parser defaults in `BaseHostManager` class

The existing TODO comment in the BaseHostManager class requires further clarification on how to address the task mentioned in the todo, it's quite unclear what specific action is required to surface the parser defaults and how it should be implemented.

Code Snippet:

def get_host_manager(*args, **kwargs):
    """Initialize and return a HostManager instance."""
    if has_ansible_v213:
        from pytest_ansible.host_manager.v213 import HostManagerV213 as HostManager
    elif has_ansible_v212:
        from pytest_ansible.host_manager.v212 import HostManagerV212 as HostManager
    elif has_ansible_v29:
        from pytest_ansible.host_manager.v29 import HostManagerV29 as HostManager
    elif has_ansible_v28:
        from pytest_ansible.host_manager.v28 import HostManagerV28 as HostManager
    elif has_ansible_v24:
        from pytest_ansible.host_manager.v24 import HostManagerV24 as HostManager
    elif has_ansible_v2:
        from pytest_ansible.host_manager.v2 import HostManagerV2 as HostManager
    else:
        from .v1 import HostManagerV1 as HostManager

    # TODO: - figure out how to surface the parser defaults here too  # noqa: TD002, FIX002
    return HostManager(*args, **kwargs)

run ansible module with vars

how can i implement such playbook task

    - name: Generate configurations for exabgp instances
      template: src=roles/test/templates/exabgp/config.j2 dest={{exabgp_dir}}/{{item.file_name}}
      with_items:
        - {file_name: "config_1.ini", local_ip: '{{speaker_ips[0]}}', port_num: '5000'}
        - {file_name: "config_2.ini", local_ip: '{{speaker_ips[1]}}', port_num: '6000'}
        - {file_name: "config_3.ini", local_ip: '{{vlan_ips[0]}}', port_num: '7000'}

when I call template module, how can i supply additional vars such as local_ip, port_num. Is there an example?

Error using this library - ansible_collections module not found

I noticed that when I upgraded Ansible-core I got errors from any fixtures that are running pytest-ansible commands. I did a git bisect and narrowed it down to breaking with this pull request:

ansible/ansible#78915

________________________________________________ Test_Job.test_alan _________________________________________________
Traceback (most recent call last):
  File "/home/alancoding/repos/tower-qa/tests/api/jobs/test_jobs.py", line 178, in test_alan
    ansible_adhoc()['all'].file(path='/tmp/alan', state='absent')
  File "/home/alancoding/repos/awx/env/lib64/python3.11/site-packages/pytest_ansible/module_dispatcher/v213.py", line 171, in _run
    tqm.run(play)
  File "/home/alancoding/repos/ansible/lib/ansible/executor/task_queue_manager.py", line 333, in run
    play_return = strategy.run(iterator, play_context)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alancoding/repos/ansible/lib/ansible/plugins/strategy/linear.py", line 193, in run
    action = action_loader.get(task_action, class_only=True, collection_list=task.collections)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alancoding/repos/ansible/lib/ansible/plugins/loader.py", line 855, in get
    return self.get_with_context(name, *args, **kwargs).object
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alancoding/repos/ansible/lib/ansible/plugins/loader.py", line 865, in get_with_context
    plugin_load_context = self.find_plugin_with_context(name, collection_list=collection_list)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alancoding/repos/ansible/lib/ansible/plugins/loader.py", line 586, in find_plugin_with_context
    result = self._resolve_plugin_step(name, mod_type, ignore_deprecated, check_aliases, collection_list, plugin_load_context=plugin_load_context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alancoding/repos/ansible/lib/ansible/plugins/loader.py", line 682, in _resolve_plugin_step
    return self._find_plugin_legacy(name, plugin_load_context, ignore_deprecated, check_aliases, suffix)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alancoding/repos/ansible/lib/ansible/plugins/loader.py", line 787, in _find_plugin_legacy
    return self._find_fq_plugin(fq_name=candidate_fqcr, extension=suffix, plugin_load_context=plugin_load_context, ignore_deprecated=ignore_deprecated)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alancoding/repos/ansible/lib/ansible/plugins/loader.py", line 476, in _find_fq_plugin
    routing_metadata = self._query_collection_routing_meta(acr, plugin_type, extension=extension)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alancoding/repos/ansible/lib/ansible/plugins/loader.py", line 434, in _query_collection_routing_meta
    collection_pkg = import_module(acr.n_python_collection_package_name)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.11/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1206, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1178, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1128, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1206, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1178, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1128, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1206, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1178, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1142, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'ansible_collections'

I couldn't find this particular error documented anywhere in this repo, so I wanted to go ahead and file it. Maybe it's related to the "v3" work? But I installed from the stable 2.x branch, and the bug is from a commit that went into ansible-core.

Ping @sivel, is there something obvious I missed?

How to apply ansible arguments in runtime?

I have an inventory file just like below:

; Inventory file
[vms]
VM0100 ansible_host=10.250.0.51

I want to indicate the connection type/network_os/... etc. in runtime and I write following code:

hosts = ansible_adhoc(
            become=True,
            become_method='enable',
            connection='network_cli',
            network_os='eos')

I can see the HostManager accept my input(I printed the hosts.option and found that the connection type has been changed from 'smart' to 'network_cli') but when I run a module on a target host:

hosts['VM0100'].eos_config()

It returns following message and seems like my custom ansible arguments are not apply to the ansible_adhoc

Connection type ssh is not valid for this module

Did I missed something here? Is there a way to apply ansible arguments in runtime?

Thanks

Revamp `pytest-ansible`

We are planning to combine several pytest plugins related to ansible.
You can follow this discussion for more insights.
Posting here to keep everyone informed and we look forward to any feedback, suggestions, etc related to it.

pytest-ansible hangs when using shell/command with 2.0.2 & it is unable to parse inventory file

Issue 1 : It is unable to parse inventory file with recently supported format as ansible.

Ansible 2.7 host file format for python3:

[master] 10.8.241.181 hostname=10.8.241.181

[all]
10.8.241.181 hostname=10.8.241.181

[localhost:vars]
ansible_python_interpreter=/usr/bin/python3

[master:vars]
ansible_python_interpreter=/usr/libexec/platform-python

DEBUG:pytest_ansible.host_manager.v24:HostManagerV24.initialize_inventory() [WARNING]: * Failed to parse /pki-pytest-ansible/hosts with yaml plugin: Syntax Error while loading YAML. expected '', but found '' The error appears to have been in '/pki-pytest-ansible/hosts': line 2, column 1, but may be elsewhere in the file depending on the exact syntax problem. The offending line appears to be: [master] 10.8.250.108 ^ here

[WARNING]: * Failed to parse /pki-pytest-ansible/hosts with ini plugin: /pki-pytest-ansible/hosts:7: Section
[localhost:vars] not valid for undefined group: localhost

[WARNING]: Unable to parse /pki-pytest-ansible/hosts as an inventory source

[WARNING]: No inventory was parsed, only implicit localhost is available

Issue 2: We see a hang at times in pytest-ansible during shell/command execution in remote hosts.

We are simply running a command using shell. and it never gets executed on the remote instance.
versions:

pytest-ansible==2.0.2

INFO:utils:Submit csr to the RootCA: Dogtagpki
DEBUG:pytest_ansible.module_dispatcher.v24:[master] shell: {'_raw_params': 'pki -p 20080 ca-cert-request-submit --profile caCACert --csr-file /tmp/ca_signing.csr'}
DEBUG:pytest_ansible.module_dispatcher.v24:Play({'gather_facts': 'no', 'tasks': [{'action': {'args': {'_raw_params': 'pki -p 20080 ca-cert-request-submit --profile caCACert --csr-file /tmp/ca_signing.csr'}, 'module': 'shell'}}], 'hosts': 'master', 'name': 'pytest-ansible'})
DEBUG:pytest_ansible.module_dispatcher.v24:TaskQueueManager({'passwords': {'conn_pass': None, 'become_pass': None}, 'inventory': <ansible.inventory.manager.InventoryManager object at 0x7f816c409d50>, 'stdout_callback': <pytest_ansible.module_dispatcher.v24.ResultAccumulator object at 0x7f8169a1c050>, 'variable_manager': <ansible.vars.manager.VariableManager object at 0x7f816b13ca50>, 'options': <Values at 0x7f8169990b90: {'subset': None, 'ask_pass': False, 'become_user': u'root', 'poll_interval': 15, 'sudo': False, 'private_key_file': None, 'syntax': None, 'one_line': None, 'diff': False, 'sftp_extra_args': '', 'check': False, 'remote_user': None, 'become_method': u'sudo', 'ask_su_pass': False, 'vault_ids': [], 'become_ask_pass': False, 'inventory': None, 'vault_password_files': [], 'forks': 5, 'listhosts': None, 'ssh_extra_args': '', 'seconds': 0, 'become': False, 'su_user': None, 'ask_sudo_pass': False, 'extra_vars': [], 'verbosity': 5, 'tree': None, 'su': False, 'ssh_common_args': '', 'connection': u'smart', 'ask_vault_pass': False, 'timeout': 10, 'module_path': [u'/builds/gkapoor/pki-pytest-ansible/common-modules'], 'sudo_user': None, 'scp_extra_args': ''}>, 'loader': <ansible.parsing.dataloader.DataLoader object at 0x7f816c409cd0>})
DEBUG:pytest_ansible.module_dispatcher.v24:{'unreachable': {}, 'contacted': {u'10.8.249.214': {'_ansible_parsed': True, 'stderr_lines': [], u'changed': True, u'end': u'2�[31;1mERROR: Job failed: execution took longer than 1h0m0s seconds<

  1. With pytest-ansible==1.3

INFO:utils:Submit csr to the RootCA: Dogtagpki
DEBUG:pytest_ansible.plugin:[master] shell: {'_raw_params': 'pki -p 20080 ca-cert-request-submit --profile caCACert --csr-file /tmp/ca_signing.csr'}
DEBUG:pytest_ansible.plugin:{'unreachable': {}, 'contacted': {u'10.8.252.3': {'_ansible_parsed': True, 'stderr_lines': [], u'cmd': [u'pki', u'-p', u'20080', u'ca-cert-request-submit', u'--profile', u'caCACert', u'--csr-file', u'/tmp/ca_signing.csr'], u'end': u'2019-04-02 05:14:02.020011', '_ansible_no_log': False, u'stdout': u'-----------------------------
Submitted certificate request

Request ID: 18
Type: enrollment
Request Status: pending
Operation Result: success', u'changed': True, u'rc': 0, u'start': u'2019-04-02 05:14:00.668801', u'stderr': u'', u'delta': u'0:00:01.351210', u'invocation': {u'module_args': {u'warn': True, u'executable': None, u'_uses_shell': False, u'_raw_params': u'pki -p 20080 ca-cert-request-submit --profile caCACert --csr-file /tmp/ca_signing.csr', u'removes': None, u'creates': None, u'chdir': None}}, 'stdout_lines': [u'-----------------------------', u'Submitted certificate request', u'-----------------------------', u' Request ID: 18', u' Type: enrollment', u' Request Status: pending', u' Operation Result: success']}}}
INFO:test_externalca_dogtagpki: Checking output : {u'10.8.252.3': {'_ansible_parsed': True, 'stderr_lines': [], u'cmd': [u'pki', u'-p', u'20080', u'ca-cert-request-submit', u'--profile', u'caCACert', u'--csr-file', u'/tmp/ca_signing.csr'], u'end': u'2019-04-02 05:14:02.020011', '_ansible_no_log': False, u'stdout': u'-----------------------------
Submitted certificate request

Request ID: 18
Type: enrollment
Request Status: pending

How do to run different commands on indvidual hosts matching pattern all

i would like to know how i can run different commands on individual hosts matching pattern all

Example:
Inventory File
[server]
192.168.122.12

[client]
192.168.122.13

@pytest.mark.ansible(host_pattern='all')
def test_1(ansible_module):
     <individual_host1/server>.ansible_module.command ('hostname')
    <individual_host2/client>.ansible_module.command('date')

I am aware i could do the below:

@pytest.mark.ansible(host_pattern='server')
output = ansible_module.command('hostname')

@pytest.mark.ansible(host_pattern='client')
output = ansible_module.command('date')

But i have a requirement where i need certain tests
where i need to match pattern "all" but inside the tests i need to run individual commands on
each host.

[Feature] Add command line parameter to pass Ansible Vault Password file

When running ansible-playbook, there exists a command line parameter, --vault-password-file which allows one to pass the path of a file containing the password for decrypting Ansible-Vault-encrypted files (documentation here).

Pytest-Ansible needs to provide a similar command line argument in order to obtain secrets from Ansible-Vault-encrypted secrets files for the sake of parity.

Todo: ignore if they are using a marker

The current implementation in the pytest_collection_modifyitems method involves identifying test items that use Ansible-related fixtures. There is a TODO comment indicating that the code should be modified to ignore certain cases where a marker is used. This comment addresses a potential issue with the handling of test markers in the context of Ansible fixtures.

Make ansible facts available via a "session" scope fixture

This came up in the course of discussing a proposal (ansible/tower-qa#2495). This needs to happen if we are going to start using ansible facts more frequently in tests.

The ansible_facts fixture is executed every time it is used by a test. This represents a pretty major performance hit, since it's going to perform a setup task (ssh connections and all).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.