Repository: ansible/molecule

Issue:
Question: accessing values of variables as they are being used for provisioning an instance inside Testinfra tests
I want to use Testinfra tests to test my role.

Inside an Testinfra test I would like to access the values of variables as they are being used for provisioning the machine when `playbook.yml` is converged for some instance.
I need this since the instance's state, which I want to check, depends on the chosen values of the variables defined in role's `default/main.yml` or `vars/main.yml` file.

I tried using the following ['trick' suggested by Testinfra maintainer](https://github.com/philpep/testinfra/issues/61#issuecomment-178503626), but it only works for Ansible facts and `group_vars`/`host_vars`, not for variables defined within a role, either in `default/main.yml` or `vars/main.yml`.

Here is an example test for PostgreSQL service:

``` python
def test_postgresql_running_and_enabled(Ansible, Service):
    postgresql_unit_name = Ansible("debug", "msg={{ postgresql_unit_name }}")["msg"]
    postgresql = Service(postgresql_unit_name)
    assert postgresql.is_running
    assert postgresql.is_enabled
```

Variable `postgresql_unit_name` is defined in role's `vars/main.yml`, but apparently the invocation of Ansible through Testinfra is unable to find it. Here is the error:

``` python
    def test_postgresql_running_and_enabled(Ansible, Service):
        postgresql_unit_name = Ansible("debug", "msg={{ postgresql_unit_name }}")["msg"]
        postgresql = Service(postgresql_unit_name)
>       assert postgresql.is_running
E       assert <service 'postgresql_unit_name' is undefined>.is_running
```

Any ideas how I could achieve this?



Discussion:
Hi tjanez,

We load ansible variables files  (defaults and vars)  with the following functions, maybe it works for you:

@pytest.fixture(scope="module")
def AnsibleDefaults(Ansible):
    return Ansible("include_vars","./defaults/main.yml")["ansible_facts"]

@pytest.fixture(scope="module")
def AnsibleVars(Ansible):
    return Ansible("include_vars","./vars/main.yml")["ansible_facts"]

Regards

Raúl Melo

Also, molecule executes `ansible-playbook` with an inventory path of `-i .molecule/ansible_inventory`.  However, in molecule's case this is host inventory.  Only the contents of this file, are available to testinfra.  I would have expected Ansible to provide defaults as part of this inventory.  However, @melodous has come up with a rather creative solution. 

@melodous, thanks for sharing your code, it was very valuable.

I had to adapt your approach a bit since I want to create a role that loads different variables depending on value of `ansible_distribution` and another variable (`postgresql_install_source` in the example code below):

``` python
import os.path

import pytest

@pytest.fixture()
def AnsibleDistribution(Ansible):
    return Ansible("setup")["ansible_facts"]["ansible_distribution"]

@pytest.fixture()
def AnsiblePostgresqlInstallSource(Ansible):
    return Ansible("debug", "msg={{ postgresql_install_source }}")["msg"]

@pytest.fixture()
def AnsibleVars(Ansible, AnsibleDistribution, AnsiblePostgresqlInstallSource):
    if AnsibleDistribution == "fedora":
        vars_file = "fedora.yml"
    elif AnsibleDistribution in ["CentOS", "RedHat"]:
        if AnsiblePostgresqlInstallSource == "centos_scl_repo":
            vars_file = "centos_scl_repo.yml"
        else:
            vars_file = "RedHat.yml"
    else:
        raise ValueError("Unsupported distribution: " + AnsibleDistribution)
    return Ansible("include_vars", os.path.join("./vars/", vars_file))["ansible_facts"]

def test_postgresql_running_and_enabled(Service, AnsibleVars):
    postgresql = Service(AnsibleVars["postgresql_unit_name"])
    assert postgresql.is_running
    assert postgresql.is_enabled
```

I had to modify `.molecule/ansible_inventory` and define the `postgresql_install_source` variable there since this is what gets passed to Testinfra (as @retr0h explained in the [previous comment](https://github.com/metacloud/molecule/issues/151#issuecomment-205863371)).

However, this is not very usable yet since `.molecule/ansible_inventory` is over-written after performing a cycle of commands:

```
molecule destroy
molecule create
molecule converge
```

Is it possible to specify additional variables to put into the inventory file for an instance at the level of `molecule.yml`?
Or is there another approach that would be able to control the values of variables of a role on a particular instance and the values could also be accessed from Testinfra tests?

> Is it possible to specify additional variables to put into the inventory file for an instance at the level of molecule.yml?
> Or is there another approach that would be able to control the values of variables of a role on a particular instance and the values could also be accessed from Testinfra tests?

Not currently, but welcome PRs.

I had an use case in which I wanted to inspect variables set with `set_fact` during the role's tasks. In order to have access to those variables in TestInfra code, the variables are dumped to the instance filesystem as a `post_tasks`of the converge playbook and loaded from filesystem during TestInfra test.

Molecule converge playbook:
```
---
- name: Converge
  hosts: all
  roles:
    - role: <role>

  post_tasks:
    - name: dump
      template:
        src: templates/ansible-vars.yml.j2
        dest: /tmp/ansible-vars.yml
      changed_when: False
```
Note:  `changed_when: False` is required since the the vars being dumped contains variables that change at each Ansible run (timestamp, run duration, ..). Without it the idempotence step will fail.

`templates/ansible-vars.yml.j2`:
```
{{ vars | to_yaml }}
```

`test_default.py`:
```
import os

import yaml

import testinfra.utils.ansible_runner

testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')


def test_ansible_vars(host):

    stream = host.file('/tmp/ansible-vars.yml').content
    ansible_vars = yaml.load(stream)
    assert ansible_vars['var_to_test'] == 'expected_value_of_var'
```
@Perdjesk Very nice!
You can remove the need for an external file by using `copy` instead of `template`:

```yaml
   - name: dump
      copy:
        content: |
          {{ vars | to_yaml }}
        dest: /tmp/ansible-vars.yml
```



Resolving patch:
diff --git a/src/molecule/util.py b/src/molecule/util.py
--- a/src/molecule/util.py
+++ b/src/molecule/util.py
@@ -143,6 +143,7 @@ def run_command(
         else:
             env = cmd.env or env
         args = cmd.cmd
+        cwd = cmd.cwd
         stdout = cmd.stdout
         stderr = cmd.stderr
     else:


Test patch: