I frequent the r/ansible sub and try to help out and answer as many questions as I can. One area that I see most people struggling with is using some of the more advanced jinja filters such as select
, selectattr
and map
.
In this post I will try to give some easy to understand examples of how to use these filters in order to filter lists and dictionaries in your Ansible playbooks.
In jinja a test is a template expression that evaluates to either True
or False
.
Test are used to compare objects.
Jinja has a number of built-in tests but Ansible also provies some filters of their own:
https://jinja.palletsprojects.com/en/3.0.x/templates/#list-of-builtin-tests
https://docs.ansible.com/ansible/latest/user_guide/playbooks_tests.html
In jinja filters are used to modify/manipulate variables. For example converting strings from lowercase to uppercase or selecting items from a list. Both jinja tests and filters run on on Ansible controller itself, not on the target host.
Select is used to filter a list by applying a test to each object in the list and only returning those items where the test succeeded.
The reverse of select
is reject
that works the same way, only that it returns the objects that fail the test.
The select filter takes one or 2 arguments; the test to apply and an optional argument to that test:
{{ the_list_to_filter | select('name_of_the_test', 'optional_argument_to_test') }}
Let's say that you have a list of interface names and your want to filter out only the interfaces that start with eth
.
interfaces:
- eth0
- eth1
- wg0
- docker0
You could filter the list of interfaces using this expression:
- name: Print filtered interface list
ansible.builtin.debug:
msg: "{{ interfaces | select('regex', '^eth') | list }}"
TASK [debug]
ok: [localhost] => {
"msg": [
"eth0",
"eth1"
]
}
You may wonder about what the | list
at the and does here. Because the select filter returns a "generator object" you have to convert it to a list before printing it with debug
.
The select filter is similar to doing something like this in python:
filtered_interfaces = (interface for interface in interfaces if regex(interface, '^eth'))
This is only necessary when you need to print the object or when subequent filters cannot take a generator object.
For the rest of the examples in this post I will leave out | list
.
For more information on how a generator object differs from lists, just google "python generator object"
Much like select
and reject
is used for filtering lists of items selectattr
and rejectattr
filters a sequence by applying a test to an objects attributes.
You are generating a sudoers file for a system using the template
module. You have a list of usergroups but only some of them should have sudo access.
The users that should have sudo access are identified by having the sudo
attribute set to true
.
user_groups:
- name: sysadmin
sudo: true
- name: helpdesk
sudo: false
- name: consultant
sudo: true
In your template you could either to this:
{% for group in user_groups %}
{% if group.sudo is true %}
%{{ group.name }} ALL=(ALL) ALL
{% endif %}
{% endfor %}
Or you could use selectattr
:
{% for group in user_groups | selectattr('sudo', 'true') %}
%{{ group.name }} ALL=(ALL) ALL
{% endfor %}
While we might not be saving ourselves that much typing in this example you could imagine defining these kind of 'helper' variables in your playbook or hostvars for easy access. For example:
sudo_users: "{{ users | selectattr('sudo', 'true') }}"
normal_users: "{{ users | selectattr('sudo', 'false') }}"
From the jinja2 documentation:
Applies a filter on a sequence of objects or looks up an attribute. This is useful when dealing with lists of objects but you are really only interested in a certain value of it.
In smaller words this means 'take a list and either apply a test or get an attribute from each object'.
You use map when you find yourself thinking 'I want to do X to every object in Y'.
You need to configure iptables rules for allowing certain hosts to connect to a webserver.
You have a list of hosts with their hostname, ip address and a description. Because we only need the IP address of each host we need to filter the list of hosts:
allowed_hosts:
- hostname: host1.domain.tld
ip: 192.0.2.1
description: This host does some stuff
- hostname: host2.domain.tld
ip: 192.0.2.2
description: This host does some other stuff
- hostname: host3.domain.tld
ip: 192.0.2.3
description: We don't know what this host does
Grabbing only the IP addresses and creating a comma separated string for use in iptables:
allowed_ips: "{{ allowed_hosts | map(attribute='ip') | join(',') }}"
Your boss has asked you to compile a monthly report of which hosts that can access the webserver.
You are smart and decide to do this with a template. But your boss demands that all hostnames are to be written in uppercase.
You are smart and decide to not die on this hill and just do what the bossman says.
In the template that generates your report you write:
Hosts that are allowed to access web server:
{% for uppercase_hostname in allowed_hosts | map(attribute='hostname') | map('upper') %}
{{ uppercase_hostname }}
{% endfor %}
Of course this could be written like this instead:
Hosts that are allowed to access web server:
{% for host in allowed_hosts %}
{{ host.hostname | upper }}
{% endfor %}
but that wouldn't get you street rep from the person reviewing your code (and it was the first example I came up with).
You are generating configuration for some kind of cluster. Each node in the cluster need to be able to connect to the other nodes in the same cluster.
The hosts in this cluster are in the 'cluster_app' group in your Ansible inventory. In order to generate firewall rules you need to get the IP address of each host in the same cluster group.
# Inventory:
[cluster_app]
host1 ansible_host=192.0.2.1
host2 ansible_host=192.0.2.2
host3 ansible_host=192.0.2.3
# Playbook:
cluster_member_ips: "{{ groups.cluster_app | map('extract', hostvars, 'ansible_host') }}"
What we are doing here is essentially this:
For each host in the group 'cluster_app' extract that host's IP from the 'ansible magic variable' hostvars.
If you were to write this in python using a simple loop it would look something like this:
cluster_member_ips = []
for hostname in groups['cluster_app']:
host_ip = hostvars[hostname]['ansible_host']
cluster_member_ips.append(host_ip)