How to Create Custom Jinja2 Filters?

How to Create Custom Jinja2 Filters?
In: NetDevOps, Python

Hi everyone, welcome back to another blog post on Jinja2 and Python. I'm not an expert in Jinja2; I know enough to get by and I'm always learning new things. I'm familiar with using Jinja2's built-in filters like upper, lower, and capitalize, but just a few days ago, I discovered something new. I can make my own filters! It was a real "wow, how did I not know that?" moment. In this post, let's dive into an example of how to do just that.

Generating Cisco Interface Configurations with Jinja2 Template
In this blog post, we will explore the process of generating Cisco interface configurations using Python and Jinja2. An interface configuration can vary depending on

A Very Simple Example

Let's break down a very simple example of creating a custom Jinja2 filter. First, you need to understand the basic steps and the syntax involved. To start, you'll need to define a custom filter function in Python. This function will take an input, manipulate it as you specify, and return the modified output. In our example, the custom function will convert text to uppercase and add three exclamation marks at the end.

from jinja2 import Environment, Template

# Define the custom filter function
def custom_uppercase(input):
    return input.upper() + "!!!"

# Create an environment and register the custom filter
env = Environment()
env.filters['shout'] = custom_uppercase

# Example template using the custom filter
template = env.from_string("Hello {{ 'world'|shout }}")

# Render the template
output = template.render()

Once you've defined your function, the next step is to create a Jinja2 environment. This environment acts as a sandbox where your templates live and where filters are applied. Here, you'll register your custom filter so it can be used in templates. This is done by adding your filter function to the filters dictionary of the environment. The key is the name you want to use for the filter in your templates (like 'shout' in our example), and the value is the function itself.

This tells Jinja2 that whenever you use |shout in a template, it should apply the custom_uppercase function.

Finally, you create a template that uses your custom filter. In the example, {{ 'world'|shout }} in the template will apply the shout filter to the word 'world'. When you render this template, the filter transforms world into WORLD!!!, and that's what gets printed.

A More Realistic Example

  - name: Eth1
    as: 5678
    description: CUST-1

  - name: Eth2
    as: 1234
    description: CUST-2

  - name: Eth3
    as: 9101
    description: CUST-3

Here, I have a variable file that contains an interface, a /30 subnet, the BGP AS number of the peer, and a description. My goal is to configure an interface with the first available IP, add a description, and then create a BGP peer with the last available IP from the subnet.

By creating custom Jinja2 filters, I didn't have to maintain individual IP addresses in my variable file.

How to Use Nornir Napalm Plugin?
While Nornir will continue to manage device inventory and task execution, we’ll use Napalm to push configurations to the devices.
import yaml
from jinja2 import Environment, FileSystemLoader
from netaddr import IPNetwork, IPAddress

def get_first_ip(ip):
    return IPAddress(IPNetwork(ip).first+1).__str__()

def get_last_ip(ip):
    return IPAddress(IPNetwork(ip).last-1).__str__()

def get_cidr(ip):
    return IPNetwork(ip).prefixlen

env = Environment(loader=FileSystemLoader('.'),

env.filters['first_ip'] = get_first_ip
env.filters['last_ip'] = get_last_ip
env.filters['cidr'] = get_cidr

template = env.get_template('template.j2')

with open ('vars.yaml', 'r') as f:
    data = yaml.safe_load(f)

config = template.render(data)
with open ('config.txt', 'w') as fw:
{% for interface in interfaces %}
interface {{ }}
 description {{ interface.description }}
 ip address {{ interface.p2p | first_ip }}/{{ interface.p2p | cidr }}
 no switchport
 no shut
{% endfor %} 
router bgp 5000
{% for neighbor in interfaces %}
 neighbor {{ neighbor.p2p | last_ip }} remote-as {{ }}
{% endfor %}

This script introduces three custom Jinja2 filters designed to work with IP addresses. The first_ip filter calculates the first usable IP address in a subnet by using the IPNetwork object from the netaddr library. The last_ip filter finds the last usable IP in a subnet and the cidr filter retrieves the prefix length from a subnet address.

By using these custom filters into the Jinja2 environment, the script can dynamically render templates that include the correct IP addresses and subnet. This makes it easier to manage network configurations without manually editing each detail.

Finally, here is the generated configuration

interface Eth1
 description CUST-1
 ip address
 no switchport
 no shut
interface Eth2
 description CUST-2
 ip address
 no switchport
 no shut
interface Eth3
 description CUST-3
 ip address
 no switchport
 no shut
router bgp 5000
 neighbor remote-as 5678
 neighbor remote-as 1234
 neighbor remote-as 9101

So, as you can see from the example, I was able to dynamically pick the first and last IP from a given subnet. Before, I used to hard-code these IPs in the variables file, but now, this approach greatly simplifies the workflow for me.

I got this idea from a Python library called 'j2ipaddr.' Instead of creating your own functions, this library comes with a set of pre-built functions that you just need to import and add as filters. Feel free to check it out and a big shoutout to the owner for creating such a helpful tool.

Written by
Suresh Vina
Tech enthusiast sharing Networking, Cloud & Automation insights. Join me in a welcoming space to learn & grow with simplicity and practicality.
More from Packetswitch
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Packetswitch.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.