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.
![](https://www.packetswitch.co.uk/content/images/size/w1200/2023/06/jinja-cisco-interface.png)
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()
print(output)
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
---
interfaces:
- name: Eth1
p2p: 10.10.10.0/30
as: 5678
description: CUST-1
- name: Eth2
p2p: 10.10.20.0/30
as: 1234
description: CUST-2
- name: Eth3
p2p: 10.10.30.0/30
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.
![](https://www.packetswitch.co.uk/content/images/size/w1200/2024/05/nornir_napalm.png)
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('.'),
trim_blocks=True,
lstrip_blocks=True)
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:
fw.write(config)
{% for interface in interfaces %}
interface {{ interface.name }}
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 {{ neighbor.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 10.10.10.1/30
no switchport
no shut
!
interface Eth2
description CUST-2
ip address 10.10.20.1/30
no switchport
no shut
!
interface Eth3
description CUST-3
ip address 10.10.30.1/30
no switchport
no shut
!
router bgp 5000
neighbor 10.10.10.2 remote-as 5678
neighbor 10.10.20.2 remote-as 1234
neighbor 10.10.30.2 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.