Ansible - iterate over a list of dictionaries - ansible

I built the following list but I don't succeed to iterate over it.
Should I use with_items? with_elements? or something else?
My goal is to iterate over all the hosts in the inventory, get their name and their IP, and finally print it.
- set_fact:
list_of_hosts: |
{% set myList = [] %}
{% for host in groups['all'] %}
{% set ignored = myList.extend([{'server_name': host, 'server_ip': hostvars[host].ansible_eth0.ipv4.address }]) %}
{% endfor %}
{{ myList }}
- debug: msg="{{ item.server_name }}"
with_items: "{{ list_of_hosts }}"
Here is my list when I debug it:
TASK [common : debug] ************************************************************************************************
ok: [my1stServer] => {
"msg": " [{'server_ip': u'192.168.0.1', 'server_name': u'my1stServer'}, {'server_ip': u'192.168.0.2', 'server_name': u'my2ndServer'}]\n"
}
And here is the error but it is not really relevant :
fatal: [my1stServer]: FAILED! => {"failed": true, "msg": "the field 'args' has an invalid value, which appears to include a variable that is undefined. The error was: 'ansible.vars.unsafe_proxy.AnsibleUnsafeText object' has no attribute 'server_name'\n\nThe error appears to have been in 'hosts.yml': line 19, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n- debug: msg=\"{{ item.server_name }}\"\n ^ here\nWe could be wrong, but this one looks like it might be an issue with\nmissing quotes. Always quote template expression brackets when they\nstart a value. For instance:\n\n with_items:\n - {{ foo }}\n\nShould be written as:\n\n with_items:\n - \"{{ foo }}\"\n"}

Please forgive me for bluntness, but the proposed implementation makes it an effort to understand what the idea actually is. which is simple: to print some variables anyway present in hostvars[host] for a list of hosts picked by various criteria.
If we keep implementation close to that idea, implementation is simpler.
So what I'd do to create a list of hosts picked by group membership, or possibly 'hand picked' is to actually do what I just wrote :).
Consider this task list:
# this task creates an empty list
- name: create my_cool_list
set_fact:
my_cool_list: []
# this task adds to the list all hosts in groups we're iterating over
- name: update my cool list with whole groups
set_fact: '{{my_cool_list + groups[curr_grp]}}'
with_items:
- grp1
- grp2
loop_control:
loop_var: curr_grp
# this task adds to the list all hosts we're iterating over
- name: update my cool list with specific hosts
set_fact: '{{my_cool_list + [curr_node]}}'
with_items:
- node001
- node101
loop_control:
loop_var: curr_node
# now we can iterate over the list, accessing specific fields on each host
- name: go over my cool list and print ansible_init_mgr
debug:
msg: 'hostvars["{{curr_host}}"].ansible_init_mgr: {{hostvars[curr_host].ansible_init_mgr}}'
with_items: '{{my_cool_list|default([], true)|list}}'
Furthermore, you can add safety when: by validating the key you're accessing is defined..
And, to print a selection of variables about each host, you should use jinja filter map('extract',...):
- name: print some vars from each host
debug:
msg: {'server_name': '{{hostvars[curr_node]|selectattr("ansible_hostname")}}', 'ip_address': '{{hostvars[curr_node]|selectattr("ansible_eth0.ipv4.address")}}'}
with_items: '{{my_cool_list|default([], true)|list}}'
loop_control:
loop_var: curr_node
IF you want to increase readability though, you better write a filter plugin, which would do the above stuff, and hide iteration ugliness in readable way, so you can have:
Either (For generic approach, i.e. without renaming attributes)
- name: print some vars from each host
debug:
msg: '{{my_cool_list|multi_select_in_dict(["ansible_hostname", "ansible_eth0.ipv4.address")}}'
Or specific approach (so that you are using specific hard coded remapping of attributes...)
- name: print some vars from each host
debug:
msg: '{{my_cool_list|my_cool_filter}}'

Related

Ansible conditionals with nested loops

I've read the ansible docs on conditionals and loops. But it's still not clear to me how it exactly works.
my yaml structure looks like this:
---
myusers:
- username: user1
homedir: 'home1'
sshkey: 'ssh-rsa bla1'
- username: user2
homedir: 'home2'
sshkey: 'ssh-rsa bla2'
process:
- transfer:
transtype: 'curl'
traname: 'ftps://targetsystem'
my playbook part looks like this:
- name: test j2
debug:
msg: |-
dest: "/var/tmp/{{ item.0.username }}/{{ item.1.traname }} {{ item.1.transtype }}"
when: item.0.process is not none
loop: "{{ myusers | subelements('process')}}"
Now I only want to loop when the sub-element process exists. I had this working at one point but don't understand what I changed to break it.
Mainly I don't understand what the effect of the sequence of 'when' and 'loop' has. It appears to me when I run it that the condition 'when' is ignored. Also when I swap the sequence of when and loop.
The error I get when running the playbook is :
FAILED! => {"msg": "could not find 'process' key in iterated item {u'username': u'user1' ...
I've also tried with different conditions like:
item.0.process is defined
myusers.username.process is not none
etc...
By default, the subelements filter (and the corresponding lookup) requires each top level element to have the subelement key (and will error with the above message if it does not exist)
You can change this behavior by setting the skip_missing parameter (note: I also fixed the index to address the traname key which was the wrong one in your question example)
- name: test j2
debug:
msg: |-
dest: "/var/tmp/{{ item.0.username }}/{{ item.1.traname }} {{ item.1.transtype }}"
loop: "{{ myusers | subelements('process', skip_missing=true) }}"

Remove last character from variable

I am trying to remove the last character of the below output but I am having trouble doing it:
- debug:
msg: "{{ ansible_mounts|json_query('[?mount == `/`].device') }}"
register: rootpart
The below works with simple text:
- debug:
msg: "{{ '/dev/sda4'[:-1] }}"
But not with a variable:
- debug:
msg: "{{ rootpart[:-1] }}"
Error:
msg: Unexpected templating type error occurred on ({{ rootpart[:-1] }}): unhashable type: 'slice'
There is a big "don't" in your current attempt: you should not register on a debug task.
If you want to create a new variable, then use the set_fact module.
This is not only a better way to do it, it is also saving you from having a dictionary with the keys changed, failed and msg to dive into in order to get the variable you were expecting out of the msg property.
Then, your json_query is going to return you a list of devices, no matter if there is only one match thanks to your filter. So, you also need to get the first element of this list.
So, with all this, here are your two tasks:
- set_fact:
root_part: "{{ ansible_mounts | json_query(_query) }}"
vars:
_query: "[?mount == `/`].device | [0]"
- debug:
var: root_part[:-1]

Jinja templates in ansible loop

I need to run an ansible loop based on input from a CSV file. I am using the following question / answer as reference. However, I cannot seem to figure out where to actually include the jinja part for the loop.
So far this is what I have, but it throws an error:
---
- hosts: localhost
connection: local
gather_facts: no
vars:
csv_var: "{{ lookup ('file', 'file.csv') }}"
tasks:
- debug:
msg: "{{ item }}"
with_items:
- {% set list = csv_var.split(",") %}
file.csv has the following content: 345,1234,1234
Ideally the message should print out the numbers above.
The syntax error I was getting is:
The offending line appears to be:
with_items:
- {% set list = csv_var.split(",") %}
^ here
exception type: <class 'yaml.scanner.ScannerError'>
exception: while scanning for the next token
found character that cannot start any token
in "<unicode string>", line 19, column 10
You should use Jinja2 expression not a statement.
You should also quote any string that starts with { in Ansible:
- debug:
msg: "{{ item }}"
with_items: "{{ csv_var.split(',') }}"
And there is no need to wrap the resulting list in another list (dash before element), although Ansible handles this automatically.

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 }}"

Ansible with_items if item is defined

Ansible 1.9.4.
The script should execute some task only on hosts where some variable is defined. It works fine normally, but it doesn't work with the with_items statement.
- debug: var=symlinks
when: symlinks is defined
- name: Create other symlinks
file: src={{ item.src }} dest={{ item.dest }} state=link
with_items: "{{ symlinks }}"
when: symlinks is defined
But I get:
TASK: [app/symlinks | debug var=symlinks] *********************
skipping: [another-host-yet]
TASK: [app/symlinks | Create other symlinks] ******************
fatal: [another-host-yet] => with_items expects a list or a set
Maybe I am doing something wrong?
with_items: "{{ symlinks | default([]) }}"
The reason for this behavior is conditions work differently inside loops. If a loop was defined the condition is evaluated for every item while iterating over the items. But the loop itself requires a valid list.
This is also mentioned in the docs:
Note that when combining when with with_items (see Loops), be aware that the when statement is processed separately for each item. This is by design:
tasks:
- command: echo {{ item }}
with_items: [ 0, 2, 4, 6, 8, 10 ]
when: item > 5
I think this is a bad design choice and for this functionality they better should have introduced something like with_when.
As you have already figured out yourself, you can default to an empty list.
with_items: "{{ symlinks | default([]) }}"
Finally if the list is dynamically loaded from a var, say x, use:
with_items: "{{ symlinks[x|default('')] | default([])}}"
This will default to an empty list when 'x' is undefined
Accordingly, fall back to an empty dict with default({}):
# service_facts skips, then dict2items fails?
with_dict: "{{ ansible_facts.services|default({})|dict2items|selectattr('key', 'match', '[^#]+#.+\\.service')|list|items2dict }}"

Resources