As emphasized by the prior lab, the Jinja2 templating engine is utilized throughout the Ansible ecosystem. One of the features provided by Jinja is filtering. Filtering can be accomplished within {{ braces }} in both playbooks and template files and resemble Unix pipes. Jinja itself provides a set of built in filters which are then augmented by those provided by Ansible.
Ensure you are in the ciq-basics directory
Create a folder for this lab, let’s call it lab03
You are now ready to start the lab.
Here is a simple playbook that prints out an “Invalid Variable” if a variable has yet to be defined
---
- name: Print variable if it exists, print default message if not
hosts: localhost
gather_facts: false
connection: local
tasks:
- debug:
msg: "{{ does_not_exist_variable | default('Invalid Variable') }}"Sample Output
localhost| SUCCESS => {
"msg": "Invalid Variable"
}
The default filter becomes especially powerful when combined with Ansible’s omit special value. This allows you to conditionally include or exclude parameters from tasks based on whether variables are defined, creating more flexible and reusable playbooks.
- hosts: basics-host
gather_facts: false
vars:
file_owner: "{{ ansible_user }}"
# file_group is intentionally not defined
# file_mode is intentionally not defined
tasks:
- name: Create file with conditional parameters
file:
path: /tmp/test_file.txt
state: touch
owner: "{{ file_owner | default(omit) }}"
group: "{{ file_group | default(omit) }}"
mode: "{{ file_mode | default(omit) }}"
- name: Display file info
stat:
path: /tmp/test_file.txt
register: file_info
- name: Show file ownership
debug:
msg: |
File owner: {{ file_info.stat.pw_name }}
File group: {{ file_info.stat.gr_name }}
File mode: {{ file_info.stat.mode }}- name: User management with conditional parameters
hosts: basics-host
gather_facts: false
become: true
vars:
username: testuser
user_shell: /bin/bash
# user_home is not defined - will use system default
# user_groups is not defined - no supplementary groups
tasks:
- name: Create user with conditional parameters
user:
name: "{{ username }}"
shell: "{{ user_shell | default(omit) }}"
home: "{{ user_home | default(omit) }}"
groups: "{{ user_groups | default(omit) }}"
state: present
- name: Display user info
getent:
database: passwd
key: "{{ username }}"
register: user_info
- name: Show user details
debug:
msg: |
User: {{ username }}
Shell: {{ user_info.ansible_facts.getent_passwd[username][5] }}
Home: {{ user_info.ansible_facts.getent_passwd[username][4] }}Sample Output
TASK [Create file with conditional parameters] *****************************
changed: [localhost]
TASK [Show file ownership] *********************************************
ok: [localhost] => {
"msg": "File owner: ansible\nFile group: ansible\nFile mode: 644\n"
}
TASK [Create user with conditional parameters] *************************
changed: [localhost]
TASK [Show user details] ********************************************
ok: [localhost] => {
"msg": "User: testuser\nShell: /bin/bash\nHome: /home/testuser\n"
}
- name: Service management with conditional parameters
hosts: basics-host
gather_facts: false
become: true
vars:
service_name: httpd
service_enabled: true
# service_masked is not defined
tasks:
- name: Manage service with conditional parameters
service:
name: "{{ service_name }}"
enabled: "{{ service_enabled | default(omit) }}"
masked: "{{ service_masked | default(omit) }}"
state: startedWhen service_masked is undefined, the masked parameter is omitted entirely, allowing the service to maintain its current masked state.
In this section, we will create a series of playbooks that demonstrate how to utilize Ansible filters. Playbooks, when utilized, will have a common structure. Either a custom fact will be created using the set_fact module, or a variable provided by Ansible’s fact gathering module will be utilized.
- hosts: localhost
gather_facts: false
connection: local
tasks:
- debug:
msg: "{{ 'text I want to obtain a checksum' | checksum }}"Sample Output
localhost | SUCCESS => {
"msg": "394f552ed03a06108ca7233b0235eba100270820"
}
First, display the maximum value from a list containing -2, 5 and 10
- hosts: localhost
gather_facts: false
connection: local
tasks:
- debug:
msg: "{{ [-2, 5, 10] | max }}"Sample Output
localhost | SUCCESS => {
"msg": "10"
}
- hosts: localhost
gather_facts: false
connection: local
tasks:
- debug:
msg: "{{ [-2, 5, 10] | min }}"Sample Output
localhost | SUCCESS => {
"msg": "-2"
}
First, print out a random number between 0 and 100
- hosts: localhost
gather_facts: false
connection: local
tasks:
- debug:
msg: "{{ 100 | random }}"Sample Output
localhost | SUCCESS => {
"msg": "31"
}
- hosts: localhost
gather_facts: false
connection: local
tasks:
- debug:
msg: "{{ 100 | random(step=2) }}"Sample Output
localhost | SUCCESS => {
"msg": "48"
}
First, find the square root of 144
- hosts: localhost
gather_facts: false
connection: local
tasks:
- debug:
msg: "{{ 144 | root | int }}"Sample Output
localhost | SUCCESS => {
"msg": "12"
}
In the previous example, we demonstrated how filters can be chained together. The result of the root filter is a decimal value (12.0). Much like piping in Linux, Ansible filters can be combined to take the value of the prior result as their input. The int filter converts a number into an Integer.
Now, lets find the absolute value of -169
- hosts: localhost
gather_facts: false
connection: local
tasks:
- debug:
msg: "{{ -169 | abs }}"Sample Output
localhost | SUCCESS => {
"msg": "169"
}
- hosts: localhost
gather_facts: false
connection: local
tasks:
- debug:
msg: "{{ 13.6 | round | int }}"Sample Output
localhost | SUCCESS => {
"msg": "14"
}
The map filter is one of the most powerful filters in Ansible, allowing you to extract specific attributes from complex data structures or transform lists of objects. It’s particularly useful when working with Ansible facts that contain nested dictionaries and lists.
- hosts: all
gather_facts: yes
tasks:
- name: Display all mount points as a comma-separated list
debug:
msg: "Mount points: {{ ansible_mounts | map(attribute='mount') | join(',') }}"
- name: Display all filesystem types
debug:
msg: "Filesystem types: {{ ansible_mounts | map(attribute='fstype') | unique | join(', ') }}"
- name: Display device names with their mount points
debug:
msg: "{{ ansible_mounts | map('regex_replace', '^(.*)$', item.device + ' -> ' + item.mount) | list }}"
vars:
item: "{{ ansible_mounts[0] }}"
when: ansible_mounts | length > 0
- name: Show network interface names
debug:
msg: "Network interfaces: {{ ansible_interfaces | map('upper') | join(', ') }}"Sample Output
TASK [Display all mount points as a comma-separated list] **********************
ok: [node-1] => {
"msg": "Mount points: /,/boot,/home,/var"
}
TASK [Display all filesystem types] ********************************************
ok: [node-1] => {
"msg": "Filesystem types: ext4, xfs"
}
TASK [Show network interface names] ********************************************
ok: [node-1] => {
"msg": "Network interfaces: LO, ETH0, ETH1"
}
The map filter can be used in several ways:
- hosts: localhost
gather_facts: false
vars:
users:
- name: alice
email: alice@example.com
role: admin
- name: bob
email: bob@example.com
role: user
- name: charlie
email: charlie@example.com
role: admin
tasks:
- name: Extract just the usernames
debug:
msg: "Usernames: {{ users | map(attribute='name') | join(', ') }}"
- name: Get admin email addresses
debug:
msg: "Admin emails: {{ users | selectattr('role', 'equalto', 'admin') | map(attribute='email') | join('; ') }}"
- name: Transform usernames to uppercase
debug:
msg: "Uppercase usernames: {{ users | map(attribute='name') | map('upper') | join(', ') }}"Sample Output
TASK [Extract just the usernames] **********************************************
ok: [localhost] => {
"msg": "Usernames: alice, bob, charlie"
}
TASK [Get admin email addresses] ***********************************************
ok: [localhost] => {
"msg": "Admin emails: alice@example.com; charlie@example.com"
}
A common programming construct is a Ternary Operator. This conditional operation allows for an if statement to be shortcut. The operator provides a value if a condition is true or false.
Create a playbook called ternary_filter.yml with the following content:
- hosts: all
gather_facts: yes
tasks:
- name: Ternary Example
debug:
msg: "{{ (ansible_os_family == 'RedHat') | ternary('Rocky','Not Rocky') }}"The result of the playbook will display “RedHat/Rocky” if the machine is RedHat/Rocky based. Otherwise it will display “Not RedHat”.
Execute the playbook
ok: [node-1] => {
"msg": "Rocky"
}While not technically filters, lookup plugins are often used alongside filters in Jinja2 expressions and provide powerful ways to retrieve data from external sources during playbook execution. Lookup plugins can read files, query databases, access environment variables, and much more.
web01
web02
db01
cache01
{
"database": {
"host": "localhost",
"port": 3306,
"name": "myapp"
},
"cache": {
"host": "redis.local",
"port": 6379
}
}- hosts: localhost
gather_facts: false
tasks:
- name: Read file contents using lookup
debug:
msg: "Server list: {{ lookup('file', 'servers.txt').split('\n') | join(', ') }}"
- name: Parse JSON file using lookup
debug:
msg: "Database host: {{ (lookup('file', 'config.json') | from_json).database.host }}"
- name: Get environment variable with lookup
debug:
msg: "Current user: {{ lookup('env', 'USER') | default('unknown') }}"
- name: Generate random password using lookup
debug:
msg: "Random password: {{ lookup('password', '/dev/null length=12 chars=ascii_letters,digits') }}"
- name: Use lookup with loops
debug:
msg: "Processing server: {{ item }}"
loop: "{{ lookup('file', 'servers.txt').split('\n') }}"
when: item != ""
- name: Combine multiple lookups with filters
set_fact:
server_config: |
{%- set servers = lookup('file', 'servers.txt').split('\n') -%}
{%- set config = lookup('file', 'config.json') | from_json -%}
Servers: {{ servers | select | join(', ') }}
DB Host: {{ config.database.host }}
DB Port: {{ config.database.port }}
- name: Display combined configuration
debug:
msg: "{{ server_config }}"
- name: Use lookup to check if files exist
debug:
msg: "Config file exists: {{ lookup('first_found', ['config.json', 'config.yaml'], errors='ignore') != '' }}"Sample Output
TASK [Read file contents using lookup] *****************************************
ok: [localhost] => {
"msg": "Server list: web01, web02, db01, cache01"
}
TASK [Parse JSON file using lookup] ********************************************
ok: [localhost] => {
"msg": "Database host: localhost"
}
TASK [Get environment variable with lookup] ************************************
ok: [localhost] => {
"msg": "Current user: ansible"
}
TASK [Generate random password using lookup] ***********************************
ok: [localhost] => {
"msg": "Random password: aB3kL9mP4xR2"
}
TASK [Use lookup with loops] ************************************************
ok: [localhost] => (item=web01) => {
"msg": "Processing server: web01"
}
ok: [localhost] => (item=web02) => {
"msg": "Processing server: web02"
}
ok: [localhost] => (item=db01) => {
"msg": "Processing server: db01"
}
ok: [localhost] => (item=cache01) => {
"msg": "Processing server: cache01"
}
Common lookup plugins include:
Custom filters are python scripts that contain the logic to format text appropriately. Ansible will look for custom filters in a filter_plugins directory relative to the executing playbook.
First, lets create the filter_plugins directory within the lab03 folder we are already in
Create a new file called trademark.py in the filter_plugins directory with the following content:
class FilterModule(object):
def filters(self):
return {
'trademark': self.format_trademark
}
def format_trademark(self, input_text):
return input_text + u"\u2122"The actual formatting logic occurs in the format_trademark method. The unicode value for the trademark symbol (™) is utilized, otherwise an error would occur. Within the return statement in the filters method, the dictionary key trademark represents the name of the filter that end users can ultimately use. The dictionary value signifies the method to invoke.
- hosts: localhost
gather_facts: no
connection: local
tasks:
- name: Custom Fact
set_fact:
trademark_string: "Ansible"
- name: Print Trademark
debug:
msg: "{{ trademark_string | trademark }}"Sample Output
PLAY [localhost] ***************************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Custom Fact] *************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Print Trademark] *********************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "Ansible™"
}
PLAY RECAP *********************************************************************************************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0
Notice how the trademark symbol appears appended to the Ansible text. This validates that the custom filter plugin is being executed successfully.