Loop vars with loop_control and loop_var Ansible - loops

I would need to loop some vars inside an include_task.
What is currently working:
- include_tasks: example.yml
loop: "{{ flowers|flatten(levels=1) }}"
loop_control:
loop_var: flower
I would like to achieve something like this:
- include_tasks: example.yml
loop: - "{{ flowers|flatten(levels=1) }}"
- "{{ cars|flatten(levels=1) }}"
loop_control:
loop_var:
- flower
- car
In the example.yml, I will have 2 sections: "flowers" and "cars". In "flowers" I should trigger just "flower" var and in "cars" I should trigger just "car" var.
Is any way to achieve this using loop and loop_var?
Thank you in advance.

You can have one loop_var only. But this loop_var may be any data-structure that fits your problem. You can for example loop combined flowers and cars, and select the elements in the included tasks.

Related

Ansible loop with array

Could someone let me know how we can create a code as below?
- name: TEST1
set_fact:
list_a: "{{ list_a + [item.json.SearchResult.resources] }}"
with_items:
- "{{ source_list.results[0] }}"
- "{{ source_list.results[1] }}"
- "{{ source_list.results[x] }}"
... (unknown how many items in result from API)
vars:
list_a: []
source_list.results[x] comes from an API result. The reason why I need to create an array is that the number of API result is maximum 100. But there are over 500 items.
Note: since we have no idea what you initial data structure looks like exactly, the below might not be 100% fitting your use case. For your next questions, please read How to ask and pay attention to the Minimal, complete and reproducible example section. Thanks
You are taking this the wrong way. Simply extract the attribute you need from each result using the map(attribute=x) Jinja2 filter.
For the below I inferred (see above note) that:
you called your API with ansible.builtin.uri in a loop to get batches of 100 results which are returned as a list in the SearchResult.ressources field
you want in the end a flattened list where all resources are at top level
- name: Show my list of single attributes
ansible.builtin.debug:
var: "source_list.results
| map(attribute='json.SearchResult.resources') | flatten"
You actually don't need to set_fact:
For a single use, just use the above expression directly in the relevant parameter (e.g. loop or a module param....) or eventually declare this in a var at task level.
If you want to reuse this in different parts of your playbook, just declare a var at play level and expand it anywhere once you have called your API and populated the source_list var. In that case, just add a default value to prevent an error if API was not yet called.
Example for the second case above in this pseudo playbook
---
- hosts: localhost
gather_facts: false
vars:
list_a: "{{ source_list.results | d([])
| map(attribute='json.SearchResult.resources') | flatten }}"
tasks:
- name: "This will return an empty list (i.e. [])
as we did not populate source_list yet"
ansible.builtin.debug:
var: list_a
- name: Call our API and register source_list
ansible.builtin.uri:
uri: https://my.api.com/api/v1/some/endpoint
# [... more parameters here ... ]
loop: "{{ my_list_of_ressources }}"
register: source_list
- name: "This will now return a populated list
after calling the API and registering source_list"
ansible.builtin.debug:
var: list_a
Now, to still give a direct answer to your initial question: you can construct that list iteratively inside a set_fact task. This is definitely not efficient as it involves a task running inside a loop (both unneeded as demonstrated above) and possibly on multiple hosts in your play. But for learning purpose, here it is:
- name: very inefficient way to get the same result as above
set_fact:
list_a: "{{ list_a | d([]) + item.SearchResult.resources }}"
loop: "{{ source_list.results }}"
After reading #Zeitounator's answer, I realized I was taking this the wrong way. I used the idea in the answer (json query and flatten) and I changed my task to the below which works as expected.
- name: Create ID list from API result data
set_fact:
list_a: "{{ source_list | json_query('results[*].json.SearchResult.resources[*].id') | flatten }}"
- name: API get again with id
uri:
url: "{{ request_url }}/{{ item }}"
...
register: result_data
with_items:
- "{{ list_a }}"

Ansible: loop trough list of files of different users, sort by date, delete oldest x ones of each user

I am struggling with this issue since several days and would appreciate your help. I would like to check if the crontab file for several users exist on a host. If so, a backup shall be done and the oldest x backups shall be removed.
To achieve this I first check if the crontab exists:
- name: check if crontab exists
ansible.builtin.stat:
path: "/var/spool/cron/tabs/{{ item }}"
loop: "{{ users }}"
register: crontabFile
Then I perform a backup of the file:
- name: backup old crontab
ansible.builtin.copy:
src: "{{ item.stat.path }}"
dest: "{{ item.stat.path }}_{{ ansible_date_time.iso8601_basic_short }}"
mode: "preserve"
remote_src: true
loop: "{{ crontabFile.results }}"
when: item.stat.exists
This works fine so far. Afterwards I check for existing backups:
- name: find backups of crontab
ansible.builtin.find:
paths: "{{ item.stat.path | dirname }}"
patterns: "{{ item.stat.path | basename }}_*"
loop: "{{ crontabFile.results }}"
register: crontabBackup
when: item.stat.exists
The next step would be to loop over the results of crontabBackup and remove the oldest x files of the backups for each user individually.
The following does not work:
- name: delete oldest backups of crontab, keep last 5
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop: "{{ (crontabBackup.results|selectattr('files', 'defined')|map(attribute='files')|flatten|sort(attribute='mtime')|map(attribute='path')|list)[:-5] }}"
What happens is, that a flat list of all files is generated and the oldest files of this list get removed. What I want is to sort the list per user and remove the files per user.
After trying many different approaches, I think I've figured it out for myself. I will share my solution in case anyone is facing the same issue.
The solution is to use an outer loop that includes the inner loop via include_tasks as mentioned here.
# main.yml
- include_tasks: delete_backups_inner_loop.yml
loop: "{{ crontabBackup.results|selectattr('files', 'defined')|map(attribute='files') }}"
loop_control:
loop_var: userLoop
# delete_backups_inner_loop.yml
- name: delete oldest backups of crontab, keep last 5
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ (userLoop|sort(attribute='mtime'))[:-5] }}"

how to pass split values into a loop in ansible based on input range?

I am pretty new to Ansible loops.
I am trying to replace a set of ip addresses (oldip to newip) in a file.
It needs to accept the input ip address as a dynamic list in the form oldip1:newip1,oldip2:newip2,oldip3:newip3...
I want to split the above ip address pair (old:new) and then pass them over to Ansible replace module in a loop based on input list.
I am trying something like this, but am stuck on how to pass the dynamic range of ip address pair into the loop.
My input variable is like 192.123.12.11;192.123.12.20, 192.123.12.12;192.123.12.19 , 192.123.12.13;192.123.12.18 ... (dynamic ip pairs range)
vars:
ip_pairs: oldIP_newIP
tasks:
- debug: "{{ oldIP_newIP.split (',') }}"
- replace:
path: /home/ubuntu/cassandra.properties
regexp: "{{ item.regexp }}"
replace: "{{ item.replace }}"
backup: yes
with_items:
- { regexp = "{{ oldIP }}", replace = "{{ newIP }}"
- { regexp = "{{ oldIP }}", replace = "{{ newIP }}"
.
.
.
.
This loop should continue based on input ip pairs entry (5 times for 5 ip pairs (old;new).
Something really useful you can use in order to achieve this is the fact that you can register variables at the task level, via the vars property.
For exemple:
- name: This will display 'bar', the content of foo
debug:
msg: "{{ foo }}"
vars:
foo: 'bar'
Base on this, you can achieve what you want quite easily doing:
- name: Replace IPs
replace:
path: /home/ubuntu/cassandra.properties
regexp: "{{ oldNewIp[0] | trim }}"
replace: "{{ oldNewIp[1] | trim }}"
backup: yes
vars:
oldNewIp: "{{ item.split(';') }}"
with_items: "{{ ipPairs.split(',') }}"
Please mind that I added some extra Jinja trim in order to cope with the fact that, in your example input, you do have some space after your commas.
Here is a full example play to demo this:
Given the playbook
- hosts: local
vars:
ipPairs: '192.123.12.11;192.123.12.20, 192.123.12.12;192.123.12.19, 192.123.12.13;192.123.12.18'
tasks:
- name: Create an example file to replace in
copy:
dest: /tmp/ips.txt
content: ''
- name: Populate a file with old IPs
lineinfile:
path: /tmp/ips.txt
line: "{{ oldNewIp[0] | trim }}"
vars:
oldNewIp: "{{ item.split(';') }}"
with_items: "{{ ipPairs.split(',') }}"
- name: Replace IPs
replace:
path: /tmp/ips.txt
regexp: "{{ oldNewIp[0] | trim }}"
replace: "{{ oldNewIp[1] | trim }}"
backup: yes
vars:
oldNewIp: "{{ item.split(';') }}"
with_items: "{{ ipPairs.split(',') }}"
I have the following play:
/ # ansible-playbook -i inventory.yaml play.yaml
PLAY [local] *********************************************************************************************
TASK [Gathering Facts] ***********************************************************************************
[WARNING]: Platform linux on host local is using the discovered Python interpreter at /usr/bin/python3,
but future installation of another Python interpreter could change this. See
https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more
information.
ok: [local]
TASK [Create an example file to replace in] **************************************************************
changed: [local]
TASK [Populate a file with old IPs] ***********************************************************************
changed: [local] => (item=192.123.12.11;192.123.12.20)
changed: [local] => (item= 192.123.12.12;192.123.12.19)
changed: [local] => (item= 192.123.12.13;192.123.12.18)
TASK [Replace IPs] ***************************************************************************************
changed: [local] => (item=192.123.12.11;192.123.12.20)
changed: [local] => (item= 192.123.12.12;192.123.12.19)
changed: [local] => (item= 192.123.12.13;192.123.12.18)
PLAY RECAP ***********************************************************************************************
local : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
/ # cat /tmp/ips.txt
192.123.12.20
192.123.12.19
192.123.12.18

How to create a loop inside a loop in ansible and export variables?

So basically, I have these two dictionaries:
dict_1:
- name: one
- name: two
dict_2:
- identifier: one
type: typeA
- identifier: two
type: typeB
I need to iterate over both and "export" the vars to process.yml. This is what I am doing right now but not working properly:
#main.yml
- name:
include_tasks: process.yml
loop: "{{ dict_1 | flatten }}"
loop_control:
loop_var: entity
loop: "{{ dict_2 | selectattr('identifier', 'equalto', entity) | map(attribute='type') | flatten }}"
loop_control:
loop_var: type
#process.yml
- name: Print variables
debug:
msg: "{{ entity }}_{{ type }}"
What I wanted to see after this was:
one_typeA
two_typeB
How can I get this result from the loop?
I think your original solution is close, just a little more complicated than it needs to be. Please note that this solution assumes that there will not be more than one item from dict2 that you will be finding using the value from dict1, it's just the order that you can't predict.
- debug: msg="{{ item.name }}_{{ dict_2 | selectattr('identifier', 'equalto', item.name) | map(attribute='type') | list | join() }}"
with_items: "{{ dict_1 }}"
If you CAN control order of the lists, this gets even easier.
- debug: msg="{{ item.0.name }}_{{ item.1.type }}"
with_together:
- "{{ dict_1 }}"
- "{{ dict_2 }}"
And if you can control the order and the content, and you want to have every possible combination of the two lists, you can do this:
- debug: msg="{{ item.0.name }}_{{ item.1.type }}"
with_cartesian:
- "{{ dict_1 }}"
- "{{ dict_2 }}"

How to use with_nested on 2 dicts to get value in ansible?

I have the following 2 maps setup
prefix_color:
eu_: "blue"
us_: "red"
country_shade:
sweden: "light"
belgium: "dark"
I want to loop through both maps in the same manner one can do with with_nested in ansible but I need to access both the key and value from both dicts.
Essentially what I need is something like this
- name: loop
debug:
msg: "key: {{ item[0].key + item[1].key }}, value: {{ item[0].value + item[1].value }}"
with_nested:
- "{{ prefix_color }}"
- "{{ country_shade }}"
The above does not work as only the keys are available with with_nested and with_dict does not allow for more than 1 dictionary.
How does one go about this? It seems that it should be something simple to do given that both concepts of nested loops and dictionaries exists in ansible.
You can use dictsort to convert dictionaries into lists:
- name: loop
debug:
msg: "key: {{ item[0] + item[2] }}, value: {{ item[1] + item[3] }}"
with_nested:
- "{{ prefix_color | dictsort }}"
- "{{ country_shade | dictsort }}"
In your setup original keys will be at even indices, and values – at uneven.

Resources