Image
![IT Automation ebook](/sysadmin/sites/default/files/styles/image_cta/public/2021-07/automation-expert-e-book-medium.jpg?itok=X_OfbjdH)
Image by Benjamin Hartwich on Pexels
My friend and peer Roberto Nozaki recently wrote How to work with lists and dictionaries in Ansible. After reading it, this felt like a perfect opportunity to follow up with some related content. If you're unsure of what dictionaries or lists are in Ansible, give his article a quick read and then come back here. Otherwise, keep reading, as I'm confident you'll find it worthwhile if you're new to using Jinja filtering.
[ Get started with IT automation with the Ansible Automation Platform beginner's guide. ]
When you're working with Ansible, you typically work with lists and dictionaries. On occasion, the data you're working with is actually a list of dictionaries. This is where Jinja filtering comes in handy.
Jinja is powerful templating language that has two filters that are ideal for extracting data from lists of dictionaries. They are: selectattr()
and map()
.
The Jinja documentation explains the filters like so:
selectattr()
filters a sequence of objects by applying a test to the specified attribute of each object and only selecting the objects when the test succeeds.map()
applies a filter on a sequence of objects or looks up an attribute. This is useful when you have lists of objects but you are interested in only a certain value.Learning how to use these filters properly will take your Ansible skills to the next level. To be fair, there are a lot of Jinja filters. In this article, I focus on selectattr()
and map()
.
While some Ansible users prefer to use the json_query
filter, I personally prefer to use Jinja's selectattr()
and map()
filters. Ansible natively supports Jinja. Since selectattr()
and map()
are Jinja filters, you don't need to add anything to your server if Ansible is installed.
The json_query
module, however, is part of the community.general
collection. Thus, it is not natively installed with Ansible. You may not have the luxury of adding collections on the fly without some level of approval beforehand. Using Jinja filters mitigates the immediate need for external collections to extract data from lists of dictionaries.
[ Download now: A system administrator's guide to IT automation. ]
First, create a variable called bands
. The variable bands
is a list of dictionaries that will be used in the example.yml
playbook. As you put the playbook together, you'll begin to see how to select data from the variable.
# List of dictionaries
bands:
- name: The Beatles
members:
- Lennon
- McCartney
- Harrison
- Starr
formed: 1960
decade: 60s
- name: The Eagles
members:
- Frey
- Henley
- Leadon
- Meisner
formed: 1971
decade: 70s
- name: Run DMC
members:
- Simmons
- McDaniels
- Mizell
formed: 1983
decade: 80s
- name: Red Hot Chili Peppers
members:
- Kiedis
- Smith
- Frusciante
- Balzary
formed: 1982
decade: 90s
- name: "Destiny's Child"
members:
- Knowles
- Rowland
- Williams
formed: 1990
decade: 00s
- name: "Black Eyed Peas"
members:
- Adams
- Lindo
- Gomez
formed: 1995
decade: 00s
...
Next, create a set of tasks that show what kind of data you are dealing with. In the first task, you should see that the bands
variable itself is a list. The second task should show the bands
variable is made up of dictionaries. Simply put, this shows you're dealing with a list of dictionaries.
- name: Show what type of variable bands is
ansible.builtin.debug:
msg: "{{ bands | type_debug }}"
- name: Show what type of data the bands variable is made up of
ansible.builtin.debug:
msg: "{{ item | type_debug }}"
loop: "{{ bands }}"
For the next task, list all members of all bands. You're selecting the members
key of the dictionary if the members
key is "defined." In this case you know it is, so you can expect data to be returned. Once you select the data, pass it to the map
filter. The map
filter applies a filter on a sequence of objects or looks up an attribute. In this case, you're looking up the members
attribute because that's the data you're interested in:
- name: List ALL band members of ALL bands
ansible.builtin.debug:
msg: "{{ bands | selectattr('members', 'defined') | map(attribute='members') }}"
Say you're interested in when the bands were formed rather than who the members are. To do so, create a task to find the years that the bands were formed, regardless of other key/value pairs:
- name: List the years the bands were formed
ansible.builtin.debug:
msg: "{{ bands | selectattr('formed', 'defined') | map(attribute='formed') }}"
Or maybe you want to know the members of a band that formed in a specific year. In this task, you're looking for the members of the band that was formed in the year 1971:
- name: List members of band formed in 1971
ansible.builtin.debug:
msg: "{{ bands | selectattr('formed', 'match', '1971') | map(attribute='members') }}"
For the next task, find the name of a band that has a member named "Starr." When doing searches like this, the search/match parameters are case sensitive. For this example, you're specifically looking for the name of the band in which "Starr" was a member:
- name: List band formed with a member named Starr
ansible.builtin.debug:
msg: "{{ bands | selectattr('members', 'search', 'Starr') | map(attribute='name') }}"
What if more than one list of dictionaries has the same data? No problem! You can grab as much or as little as you wish. The first example below grabs all decades that start with a single character followed by "0" and ending with "s." The second example grabs all band names that have "00" in the "decade" value.
- name: List band name when band decade ends with 0s regardless of the decade
ansible.builtin.debug:
msg: "{{ bands | selectattr('decade', 'search', '^.0s$') | map(attribute='name') }}"
- name: List band name when band decade contains 00 in the name
ansible.builtin.debug:
msg: "{{ bands | selectattr('decade', 'search', '00') | map(attribute='name') }}"
[ Write your first Ansible playbook in this hands-on interactive lab. ]
Now that the tasks are defined, put it altogether in a single playbook:
---
- name: 'Jinja selectattr() and map() example with list of dictionaries'
hosts: localhost
gather_facts: false
vars:
# This is a list of dictionaries
bands:
- name: The Beatles
members:
- Lennon
- McCartney
- Harrison
- Starr
formed: 1960
decade: 60s
- name: The Eagles
members:
- Frey
- Henley
- Leadon
- Meisner
formed: 1971
decade: 70s
- name: Run DMC
members:
- Simmons
- McDaniels
- Mizell
formed: 1983
decade: 80s
- name: Red Hot Chili Peppers
members:
- Kiedis
- Smith
- Frusciante
- Balzary
formed: 1982
decade: 90s
- name: "Destiny's Child"
members:
- Knowles
- Rowland
- Williams
formed: 1990
decade: 00s
- name: "Black Eyed Peas"
members:
- Adams
- Lindo
- Gomez
formed: 1995
decade: 00s
tasks:
- name: Show what type of variable bands be
ansible.builtin.debug:
msg: "{{ bands | type_debug }}"
- name: Show what type of data the bands variable consists of
ansible.builtin.debug:
msg: "{{ item | type_debug }}"
loop: "{{ bands }}"
- name: List band members of all bands
ansible.builtin.debug:
msg: "{{ bands | selectattr('members', 'defined') | map(attribute='members') }}"
- name: List the years the bands were formed
ansible.builtin.debug:
msg: "{{ bands | selectattr('formed', 'defined') | map(attribute='formed') }}"
- name: List members of band formed in 1971
ansible.builtin.debug:
msg: "{{ bands | selectattr('formed', 'match', '1971') | map(attribute='members') }}"
- name: List band with a member named Starr
ansible.builtin.debug:
msg: "{{ bands | selectattr('members', 'search', 'Starr') | map(attribute='name') }}"
- name: List band name when band decade ends with 0s regardless of the decade
ansible.builtin.debug:
msg: "{{ bands | selectattr('decade', 'search', '^.0s$') | map(attribute='name') }}"
- name: List band name when band decade contains 00 in the name
ansible.builtin.debug:
msg: "{{ bands | selectattr('decade', 'search', '00') | map(attribute='name') }}"
- name: Loop through the the 80s and 90s bands and display a message if Run DMC is in the name
ansible.builtin.debug:
msg:
- !unsafe It's Tricky to rock a rhyme
loop: "{{ bands | selectattr('decade', 'search', '^[8-9]0s$') | map(attribute='name') }}"
when: "'run' in item | lower"
...
Execute the playbook to see the filters in action.
Note that I performed this test on a Fedora 37 host with Ansible 7.3. Be sure your version of Jinja supports the arguments you're using for selectattr()
if you want to replicate this exercise in your own environment.
$ ansible-playbook example.yml
PLAY [Jinja selectattr() and map() example with list of dictionaries] *********
TASK [Show what type of variable bands be] ************************************
ok: [localhost] => {
"msg": "list"
}
TASK [Show what type of data the bands variable consists of] ******************
ok: [localhost] => (item={'name': 'The Beatles', 'members': ['Lennon', 'McCartney', 'Harrison', 'Starr'], 'formed': 1960, 'decade': '60s'}) => {
"msg": "dict"
}
ok: [localhost] => (item={'name': 'The Eagles', 'members': ['Frey', 'Henley', 'Leadon', 'Meisner'], 'formed': 1971, 'decade': '70s'}) => {
"msg": "dict"
}
ok: [localhost] => (item={'name': 'Run DMC', 'members': ['Simmons', 'McDaniels', 'Mizell'], 'formed': 1983, 'decade': '80s'}) => {
"msg": "dict"
}
ok: [localhost] => (item={'name': 'Red Hot Chili Peppers', 'members': ['Kiedis', 'Smith', 'Frusciante', 'Balzary'], 'formed': 1982, 'decade': '90s'}) => {
"msg": "dict"
}
ok: [localhost] => (item={'name': "Destiny's Child", 'members': ['Knowles', 'Rowland', 'Williams'], 'formed': 1990, 'decade': '00s'}) => {
"msg": "dict"
}
ok: [localhost] => (item={'name': 'Black Eyed Peas', 'members': ['Adams', 'Lindo', 'Gomez'], 'formed': 1995, 'decade': '00s'}) => {
"msg": "dict"
}
TASK [List band members of all bands] *****************************************
ok: [localhost] => {
"msg": [
[
"Lennon",
"McCartney",
"Harrison",
"Starr"
],
[
"Frey",
"Henley",
"Leadon",
"Meisner"
],
[
"Simmons",
"McDaniels",
"Mizell"
],
[
"Kiedis",
"Smith",
"Frusciante",
"Balzary"
],
[
"Knowles",
"Rowland",
"Williams"
],
[
"Adams",
"Lindo",
"Gomez"
]
]
}
TASK [List the years the bands were formed] ***********************************
ok: [localhost] => {
"msg": [
1960,
1971,
1983,
1982,
1990,
1995
]
}
TASK [List members of band formed in 1971] ************************************
ok: [localhost] => {
"msg": [
[
"Frey",
"Henley",
"Leadon",
"Meisner"
]
]
}
TASK [T08 - List band with a member named Starr] ******************************
ok: [localhost] => {
"msg": [
"The Beatles"
]
}
TASK [List band name when band decade ends with 0s regardless of the decade] **
ok: [localhost] => {
"msg": [
"The Beatles",
"The Eagles",
"Run DMC",
"Red Hot Chili Peppers",
"Destiny's Child",
"Black Eyed Peas"
]
}
TASK [List band name when band decade contains 00 in the name] ****************
ok: [localhost] => {
"msg": [
"Destiny's Child",
"Black Eyed Peas"
]
}
TTASK [Loop through the the 80s and 90s bands and display a message if Run DMC is in the name] *****
ok: [localhost] => (item=Run DMC) => {
"msg": [
"It's Tricky.....to rock a rhyme"
]
}
skipping: [localhost] => (item=Red Hot Chili Peppers)
PLAY RECAP ********************************************************************
localhost : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Manipulating lists of dictionaries is a critical skill when automating tasks with Ansible or Red Hat Ansible Automation Platform (AAP), but it can be intimidating when you're first learning how to use it.
Copy this example and practice using Jinja filtering as part of your skills development. Hopefully this article gave you enough of a glimpse to increase your interest in using Jinja filtering.
Randy Romero is a Senior Ansible Consultant and Red Hat Certified Architect at Red Hat where he specializes in IT automation with Ansible and Ansible Automation Platform. He has experience in the cable, insurance, and loyalty-marketing industries, having performed multiple roles starting with ju More about me