Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: accessing values of variables as they are being used for provisioning an instance inside Testinfra tests #151

Closed
tjanez opened this issue Apr 5, 2016 · 7 comments · Fixed by #3313

Comments

@tjanez
Copy link

tjanez commented Apr 5, 2016

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, 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:

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:

    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?

@melodous
Copy link

melodous commented Apr 5, 2016

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

@retr0h
Copy link
Contributor

retr0h commented Apr 5, 2016

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.

@tjanez
Copy link
Author

tjanez commented Apr 6, 2016

@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):

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

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?

@retr0h
Copy link
Contributor

retr0h commented May 13, 2016

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.

@retr0h retr0h closed this as completed May 13, 2016
@Perdjesk
Copy link

Perdjesk commented Apr 3, 2019

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

@abeluck
Copy link

abeluck commented Mar 27, 2020

@Perdjesk Very nice!
You can remove the need for an external file by using copy instead of template:

   - name: dump
      copy:
        content: |
          {{ vars | to_yaml }}
        dest: /tmp/ansible-vars.yml

@akerouanton
Copy link
Contributor

Once #3313 got merged, you can use the following line to get all the vars defined for a given host (including host/group vars and recursive variable expansion):

# molecule/defaults/group_vars/my_group.yml
foo: foo
bar: "{{ foo }}bar"
ansible_vars = host.ansible('debug', 'msg={{ hostvars[inventory_hostname] }}')
print(ansible_vars['msg']['bar']) # Will output: foobar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants