Jinja2 is a very popular and powerful Python-based template engine. Since Ansible is written in Python, it becomes the default choice for most users, just like other Python-based configuration management systems, such as Fabric and SaltStack. The name Jinja originated from the Japanese word for temple, which is similar in phonetics to the word template.
Some of the important features of Jinja2 are:
- It is fast and compiled just in time with the Python byte code
- It has an optional sandboxed environment
- It is easy to debug
- It supports template inheritance
Variables
As we have seen, we can print variable content simply with the
'{{ VARIABLE_NAME }}' syntax. If we want to print just an element of
an array we can use '{{ ARRAY_NAME['KEY'] }}', and if we want to print
a property of an object, we can use '{{ OBJECT_NAME.PROPERTY_NAME }}'.
For example here I have a playbook jinja2_temp_1.yml where I have
defined a variable inside the playbook using vars:
---
- name: Data Manipulation
hosts: localhost
gather_facts: false
vars:
my_name: Deepak Prasad
tasks:
- name: Print message
debug:
msg: "My name is {{ my_name }}"
Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook jinja2_temp_1.yml
PLAY [Data Manipulation] *************************************************************************************************
TASK [Print message] *****************************************************************************************************
ok: [localhost] => {
"msg": "My name is Deepak Prasad"
}
PLAY RECAP ***************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
So the variable was accessed using {{ my_name }} where {{ }} is
referred as Jinja2 syntax. This was a simple method to access any
variable inside the playbook.
Use built-in filters
Filters are same as small functions, or methods, that can be run on the variable. Some filters operate without arguments, some take optional arguments, and some require arguments. Filters can be chained together, as well, where the result of one filter action is fed into the next filter and the next. Jinja2 comes with many built-in filters
A filter is applied to a variable by way of the pipe symbol (|)
followed by the name of the filter and then any arguments for the filter
inside parentheses. There can be a space between the variable name and
the pipe symbol as well as a space between the pipe symbol and the
filter name.
Syntax filter
From time to time, we may want to change the style of a string a little
bit, without writing specific code for it, for example, we may want to
capitalize some text. To do so, we can use one of Jinja2’s filters, such
as: '{{ VARIABLE_NAME | capitalize }}'. There are many filters
available for Jinja2 which can be collected from
official Jinja website.
We will go through some of the examples to perform data manipulation
using filters in this next playbook jinja2_temp_2.yml:
---
- name: Data Manipulation
hosts: localhost
gather_facts: false
vars:
my_name: Deepak Prasad
tasks:
- name: Print message
debug:
msg:
- "My name is {{ my_name }}"
- "My name is {{ my_name | lower }}"
- "My name is {{ my_name | upper }}"
- "My name is {{ my_name | capitalize }}"
- "My name is {{ my_name | title }}"
Here we are printing the same message using different filter. The filter
will be applied only to the variable which we have defined under vars
i.e my_name
Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook jinja2_temp_2.yml
PLAY [Data Manipulation] *************************************************************************************************
TASK [Print message] *****************************************************************************************************
ok: [localhost] => {
"msg": [
"My name is Deepak Prasad",
"My name is deepak prasad",
"My name is DEEPAK PRASAD",
"My name is Deepak prasad",
"My name is Deepak Prasad"
]
}
PLAY RECAP ***************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
You can observe the difference in the output where “Deepak Prasad”
value is manipulated based on the filter applied.
default filter
The default filter is a way to provide a default value for an otherwise
undefined variable, which will prevent Ansible from generating an error.
It is shorthand for a complex if statement checking if a variable is
defined before trying to use it, with an else clause to provide a
different value.
In this sample playbook jinja2_temp_3.yml I have defined first_name
variable but have not defined last_name
---
- name: Data Manipulation
hosts: localhost
gather_facts: false
vars:
first_name: Deepak
tasks:
- name: Print message
debug:
msg:
- "My name is {{ first_name }} {{ last_name }}"
So, when I execute this playbook, I get this error:
TASK [Print message] *****************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'last_name' is undefined\n\nThe error appears to be in '/home/ansible/jinja2_temp_3.yml': line 8, column 6, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n tasks:\n - name: Print message\n ^ here\n"}
This error is pretty much expected because it says
“The task includes an option with an undefined variable”
Now we can handle such situations by defining a default filter with
the variable. I have updated my playbook and added a default filter with
last_name so that if this variable is not defined then the default
value will be used:
---
- name: Data Manipulation
hosts: localhost
gather_facts: false
vars:
first_name: Deepak
tasks:
- name: Print message
debug:
msg:
- "My name is {{ first_name }} {{ last_name | default('Prasad') }}"
Now if we execute this playbook we get proper value for last_name
variable:
[ansible@controller ~]$ ansible-playbook jinja2_temp_3.yml
PLAY [Data Manipulation] *************************************************************************************************
TASK [Print message] *****************************************************************************************************
ok: [localhost] => {
"msg": [
"My name is Deepak Prasad"
]
}
PLAY RECAP ***************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
List and set
We can use inbuilt filters to iterate over the individual values of List and perform operation such as find the highest number or lowest number within a List. Let us take some examples:
In this sample playbook we are performing a bunch of operations on the
list which I have defined under my_vars:
---
- name: Data Manipulation
hosts: localhost
gather_facts: false
vars:
my_list: [1,2,3,4,5,6,5,3,7,1,9]
tasks:
- name: List and Set
debug:
msg:
- "The highest no {{ my_list | max }}"
- "The lowest no is {{ my_list | min }}"
- "Print only unique values {{ my_list | unique }}"
- "Print random no {{ my_list | random }}"
- "Join the values of list {{ my_list | join('-') }}"
Let us check the output once we execute this playlist:
TASK [List and Set] ******************************************************************************************************
ok: [localhost] => {
"msg": [
"The highest no 9",
"The lowest no is 1",
"Print only unique values [1, 2, 3, 4, 5, 6, 7, 9]",
"Print random no 1",
"Join the values of list 1-2-3-4-5-6-5-3-7-1-9"
]
}
Here using Jinja2 filters we are iterating over the individual values of
my_list and then
- printing only the highest number using
max - printing only the lowest number using
min - printing only the unique values using
unique - printing a random value from the list using
random - Joining the list values with hyphen(
-) and printing the output.
Filters dealing with pathnames
Configuration management and orchestration frequently refers to path names, but often only part of the path is desired. Ansible provides a few filters to help.
In this sample playbook jinja2_temp_5.yml I have used multiple filters
on Linux and Windows PATH:
---
- name: Data Manipulation
hosts: localhost
gather_facts: false
vars:
path1: "/opt/custom/data/bin/script.sh"
path2: 'C:\Users\deeprasa\PycharmProjects\elasticsearch\test.log'
path3: "~/jinja2_temp_5.yml"
tasks:
- name: filters to work on pathnames
debug:
msg:
- "Linux Path: {{ path1 | dirname }}"
- "Windows Path: {{ path2 | win_dirname }}"
- "Linux script name: {{ path1 | basename }}"
- "Split the path: {{ path2 | win_splitdrive }}"
- "Windows Drive: {{ path2 | win_splitdrive | first }}"
- "Windows File name: {{ path2 | win_splitdrive | last }}"
- "Show Full path: {{ path3 | expanduser }}"
Output from the playbook:
[ansible@controller ~]$ ansible-playbook jinja2_temp_5.yml
PLAY [Data Manipulation] *************************************************************************************************
TASK [filters to work on pathnames] **************************************************************************************
ok: [localhost] => {
"msg": [
"Linux Path: /opt/custom/data/bin",
"script.sh",
"Split the path: ('C:', '\\\\Users\\\\deeprasa\\\\PycharmProjects\\\\elasticsearch\\\\test.log')",
"Windows Drive: C:",
"Windows File name: \\Users\\deeprasa\\PycharmProjects\\elasticsearch\\test.log",
"Show Full path: /home/ansible/jinja2_temp_5.yml"
]
}
PLAY RECAP ***************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Filters for date and time
We have different filters to work with date and time.
---
- name: Data Manipulation
hosts: localhost
gather_facts: false
vars:
mydate1: "2020-08-14 20:00:00"
mydate2: "2018-08-15 21:01:40"
tasks:
- name: Date and time filters
debug:
msg:
- "Today's date: {{ '%d-%m-%Y' | strftime }}"
- "Today's date and time: {{ '%d-%m-%Y %H:%M:%S' | strftime }}"
- "Print seconds since {{ mydate1 }}: {{ ((mydate2 | to_datetime) - (mydate1 | to_datetime)).seconds }}"
- "Print days since {{ mydate2 }}: {{ ((mydate2 | to_datetime) - (mydate1 | to_datetime)).days }}"
In this playbook jinja2_temp_6.yml we are simply printing the date and
time in the first two debug message while in the next two we are
converting the time passed since the provided date into seconds and
days.
Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook jinja2_temp_6.yml
PLAY [Data Manipulation] *************************************************************************************************
TASK [Date and time filters] *********************************************************************************************
ok: [localhost] => {
"msg": [
"Today's date: 24-09-2020",
"Today's date and time: 24-09-2020 06:53:45",
"Print seconds since 2020-08-14 20:00:00: 3700",
"Print days since 2018-08-15 21:01:40: -730"
]
}
PLAY RECAP ***************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Configure VSFTPD using Jinja2 templates
Now that we know something about Jinja2 filters and syntax, we will use
a Jinja2 template so configure vsftpd on one of our managed nodes.
I will create lab2 project directory which we will use for this
example. Here Project means nothing but a new directory which contains
everything your playbook needs such as ansible.cfg, inventory etc.
[ansible@controller ~]$ mkdir lab2
[ansible@controller ~]$ cd lab2/
Next copy the ansible.cfg from the default location to the project
directory
[ansible@controller lab2]$ cp /etc/ansible/ansible.cfg .
We will create our own inventory file with single managed node entry as I don’t need multiple managed nodes for this example:
[ansible@controller lab2]$ cat inventory
server2
Create a templates directory and navigate inside the same:
[ansible@controller lab2]$ mkdir templates
[ansible@controller lab2]$ cd templates/
We have created a Jinja2 template file with content which is required to
configure an FTP server using vsftpd. I have also used ansible facts to
get the IPv4 address from the managed node and place it in the
vsftpd.conf just for reference purpose.
[ansible@controller templates]$ cat vsftpd.j2
anonymous_enable={{ anonymous_enable }}
local_enable={{ local_enable }}
write_enable={{ write_enable }}
anon_upload_enable={{ anon_upload_enable }}
dirmessage_enable=YES
xferlog_enable=YES
connect_from_port_20=YES
pam_service_name=vsftpd
userlist_enable=YES
# MY IP Address={{ ansible_facts['default_ipv4']['address'] }}
Now that our Jinja2 template is ready, we will create our playbook
configure_vsftpd.yml inside the lab2 directory which will install
and configure vsftpd on server2. So this playbook will perform 2
TASK.
[ansible@controller lab2]$ cat configure_vsftpd.yml
---
- name: Install and Configure vSFTPD
hosts: server2
become: yes
vars:
anonymous_enable: yes
local_enable: yes
write_enable: yes
anon_upload_enable: yes
tasks:
- name: install vsftp
yum:
name: vsftpd
- name: use Jinja2 template to configure vsftpd
template:
src: vsftpd.j2
dest: /etc/vsftpd/vsftpd.conf
We will now execute the playbook:
[ansible@controller lab2]$ ansible-playbook configure_vsftpd.yml
PLAY [Install and Configure vSFTPD] **************************************************************************************
TASK [Gathering Facts] ***************************************************************************************************
ok: [server2]
TASK [install vsftp] *****************************************************************************************************
changed: [server2]
TASK [use Jinja2 template to configure vsftpd] ***************************************************************************
changed: [server2]
PLAY RECAP ***************************************************************************************************************
server2 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
So the playbook has executed successfully and changed=2 which means both our task have completed.
Let us verify the vsftpd.conf content from server2
[ansible@server2 ~]$ sudo cat /etc/vsftpd/vsftpd.conf
anonymous_enable=True
local_enable=True
write_enable=True
anon_upload_enable=True
dirmessage_enable=YES
xferlog_enable=YES
connect_from_port_20=YES
pam_service_name=vsftpd
userlist_enable=YES
# MY IP Address=172.31.23.18
What’s Next
Next in our Ansible Tutorial we will learn all about Ansible Facts


