Skip to content
Jan 10 / Greg

Ansible Looping With Lists And Dictionaries With Advanced Filtering


One of the real strengths of Ansible is it’s ability to loop through information. This can be done to generate reports or configuring network or server kit.
The aim of this post is teach you how to loop over various data types be them lists(which are easy) or dictionaries(a little more challenging). Another important piece here is the fact that often I need to do some advanced filtering. This could be done with complex nested loops, OR I can do it with some special plugins(which will be demonstrated).

Sample Files

My public git repository with the files can be found here.
If you are in a workshop you can issue the following command to clone the repository to your workstation in your current working directory:

1
git clone https://github.com/gregsowell/ansible-looping-lesson.git

Looping Over Lists

The file I’m pulling from is loop-test.yml. I’ll break down the various parts below.
The play section and variables I’m working with are as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
---
- name: loop examples
  hosts: localhost
  gather_facts: false
  vars:
    list1:
      - thing1
      - thing2
      - thing3
 
    list2:
      - fname: Greg
        lname: Sowell
      - fname: Andy
        lname: Garrett
 
    list3:
      - 1:
        - n: 1
        - m: 1
      - 2:
        - n: 2
        - m: 2

Looking at the above, “list1” variable is a standard list. It has just values as the items inside the list.
“list2” is actually a list of dictionaries. List items begin with a “-” character. Each list item has two variables, both “fname” and “lname”.
“list3 is a list of lists.
You will often see variables returned in ansible in these forms. I would say I see combinations of list2 and list3 most often…probably list2 the most often.

Here are two tasks that will display list1 in different ways:

1
2
3
4
5
6
7
8
  - name: debug list1
    ansible.builtin.debug:
      var: list1
 
  - name: loop through standard list
    ansible.builtin.debug:
      msg: "{{ item }}"
    loop: "{{ list1 }}"

Here’s the returned format once they are run:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TASK [debug list1] *************************************************************
ok: [localhost] => {
    "list1": [
        "thing1",
        "thing2",
        "thing3"
    ]
}
 
TASK [loop through standard list] **********************************************
ok: [localhost] => (item=thing1) => {
    "msg": "thing1"
}
ok: [localhost] => (item=thing2) => {
    "msg": "thing2"
}
ok: [localhost] => (item=thing3) => {
    "msg": "thing3"
}

The initial debug isn’t that complex; all I did was request that variable 1 be dumped out to screen.
The second debug where I “loop through standard list” is slightly more interesting. I added a “loop” option with the “list1 variable. Notice the format:

1
loop: "{{ list1 }}"

Have a look at how the word loop is on the same column level as the module itself. This says that the loop option is applied to the task itself and is not a sub parameter of the module.
Also notice, in the line below, that the result of the loop is contained in a temporary variable named “item”. This is the default behavior, though the returned iteration name “item” can be changed to something else if you want to.

1
msg: "{{ item }}"

Picking specific items inside the array
A list is like an array. You can pick specific values like so:

1
2
3
  - name: pulling items from list 1
    ansible.builtin.debug:
      msg: "{{ list1[1] }}"

This returns the following results:

1
2
3
4
TASK [pulling items from list 1] ***********************************************
ok: [localhost] => {
    "msg": "thing2"
}

This list contained 3 items and I specified 1, so why did it return thing2 and not thing1? That’s because ansible, like most programming languages, numbers its arrays starting at 0. So for this list it goes 0, then 1, and last 2.

Loop through lists of dictionaries
Looping through lists of dictionaries is virtually the same as before, but with more options. See the following example:

1
2
3
4
  - name: loop through list of dictionaries
    ansible.builtin.debug:
      msg: "First name is:{{ item.fname }} last name is:{{ item.lname }}"
    loop: "{{ list2 }}"

Notice the loop section is the same, and it returns iterations as “item” just the same, but look at the variables in the “msg” option…not quite the same.
The contents of item for each iteration will be the dictionary key/value pairs. This allows me to reference the contents of the variable, so in each list item I have a fname and a lname. So to reference each I say item.fname or item.lname.

Here are the results of the run:

1
2
3
4
5
6
7
TASK [loop through list of dictionaries] ***************************************
ok: [localhost] => (item={'fname': 'Greg', 'lname': 'Sowell'}) => {
    "msg": "First name is:Greg last name is:Sowell"
}
ok: [localhost] => (item={'fname': 'Andy', 'lname': 'Garrett'}) => {
    "msg": "First name is:Andy last name is:Garrett"
}

Looping Over Dictionaries

This gets a little harrier. I say that because ansible won’t natively loop over a dictionary. The trick is, I need to convert the dictionary into a list. I’m specifically going to be focusing on the “dict2items” plugin. This will take that dictionary and create key/value pairs with it. Often the easiest way to understand it is an example.

Task:

1
2
3
  - name: debug dict3 with dict2items
    ansible.builtin.debug:
      var: dict3 | dict2items

Dictionary variable I am manipulating:

1
2
3
4
5
6
7
8
9
10
    dict3:
      1:
        name: New Greg
        color: Bright
      2:
        name: Andy
        color: Green
      3:
        name: Andrew
        color: Baby blue

Now the results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
TASK [debug dict3 with dict2items] *********************************************
ok: [localhost] => {
    "dict3 | dict2items": [
        {
            "key": 1,
            "value": {
                "color": "Bright",
                "name": "New Greg"
            }
        },
        {
            "key": 2,
            "value": {
                "color": "Green",
                "name": "Andy"
            }
        },
        {
            "key": 3,
            "value": {
                "color": "Baby blue",
                "name": "Andrew"
            }
        }
    ]
}

First, notice in the original dictionary variable, there are no “-“s. That shows you that it is, in fact, not a list. Remember, a list has dashes in standard variable form.
Second, take a look at the output…there are no “-” in there, so how is it a list? In json output a list is surrounded by square brackets like so “[]”. If you notice at the very beginning and very end of the output you can see the square brackets! So, dashes in defined variables and square brackets in output means list.
Now for the dict2items output. I see that it breaks the original dictionary into two sections, first a key and then a value.
I can see that the left most part of the dictionary column becomes the “key”. So in this example it’s 1, 2, and 3.
Next, the indented information to the right of the key gets put into the “value” section.

I can actually access the individual pieces under the value section…let me show you an example:

1
2
3
4
  - name: loop through dictionary with many entries
    ansible.builtin.debug:
      msg: "What is the key:{{ item.key }}. What is the name:{{ item.value.name }}. => {{ item.value.color }}"
    loop: "{{ dict3 | dict2items }}"

Notice in the above that inside the loop section I piped the dictionary over to the dict2items plugin right there; I don’t have to do this ahead of time, rather I can do it right when I need it.
Also notice that in the returned info I referenced the key directly with “item.key” and then referenced the value information directly by following the variable chain with “item.value.name”.
Imagine that any time I’m moving a column to the right to access another variable name, just add a “.” between the variable names. So looking above where I debugged the dict2items I can pretty easily see myself moving to the right and just add periods.

Results:

1
2
3
4
5
6
7
8
9
10
TASK [loop through dictionary with many entries] *******************************
ok: [localhost] => (item={'key': 1, 'value': {'name': 'New Greg', 'color': 'Bright'}}) => {
    "msg": "What is the key:1. What is the name:New Greg. => Bright"
}
ok: [localhost] => (item={'key': 2, 'value': {'name': 'Andy', 'color': 'Green'}}) => {
    "msg": "What is the key:2. What is the name:Andy. => Green"
}
ok: [localhost] => (item={'key': 3, 'value': {'name': 'Andrew', 'color': 'Baby blue'}}) => {
    "msg": "What is the key:3. What is the name:Andrew. => Baby blue"
}

Advanced Filtering with selectattr

Additional resources here and here.
There are MANY ways to do additional filtering, but what I’m going to focus on is using the “selectattr” plugin. This plugin takes a list of dictionaries and applies a “test” to each iteration in the list.

It’s utilization is something like the following:

1
selectattr('key','search','^Ethernet.*')

So selectattr(field to be evaluated, jinja2 test, optional value to add to the test).
There are several options for the test(some examples follow, but there are more): search, match(search and match are pretty much the same), eq, ==(eq and == are the same), >, <, >=, <=, !=, defined. Let's see a few examples based on some of this variable:

1
2
3
4
5
6
7
8
9
10
    dict3:
      1:
        name: New Greg
        color: Bright
      2:
        name: Andy
        color: Green
      3:
        name: Andrew
        color: Baby blue

Equals or not equals
Tasks:

1
2
3
4
5
6
7
8
9
  - name: loop through dict3 selectattr == 1
    ansible.builtin.debug:
      msg: "{{ item }}"
    loop: "{{ dict3 | dict2items | selectattr('key','==',1) }}"
 
  - name: loop through dict3 selectattr != 1
    ansible.builtin.debug:
      msg: "{{ item }}"
    loop: "{{ dict3 | dict2items | selectattr('key','!=',1) }}"

Notice here I’m specifying that they key field either matches 1 in the first task or doesn’t match 1 in the second task.

Results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
TASK [loop through dict3 selectattr == 1] **************************************
ok: [localhost] => (item={'key': 1, 'value': {'name': 'New Greg', 'color': 'Bright'}}) => {
    "msg": {
        "key": 1,
        "value": {
            "color": "Bright",
            "name": "New Greg"
        }
    }
}
 
TASK [loop through dict3 selectattr != 1] **************************************
ok: [localhost] => (item={'key': 2, 'value': {'name': 'Andy', 'color': 'Green'}}) => {
    "msg": {
        "key": 2,
        "value": {
            "color": "Green",
            "name": "Andy"
        }
    }
}
ok: [localhost] => (item={'key': 3, 'value': {'name': 'Andrew', 'color': 'Baby blue'}}) => {
    "msg": {
        "key": 3,
        "value": {
            "color": "Baby blue",
            "name": "Andrew"
        }
    }
}

Regex searching
Task:

1
2
3
4
  - name: loop through dict3 selectattr search for A in value.name
    ansible.builtin.debug:
      msg: "{{ item }}"
    loop: "{{ dict3 | dict2items | selectattr('value.name','search','^A.*') }}"

This one is pretty darn handy. It allows you to do a regex search on any field, so it will narrow down what results are returned based on what you match.

Results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TASK [loop through dict3 selectattr search for A in value.name] ****************
ok: [localhost] => (item={'key': 2, 'value': {'name': 'Andy', 'color': 'Green'}}) => {
    "msg": {
        "key": 2,
        "value": {
            "color": "Green",
            "name": "Andy"
        }
    }
}
ok: [localhost] => (item={'key': 3, 'value': {'name': 'Andrew', 'color': 'Baby blue'}}) => {
    "msg": {
        "key": 3,
        "value": {
            "color": "Baby blue",
            "name": "Andrew"
        }
    }
}

Compound tests
Yes, as with any filter plugin you can stack them to check for multiple conditions. I’ll take the example from before and build on it.

Task:

1
2
3
4
  - name: loop through dict3 selectattr search for A in value.name and like green
    ansible.builtin.debug:
      msg: "{{ item }}"
    loop: "{{ dict3 | dict2items | selectattr('value.name','search','^A.*') | selectattr('value.color','eq','Green') }}"

It is as simple as adding another pipe and then the next test. Notice too that I checked different variables in each test.

Results:

1
2
3
4
5
6
7
8
9
10
TASK [loop through dict3 selectattr search for A in value.name and like green] ***
ok: [localhost] => (item={'key': 2, 'value': {'name': 'Andy', 'color': 'Green'}}) => {
    "msg": {
        "key": 2,
        "value": {
            "color": "Green",
            "name": "Andy"
        }
    }
}

Make sure the variable is defined
The selectattr plugin will fail if one of the entries happens to not have the field to be checked not exist, so I can first check defined, then build on that as follows.
Task:

1
2
3
4
  - name: loop through dict3 selectattr search for A in value.name and check that it is defined
    ansible.builtin.debug:
      msg: "{{ item }}"
    loop: "{{ dict3 | dict2items | selectattr('value.name','defined') | selectattr('value.name','search','^A.*') }}"

So in this example, it will first check that value.name exists for this host, and if it does, then it will do a regex search on it. You don’t necessarily need to do this for every variable you plan to check, but if there’s a chance it may not be instantiated, then add it; it won’t hurt to have it and not need it.

Use jinja2 loop to build a variable
Here I’m breaking into a jinja2 loop in conjunction with selectattr to build a custom variable:

1
2
3
4
5
6
7
8
9
  - name: create variable in a jinja2 loop using selectattr
    ansible.builtin.set_fact:
      new_var: "{% for result in dict3 | dict2items | selectattr('value.name','defined') | selectattr('value.name','search','^A.*') %}\
                {{ result.key }}: {{ result.value.name }}_{{ result.value.color }},
                {% endfor %}"
 
  - name: print out new_var
    ansible.builtin.debug:
      var: new_var.split(',')

Notice that in the debug statement I’m using the split option to break the string into a list separated via a comma.

Results:

1
2
3
4
5
6
7
8
9
10
11
TASK [create variable in a jinja2 loop using selectattr] ***********************
ok: [localhost]
 
TASK [print out new_var] *******************************************************
ok: [localhost] => {
    "new_var.split(',')": [
        "2: Andy_Green",
        " 3: Andrew_Baby blue",
        " "
    ]
}

Advanced Filtering with map

Additional resources on map here and here.
The map plugin allows me to take a list of dictionaries and make a simpler list from it. Let’s jump into some examples.
Tasks:

1
2
3
4
  - name: loop through dict3 using map to create a list of names
    ansible.builtin.debug:
      msg: "{{ item }}"
    loop: "{{ dict3 | dict2items | map(attribute='value.name') }}"

Results:

1
2
3
4
5
6
7
8
9
10
TASK [loop through dict3 using map to create a list of names] ******************
ok: [localhost] => (item=New Greg) => {
    "msg": "New Greg"
}
ok: [localhost] => (item=Andy) => {
    "msg": "Andy"
}
ok: [localhost] => (item=Andrew) => {
    "msg": "Andrew"
}

So as you can see it simply returns a list of values, not key and value.

Say, for example, I wanted to return it all on a single line instead of a list I can do something like the following:

1
2
3
  - name: loop through dict3 using map to create a list of names all on one line
    ansible.builtin.debug:
      msg: "{{ dict3 | dict2items | map(attribute='value.name') | join(',') }}"

I’m simply using the join plugin and instructing it to use a comma as the joining character.

Results:

1
2
3
4
TASK [loop through dict3 using map to create a list of names all on one line] ***
ok: [localhost] => {
    "msg": "New Greg,Andy,Andrew"
}

Conclusion

This is a living document, so I plan to make updates periodically. I hope this gives you a starting point to build some awesome automations. If you have any questions or comments, please let me know.

Thanks and happy automated looping!

Leave a Comment

 

*