Skip to content

Instantly share code, notes, and snippets.

@oskaralmlov
Last active September 18, 2021 12:55
Show Gist options
  • Save oskaralmlov/fa8b5e19701c04e8545a21e8604c8e3e to your computer and use it in GitHub Desktop.
Save oskaralmlov/fa8b5e19701c04e8545a21e8604c8e3e to your computer and use it in GitHub Desktop.
Jinja filtering for Ansible users

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.

What are tests?

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

What are filters?

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.

Using select

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') }}

Example

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"

Using selectattr

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.

Example

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') }}"                                                               

Using map

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'.

Examples

Example 1

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(',') }}"                                                    

Example 2

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).

Example 3

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)   
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment