Sometimes we need to read a list of files, a list of lines, a list of directories or other list, which we need to use some parameter to read those list. With Ansible we have a lot of different kind of options to do that, sometimes looks complex, due to the reason to have many ways to do the same thing, but lets uncomplicate that.
How I said Ansible support many mechanism to interact over loops, the most common is with_items
, the documentation with all the avalable option is here.
Let's see some good way and option, to review and remember later.
The official documentation covers these quite thoroughly, so let's try some example to see the behaviour.
The with_lines looping construct lets you run an arbitrary command on your control machine and iterate over the output, one line at a time. Imagine you have a file that contains a list of names, and you want to send a Slack message for each name, something like this:
Luiz Eduardo
Silvio Micali
Joao Silva
- name: Send out a slack message
slack:
domain: example.slack.com
token: "{{ slack_token }}"
msg: "{{ item }} was in the list"
with_lines:
- cat files/turing.txt
Let's see in a debug way:
---
- name: with_line test
hosts: localhost
connection: local
tasks:
- name: We could read the file directly, but this shows output from command
debug:
msg: "{{ item }} is an output line from running cat on turning.txt"
with_lines: cat ./turning.txt
The output should be like this:
[vagrant@node-centos-1 ansible]$ ansible-playbook playbooks/with_lines.yml
PLAY [with_line test] *****************************************************************************************************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************************************************************************************************************
Thursday 06 October 2022 00:55:44 +0100 (0:00:00.023) 0:00:00.023 ******
ok: [localhost]
TASK [We could read the file directly, but this shows output from command] ************************************************************************************************************************************
Thursday 06 October 2022 00:55:45 +0100 (0:00:00.531) 0:00:00.555 ******
ok: [localhost] => (item=Luiz Eduardo) => {
"msg": "Luiz Eduardo is an output line from running cat on turning.txt"
}
ok: [localhost] => (item=Silvio Micali) => {
"msg": "Silvio Micali is an output line from running cat on turning.txt"
}
ok: [localhost] => (item=Joao Silva) => {
"msg": "Joao Silva is an output line from running cat on turning.txt"
}
PLAY RECAP ****************************************************************************************************************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Thursday 06 October 2022 00:55:45 +0100 (0:00:00.029) 0:00:00.584 ******
===============================================================================
Gathering Facts ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.53s
We could read the file directly, but this shows output from command ------------------------------------------------------------------------------------------------------------------------------------ 0.03s
The with_fileglob
construct is useful for iterating over a set of files on the control machine.
---
- name: Copy ssh key
hosts: web:db
become: true
vars:
state: present
tasks:
- name: create user ansible
user:
name: ansible
state: "{{ state }}"
shell: /bin/bash
tags: user
- name: Copy ssh-pub-key
authorized_key:
user: ansible
key: "{{ lookup('file', item ) }}"
state: "{{ state }}"
with_fileglob:
- /home/ansible/.ssh/*.pub
tags: [user,ssh]
- name: Grant sudo for ansible user
lineinfile:
path: /etc/sudoers
line: "%ansible ALL=(ALL) NOPASSWD: ALL"
state: "{{ state }}"
validate: /usr/sbin/visudo -cf %s
Let's check the result and output:
[vagrant@node-centos-1 ansible]$ ansible-playbook playbooks/ssh-key-copy.yml -t ssh
PLAY [Copy ssh key] *******************************************************************************************************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************************************************************************************************************
Thursday 06 October 2022 01:23:27 +0100 (0:00:00.022) 0:00:00.022 ******
ok: [192.168.56.13]
ok: [192.168.56.12]
TASK [Copy ssh-pub-key] ********************************************************************
Thursday 06 O*******************************************************************************************************************ctober 2022 01:23:29 +0100 (0:00:00.501) 0:00:01.330 ******
changed: [192.168.56.13] => (item=/home/ansible/.ssh/id_rsa.pub)
changed: [192.168.56.12] => (item=/home/ansible/.ssh/id_rsa.pub)
The with_dict construct lets you iterate over a dictionary instead of a list. When you use this looping construct, the item loop variable is a dictionary with two keys:
key
One of the keys in the dictionary
value
The value in the dictionary that corresponds to key
For example, if your host has an eth0 interface, there will be an Ansible fact named ansible_eth0 , with a key named ipv4 that contains a dictionary that looks some‐ thing like this:
{
"address": "10.0.2.15",
"netmask": "255.255.255.0",
"network": "10.0.2.0"
}
We could iterate over this dictionary and print out the entries one at a time:
---
- name: interact over loops
hosts: localhost
connection: local
tasks:
- name: iterate over ansible_eth0
debug:
msg: "{{ item.key }}={{ item.value }}"
with_dict: "{{ ansible_eth0.ipv4 }}"
- name: Debug json file item
debug:
msg: "{{ item }}"
with_dict: "{{ lookup('file', './file.json') }}"
- name: Debug json file item.value
debug:
msg: "{{ item.value }}"
with_dict: "{{ lookup('file', './file.json') }}"
- name: Get a value based on the data
debug:
msg: "The employee {{ item.value.name }} is married"
with_dict: "{{ lookup('file', './file.json') }}"
when: item.value.married == true
...
Sample json file:
{
"employee": {
"name": "luizzzzzzz",
"salary": 56000,
"married": true
}
}
The output looks like this:
[vagrant@node-centos-1 ansible]$ ansible-playbook playbooks/with_dict.yml
PLAY [interact over loops] ************************************************************************************************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************************************************************************************************************
Thursday 06 October 2022 01:49:13 +0100 (0:00:00.022) 0:00:00.022 ******
ok: [localhost]
TASK [iterate over ansible_eth0] ******************************************************************************************************************************************************************************
Thursday 06 October 2022 01:49:14 +0100 (0:00:00.534) 0:00:00.557 ******
ok: [localhost] => (item={'key': 'address', 'value': '10.0.2.15'}) => {
"msg": "address=10.0.2.15"
}
ok: [localhost] => (item={'key': 'broadcast', 'value': '10.0.2.255'}) => {
"msg": "broadcast=10.0.2.255"
}
ok: [localhost] => (item={'key': 'netmask', 'value': '255.255.255.0'}) => {
"msg": "netmask=255.255.255.0"
}
ok: [localhost] => (item={'key': 'network', 'value': '10.0.2.0'}) => {
"msg": "network=10.0.2.0"
}
TASK [Debug json file item] ***********************************************************************************************************************************************************************************
Thursday 06 October 2022 01:49:14 +0100 (0:00:00.034) 0:00:00.591 ******
ok: [localhost] => (item={'key': 'employee', 'value': {'name': 'luizzzzzzz', 'salary': 56000, 'married': True}}) => {
"msg": {
"key": "employee",
"value": {
"married": true,
"name": "luizzzzzzz",
"salary": 56000
}
}
}
TASK [Debug json file item.value] *****************************************************************************************************************************************************************************
Thursday 06 October 2022 01:49:14 +0100 (0:00:00.029) 0:00:00.621 ******
ok: [localhost] => (item={'key': 'employee', 'value': {'name': 'luizzzzzzz', 'salary': 56000, 'married': True}}) => {
"msg": {
"married": true,
"name": "luizzzzzzz",
"salary": 56000
}
}
TASK [Get a value based on the data] **************************************************************************************************************************************************************************
Thursday 06 October 2022 01:49:14 +0100 (0:00:00.035) 0:00:00.656 ******
ok: [localhost] => (item={'key': 'employee', 'value': {'name': 'luizzzzzzz', 'salary': 56000, 'married': True}}) => {
"msg": "The employee luizzzzzzz is married"
}
PLAY RECAP ****************************************************************************************************************************************************************************************************
localhost : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Thursday 06 October 2022 01:49:14 +0100 (0:00:00.033) 0:00:00.690 ******
===============================================================================
Gathering Facts ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.53s
Debug json file item.value ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.04s
iterate over ansible_eth0 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 0.03s
Get a value based on the data -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.03s
Debug json file item ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.03s
with_items
is replaced by loop
and the flatten
filter. I'm using the module dnf with items, but for the first task you can see I'm using name: "{{ item }}"
this can be replaced by name: ['package1','package2']
, the first one is deprecated after ansible 2.11.
---
- name: Install and Uninstall
hosts: all
become: true
tasks:
- name: Install packages
dnf:
name: "{{ item }}"
state: present
with_items:
- httpd
- mysql
- git
- firewalld
- wget
- name: Uninstall packages
dnf:
name: "{{ item.package }}"
state: "{{ item.state }}"
with_items:
- { package: 'httpd', state: 'absent'}
- { package: 'mysql', state: 'absent'}
- { package: 'git', state: 'absent'}
- { package: 'firewalld', state: 'absent'}
- { package: 'wget', state: 'absent'}
- include_vars: ./packages.yml
- name: Install packages
dnf:
name: "{{ item }}"
state: present
with_items:
- "{{ packages }}"
Let's see the result and output:
[vagrant@node-centos-1 ansible]$ ansible-playbook playbooks/install-httpd.yml
PLAY [Install and Uninstall] **********************************************************************************************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************************************************************************************************************
Thursday 06 October 2022 02:22:16 +0100 (0:00:00.021) 0:00:00.021 ******
ok: [192.168.56.13]
ok: [192.168.56.12]
TASK [Install packages] ***************************************************************************************************************************************************************************************
Thursday 06 October 2022 02:22:17 +0100 (0:00:00.974) 0:00:00.996 ******
changed: [192.168.56.13] => (item=['httpd', 'mysql', 'git', 'firewalld', 'wget'])
changed: [192.168.56.12] => (item=['httpd', 'mysql', 'git', 'firewalld', 'wget'])
TASK [Uninstall packages] *************************************************************************************************************************************************************************************
Thursday 06 October 2022 02:22:19 +0100 (0:00:02.424) 0:00:03.420 ******
changed: [192.168.56.13] => (item={'package': 'httpd', 'state': 'absent'})
changed: [192.168.56.12] => (item={'package': 'httpd', 'state': 'absent'})
changed: [192.168.56.13] => (item={'package': 'mysql', 'state': 'absent'})
changed: [192.168.56.12] => (item={'package': 'mysql', 'state': 'absent'})
changed: [192.168.56.13] => (item={'package': 'git', 'state': 'absent'})
changed: [192.168.56.12] => (item={'package': 'git', 'state': 'absent'})
changed: [192.168.56.13] => (item={'package': 'firewalld', 'state': 'absent'})
changed: [192.168.56.12] => (item={'package': 'firewalld', 'state': 'absent'})
changed: [192.168.56.13] => (item={'package': 'wget', 'state': 'absent'})
changed: [192.168.56.12] => (item={'package': 'wget', 'state': 'absent'})
TASK [include_vars] *******************************************************************************************************************************************************************************************
Thursday 06 October 2022 02:22:38 +0100 (0:00:18.058) 0:00:21.479 ******
ok: [192.168.56.12]
ok: [192.168.56.13]
TASK [Install packages] ***************************************************************************************************************************************************************************************
Thursday 06 October 2022 02:22:38 +0100 (0:00:00.090) 0:00:21.570 ******
changed: [192.168.56.12] => (item=['httpd', 'mysql', 'git', 'firewalld'])
changed: [192.168.56.13] => (item=['httpd', 'mysql', 'git', 'firewalld'])
PLAY RECAP ****************************************************************************************************************************************************************************************************
192.168.56.12 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.56.13 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Thursday 06 October 2022 02:22:46 +0100 (0:00:08.075) 0:00:29.645 ******
===============================================================================
Uninstall packages ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 18.06s
Install packages --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 8.08s
Install packages --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 2.42s
Gathering Facts ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.97s
include_vars ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.09s
After version 2.1, Ansible provides users with more control over loop handling.
The loop_var
control allows us to give the iteration variable a different name than
the default name, item. Let's explore how this works, let's create a list of files with a md5 hash to compare if the file was changed or not based on the md5 checksum:
files:
- name: /tmp/file1_tar.gz
hash: aa767b0503c4e162407b452c5d399549 /tmp/file1_tar.gz
- name: /tmp/file2_tar.gz
hash: aa767b0503c4e162407b452c5d399549 /tmp/file2_tar.gz
Then we will interact over the variables and set the name
as the label/index to interact over the dictionaries list. Have a look and see the output ;)
---
- name: "Compare hashs"
hosts: localhost
connection: local
vars_files:
- ./files.yml
tasks:
- name: Get stat info about files
stat:
path: "{{ item.name }}"
checksum_algorithm: md5
register: stat_check
loop: "{{ files }}"
- name: DEBUG FILES
debug:
msg: "NOT CHANGED {{ item.stat.path }}"
when: item.stat.checksum == item.item.hash.split()|first
loop: "{{ stat_check.results }}"
loop_control:
label: "{{ item.stat.path }}"
- name: DEBUG FILES
debug:
msg: "CHANGED {{ item.stat.path }}"
when: item.stat.checksum != item.item.hash.split()|first
loop: "{{ stat_check.results }}"
loop_control:
label: "{{ item.stat.path }}"
Result and output looks like this:
[vagrant@node-centos-1 ansible]$ ansible-playbook playbooks/hash.yml
PLAY [Compare hashs] ******************************************************************************************************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************************************************************************************************************
Thursday 06 October 2022 02:31:48 +0100 (0:00:00.023) 0:00:00.023 ******
ok: [localhost]
TASK [Get stat info about files] ******************************************************************************************************************************************************************************
Thursday 06 October 2022 02:31:49 +0100 (0:00:00.529) 0:00:00.552 ******
ok: [localhost] => (item={'name': '/tmp/file1_tar.gz', 'hash': 'aa767b0503c4e162407b452c5d399549 /tmp/file1_tar.gz'})
ok: [localhost] => (item={'name': '/tmp/file2_tar.gz', 'hash': 'aa767b0503c4e162407b452c5d399549 /tmp/file2_tar.gz'})
TASK [DEBUG FILES] ********************************************************************************************************************************************************************************************
Thursday 06 October 2022 02:31:49 +0100 (0:00:00.348) 0:00:00.901 ******
ok: [localhost] => (item=/tmp/file1_tar.gz) => {
"msg": "NOT CHANGED /tmp/file1_tar.gz"
}
skipping: [localhost] => (item=/tmp/file2_tar.gz)
TASK [DEBUG FILES] ********************************************************************************************************************************************************************************************
Thursday 06 October 2022 02:31:49 +0100 (0:00:00.029) 0:00:00.931 ******
skipping: [localhost] => (item=/tmp/file1_tar.gz)
ok: [localhost] => (item=/tmp/file2_tar.gz) => {
"msg": "CHANGED /tmp/file2_tar.gz"
}
PLAY RECAP ****************************************************************************************************************************************************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Thursday 06 October 2022 02:31:49 +0100 (0:00:00.026) 0:00:00.957 ******
===============================================================================
Gathering Facts ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.53s
Get stat info about files ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 0.35s
DEBUG FILES -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.03s
DEBUG FILES -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.03s
This is funny but I like it, we can see the ansible potential to manipulate things.
The loop_var control allows us to give the iteration variable a different name than the default name, item, as I will show below:
---
- name: Loop var
hosts: localhost
connection: local
tasks:
- user:
name: "{{ user.name }}"
state: present
with_items:
- { name: luiz }
- { name: eduardo }
- { name: blog }
loop_control:
loop_var: user
The result and output I will show below:
[vagrant@node-centos-1 ansible]$ ansible-playbook playbooks/loop_var.yml -b
PLAY [Loop var] ***********************************************************************************************************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************************************************************************************************************
Thursday 06 October 2022 02:46:24 +0100 (0:00:00.024) 0:00:00.024 ******
ok: [localhost]
TASK [user] ***************************************************************************************************************************************************************************************************
Thursday 06 October 2022 02:46:24 +0100 (0:00:00.564) 0:00:00.588 ******
changed: [localhost] => (item={'name': 'luiz'})
changed: [localhost] => (item={'name': 'eduardo'})
changed: [localhost] => (item={'name': 'blog'})
PLAY RECAP ****************************************************************************************************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Thursday 06 October 2022 02:46:26 +0100 (0:00:01.436) 0:00:02.025 ******
===============================================================================
user --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 1.44s
Gathering Facts ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0.56s