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.
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:
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.
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.
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
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).
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.
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.
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
In AWX, we “use” (feels more like “abuse”) ansible.builtin.set_stats to pass computed values from one job to a downstream job in the same workflow. This seems like a related thing, but I’ve not seen anything that allows tasks to see job “stats”. Rather this is an extra “thing” that AWX does outside of plays/playbooks.
Ignoring the obvious “don’t set_stats secret values”, would someone who know how these things actually work care to compare and contrast facts as facts, facts as vars, and stats?
Hey this worked great! It felt really slow but I might need to debug that more. The bitwarden plugin is known to be pretty slow but I was only expecting initial slowness during the lookup.
I’m unsure if I’m understanding anchoring correctly. I understand that we’re anchoring our variables up top, then referencing them within a subsequent play via <<: *shared_playbook_variables. We’re not running the lookup from Bitwarden again, right? The Bitbucket doc says <<: is overriding things, but I think that means we’re overriding with what we already looked up.
unfortunately this is exactly what is happening, everytime ansible reads a variable that depends on that lookup, ansible will run the lookup again - this is a lazy nature of ansible. One way to not do lazy evaluations is to use set_fact plugin.
Ancoring is yaml feature, not ansible, from ansible point of view you had this variable definition typed several times. And this can be used to your advantage! You can “copy” any yaml content this way, here is an example for an action that is copied across several playbooks.
---
- name: Playbook with all shared variables (does not do anything)
hosts: all
gather_facts: false
tasks:
- &set_fact_bitwarden
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] }}
delegate_to: localhost
- name: First playbook
hosts: all
gather_facts: false
tasks:
- *set_fact_bitwarden
# This will copy the set_fact action from above, consider is copied verbatim from the playbook above, you can use it in many playbooks.
- name: Print bw_data
ansible.builtin.debug:
var: bw_data
- name: Second playbook
hosts: all
gather_facts: false
tasks:
- *set_fact_bitwarden
- name: Print bw_data
ansible.builtin.debug:
var: bw_data
The YAML anchoring happens BEFORE any execution or templating on the Ansible side, that is basically the same as writing YAML, it would result in the same thing as you manually copying and pasting the entry.
Whole document is worth reading, but this fragment is most relevent in this case
# Anchors can be used to duplicate/inherit properties
base: &base
name: Everyone has same name
# The expression << is called 'Merge Key Language-Independent Type'. It is used to
# indicate that all the keys of one or more specified maps should be inserted
# into the current map.
# NOTE: If key already exists alias will not be merged
foo:
<<: *base # doesn't merge the anchor
age: 10
name: John
bar:
<<: *base # base anchor will be merged
age: 20
That’s what I was trying prior with the hostvars, but unfortunately I hit a snag block with my second attempt using delegation instead of distinct plays.
While I really like what you suggested, because the Bitwarden plugin is so slow, it’s a bit like watching paint dry now.
edit, I totally misread your new code block, let me give that a try
So this new way it still does the lookup at the start of each play, but not for each task (I omitted some tasks below). That’s intended, but much more bearable and I suspect this scales well if I’m using a variable for the key I look up (such as the host name in your example)?
---
- name: Testing Bitwarden lookups
hosts: localhost
gather_facts: false
tasks:
- &set_fact_bitwarden
name: Get Bitwarden password for badmin lab user
ansible.builtin.set_fact:
ansible_become_pass: >-
{{ lookup('community.general.bitwarden', 'badmin - Lab User', field='password')[0] }}
delgate_to: localhost
- name: Test using Bitwarden varaible
hosts: ansible
gather_facts: false
tasks:
- *set_fact_bitwarden
- name: Check if `/etc/profile.d/ps1.sh` exists
ansible.builtin.stat:
path: /etc/profile.d/ps1.sh
become: true
register: stat_ps1_sh
Yes that will work with hostnames as variables in set fact. Just bear in mind that set_fact “fixes” variable value so if one of the variables password depends on changed password will not be recalculated
Hmm, why not add a play at the very top with hosts: all, a single task set_facts from lookup but with run_once true? This would call the task just once for a random host (typically first) without really connecting to it (as this is set_facts). Due to run_once it will set a host-bound variable for all hosts targeted by the inventory, so all hosts in this example. In AWX you might consider using credentials for that, as injectors allow specifying hostvars and environment variables…