diff --git a/.config/requirements-test.txt b/.config/requirements-test.txt index 23ec3a6a8f..e416b0a8c3 100644 --- a/.config/requirements-test.txt +++ b/.config/requirements-test.txt @@ -8,6 +8,7 @@ filelock >= 3.9.0 pexpect >= 4.8.0, < 5 pytest-mock >= 3.10.0 pytest-plus >= 0.4.0 +pytest-testinfra >= 7.0.0 pytest-xdist >= 3.1.0 pytest >= 7.2.0 check-jsonschema diff --git a/.config/requirements.txt b/.config/requirements.txt index dfe67fb7cf..e7c57e4231 100644 --- a/.config/requirements.txt +++ b/.config/requirements.txt @@ -76,6 +76,7 @@ pymdown-extensions==10.0.1 pytest==7.4.0 pytest-mock==3.11.1 pytest-plus==0.4.0 +pytest-testinfra==8.1.0 pytest-xdist==3.3.1 python-dateutil==2.8.2 python-slugify==8.0.1 diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 2104493568..1b999ab3ba 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -51,7 +51,7 @@ jobs: matrix: ${{ fromJson(needs.pre.outputs.matrix) }} env: - PYTEST_REQPASS: 424 + PYTEST_REQPASS: 446 steps: - uses: actions/checkout@v3 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 719947ec1f..ffee93fb33 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -91,6 +91,7 @@ repos: - jsonschema - pexpect - pytest-mock + - pytest-testinfra - repo: https://github.com/jazzband/pip-tools rev: 6.14.0 hooks: diff --git a/docs/ci.md b/docs/ci.md index 72f7ff11a7..b9bcd31ceb 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -401,6 +401,8 @@ won't create any conflict. --- dependency: name: galaxy +driver: + name: docker platforms: - name: instance1-$TOX_ENVNAME image: mariadb @@ -410,4 +412,6 @@ platforms: command: /usr/sbin/init provisioner: name: ansible +verifier: + name: testinfra ``` diff --git a/docs/configuration.md b/docs/configuration.md index 0e37380cd1..91cd5e4735 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -4,18 +4,18 @@ ## Prerun -In order to help Ansible find used modules and roles, molecule will +To help Ansible find used modules and roles, molecule will perform a prerun set of actions. These involve installing dependencies -from `requirements.yml` specified at project level, install a standalone +from `requirements.yml` specified at the project level, installing a standalone role or a collection. The destination is `project_dir/.cache` and the code itself was reused from ansible-lint, which has to do the same actions. (Note: ansible-lint is not included with molecule.) This assures that when you include a role inside molecule playbooks, -Ansible will be able to find that role, and that the include is exactly +Ansible will be able to find that role and that the include is exactly the same as the one you are expecting to use in production. -If for some reason the prerun action does not suits your needs, you can +If for some reason the prerun action does not suit your needs, you can still disable it by adding `prerun: false` inside the configuration file. @@ -28,7 +28,7 @@ your project, in order to avoid adding it to each scenario. By default, `Molecule` will check whether the role name follows the name standard. If not, it will raise an error. -If computed fully qualified role name does not follow current galaxy +If the computed fully qualified role name does not follow current galaxy requirements, you can ignore it by adding `role_name_check:1` inside the configuration file. It is strongly recommended to follow the name standard of @@ -40,7 +40,7 @@ and ::: molecule.interpolation.Interpolator -There are following environment variables available in `molecule.yml`: +Following are the environment variables available in `molecule.yml`: MOLECULE_DEBUG @@ -57,7 +57,7 @@ MOLECULE_ENV_FILE MOLECULE_STATE_FILE -: Path to molecule state file, contains state of the instances +: The path to molecule state file contains the state of the instances (created, converged, etc.). Usually `~/.cache/molecule///state.yml` @@ -249,12 +249,12 @@ test_sequence: `provisioner.playbooks` section of molecule.yml. `side_effect` can have one or more arguments (separated by spaces) which is -a playbook (plabyooks) to execute. If the argument for `side_effect` is present, +a playbook (playbooks) to execute. If the argument for `side_effect` is present, it's executed instead. The path to the playbook is relative to the molecule.yml location. Normal side effect settings (from `provisioner.playbooks`) are ignored for action with argument. -`verify` without an argument is executing usual tests configured in the verifier section +`verify` without an argument is executing the usual tests configured in the verifier section of molecule.yml. If one or more arguments (separated by spaces) are present, each argument is treated @@ -263,9 +263,9 @@ The kind of verifier is set in the `verifier` section of molecule.yml and is app `verify` actions in the scenario. The path to tests is relative to the molecule.yml file location. The `additional_files_or_dirs` -setting for verifier is ignored if the `verify` action has an argument. +setting for the verifier is ignored if the `verify` action is provided with an argument. -Multiple `side_effect` and `verify` actions can be used to a create a combination +Multiple `side_effect` and `verify` actions can be used to create a combination of playbooks and tests, for example, for end-to-end playbook testing. Additional `converge` and `idempotence` actions can be used multiple times: @@ -294,3 +294,7 @@ Molecule handles role testing by invoking configurable verifiers. ### Ansible ::: molecule.verifier.ansible.Ansible + +### Testinfra + +::: molecule.verifier.testinfra.Testinfra diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000000..0ceedef299 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,41 @@ +# Using docker containers + +Below you can see a scenario that is using docker containers as test hosts. +When you run `molecule test --scenario docker` the `create`, `converge` and +`destroy` steps will be run one after another. + +This example is using Ansible playbooks and it does not need any molecule +plugins to run. You can fully control which test requirements you need to be +installed. + +## Config playbook + +```yaml title="molecule.yml" +{!../molecule/docker/molecule.yml!} +``` + +```yaml title="requirements.yml" +{!../molecule/docker/requirements.yml!} +``` + +## Create playbook + +```yaml title="create.yml" +{!../molecule/docker/create.yml!} +``` + +```yaml title="tasks/create-fail.yml" +{!../molecule/docker/tasks/create-fail.yml!} +``` + +## Converge playbook + +```yaml title="converge.yml" +{!../molecule/docker/converge.yml!} +``` + +## Destroy playbook + +```yaml title="destroy.yml" +{!../molecule/docker/destroy.yml!} +``` diff --git a/docs/examples.md b/docs/examples.md index e7f279d3af..4a86f1ba7c 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -359,6 +359,15 @@ shared-tests Tests and playbooks can be shared across scenarios. +In this example the `tests` directory lives in a shared +location and `molecule.yml` points to the shared tests. + +```yaml +verifier: + name: testinfra + directory: ../resources/tests/ +``` + In this second example the actions `create`, `destroy`, `converge` and `prepare` are loaded from a shared directory. diff --git a/docs/faq.md b/docs/faq.md index 69e8815110..a4de683d81 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -84,12 +84,10 @@ Molecule will resolve the `INSTANCE_UUID` environment variable when creating and looking up the instance name. You can confirm all is in working order by running `molecule list`. -## Can I test Ansible Collections with Molecule? +## Where can I configure my `roles-path` and `collections-paths`? -This is not currently officially supported. Also, collections remain in -"tech preview" status. However, you can take a look at [this blog -post](https://www.jeffgeerling.com/blog/2019/how-add-integration-tests-ansible-collection-molecule) -outlining a workable DIY solution as a stop gap for now. +As of molecule v6, users are expected to make use of [`ansible.cfg`](https://docs.ansible.com/ansible/latest/reference_appendices/config.html) file to +alter them when needed. ## Does Molecule support monorepos? diff --git a/docs/getting-started.md b/docs/getting-started.md index 57659352ab..7198dd633d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -44,7 +44,8 @@ INSTALL.rst molecule.yml converge.yml verify.yml - `verify.yml` is the Ansible file used for testing as Ansible is the default [verifier](configuration.md#verifier). This allows you to write specific tests against the state of the container after your - role has finished executing. + role has finished executing. Other verifier tools are available + Note that [testinfra](https://testinfra.readthedocs.io/en/latest/) was the default verifier prior to molecule version 3. !!! note diff --git a/docs/next.md b/docs/next.md index 5a8ca94537..09ea39b172 100644 --- a/docs/next.md +++ b/docs/next.md @@ -7,20 +7,21 @@ reduce the amount of magic and just rely on ansible core features. # Implemented changes - `roles-path` and `collections-paths` are no longer configurable for - dependencies. Users are expected to make use of `ansible.cfg` file to + dependencies. Users are expected to make use of [`ansible.cfg`](https://docs.ansible.com/ansible/latest/reference_appendices/config.html) file to alter them when needed. -- testinfra verifier driver was removed but current users should be able to - keep calling their testinfra tests by using `command` or `shell` ansible - modules from within `verify.yml` playbook. + +- `molecule init` command is now only available to create a scenario + using `molecule init scenario`. + Users will no longer be able to create a role. + Instead, users can make use of [`ansible-galaxy`](https://docs.ansible.com/ansible/latest/galaxy/dev_guide.html#) to [create a role](https://docs.ansible.com/ansible/latest/galaxy/dev_guide.html#creating-roles-for-galaxy). # Planned changes -- Removal of provisioning drivers support and documenting, with examples, how to easily migrate to a self-provisioning approach. - Refactoring how dependencies are installed - Bringing ephemeral directory under scenario folder instead of the current inconvenient location under `~/.cache/molecule/...` -- Addition of a minimal `ansible.cfg` file under scenario folder that can - be used to tell ansible from where to load testing content. This is to replace +- Addition of a minimal `ansible.cfg` file under the scenario folder that can + be used to tell Ansible from where to load testing content. This is to replace current Molecule magic around roles, collections and library paths and - test inventory location. Once done you will be able to run molecule playbooks with ansible directly without + test inventory location. Once done you will be able to run molecule playbooks with Ansible directly without having to define these folders. diff --git a/mkdocs.yml b/mkdocs.yml index bcb6813476..c15c13277a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,6 +61,7 @@ nav: - configuration.md - ci.md - Examples: + - docker.md - podman.md - examples.md - faq.md diff --git a/molecule/docker/converge.yml b/molecule/docker/converge.yml new file mode 100644 index 0000000000..5000a2be97 --- /dev/null +++ b/molecule/docker/converge.yml @@ -0,0 +1,29 @@ +- name: Fail if molecule group is missing + hosts: localhost + tasks: + - name: Print some info + ansible.builtin.debug: + msg: "{{ groups }}" + + - name: Assert group existence + ansible.builtin.assert: + that: "'molecule' in groups" + fail_msg: | + molecule group was not found inside inventory groups: {{ groups }} + +- name: Converge + hosts: molecule + # We disable gather facts because it would fail due to our container not + # having python installed. This will not prevent use from running 'raw' + # commands. Most molecule users are expected to use containers that already + # have python installed in order to avoid notable delays installing it. + gather_facts: false + tasks: + - name: Check uname + ansible.builtin.raw: uname -a + register: result + changed_when: false + + - name: Print some info + ansible.builtin.assert: + that: result.stdout | regex_search("^Linux") diff --git a/molecule/docker/create.yml b/molecule/docker/create.yml new file mode 100644 index 0000000000..f2b363b69a --- /dev/null +++ b/molecule/docker/create.yml @@ -0,0 +1,79 @@ +- name: Create + hosts: localhost + gather_facts: false + vars: + molecule_inventory: + all: + hosts: {} + molecule: {} + tasks: + - name: Create a container + community.docker.docker_container: + name: "{{ item.name }}" + image: "{{ item.image }}" + state: started + command: sleep 1d + log_driver: json-file + register: result + loop: "{{ molecule_yml.platforms }}" + + - name: Print some info + ansible.builtin.debug: + msg: "{{ result.results }}" + + - name: Fail if container is not running + when: > + item.container.State.ExitCode != 0 or + not item.container.State.Running + ansible.builtin.include_tasks: + file: tasks/create-fail.yml + loop: "{{ result.results }}" + loop_control: + label: "{{ item.container.Name }}" + + - name: Add container to molecule_inventory + vars: + inventory_partial_yaml: | + all: + children: + molecule: + hosts: + "{{ item.name }}": + ansible_connection: community.docker.docker + ansible.builtin.set_fact: + molecule_inventory: > + {{ molecule_inventory | combine(inventory_partial_yaml | from_yaml) }} + loop: "{{ molecule_yml.platforms }}" + loop_control: + label: "{{ item.name }}" + + - name: Dump molecule_inventory + ansible.builtin.copy: + content: | + {{ molecule_inventory | to_yaml }} + dest: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" + mode: 0600 + + - name: Force inventory refresh + ansible.builtin.meta: refresh_inventory + + - name: Fail if molecule group is missing + ansible.builtin.assert: + that: "'molecule' in groups" + fail_msg: | + molecule group was not found inside inventory groups: {{ groups }} + run_once: true # noqa: run-once[task] + +# we want to avoid errors like "Failed to create temporary directory" +- name: Validate that inventory was refreshed + hosts: molecule + gather_facts: false + tasks: + - name: Check uname + ansible.builtin.raw: uname -a + register: result + changed_when: false + + - name: Display uname info + ansible.builtin.debug: + msg: "{{ result.stdout }}" diff --git a/molecule/docker/destroy.yml b/molecule/docker/destroy.yml new file mode 100644 index 0000000000..2b682c9a30 --- /dev/null +++ b/molecule/docker/destroy.yml @@ -0,0 +1,19 @@ +- name: Destroy molecule containers + hosts: molecule + gather_facts: false + tasks: + - name: Stop and remove container + delegate_to: localhost + community.docker.docker_container: + name: "{{ inventory_hostname }}" + state: absent + auto_remove: true + +- name: Remove dynamic molecule inventory + hosts: localhost + gather_facts: false + tasks: + - name: Remove dynamic inventory file + ansible.builtin.file: + path: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" + state: absent diff --git a/molecule/docker/molecule.yml b/molecule/docker/molecule.yml new file mode 100644 index 0000000000..ec9b1904dc --- /dev/null +++ b/molecule/docker/molecule.yml @@ -0,0 +1,7 @@ +dependency: + name: galaxy + options: + requirements-file: requirements.yml +platforms: + - name: molecule-ubuntu + image: ubuntu:18.04 diff --git a/molecule/docker/requirements.yml b/molecule/docker/requirements.yml new file mode 100644 index 0000000000..256c83d992 --- /dev/null +++ b/molecule/docker/requirements.yml @@ -0,0 +1,2 @@ +collections: + - community.docker diff --git a/molecule/docker/tasks/create-fail.yml b/molecule/docker/tasks/create-fail.yml new file mode 100644 index 0000000000..34915c6b40 --- /dev/null +++ b/molecule/docker/tasks/create-fail.yml @@ -0,0 +1,13 @@ +- name: Retrieve container log + ansible.builtin.command: + cmd: >- + {% raw %} + docker logs + {% endraw %} + {{ item.stdout_lines[0] }} + changed_when: false + register: logfile_cmd + +- name: Display container log + ansible.builtin.fail: + msg: "{{ logfile_cmd.stderr }}" diff --git a/pyproject.toml b/pyproject.toml index 20d91a730e..21bbd38d23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,10 +47,11 @@ changelog = "https://github.com/ansible-community/molecule/releases" [project.scripts] molecule = "molecule.__main__:main" -[project.entry-points."molecule.driver.next"] +[project.entry-points."molecule.driver"] default = "molecule.driver.delegated:Delegated" -[project.entry-points."molecule.verifier.next"] +[project.entry-points."molecule.verifier"] +testinfra = "molecule.verifier.testinfra:Testinfra" ansible = "molecule.verifier.ansible:Ansible" [tool.coverage.run] @@ -96,6 +97,7 @@ module = [ "click_help_colors", # https://github.com/click-contrib/click-help-colors/issues/20 "pexpect", # https://github.com/pexpect/pexpect/issues/759 "pluggy", # https://github.com/pytest-dev/pluggy/pull/414 + "testinfra.*", # https://github.com/pytest-dev/pytest-testinfra/issues/619 "pytest_mock", ] ignore_missing_imports = true diff --git a/src/molecule/api.py b/src/molecule/api.py index 6b14a6bab3..3f7e0908bb 100644 --- a/src/molecule/api.py +++ b/src/molecule/api.py @@ -48,9 +48,9 @@ class IncompatibleMoleculeRuntimeWarning(MoleculeRuntimeWarning): def drivers(config=None) -> UserListMap: """Return list of active drivers.""" plugins = UserListMap() - pm = pluggy.PluginManager("molecule.driver.next") + pm = pluggy.PluginManager("molecule.driver") try: - pm.load_setuptools_entrypoints("molecule.driver.next") + pm.load_setuptools_entrypoints("molecule.driver") except (Exception, SystemExit): # These are not fatal because a broken driver should not make the entire # tool unusable. @@ -68,9 +68,9 @@ def drivers(config=None) -> UserListMap: def verifiers(config=None) -> UserListMap: """Return list of active verifiers.""" plugins = UserListMap() - pm = pluggy.PluginManager("molecule.verifier.next") + pm = pluggy.PluginManager("molecule.verifier") try: - pm.load_setuptools_entrypoints("molecule.verifier.next") + pm.load_setuptools_entrypoints("molecule.verifier") except Exception: # These are not fatal because a broken verifier should not make the entire # tool unusable. diff --git a/src/molecule/driver/base.py b/src/molecule/driver/base.py index 51344d1a38..c81cf1ea52 100644 --- a/src/molecule/driver/base.py +++ b/src/molecule/driver/base.py @@ -58,6 +58,17 @@ def name(self, value): # pragma: no cover :returns: None """ + @property + def testinfra_options(self): + """Testinfra specific options and returns a dict. + + :returns: dict + """ + return { + "connection": "ansible", + "ansible-inventory": self._config.provisioner.inventory_directory, + } + @property @abstractmethod def login_cmd_template(self): # pragma: no cover diff --git a/src/molecule/driver/delegated.py b/src/molecule/driver/delegated.py index 5c712f5be7..f6802aea98 100644 --- a/src/molecule/driver/delegated.py +++ b/src/molecule/driver/delegated.py @@ -227,6 +227,8 @@ def ansible_connection_options(self, instance_name): ) if d.get("password", None): conn_dict["ansible_password"] = d.get("password") + # Based on testinfra documentation, ansible password must be passed via ansible_ssh_pass + # issue to fix this can be found https://github.com/pytest-dev/pytest-testinfra/issues/580 conn_dict["ansible_ssh_pass"] = d.get("password") return conn_dict diff --git a/src/molecule/interpolation.py b/src/molecule/interpolation.py index 7fd45d0676..a09f86814b 100644 --- a/src/molecule/interpolation.py +++ b/src/molecule/interpolation.py @@ -32,6 +32,22 @@ def __init__(self, string: str, place: Exception) -> None: class Interpolator: """Configuration options may contain environment variables. + For example, suppose the shell contains ``VERIFIER_NAME=testinfra`` and + the following molecule.yml is supplied. + + ```yaml + verifier: + - name: ${VERIFIER_NAME} + ``` + + Molecule will substitute ``$VERIFIER_NAME`` with the value of the + ``VERIFIER_NAME`` environment variable. + + !!! warning + + If an environment variable is not set, Molecule substitutes with an + empty string. + Both ``$VARIABLE`` and ``${VARIABLE}`` syntax are supported. Extended shell-style features, such as ``${VARIABLE-default}`` and ``${VARIABLE:-default}`` are also supported. Even the default as another diff --git a/src/molecule/test/a_unit/cookiecutter/__init__.py b/src/molecule/test/a_unit/cookiecutter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/molecule/test/a_unit/cookiecutter/test_molecule.py b/src/molecule/test/a_unit/cookiecutter/test_molecule.py new file mode 100644 index 0000000000..ee217a770f --- /dev/null +++ b/src/molecule/test/a_unit/cookiecutter/test_molecule.py @@ -0,0 +1,67 @@ +# Copyright (c) 2015-2018 Cisco Systems, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import os + +import pytest + +from molecule.command.init import base + + +class CommandBase(base.Base): + """CommandBase Class.""" + + +@pytest.fixture() +def _base_class(): + return CommandBase + + +@pytest.fixture() +def _instance(_base_class): + return _base_class() + + +@pytest.fixture() +def _role_directory(): + return "." + + +@pytest.fixture() +def _command_args(): + return { + "dependency_name": "galaxy", + "driver_name": "default", + "provisioner_name": "ansible", + "scenario_name": "default", + "role_name": "test-role", + "verifier_name": "ansible", + } + + +@pytest.fixture() +def _molecule_file(_role_directory): + return os.path.join( + _role_directory, + "test-role", + "molecule", + "default", + "molecule.yml", + ) diff --git a/src/molecule/test/a_unit/driver/test_delegated.py b/src/molecule/test/a_unit/driver/test_delegated.py index bb49103e0d..3e1b32cc76 100644 --- a/src/molecule/test/a_unit/driver/test_delegated.py +++ b/src/molecule/test/a_unit/driver/test_delegated.py @@ -61,6 +61,13 @@ def test_config_private_member(_instance): assert isinstance(_instance._config, config.Config) +def test_testinfra_options_property(_instance): + assert { + "connection": "ansible", + "ansible-inventory": _instance._config.provisioner.inventory_directory, + } == _instance.testinfra_options + + def test_name_property(_instance): assert _instance.name == "default" diff --git a/src/molecule/test/a_unit/model/v2/test_verifier_section.py b/src/molecule/test/a_unit/model/v2/test_verifier_section.py index b57b277e02..688b9b1485 100644 --- a/src/molecule/test/a_unit/model/v2/test_verifier_section.py +++ b/src/molecule/test/a_unit/model/v2/test_verifier_section.py @@ -23,6 +23,21 @@ from molecule.model import schema_v3 +@pytest.fixture() +def _model_verifier_section_data(): + return { + "verifier": { + "name": "testinfra", + "enabled": True, + "directory": "foo", + "options": {"foo": "bar"}, + "env": {"FOO": "foo", "FOO_BAR": "foo_bar"}, + "additional_files_or_dirs": ["foo"], + }, + } + + +@pytest.mark.parametrize("_config", ["_model_verifier_section_data"], indirect=True) def test_verifier(_config): assert not schema_v3.validate(_config) @@ -47,6 +62,11 @@ def test_verifier_has_errors(_config): assert x == schema_v3.validate(_config) +@pytest.fixture() +def _model_verifier_allows_testinfra_section_data(): + return {"verifier": {"name": "testinfra"}} + + @pytest.fixture() def _model_verifier_allows_ansible_section_data(): return {"verifier": {"name": "ansible"}} @@ -55,6 +75,7 @@ def _model_verifier_allows_ansible_section_data(): @pytest.mark.parametrize( "_config", [ + ("_model_verifier_allows_testinfra_section_data"), ("_model_verifier_allows_ansible_section_data"), ], indirect=True, diff --git a/src/molecule/test/a_unit/test_api.py b/src/molecule/test/a_unit/test_api.py index 3f570b0f5e..bcf2d4d26d 100644 --- a/src/molecule/test/a_unit/test_api.py +++ b/src/molecule/test/a_unit/test_api.py @@ -38,6 +38,6 @@ def test_api_drivers(): def test_api_verifiers(): - x = ["ansible"] + x = ["testinfra", "ansible"] assert all(elem in api.verifiers() for elem in x) diff --git a/src/molecule/test/a_unit/verifier/test_testinfra.py b/src/molecule/test/a_unit/verifier/test_testinfra.py new file mode 100644 index 0000000000..379353e2f3 --- /dev/null +++ b/src/molecule/test/a_unit/verifier/test_testinfra.py @@ -0,0 +1,286 @@ +# Copyright (c) 2015-2018 Cisco Systems, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import os + +import pytest + +from molecule import config, util +from molecule.verifier import testinfra + + +@pytest.fixture() +def _patched_testinfra_get_tests(mocker): + m = mocker.patch("molecule.verifier.testinfra.Testinfra._get_tests") + m.return_value = ["foo.py", "bar.py"] + + return m + + +@pytest.fixture() +def _verifier_section_data(): + return { + "verifier": { + "name": "testinfra", + "options": {"foo": "bar", "v": True, "verbose": True}, + "additional_files_or_dirs": ["file1.py", "file2.py", "match*.py", "dir/*"], + "env": {"FOO": "bar"}, + }, + } + + +# NOTE(retr0h): The use of the `patched_config_validate` fixture, disables +# config.Config._validate from executing. Thus preventing odd side-effects +# throughout patched.assert_called unit tests. +@pytest.fixture() +def _instance(patched_config_validate, config_instance: config.Config): + return testinfra.Testinfra(config_instance) + + +@pytest.fixture() +def inventory_file(_instance): + return _instance._config.provisioner.inventory_file + + +@pytest.fixture() +def inventory_directory(_instance): + return _instance._config.provisioner.inventory_directory + + +def test_config_private_member(_instance): + assert isinstance(_instance._config, config.Config) + + +def test_default_options_property(inventory_directory, _instance): + x = { + "connection": "ansible", + "ansible-inventory": inventory_directory, + "p": "no:cacheprovider", + } + + assert x == _instance.default_options + + +def test_default_options_property_updates_debug(inventory_directory, _instance): + _instance._config.args = {"debug": True} + x = { + "connection": "ansible", + "ansible-inventory": inventory_directory, + "debug": True, + "vvv": True, + "p": "no:cacheprovider", + } + + assert x == _instance.default_options + + +def test_default_options_property_updates_sudo( + inventory_directory, + _instance, + _patched_testinfra_get_tests, +): + _instance._config.args = {"sudo": True} + x = { + "connection": "ansible", + "ansible-inventory": inventory_directory, + "sudo": True, + "p": "no:cacheprovider", + } + + assert x == _instance.default_options + + +def test_default_env_property(_instance): + assert "MOLECULE_FILE" in _instance.default_env + assert "MOLECULE_INVENTORY_FILE" in _instance.default_env + assert "MOLECULE_SCENARIO_DIRECTORY" in _instance.default_env + assert "MOLECULE_INSTANCE_CONFIG" in _instance.default_env + + +@pytest.mark.parametrize("config_instance", ["_verifier_section_data"], indirect=True) +def test_additional_files_or_dirs_property(_instance): + tests_directory = _instance._config.verifier.directory + file1_file = os.path.join(tests_directory, "file1.py") + file2_file = os.path.join(tests_directory, "file2.py") + match1_file = os.path.join(tests_directory, "match1.py") + match2_file = os.path.join(tests_directory, "match2.py") + test_subdir = os.path.join(tests_directory, "dir") + test_subdir_file = os.path.join(test_subdir, "test_subdir_file.py") + + os.mkdir(tests_directory) + os.mkdir(test_subdir) + for f in [file1_file, file2_file, match1_file, match2_file, test_subdir_file]: + util.write_file(f, "") + + x = [file1_file, file2_file, match1_file, match2_file, test_subdir_file] + assert sorted(x) == sorted(_instance.additional_files_or_dirs) + + +@pytest.mark.parametrize("config_instance", ["_verifier_section_data"], indirect=True) +def test_env_property(_instance): + assert _instance.env["FOO"] == "bar" + assert "ANSIBLE_CONFIG" in _instance.env + assert "ANSIBLE_ROLES_PATH" in _instance.env + assert "ANSIBLE_LIBRARY" in _instance.env + assert "ANSIBLE_FILTER_PLUGINS" in _instance.env + + +def test_name_property(_instance): + assert _instance.name == "testinfra" + + +def test_enabled_property(_instance): + assert _instance.enabled + + +def test_directory_property(_instance): + parts = _instance.directory.split(os.path.sep) + + assert ["molecule", "default", "tests"] == parts[-3:] + + +@pytest.fixture() +def _verifier_testinfra_directory_section_data(): + return {"verifier": {"name": "testinfra", "directory": "/tmp/foo/bar"}} + + +@pytest.mark.parametrize( + "config_instance", + ["_verifier_testinfra_directory_section_data"], + indirect=True, +) +def test_directory_property_overridden(_instance): + assert _instance.directory == "/tmp/foo/bar" + + +@pytest.mark.parametrize("config_instance", ["_verifier_section_data"], indirect=True) +def test_options_property(inventory_directory, _instance): + x = { + "connection": "ansible", + "ansible-inventory": inventory_directory, + "foo": "bar", + "v": True, + "verbose": True, + "p": "no:cacheprovider", + } + + assert x == _instance.options + + +@pytest.mark.parametrize("config_instance", ["_verifier_section_data"], indirect=True) +def test_options_property_handles_cli_args(inventory_directory, _instance): + _instance._config.args = {"debug": True} + x = { + "connection": "ansible", + "ansible-inventory": inventory_directory, + "foo": "bar", + "debug": True, + "vvv": True, + "verbose": True, + "p": "no:cacheprovider", + } + + assert x == _instance.options + + +@pytest.mark.parametrize("config_instance", ["_verifier_section_data"], indirect=True) +def test_bake(_patched_testinfra_get_tests, inventory_directory, _instance): + _instance._tests = ["foo.py", "bar.py"] + _instance.bake() + args = [ + "pytest", + "--ansible-inventory", + inventory_directory, + "--connection", + "ansible", + "--foo", + "bar", + "-p", + "no:cacheprovider", + "foo.py", + "bar.py", + "-v", + ] + + assert _instance._testinfra_command == args + + +def test_execute( + caplog, + patched_run_command, + _patched_testinfra_get_tests, + _instance, +): + _instance.execute() + + patched_run_command.assert_called_once() + + msg = f"Executing Testinfra tests found in {_instance.directory}/..." + msg2 = "Verifier completed successfully." + assert msg in caplog.text + assert msg2 in caplog.text + assert "pytest" == patched_run_command.call_args[0][0][0] + + +def test_execute_does_not_execute( + patched_run_command, + caplog, + _instance, +): + _instance._config.config["verifier"]["enabled"] = False + _instance.execute() + + assert not patched_run_command.called + + msg = "Skipping, verifier is disabled." + assert msg in caplog.text + + +def test_does_not_execute_without_tests( + patched_run_command, + caplog, + _instance, +): + _instance.execute() + + assert not patched_run_command.called + + msg = "Skipping, no tests found." + assert msg in caplog.text + + +def test_execute_bakes(patched_run_command, _patched_testinfra_get_tests, _instance): + _instance.execute() + + assert _instance._testinfra_command is not None + + assert patched_run_command.call_count == 1 + + +def test_testinfra_executes_catches_and_exits_return_code( + patched_run_command, + _patched_testinfra_get_tests, + _instance, +): + patched_run_command.side_effect = SystemExit(1) + with pytest.raises(SystemExit) as e: + _instance.execute() + + assert e.value.code == 1 diff --git a/src/molecule/test/b_functional/test_command.py b/src/molecule/test/b_functional/test_command.py index 28b23e4a43..87ba3e674c 100644 --- a/src/molecule/test/b_functional/test_command.py +++ b/src/molecule/test/b_functional/test_command.py @@ -343,3 +343,12 @@ def test_podman() -> None: ).returncode == 0 ) + + +def test_docker() -> None: + assert ( + run_command( + ["molecule", "test", "--scenario-name", "docker"], + ).returncode + == 0 + ) diff --git a/src/molecule/test/resources/schema_instance_files/valid/molecule.yml b/src/molecule/test/resources/schema_instance_files/valid/molecule.yml index 5d51fa6458..3bda50eb1b 100644 --- a/src/molecule/test/resources/schema_instance_files/valid/molecule.yml +++ b/src/molecule/test/resources/schema_instance_files/valid/molecule.yml @@ -68,4 +68,4 @@ scenario: - destroy verifier: - name: ansible + name: testinfra diff --git a/src/molecule/test/scenarios/.flake8 b/src/molecule/test/scenarios/.flake8 new file mode 100644 index 0000000000..a968387671 --- /dev/null +++ b/src/molecule/test/scenarios/.flake8 @@ -0,0 +1,8 @@ +[flake8] +# TODO(ssbarnea): Remove this file once we make entire codebase pydocstyle compliant +# Prevents tests failure when run on systems that have pytest-docstrings already installed, the other settings are copied from root version. + +# E203: https://github.com/python/black/issues/315 +ignore = D,E741,W503,W504,H,E501,E203 +# 88 is official black default: +max-line-length = 88 diff --git a/src/molecule/test/scenarios/cleanup/molecule/default/tests/test_cleanup.py b/src/molecule/test/scenarios/cleanup/molecule/default/tests/test_cleanup.py new file mode 100644 index 0000000000..e217d449be --- /dev/null +++ b/src/molecule/test/scenarios/cleanup/molecule/default/tests/test_cleanup.py @@ -0,0 +1,18 @@ +"""Testinfra tests.""" + +import os + +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ["MOLECULE_INVENTORY_FILE"], +).get_hosts("all") + + +def test_hosts_file(host): + """Validate host file.""" + f = host.file("/etc/hosts") + + assert f.exists + assert f.user == "root" + assert f.group == "root" diff --git a/src/molecule/test/scenarios/host_group_vars/molecule/links/converge.yml b/src/molecule/test/scenarios/host_group_vars/molecule/links/converge.yml new file mode 100644 index 0000000000..b149009b2f --- /dev/null +++ b/src/molecule/test/scenarios/host_group_vars/molecule/links/converge.yml @@ -0,0 +1,15 @@ +--- +- hosts: example + gather_facts: false + tasks: + - name: Host vars from host_vars links + ansible.builtin.debug: + var: host_group_vars_host_vars_linked + + - name: Group vars from group_vars links + ansible.builtin.debug: + var: host_group_vars_group_vars_linked + + - name: Variable from extra inventory link + ansible.builtin.debug: + var: hostvars['extra-host']['host_group_vars_extra_host_linked'] diff --git a/src/molecule/test/scenarios/host_group_vars/molecule/links/molecule.yml b/src/molecule/test/scenarios/host_group_vars/molecule/links/molecule.yml new file mode 100644 index 0000000000..52356d03f2 --- /dev/null +++ b/src/molecule/test/scenarios/host_group_vars/molecule/links/molecule.yml @@ -0,0 +1,23 @@ +--- +dependency: + name: galaxy +driver: + name: default +platforms: + - name: instance + image: ${TEST_BASE_IMAGE} + groups: + - example + children: + - example_1 +provisioner: + name: ansible + inventory: + links: + hosts: ../../hosts + host_vars: ../../host_vars + group_vars: ../../group_vars +scenario: + name: links +verifier: + name: testinfra diff --git a/src/molecule/test/scenarios/side_effect/molecule/default/tests/test_side_effect.py b/src/molecule/test/scenarios/side_effect/molecule/default/tests/test_side_effect.py index 40a5ed59da..d4e179b0ed 100644 --- a/src/molecule/test/scenarios/side_effect/molecule/default/tests/test_side_effect.py +++ b/src/molecule/test/scenarios/side_effect/molecule/default/tests/test_side_effect.py @@ -1,5 +1,13 @@ """Testinfra tests.""" +import os + +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ["MOLECULE_INVENTORY_FILE"], +).get_hosts("all") + def test_side_effect_removed_file(host): """Validate that file was removed.""" diff --git a/src/molecule/test/scenarios/test_destroy_strategy/molecule/default/tests/test_destroy_strategy.py b/src/molecule/test/scenarios/test_destroy_strategy/molecule/default/tests/test_destroy_strategy.py index ddfc4fe807..3fd003e17d 100644 --- a/src/molecule/test/scenarios/test_destroy_strategy/molecule/default/tests/test_destroy_strategy.py +++ b/src/molecule/test/scenarios/test_destroy_strategy/molecule/default/tests/test_destroy_strategy.py @@ -1,5 +1,13 @@ """Testinfra tests.""" +import os + +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ["MOLECULE_INVENTORY_FILE"], +).get_hosts("all") + def test_hostname(host): """Validate hostname.""" diff --git a/src/molecule/test/scenarios/verifier/.pre-commit-config.yaml b/src/molecule/test/scenarios/verifier/.pre-commit-config.yaml new file mode 100644 index 0000000000..ce9e96bd9d --- /dev/null +++ b/src/molecule/test/scenarios/verifier/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +--- +repos: + - repo: local + hooks: + - id: flake8 + name: flake8 + entry: python -m flake8 --max-line-length=120 + language: system + types: [python] diff --git a/src/molecule/test/scenarios/verifier/molecule/testinfra-pre-commit/converge.yml b/src/molecule/test/scenarios/verifier/molecule/testinfra-pre-commit/converge.yml new file mode 100644 index 0000000000..8ad426dc0c --- /dev/null +++ b/src/molecule/test/scenarios/verifier/molecule/testinfra-pre-commit/converge.yml @@ -0,0 +1,19 @@ +--- +- name: Converge + hosts: all + tasks: + - name: Create /tmp/molecule + ansible.builtin.file: + dest: /etc/molecule + group: root + owner: root + mode: 0755 + state: directory + + - name: Create /etc/molecule/{{ ansible_hostname }} + ansible.builtin.copy: + dest: "/etc/molecule/{{ ansible_hostname }}" + group: root + owner: root + mode: 0644 + content: "{{ ansible_hostname }}" diff --git a/src/molecule/test/scenarios/verifier/molecule/testinfra-pre-commit/molecule.yml b/src/molecule/test/scenarios/verifier/molecule/testinfra-pre-commit/molecule.yml new file mode 100644 index 0000000000..016c3b00c4 --- /dev/null +++ b/src/molecule/test/scenarios/verifier/molecule/testinfra-pre-commit/molecule.yml @@ -0,0 +1,18 @@ +--- +dependency: + name: galaxy +driver: + name: default +platforms: + - name: instance + image: ${TEST_BASE_IMAGE} +provisioner: + name: ansible +scenario: + name: testinfra-pre-commit +verifier: + name: testinfra + options: + vvv: true + additional_files_or_dirs: + - ../shared/test_*.py diff --git a/src/molecule/test/scenarios/verifier/molecule/testinfra-pre-commit/tests/test_testinfra_pre_commit.py b/src/molecule/test/scenarios/verifier/molecule/testinfra-pre-commit/tests/test_testinfra_pre_commit.py new file mode 100644 index 0000000000..5a55ce3f60 --- /dev/null +++ b/src/molecule/test/scenarios/verifier/molecule/testinfra-pre-commit/tests/test_testinfra_pre_commit.py @@ -0,0 +1,7 @@ +"""Testinfra tests.""" + + +def test_ansible_hostname(host): + """Validate hostname.""" + f = host.file("/tmp/molecule/instance-1") + assert not f.exists diff --git a/src/molecule/test/scenarios/verifier/molecule/testinfra/converge.yml b/src/molecule/test/scenarios/verifier/molecule/testinfra/converge.yml new file mode 100644 index 0000000000..834b621290 --- /dev/null +++ b/src/molecule/test/scenarios/verifier/molecule/testinfra/converge.yml @@ -0,0 +1,6 @@ +--- +- name: Converge + gather_facts: false + hosts: all + roles: + - molecule diff --git a/src/molecule/test/scenarios/verifier/molecule/testinfra/molecule.yml b/src/molecule/test/scenarios/verifier/molecule/testinfra/molecule.yml new file mode 100644 index 0000000000..4414638548 --- /dev/null +++ b/src/molecule/test/scenarios/verifier/molecule/testinfra/molecule.yml @@ -0,0 +1,20 @@ +--- +dependency: + name: galaxy +driver: + name: default +platforms: + - name: instance + image: ${TEST_BASE_IMAGE} +provisioner: + name: ansible + env: + ANSIBLE_ROLES_PATH: ../../../../resources/roles/ +scenario: + name: testinfra +verifier: + name: testinfra + options: + vvv: true + additional_files_or_dirs: + - ../shared/test_*.py diff --git a/src/molecule/test/scenarios/verifier/molecule/testinfra/shared/test_shared.py b/src/molecule/test/scenarios/verifier/molecule/testinfra/shared/test_shared.py new file mode 100644 index 0000000000..09996eaf70 --- /dev/null +++ b/src/molecule/test/scenarios/verifier/molecule/testinfra/shared/test_shared.py @@ -0,0 +1,16 @@ +"""Testinfra tests.""" + +import os + +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ["MOLECULE_INVENTORY_FILE"], +).get_hosts("all") + + +def test_ansible_hostname(host): + """Validate hostname.""" + f = host.file("/tmp/molecule/instance-1") + + assert not f.exists diff --git a/src/molecule/test/scenarios/verifier/molecule/testinfra/tests/test_testinfra.py b/src/molecule/test/scenarios/verifier/molecule/testinfra/tests/test_testinfra.py new file mode 100644 index 0000000000..09996eaf70 --- /dev/null +++ b/src/molecule/test/scenarios/verifier/molecule/testinfra/tests/test_testinfra.py @@ -0,0 +1,16 @@ +"""Testinfra tests.""" + +import os + +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ["MOLECULE_INVENTORY_FILE"], +).get_hosts("all") + + +def test_ansible_hostname(host): + """Validate hostname.""" + f = host.file("/tmp/molecule/instance-1") + + assert not f.exists diff --git a/src/molecule/verifier/testinfra.py b/src/molecule/verifier/testinfra.py new file mode 100644 index 0000000000..ce3819c441 --- /dev/null +++ b/src/molecule/verifier/testinfra.py @@ -0,0 +1,224 @@ +# Copyright (c) 2015-2018 Cisco Systems, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +"""Testinfra Verifier Module.""" + +import glob +import logging +import os + +from molecule import util +from molecule.api import Verifier + +LOG = logging.getLogger(__name__) + + +class Testinfra(Verifier): + """`Testinfra`_ is no longer the default test verifier since version 3.0. + + Additional options can be passed to ``testinfra`` through the options + dict. Any option set in this section will override the defaults. + + !!! note + + Molecule will remove any options matching '^[v]+$', and pass ``-vvv`` + to the underlying ``pytest`` command when executing ``molecule + --debug``. + + ``` yaml + verifier: + name: testinfra + options: + n: 1 + ``` + + The testing can be disabled by setting ``enabled`` to False. + + ``` yaml + verifier: + name: testinfra + enabled: False + ``` + + Environment variables can be passed to the verifier. + + ``` yaml + verifier: + name: testinfra + env: + FOO: bar + ``` + + Change path to the test directory. + + ``` yaml + verifier: + name: testinfra + directory: /foo/bar/ + ``` + + Additional tests from another file or directory relative to the scenario's + tests directory (supports regexp). + + ``` yaml + verifier: + name: testinfra + additional_files_or_dirs: + - ../path/to/test_1.py + - ../path/to/test_2.py + - ../path/to/directory/* + ``` + .. _`Testinfra`: https://testinfra.readthedocs.io + """ + + def __init__(self, config=None) -> None: + """Set up the requirements to execute ``testinfra`` and returns None. + + :param config: An instance of a Molecule config. + :return: None + """ + super().__init__(config) + self._testinfra_command = None + self._tests = [] # type: ignore + + @property + def name(self): + return "testinfra" + + @property + def default_options(self): + d = self._config.driver.testinfra_options + d["p"] = "no:cacheprovider" + if self._config.debug: + d["debug"] = True + d["vvv"] = True + if self._config.args.get("sudo"): + d["sudo"] = True + + return d + + # NOTE(retr0h): Override the base classes' options() to handle + # ``ansible-galaxy`` one-off. + @property + def options(self): + o = self._config.config["verifier"]["options"] + # NOTE(retr0h): Remove verbose options added by the user while in + # debug. + if self._config.debug: + o = util.filter_verbose_permutation(o) + + return util.merge_dicts(self.default_options, o) + + @property + def default_env(self): + env = util.merge_dicts(os.environ, self._config.env) + env = util.merge_dicts(env, self._config.provisioner.env) + + return env + + @property + def additional_files_or_dirs(self): + files_list = [] + c = self._config.config + for f in c["verifier"]["additional_files_or_dirs"]: + glob_path = os.path.join(self._config.verifier.directory, f) + glob_list = glob.glob(glob_path) + if glob_list: + files_list.extend(glob_list) + + return files_list + + def bake(self): + """Bake a ``testinfra`` command so it's ready to execute and returns None. + + :return: None + """ + options = self.options + verbose_flag = util.verbose_flag(options) + args = verbose_flag + + self._testinfra_command = [ + "pytest", + *util.dict2args(options), + *self._tests, + *args, + ] + + def execute(self, action_args=None): + if not self.enabled: + msg = "Skipping, verifier is disabled." + LOG.warning(msg) + return + + if self._config: + self._tests = self._get_tests(action_args) + else: + self._tests = [] + if not len(self._tests) > 0: + msg = "Skipping, no tests found." + LOG.warning(msg) + return + + self.bake() + + msg = f"Executing Testinfra tests found in {self.directory}/..." + LOG.info(msg) + + result = util.run_command(self._testinfra_command, debug=self._config.debug) + if result.returncode == 0: + msg = "Verifier completed successfully." + LOG.info(msg) + else: + util.sysexit(result.returncode) + + def _get_tests(self, action_args=None): + """Walk the verifier's directory for tests and returns a list. + + :return: list + """ + if action_args: + tests = [] + for arg in action_args: + args_tests = list( + util.os_walk( + os.path.join(self._config.scenario.directory, arg), + "test_*.py", + followlinks=True, + ), + ) + tests.extend(args_tests) + return sorted(tests) + return sorted( + list( + util.os_walk( + self.directory, + "test_*.py", + followlinks=True, + ), + ) + + self.additional_files_or_dirs, + ) + + def schema(self): + return { + "verifier": { + "type": "dict", + "schema": {"name": {"type": "string", "allowed": ["testinfra"]}}, + }, + }