Using a variable from one play across other plays

I’m getting my ansible_become_pass from my Bitwarden vault successfully, but I want to re-use it across plays within the playbook. To do this, I wanted to set it like so, but I think I’m trying to fetch it incorrectly. I tried also to set delegate_facts: true but it did not work.

- name: Testing Bitwarden collection
  hosts: ansible_test_vm
  tasks:

    - name: Get password for "badmin - Lab User" from Bitwarden
      ansible.builtin.set_fact:
        bw_data: >-
          {{ lookup('community.general.bitwarden', 'badmin - Lab User', field='password')[0] }}
        cacheable: true
      delegate_to: localhost

    - name: Check if `/etc/profile.d/ps1.sh` exists
      ansible.builtin.stat:
        path: /etc/profile.d/ps1.sh
      become: true
      register: stat_ps1_sh

  vars:
    ansible_become_pass: "{{ hostvars['localhost']['bw_data'] }}"

The error I receive is:

TASK [Gathering Facts] ************************************************************************************************************
fatal: [ansible]: FAILED! => {"msg": "The field 'become_pass' has an invalid value, which includes an undefined variable.. 'ansible.vars.hostvars.HostVarsVars object' has no attribute 'bw_data'"}

Trying to call the variable this way causes Ansible to complain that the variable is empty.

  vars:
    ansible_become_pass: "{{ bw_data }}"

like so:

fatal: [ansible]: FAILED! => {"msg": "The field 'become_pass' has an invalid value, which includes an undefined variable.. 'bw_data' is undefined"}

I had this working prior but I wanted to try delegating the play instead of running in a different host section so it’s a shorter file. Really just playing around to be honest. Working playbook below:

- name: Testing Bitwarden collection
  hosts: localhost
  tasks:

    - name: 'Get password for "badmin - Lab User" from Bitwarden'
      ansible.builtin.set_fact:
        bw_data: >-
          {{ lookup('community.general.bitwarden', 'badmin - Lab User', field='password')[0] }}
        cacheable: true

- name: Use Bitwarden pass from task
  hosts: ansible_test_vm
  tasks:

    - name: Check if `/etc/profile.d/ps1.sh` exists
      ansible.builtin.stat:
        path: /etc/profile.d/ps1.sh
      become: true
      register: stat_ps1_sh

  vars:
    ansible_become_pass: "{{ hostvars['localhost']['bw_data'] }}"

How do I call this variable correctly, or is there a better way to go about this? I’m fine using the second (working) method if that’s what I’m meant to do.

short:
you have the correct way

long:
Ansible has 2 variable scopes, play objects and hosts, play objects variables are inherited only by other play objects contained in the scope defined (think functions and embeded functions). Since plays are at the same level, not contained in other plays, you cannot pass variables from one play to the other.

Host scope is sustained through the run and accessible via the hostvars variable, so variables persist across plays within the same run, this is the proper way to ‘persist’ variables across plays.

Your first attempt is failing because while you delegate execution to localhost you do not delegate_facts to it, so the facts are applied to the ‘current host’ aka inventory_hostname, do that and both ways 'will work’TM.

1 Like

When I am creating playbooks, I try to avoid using hostvars - I consider this an internal structure of ansible.
Here is my approach to do what you are trying to do:

---
- name: Playbook with all shared variables (does not do anything)
  hosts: all
  gather_facts: false
  vars: &shared_playbook_variables
    bitwarden_password: "{{ lookup('community.general.bitwarden', 'badmin - Lab User', field='password')[0] }}"
    bitwarden_password_for_current_host: "{{ lookup('community.general.bitwarden', ansible_hostname, field='password')[0] }}"
    ansible_become_pass: "{{ bitwarden_password }}"
  tasks: []

- name: First playbook
  hosts: all
  gather_facts: false
  vars:
    <<: *shared_playbook_variables
  tasks:
    - name: Simple debug for static variable
      ansible.builtin.debug:
        var: ansible_become_pass
    - name: Debug for variable that depends on the ansible hostname
      ansible.builtin.debug:
        var: bitwarden_password_for_current_host

- name: Second playbook
  hosts: all
  gather_facts: false
  vars:
    <<: *shared_playbook_variables
  tasks:
    - name: Simple debug for static variable
      ansible.builtin.debug:
        var: ansible_become_pass
    - name: Debug for variable that depends on the ansible hostname
      ansible.builtin.debug:
        var: bitwarden_password_for_current_host

Ansible playbooks are yaml files, so yaml anchoring can be used. Here is an explanation from bitbucker YAML anchors | Bitbucket Cloud | Atlassian Support.

In playbook above there are two variables - one is static bitwarden_password - we get the password only once, another one bitwarden_password_for_current_host depends on host.
First static variable, will not change from one place to another, but ansible will anyway get it for the second (and third time) because of its lazy evaluations. But this is a good thing, because we can use this feature in “dynamic” variable bitwarden_password_for_current_host - there isansible_hostname magic variable that will be filled on each run with correct info. So for each host there will be correct call to lookup plugin (and correct password for the host).

1 Like

So maybe I’m missing something else or have a typo I don’t see? I tried setting delegate_facts: true on the play where I get the password from Bitwarden but that doesn’t make it to the ansible_become_pass variable.

    - name: Get password for "badmin - Lab User" from Bitwarden
      ansible.builtin.set_fact:
        bw_data: >-
          {{ lookup('community.general.bitwarden', 'badmin - Lab User', field='password')[0] }}
        cacheable: true
      delegate_to: localhost
      delegate_facts: true

    - name: Test variable
      ansible.builtin.debug:
        msg: "{{ hostvars['localhost']['bw_data'] }}"

  vars:
    ansible_become_pass: "{{ hostvars['localhost']['bw_data'] }}"

I print the expected value in the test print statement when I run it (cheated by commenting out the ansible_become_pass to test), but I get the same error as before when running the playbook.

fatal: [ansible]: FAILED! => {"msg": "The field 'become_pass' has an invalid value, which includes an undefined variable.. 'ansible.vars.hostvars.HostVarsVars object' has no attribute 'bw_data'"}

There are of course different definitions of what scope is.
Don’t you thing that example above demonstrates that there is also a “file” scope in ansible (or yaml)?

Hi kks, would each playbook be a separate .yaml file and I would call them when I run everything, or is this all in one file? I’ll give that method a go too, I like the idea of using ansible hosts for a variable when searching for a password.

no, there is no ‘file’ scope, as you can have multiple files per run ansible-playbook play1.yml play2.yml the host scope is ‘per run’ as i mentioned before, the files are not relevant for scope as much as the contained playbook objects are, the scope would be the same in the run if the plays were defined in the same file or in different files.

What you are doing with YAML is pre generating a copy of the variables with YAML anchors, this is transparent to Ansible itself, this is not keeping variables in ‘file scope’ but using a property of YAML to make several copies of the same variables.

1 Like

in my example all the plays should be in one file.

In order to make this example work for several playbooks files you can add variable to run:

ansible-playbook playbook1.yml playbook2.yml --extra-vars "@variables.yaml"

Variables file should look something like this:

bitwarden_password: "{{ lookup('community.general.bitwarden', 'badmin - Lab User', field='password')[0] }}"
bitwarden_password_for_current_host: "{{ lookup('community.general.bitwarden', ansible_hostname, field='password')[0] }}"
ansible_become_pass: "{{ bitwarden_password }}"

Similarly this can be archived with inventory files, just set variables you need for group “all”. Inventory can span across playbook runs.
Do not worry to set variables to something that seems undefined, ansible will only read and evaluate variables when they are needed. Basically everywhere you can add variable:

this_variable_is_never_used: put here whatevery, just valid yaml syntax, this will not have any influence for playbook run
1 Like

It’s failing before set_fact even runs. To fix, you could do any of the following:

  • set vars on the block-level (instead of the whole play)
  • set gather_facts: False on the play if you don’t need to gather facts or
  • use | default(omit) in the ansible_become_pass

You’ll also need to use delegate_facts with set_fact for "{{ hostvars['localhost']['bw_data'] }}" to work.

2 Likes

I’ve opened a possible fix so the undefined variable set on the play won’t be a problem, as long as gather_facts isn’t using a become plugin Only set become_ options on PlayContext if the task uses become by s-hertel · Pull Request #86618 · ansible/ansible · GitHub.