Build variable name dynamically and access its content with Ansible - ansible

I have an ansible playbook, which I need to compare values returned from a task to a variable loaded from metadata file.
This metadata can be in any format, and I decided to go with YAML.
What I'm trying to achieve is to build a variable name from another variable + extra stuff and then lookup this value.
I've searched for answers over the web but I couldn't find any. There are also some similar questions here on SO, but they don't address exactly my issue.
Here's the code:
temp_task.yml
---
- name: Temp task
hosts: xenservers
gather_facts: no
vars_files:
- vars/xenservers_metadata.yml
tasks:
- command: ls /home # just a dummy task..
ignore_errors: yes
- set_fact: nic={{ inventory_hostname }}.network
- debug: msg={{ nic }}
- debug: msg={{ xen_perf.network }}
xenservers_metadata.yml
---
- xen:
network:
- xenbr0: "9b8be49c-....-....-...-..."
I'm trying to get the two debug messages print the same thing. One was constructed dynamically by {{ inventory_hostname }}.network while the other is explicit variable I loaded.
TASK [debug] ********************************************************************************************************************************************************
ok: [xen_perf] => {
"msg": "xen.network"
}
TASK [debug] ********************************************************************************************************************************************************
ok: [xen] => {
"msg": [
{
"xenbr0": "9b8be49c-....-....-...-..."
}
]
}
The first debug just prints the string. The second prints the actual data I need. How can I achieve the second data output by constructing the variable/attribute dynamically?

You don’t build the variable name dynamically in your example.
All variables (not facts) are stored in vars structure and you can access them this way:
- debug:
msg: "{{ vars[inventory_hostname].network }}"

Related

Ansible hostvars variable undefined when debugging/templating etc

I'm trying to dynamically generate part of an /etc/hosts file with ansible, by gathering facts from all the hosts and looping through the resulting hostvars to grab the IP of the second interface (I need a private IP on this interface, rather than the public IP of the other interface)
I can grab this information from a single host using the following plays:
- name: play 1
hosts: all
- name: play 2
hosts: localhost
connection: local
become: no
tasks:
- debug:
var: hostvars['mysinglehost'].ansible_all_ipv4_addresses[1]
...but what I'd like to do is loop through all the hosts and get this value from every host, eventually writing this information to my hosts file, but I'd settle for just getting some debug information :)
I tried
...
- debug:
var: hostvars[item].ansible_all_ipv4_addresses[1]
with_inventory_hostnames:
- all
...
...which gives the output I expect, yet when I try to output a msg with this debug task:
...
- debug:
msg: "{{ hostvars[item].ansible_all_ipv4_addresses[1] }}"
with_inventory_hostnames:
- all
...
I get the following error (as I do when I attempt to e.g. write to a file using lineinfile):
fatal: [localhost]: FAILED! =>
msg: |-
The task includes an option with an undefined variable. The error was: 'ansible.vars.hostvars.HostVarsVars object' has no attribute 'ansible_all_ipv4_addresses'
I'm not sure why this is happening, as it's trying to reference the exact same variable. Is there a way to do this?
I've not tested this, but in principle, perhaps this would work?
- hosts: all
- hosts: localhost
gather_facts: false
tasks:
- set_fact:
my_hosts: |
{
{% for a_host in hostvars | dict2items %}
"{{ a_host.key }}": "{{ a_host.value.ansible_all_ipv4_addresses[1] }}",
{% endfor %}
}
Then, instead of looping over hostvars, you just loop over my_hosts?

How can I resolve an inventory member who has a certain attribute?

I need to pass a specific host from my inventory as a parameter into a role. The host is part of a group but is demarcated by a variable that none of the other hosts have.
snippet of: hosts.yml
dbservers:
hosts:
pg01:
ansible_host: pg01.domain.com
master_slave: master
pg02:
ansible_host: pg02.domain.com
master_slave: slave
I want to be able to resolve pg01 based on the fact that the variable master_slave is set to 'master' such that I can call into a role like this:
- name: Do something
include_role:
name: a.database.role.to.run.on.master
vars:
master_database_host: {{ something that resolves to pg01 }}
How can I resolve the appropriate host from inventory?
You can use a mix of filters to extract the host you need:
tasks:
- debug:
msg: '{{groups["group_name"] | map("extract", hostvars) | selectattr("master_slave", "equalto", "master") | map(attribute="inventory_hostname") | list}}'
Step by step:
groups["group_name"] is a list of all the hosts in the group group_name.
map("extract", hostvars) takes hostvars, a dictionary mapping the host to their variables, and extracts the hosts that are in group_name (i.e. groups["group_name"]). This results in a list containing the hosts in group_name mapped to their variables.
selectattr("master_slave", "equalto", "master") selects all hosts who have an attribute master_slave that equals to master. This result in a list with all the hosts that are masters mapped to their variables.
map(attribute="inventory_hostname") takes a list as input and returns the inventory_hostname attribute of every item. This result in a list with all the hosts that are masters.
The play below (with json_query)
- hosts: dbservers
tasks:
- set_fact:
master_database_host: "{{ groups['dbservers']|
map('extract',hostvars)|
list|
json_query('[?master_slave==`master`].inventory_hostname')|
first }}"
- debug:
var: master_database_host
gives
ok: [pg02] => {
"master_database_host": "pg01"
}
ok: [pg01] => {
"master_database_host": "pg01"
}
You can use if else condition in vars to assign the values.
So your play should be Something like this.
- name: Do something
include_role:
name: a.database.role.to.run.on.master
vars:
master_database_host: "{{ hostvars['pg01']['ansible_host'] if \"{{ hostvars['pg01']['master_slave'] }}\" == 'master' else 'default value goes here'}}"
Make sure to use proper escaping to make the conditional statements work.
The reason this works is Since ansible internally uses python to do stuff it is a way to use ternary operator in python.
You could also generate dynamic groups based on the master/slave status:
---
- name: Play to create dynamic groups
hosts: dbservers
gather_facts: false
tasks:
- name: Create groups based on variable master_slave
group_by:
key: "database-{{ hostvars[inventory_hostname]['master_slave'] }}"
- name: Play to use the dynamic group database-master
hosts: database-master
gather_facts: false
tasks:
- name: Show hosts in group
debug:
msg: "This is {{ inventory_hostname }} from the dynamic database-master group."
The first play uses all dbservers and creates the dynamic groups based on the master_slave variable.
The dynamic groups are:
database-master containing pg01
database-slave containing pg02
The second play uses one of the dynamic created groups.
To use group_by the used variable has to exist for all hosts used.
This concept works best on automatic variable gathered by ansibles setup e.g. ansible_distribution to create dynamic groups based on the Distribution (Debian, Redhat, Ubuntu ...) or distribution versions using ansible_distribution_version.

Ansible environment variable or default

How do I get a value from an environment variable, but use a default if the environment variable is unset?
This is an example that does not work
---
- name: a playbook
hosts: all
vars:
build_dir: "{{ lookup('env','BUILD_DIR') | default('builds/1.0.0/LATEST') }}"
tasks:
- debug: msg="{{ build_dir }}"
Running this playbook returns an empty string instead of the default.
$ ansible-playbook build.yml
TASK [debug] ********************
ok: [amber] => {
"msg": ""
}
However, it works as expected to obtain the environment variable.
$ BUILD_DIR=LOL ansible-playbook build.yml
TASK [debug] ****************
ok: [amber] => {
"msg": "LOL"
}
Discovered this that is more concise and easier to read than some other options I have seen
"{{ lookup('env','BUILD_DIR') or 'builds/1.0.0/LATEST' }}"
The last parameter to Jinja's default template built-in function should be true, like this:
vars:
build_dir: "{{ lookup('env','BUILD_DIR')|d('builds/1.0.0/LATEST', true) }}"
Better not to have too many sources of truth, but I always try to set intelligent defaults in defaults/main.yml. I also make frequent use of the default() filter, like this:
db_url : "{{ DB_HOST }}:{{ db_port | default(1521) }}:{{ DB_SVC | default(SID|default('')) }}"
Then a playbook can always overwrite a role's variable with a lookup that defaults to a literal -
vars:
db_port: "{{ lookup('env','db_port')|default('9999') }}"
or with a value dynamically written into a vars_file before the play begins, or into the hosts file or groups file, or on the ansible command-line with --extra-vars, etc.
Look at the variable precedence rules, but be careful not to get too complex if it can be avoided. Flexibility is good, but KISS, else "that way lies madness..."

Ansible - is it possible to register multiple variable?

I have a python script which is returning / print two lists.
test.py
def getHosts():
Pool1=[x.x.x.x, x.x.x.x]
Pool2=[x.x.x.x, x.x.x.x]
Print pool1,pool2
Return pool1,pool2
getHosts()
My playbook looks like:
-task:
name: get the hosts
command: /test.py
register: result
Now, is it possible to fetch out the pool1 and pool2 seperately from the registered variable result ? If yes, please show me an example.
Any help or suggestions will be highly appreciated.
Produce JSON and feed it to Ansible. It will automatically create an appropriate data structure:
---
- hosts: localhost
gather_facts: no
connection: local
tasks:
- command: 'echo { \"Pool1\": \"[x.x.x.x, x.x.x.x]\", \"Pool2\": \"[x.x.x.x, x.x.x.x]\" }'
register: my_output
- set_fact:
my_variable: "{{ my_output.stdout | from_json }}"
- debug:
msg: "Pool1 is {{ my_variable.Pool1 }} and Pool2 is {{ my_variable.Pool2 }}"
Result:
ok: [localhost] => {
"msg": "Pool1 is [x.x.x.x, x.x.x.x] and Pool2 is [x.x.x.x, x.x.x.x]"
}
Depending on how you later use the variable, you might/might not need to from_json filter (see this).

Ansible check if variables are set

I want to automate the installation process of our software for our client. Therefore I wrote an Ansible playbook which has a task which should check if all the mandatory variables are set:
- name: Check environment variables.
hosts: all
vars_files:
- required_vars.yml
tasks:
- fail: msg="Variable '{{ item }}' is not defined"
when: item not in hostvars[inventory_hostname]
with_items:
- required_vars
The required_vars.yml looks like this:
required_vars:
- APPHOME: /home/foo/bar
- TMPDIR: /home/foo/bar/tmp
When I execute the playbook via ansible-playbook -i inventory/dev.yml playbook.yml I get the following error:
TASK [Gathering Facts] *************************************************************************************************************************************************************************************************************************ok: [localhost]
TASK [fail] ************************************************************************************************************************************************************************************************************************************failed:
[localhost] (item=required_vars) => {"changed": false, "failed": true, "item": "required_vars", "msg": "Variable 'required_vars' is not defined"}
It is obvious that I am doing something wrong, but I cannot point to the error. Can you help me please?
Edit: the accepted answer helped me out. Thank you.
But I have two more questions:
The executed task says:
TASK [fail]
skipping: [some_ip] => (item=/root)
skipping: [some_ip] => (item=TMPDIR: /home/foo/bar/tmp)
It is getting skipped because all variables are set, correct?
I think I figured out how to print the correct message, if the variable is not set:
- name: Check environment variables.
hosts: all
vars_files:
- required_vars.yml
tasks:
- fail:
msg: "Variable '{{ item }}' is not defined"
with_items: "{{ required_vars }}"
when: item is undefined
Correct? Or is there a better solution?
Two problems here:
You want to iterate over the value of required_vars variable value, so you need to provide it as an argument to with_items: "{{ required_vars }}":
with_items: "{{ required_vars }}"
Currently you are providing a list of a single element with a statically defined string required_vars.
You need to change the data type of the elements in your required_vars list to strings:
required_vars:
- "APPHOME: /home/foo/bar"
- "TMPDIR: /home/foo/bar/tmp"
Currently (because of : followed by space) you defined dictionaries, so for example in the first iteration item will have a value of { "APPHOME": "/home/foo/bar" }, which will then always fail on the when condition.
Bonus problem:
you defined a message in the form "Variable '{{ item }}' is not defined";
Ansible reports Variable 'required_vars' is not defined;
the above is not an error, as you think ("I get the following error"), but a correct result of the fail module with the message you defined yourself.
Since you have only one value for 'with_items' I think it should look like this:
with_items: "{{ required_vars }}"
On one line and with the brackets and quotation marks. Once you have more then one item, you can use the list like you did:
with_items:
- "{{ one }}"
- "{{ two }}"

Resources