Using Python functions in Jinja templates
3 min read
In certain situations, Jinja templates can become overly complex and difficult to manage due to the presence of numerous deeply nested if
statements, which can make the logic hard to follow and maintain, and methods used to handle variables can become cumbersome, leading to code that is not only difficult to read but also challenging to debug.
All these factors combined can result in templates that are visually unappealing and hard to work with, making it a daunting task for developers to make updates or changes without introducing errors.
What might be worth knowing is the fact that you can pass a Python function into your Jinja templates. Doing this can greatly improve the readability of your template as well as allow you to handle more complicated scenarios.
Using a Python function in a Jinja template
The following Python will pass 2 functions to the Jinja template.
First, we define 2 functions: ‘hello_world’ and ‘multiply’. These functions are placed in a dictionary.
After this, the render function is created. Inside this function, we use jinja_template.globals.update(func_dict) to pass the previously created func_dict and expose it during the Jinja rendering phase.
The function ends up rendering the template and returning the resulting string:
# !/usr/bin/env python3
from jinja2 import Environment, FileSystemLoader
import ipaddress
# Function to return network address for an IP
def get_network_address(ip, subnet_mask):
return str(ipaddress.IPv4Network(f"{ip}/{subnet_mask}", strict=False).network_address)
def multiply(x, y):
return str(x * y)
# Function to generate interface description
def interface_description(interface_name, vlan_id):
return f"Interface {interface_name} assigned to VLAN {vlan_id}"
# Add functions to a dictionary
func_dict = {
"get_network_address": get_network_address,
"multiply": multiply,
"interface_description": interface_description,
}
def render(template, variables):
env = Environment(loader=FileSystemLoader("./templates/"))
jinja_template = env.get_template(template)
jinja_template.globals.update(func_dict)
template_string = jinja_template.render(variables)
return template_string
# Main execution
if __name__ == "__main__":
# Example variables passed to the template
template_vars = {
"interface_name": "GigabitEthernet0/1",
"vlan_id": 100,
"ip": "192.168.1.10",
"subnet_mask": "255.255.255.0",
}
print(render(template="network_config.j2", variables=template_vars))
In the following example, Jinja /srv/templates/test.j2, we use the functions that our previous Python passes into the template:
# Network configuration template for {{ interface_name }}
interface {{ interface_name }}
description {{ interface_description(interface_name, vlan_id) }}
ip address {{ ip }} {{ subnet_mask }}
network address: {{ get_network_address(ip, subnet_mask) }}
# Sample ACL rule calculation (multiply can be used for network-related calculations)
Example of multiply function:
Result of 10 * 20 = {{ multiply(10, 20) }}
When we run the Python script, we get the following result:
# Network configuration template for GigabitEthernet0/1
interface GigabitEthernet0/1
description Interface GigabitEthernet0/1 assigned to VLAN 100
ip address 192.168.1.10 255.255.255.0
network address: 192.168.1.0
# Sample ACL rule calculation (multiply can be used for network-related calculations)
Example of multiply function:
Result of 10 * 20 = 200
Being able to use Python functions like this inside a Jinja template has been very useful to me. It increased the readability of some templates as I was able to replace large parts with straightforward Python functions.
It also allowed me to handle more complex logic in a Python function. This allowed me to introduce more complicated configurations as well as make templates behave based on the state of other systems.
It is not something that is required all the time, but it is nice to know that it is a possibility.