Skip to content

Instantly share code, notes, and snippets.

@isweluiz
Last active October 16, 2022 19:46
Show Gist options
  • Save isweluiz/ff5115bdbfc9c2188556bd37c55f4480 to your computer and use it in GitHub Desktop.
Save isweluiz/ff5115bdbfc9c2188556bd37c55f4480 to your computer and use it in GitHub Desktop.

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.

loops-options

The official documentation covers these quite thoroughly, so let's try some example to see the behaviour.

with_lines

. Documentation

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

with_fileglob

. Documentation

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)

with_dict

. Documentation

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

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

Limiting loop output with Loop Controls

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

Loop Controls Setting the Variable Name

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

Ansible

Official documentation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment