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