Ansible nested loops, how to set inner loop variable based on outer variable - ansible

I'm having problems with a nested loop on ansible.
I'm using ansible 2.5.2 with the following config files:
file hosts:
[group1]
host1
host2
host3
[group2]
hostA
file host_vars/host{N} (where N is the number for each host on group1):
variable:
- { line: "keyB" , file: "keyc"}
- { line: "key2" , file: "key3"}
I need to execute on hostA one task for each line in host_vars/host{N}.
in pseudo code, need something like this:
- name: modify file
for host in groups['group1']:
for item in host['variables']:
lineinfile: "path={{ host }}/{{ item.file }} line={{ item.line }}"
using jinja2 loops does not work:
- name: Modify files
lineinfile: "{% for linea in hostvars[item]['variables'] %}
path={{ item }}/{{ linea.file }}
line={{ linea.line }}
{% endfor %}"
loop: "{{ groups['group1'] }}"
normal nested loops does not work either because the inner loop deppends on the hostname:
- name: Modify files
lineinfile: "path={{ item[0] }}/{{ item[1]['file'] }} line={{ item[1]['line'] }}"
with_nested:
- "{{ groups['group1'] }}"
- "{{ hostvars[item[0]]['variables'] }}"
There is another way to cicle nested loops?

I solved my problem using loop_control,
I add a new file: inner.yml
- name: Modify files
lineinfile: "path={{ outer_item }}/{{ item.file }} line={{ item.line }}"
loop: "{{ hostvars[outer_item]['variables'] }}"
And defined my task file as follows
- include_tasks: inner.yml
loop: "{{ groups['group1'] }}"
loop_control:
loop_var: outer_item
which solves my problem of using two nested variables on loops.

Related

Ansible - loop over multiple items in stdout_lines

I am performing a grep with multiple items.
---
- hosts: my_host
gather_facts: false
vars:
my_list:
- whatever
- something
tasks:
- name: grep for item in search path
shell: "grep -rIL {{ item }} /tmp"
register: the_grep
loop: "{{ my_list }}"
- debug:
msg: "{{ item.stdout_lines }}"
loop: "{{ the_grep.results }}"
Depending on the result, multiple files could match.
msg:
- /tmp/something.conf
- /tmp/folder/file.txt
Q: How would I configure Ansible to loop over the items in stdout_lines?
The use case I'm solving is to delete .ini sections based on the item, but in this case, Ansible doesn't loop over the stdout_lines.
- name: remove stanza from ini file
ini_file:
path: "{{ item.stdout_lines }}"
section: "{{ item.item }}"
mode: '0600'
state: absent
loop: "{{ the_grep.results }}"
when: item.stdout_lines | length > 0
It seems that this doesn't work, but configuring item.stdout_lines[0] gives the partially expected result, since Ansible will use only the first item in that list. But ofc, not the 2nd and so on.
Perhaps there's a prettier answer, but solved it by using with_nested and creating a json_query:
- name: remove stanza from ini file
ini_file:
path: "{{ item.0 }}"
section: "{{ item.1.item }}"
mode: '0600'
state: absent
with_nested:
- "{{ the_grep | json_query('results[].stdout_lines[]') }}"
- "{{ the_grep.results }}"

Conditional to add_host module

I have an inventory that i need to add a conditional to.
My code:
- name: Create memory inventory
add_host:
name: "{{ item.0.key }}"
group: target_hosts
with_nested:
- "{{ lookup ('dict', hosts, wantlist=True) }}"
But I want something like:
- name: Create memory inventory
add_host:
name: "{{ item.0.key }}"
{% if item.0.value.OS_Choice[:3] == 'win' %}
group:
- target_hosts
- win
{% else %}
group:
- target_hosts
- linux
{% endif %}
with_nested:
- "{{ lookup ('dict', hosts, wantlist=True) }}"
With this configuration, Ansible errors:
The offending line appears to be:
{% if item.0.value.OS_Choice[:3] == 'win' %}
^ here
Any ideas on how I can implement this conditional?
You're mixing up Jinja2 with YAML. Here you go:
- name: Create memory inventory when win
add_host:
name: "{{ item.0.key }}"
with_nested:
- "{{ lookup ('dict', hosts, wantlist=True) }}"
when: item.0.value.OS_Choice[:3] == 'win'
vars:
group:
- target_hosts
- win
- name: Create memory inventory when not win
add_host:
name: "{{ item.0.key }}"
with_nested:
- "{{ lookup ('dict', hosts, wantlist=True) }}"
when: item.0.value.OS_Choice[:3] != 'win'
vars:
group:
- target_hosts
- linux
However, Ansible gather facts about the OS already. Perhaps you want to use those, instead of configuring something like this yourself.
Building up on Kevin's answer (and fixing some wrongly placed parameters)
You should definitely do this differently, like creating dynamic groups based on detected OS in facts. See:
the group_by module
the ansible_distribution* facts that you can explore as an example with
ansible localhost -m setup -a filter="ansible_distribution*"
Meanwhile, with your current logic, you can still do this in a single task:
- name: Create memory inventory
vars:
additional_group: >-
{{ (item.0.value.OS_Choice[:3] == 'win') | ternary('win', 'linux') }}
add_host:
name: "{{ item.0.key }}"
groups:
- target_hosts
- "{{ additional_group }}"
with_nested:
- "{{ lookup ('dict', hosts, wantlist=True) }}"

Can we have 2 with_items in ansible in a single task

Below is the condition
- name: Find the image
slurp:
src: "{{ IMAGE }}"
register: slurp_results
- name: Upload image
shell: |
skopeo copy -docker-archive:{{ item }}.tar docker://{{ URL }}/TESTIMAGE
with_items: "{{ (slurp_results.content|b64decode).splitlines() }}"
The above code works.
But I would need "TESTIMAGE" also to be replaced as {{ item }} like below.
skopeo copy -docker-archive:{{ item }}.tar docker://{{ URL }}/{{ item }}
How to define 2 with_items in a single shell task with 2 different slurp results
I believe you can by using the subelements module. Here is a link. Try going by this example:
- name: Setup MySQL users, given the mysql hosts and privs subkey lists
mysql_user:
name: "{{ item.0.name }}"
password: "{{ item.0.mysql.password }}"
host: "{{ item.1 }}"
priv: "{{ item.0.mysql.privs | join('/') }}"
with_subelements:
- "{{ users }}"
- mysql.hosts
Users is referred to as item.0 and hosts as item.1 and so on.

Pass dictionary to jinja2 template from task loop

I pass dictionary from play to task. I use loop to call another task from separate yml file again passing the dictionary. From there I call Jinja2 template and pass the dictionary again. I cannot access the dictionary values from Jinja2.
I tried to pass the dictionary to template with_items and with_dict. Still the same problem.
play:
- role: example
vars:
brands:
brand_1:
name: "brand1"
brand_2:
name: "brand2"
brand_3:
name: "brand_3"
task in role with loop:
- name: Loop through configuration files
include_tasks: generate_config_files.yml
loop: "{{ lookup('dict', brands) }}"
loop_control:
loop_var: outer_item
generate_config_files.yml
- name: Generate the configuration files
template:
src: "consumer.properties.j2"
dest: "{{ kafka_location }}/{{ item.key }}/consumer.properties"
owner: "{{ kafka_user }}"
group: "{{ kafka_group }}"
mode: 0644
with_dict: "{{ outer_item }}"
consumer.properties.j2
{% for item in outer_item %}
Name: "{{ item.name }}"
{% endfor %}
I expect to access the dictionary value in template and generate the same file with different values based on number of brands in dictionary. So if there are 3 brands I expect to generate 3 files with different Name: inside.
Unfortunately I am getting:
"msg": "AnsibleUndefinedVariable: 'str object' has no attribute 'name'"
Any ideas?
1) Indentation of vars: is wrong.
2) The single loop does the job.
3) Iteration in the template is not necessary.
4) Numeric mode must be quoted mode: '0644'.
The playbook below
- hosts: localhost
roles:
- role: example
vars:
kafka_user: admin
kafka_group: admin
kafka_location: /scratch
brands:
brand_1:
name: "brand1"
brand_2:
name: "brand2"
brand_3:
name: "brand_3"
with tasks
$ cat roles/example/tasks/main.yml
- include_tasks: generate_config_files.yml
, with the included task
$ cat roles/example/tasks/generate_config_files.yml
- name: Generate the configuration files
template:
src: "consumer.properties.j2"
dest: "{{ kafka_location }}/{{ item.key }}/consumer.properties"
owner: "{{ kafka_user }}"
group: "{{ kafka_group }}"
mode: '0644'
loop: "{{ brands|dict2items }}"
, and with the template
$ cat roles/example/templates/consumer.properties.j2
Name: "{{ item.value.name }}"
gives
$ tree /scratch/brand_*
/scratch/brand_1
└── consumer.properties
/scratch/brand_2
└── consumer.properties
/scratch/brand_3
└── consumer.properties
$ cat /scratch/brand_*/consumer.properties
Name: "brand1"
Name: "brand2"
Name: "brand_3"
Is this what you're looking for?

How to extract the output from stdout.lines in ansible

---
- name: Mikrotik info
hosts: mikrotik
connection: network_cli
remote_user: root
gather_facts: false
tasks:
- name: show info
routeros_command:
commands: /system routerboard print
register: rb_info
- name: Debug info
debug:
msg: "{{ rb_info.stdout_lines }}"
Output:
routerboard: yes
model: 751G-2HnD
serial-number: 3A6502B2A2E7
firmware-type: ar7240
factory-firmware: 3.0
current-firmware: 6.42.3
upgrade-firmware: 6.43.4
I need to filter it for "upgrade-firmware" string and get output like this:
upgrade-firmware: 6.43.4
I should use regex_replace? Or I can use grep or something like that?
Any thoughts are greatly appreciated.
Thank you
(update)
Use from_yaml and combine a dictionary. For example
- set_fact:
minfo: "{{ minfo|default({})|combine(item|from_yaml) }}"
loop: "{{ rb_info.stdout_lines }}"
- debug:
var: minfo['upgrade-firmware']
give
minfo['upgrade-firmware']: 6.43.4
(for the record)
Robust solution is to write the data to template and include_vars. The tasks below
- tempfile:
register: tempfile
- template:
src: minfo.j2
dest: "{{ tempfile.path }}"
- include_vars:
file: "{{ tempfile.path }}"
name: minfo
- debug:
var: minfo
with the template
shell> cat minfo.j2
{% for item in rb_info.stdout_lines %}
{{ item }}
{% endfor %}
should give
"minfo": {
"current-firmware": "6.42.3",
"factory-firmware": 3.0,
"firmware-type": "ar7240",
"model": "751G-2HnD",
"routerboard": true,
"serial-number": "3A6502B2A2E7",
"upgrade-firmware": "6.43.4"
}
The tasks below creates variable upgrade_firmware
- set_fact:
upgrade_firmware: "{{ item.split(':').1|trim }}"
loop: "{{ rb_info.stdout_lines|map('quote')|map('trim')|list }}"
when: item is search('^upgrade-firmware')
- debug:
var: upgrade_firmware
It is possible to put all the parameters into the dictionary
- set_fact:
minfo: "{{ minfo|default({})|
combine({item.split(':').0: item.split(':').1|trim}) }}"
loop: "{{ rb_info.stdout_lines|map('quote')|map('trim')|list }}"
- debug:
var: minfo['upgrade-firmware']

Resources