Python and Jinja2 introduction

What is Jinja2?

Jinja is a modern and designer-friendly templating language for Python. The idea is to combine a template with data to produce documents. A template contains variables which are replaced by the values which are passed in when the template is rendered. The easiest would be to give a very straightforward example.

In the below example, we define a template Hello {{ something }}!. The items in curly braces will get replaced with the data that we pass to the template. In out case, we pass the value ‘World’ (through the template.render method) to the template. Jinja2 will take this value and insert it into the template. The result will be a static document that contains all the information pre-filled. That resulting document will then be used further in our Python script.

from jinja2 import Template
template = Template("Hello {{ something }}!")
print(template.render(something="World"))

Let’s dive into some more examples.

VLANs: template inside code

In this example, we will be taking a vlans object and insert it into a template to create a vlan overview.

As we will be using Jinja templates in our Python script, we need to import the required Jinja libraries (line 1 in the below script). The script is pretty straigthforward. We will first define a dict called template_vars. That dict will contain a number of key-value pairs related to our vlan configuration.

We also define an inline template which would use the values from our dict object.

The vlan_template is nothing more than a String, but Jinja2 needs it to be a Template. Hence, we are using the jinja2.Template method to convert it to a Template. On that template object, we can now call the render method, which will do the heavy lifting. It would take the template variables (from template_vars) and insert them (render) into the template. The surrounding print statement obviously just prints the result of that operation.

import jinja2

template_vars = {
   "vlan_id": 620, 
   "vlan_name": "vlan-620"
}

vlan_template = """
vlan {{ vlan_id }}
   name {{ vlan_name }}
"""

template = jinja2.Template(vlan_template)
print(template.render(template_vars))

The output of the above script is:

WAUTERW-M-65P7:ACI_Python_Requests_Jinja wauterw$ python3 vlan.py 

vlan 620
   name vlan-620

This example can be found here.

VLANs: template inside code with for-loop

What if we have more than one vlan. How would we address this?

Fortunately Jinja2 suppors for-loops as well. In the below snippet, we have a dictionary object where the vlan id is the key and the vlan name is the value. The template contains a for loop to iterate over the different key/value pairs from our dictionary object. Pretty neat, no?

import jinja2

vlans = {
   "620": "VLAN-620",
   "621": "VLAN-621", 
   "622": "VLAN-622", 
   "633": "VLAN-623",   
}

template_vars = {
   "vlans": vlans
}

vlan_template = """
{% for vlan_id, vlan_name in vlans.items() %}
vlan {{ vlan_id }}
   name {{ vlan_name }}
{% endfor %}
"""

template = jinja2.Template(vlan_template)
print(template.render(template_vars))

The output of above script will be as below:

WAUTERW-M-65P7:ACI_Python_Requests_Jinja wauterw$ python3 vlans_multiple.py 

vlan 620
   name VLAN-620

vlan 621
   name VLAN-621

vlan 622
   name VLAN-622

vlan 633
   name VLAN-623

Notice the white spaces and empty lines? Have a look at the documentation on how to deal with white spaces in more depth. If you want to get rid of these, you could also use the below template (pay attention to the - sign).

vlan_template = """
{%- for vlan_id, vlan_name in vlans.items() %}
vlan {{ vlan_id }}
   name {{ vlan_name }}
{%- endfor %}
"""

That will generate a more condensed output.

WAUTERW-M-65P7:ACI_Python_Requests_Jinja wauterw$ python3 vlans_multiple.py 

vlan 620
   name VLAN-620
vlan 621
   name VLAN-621
vlan 622
   name VLAN-622
vlan 633
   name VLAN-623

This example can be found here.

VLANs: template in seperate file

What if we don’t want to have our template defined in our code itself but rather read it in from an external file? Jinja2 has that covered as well.

Let’s start with defining the template. It’s essentially the same template as the one we used above but now we store it in a specific templates folder. Create in the templates folder a file called vlans.j2.

{%- for vlan_id, vlan_name in vlans.items() %}
vlan {{ vlan_id }}
   name {{ vlan_name }}
{%- endfor %}

Our Python script will change a bit. We will need to import the FileSystemLoader method. The 4th line in our script essentially tells Jinja2 where it can find the template folder (in our case in the templates folder).

To pass the template file, we will use the get_template method. This will pass the vlans.j2 file from the templates folder into the template object. On that template object, we then call the render function while passing the vlans dictionary. Again, all pretty straightforward.

from jinja2 import Environment
from jinja2 import FileSystemLoader

my_template = Environment(loader=FileSystemLoader('../templates'))

vlans = {
   "620": "VLAN-620",
   "621": "VLAN-621", 
   "622": "VLAN-622", 
   "623": "VLAN-623",  
}

template = my_template.get_template("vlans.j2")
result = template.render(vlans=vlans)
print(result)

As expected the output will be:

WAUTERW-M-65P7:ACI_Python_Requests_Jinja wauterw$ python3 vlans_file.py 

vlan 620
   name VLAN-620
vlan 621
   name VLAN-621
vlan 622
   name VLAN-622
vlan 623
   name VLAN-623

This file can be found here.

VLANs: template in seperate file, input from YAML file

A small improvement on the previous script could be achieved by reading the vlan information from a YML file. Let’s see how that works:

Define a YML file:

620: VLAN-620
621: VLAN-621 
622: VLAN-622 
623: VLAN-623  

The Jinja2 template did not change:

{%- for vlan_id, vlan_name in vlans.items() %}
vlan {{ vlan_id }}
   name {{ vlan_name }}
{%- endfor %}

The modified script is below. All that we changed is that we are using the yaml.load function to read the YML file into the vlans variable. Hence, the vlans variable is a Python dictionary object that can be used in the get_template method.

from jinja2 import Environment
from jinja2 import FileSystemLoader
import yaml

my_template = Environment(loader=FileSystemLoader('../templates'))

vlans = yaml.load(open('vlans.yml'), Loader=yaml.SafeLoader)

template = my_template.get_template("vlans.j2")
result = template.render(vlans=vlans)
print(result)

This file can be found here.

Loopbacks

Here is an additional example, mostly re-iterating what we learned already. Have a look at below to ensure you understand what we learned previously.

interfaces:
  Loopback2000:
    description: Description for Loopback 2000
    ipv4_addr: 200.200.200.200
    ipv4_mask: 255.255.255.255
  Loopback2001:
    description: Description for Loopback 2001
    ipv4_addr: 200.200.200.201
    ipv4_mask: 255.255.255.255
interface Loopback2000
description {{interfaces.Loopback2000.description}}
 ip address {{interfaces.Loopback2000.ipv4_addr}} {{interfaces.Loopback2000.ipv4_mask}}
!
interface Loopback2001
description {{interfaces.Loopback2001.description}}
 ip address {{interfaces.Loopback2001.ipv4_addr}} {{interfaces.Loopback2001.ipv4_mask}}
end
import yaml
from jinja2 import Environment, FileSystemLoader

loopbacks = yaml.load(open('loopback.yml'), Loader=yaml.SafeLoader)

env = Environment(loader = FileSystemLoader('../../templates'), trim_blocks=True, lstrip_blocks=True)
template = env.get_template('loopback.j2')
loopback_config = template.render(loopbacks)

print(loopback_config)

This example can be found here.

Loopbacks (variant)

Let’s look at a little variant, a little more complex. In previous example, we had multiple loopback interfaces and in the YML file we just listed them sequentially. That’s doable for two interfaces but what if you have many more. This is where a for-loop (in Jinja2) comes in very handy. Let’s have a look:

The YML file is modified a little compared to previous example. We have basically changed it so we have a list of loopbacks that we can address via it’s name.

interfaces:
  - name: Loopback2001
    description: Description for Loopback 2000
    ipv4_addr: 200.200.200.200
    ipv4_mask: 255.255.255.255
  - name:  Loopback2002
    description: Description for Loopback 2001
    ipv4_addr: 200.200.200.201
    ipv4_mask: 255.255.255.255

We will rewrite the Jinja2 template. As mentioned above, we want to use a for loop inside the template. This makes it much more scalable in case we need more interfaces later on.

{%- for interface in data.interfaces %}
interface {{interface.name}}
description {{interface.description}}
 ip address {{interface.ipv4_addr}} {{interface.ipv4_mask}}
{%- endfor %}

The Python file did not change, but I’ve added it here for completeness:

import yaml
from jinja2 import Environment, FileSystemLoader

interfaces = yaml.load(open('loopback_variant.yml'), Loader=yaml.SafeLoader)

env = Environment(loader = FileSystemLoader('../../templates'), trim_blocks=True, lstrip_blocks=True)
template = env.get_template('loopback_variant.j2')
loopback_config = template.render(data=interfaces)

print(loopback_config)

Running this script will render the list of loopbacks.

WAUTERW-M-65P7:loopbacks wauterw$ python3 loopback_variant.py 
interface Loopback2001
description Description for Loopback 2000
 ip address 200.200.200.200 255.255.255.255interface Loopback2002
description Description for Loopback 2001
 ip address 200.200.200.201 255.255.255.255

This example can be found here.

That’ll do for now. In a next post, we will look into applying these concepts to Cisco ACI.

I mentioned the source files already throughout this article. The entire folder containing all examples can be found here.