Introduction
In this blog post, we’re going to explore Jinja2, an incredibly versatile tool that’s very useful for network automation. At its core, Jinja2 is a template engine. You can think of it as a stencil in arts and crafts. Just as you might use a stencil to consistently reproduce a particular shape or design on multiple surfaces, Jinja2 allows you to repeatedly generate specific text patterns in your documents or code.
We’ll start off with a very simple example instead of bombarding you with theory. I think once you see the example, you’ll quickly understand the benefits. We will also look into what problems Jinja2 solves and why it’s worth learning. After all, if we’re going to invest time in learning something new, it should be appealing and bring clear benefits, right? So, let’s get started.
A Simple Example
Raise your hand if you’ve ever worked on preparing interface configurations for network devices. Chances are, you’ve done this at least once. Typically, you’d pick the interfaces you need to configure, then use a text editor to prepare the configurations. You start with a single interface, copy and paste, then tweak the interface name, description, IP, and so on. This method works fine until you realize you forgot to add ‘no shut’. Now, you have to update all your interfaces, and it’s easy to miss one or more in the process.
Now, let’s see how we can handle this with Jinja2. I’ll keep it brief for now. You need a template, which is a blueprint for what each configuration should look like; a variable file, which stores the values specific to each interface; and a tool to merge these together, typically Python or Ansible. I’ll go into the script in more detail later in the post, but for now, here are the variables and the template file, along with the end result.
vars.yaml
---
interfaces:
- name: Eth11
p2p: 10.10.10.1/30
description: TRANSIT-LINK
- name: Eth12
p2p: 10.10.20.1/30
description: CUSTOMER-A
- name: Eth13
p2p: 10.10.30.1/30
description: OUTSIDE
template.j2
{% for interface in interfaces %}
interface {{ interface.name }}
description {{ interface.description }}
ip address {{ interface.p2p }}
no switchport
no shut
!
{% endfor %}
Here is the resulting config and this approach ensures that each interface is configured correctly, without the manual repetition and the risk of errors.
interface Eth11
description TRANSIT-LINK
ip address 10.10.10.1/30
no switchport
no shut
!
interface Eth12
description CUSTOMER-A
ip address 10.10.20.1/30
no switchport
no shut
!
interface Eth13
description OUTSIDE
ip address 10.10.30.1/30
no switchport
no shut
!
The benefit of this approach is that if you need to remove 'no shut' from all the interfaces, all you have to do is delete the line from the template and generate a new config rather than editing every interface.
Jinja2 Playground: Get Hands-on
To test out the Jinja2 example just covered, click below:
Try It Out Live ➜Breaking Down Jinja2 Components
To make the most of Jinja2, you need to understand the three key components that make this process: the template, the variable file, and a template engine.
Template - This is the blueprint of your configuration. It outlines the structure and placeholders for data that will be filled in by your variable file. The template ensures that your configuration is consistent and error-free, maintaining a standard format for all your network devices.
Variables - Often referred to as the ‘vars’ which contains all the specific values for your placeholders in the template. Each variable corresponds to a placeholder in the template, and these values are what differentiate one configuration section from another.
Template engine - To combine the template with the variable file and produce the final configuration, you use a template engine. Typically, this would be either Ansible or Python. These tools read the template and the variable file, merge them based on the logic you’ve defined, and output the complete configuration.
Jinja2 Template Syntax
There’s a lot to cover when it comes to Jinja2, but we will cover the basic syntax in this blog post. This foundational knowledge is enough for you to start experimenting with Jinja2 in your workflow and learn from there.
Curly Braces and Variable Notation
In a Jinja2 template, you use double curly braces {{ }}
to denote variables. This syntax tells Jinja2 to look for the value of a variable and render it in the template.
{{ interface.name }}
Control Structures
Jinja2 uses {% %}
to define control structures, such as loops and conditional statements. This syntax separates logic from data, allowing the template engine to interpret and execute the logic during rendering.
For Loops - In Jinja2 (or just like any other programming language), for loops are used to iterate over a collection of items, such as a list or dictionary from your variable file. This is useful when you need to apply the same template structure to multiple entries without manually repeating code.
{% for interface in interfaces %}
interface {{ interface.name }}
{% endfor %}
If-Else Statements - In Jinja2, if-else statements allow you to conditionally render parts of the template based on the values of variables. This control structure is useful for templates that need to adapt to different situations (access port vs trunk ports for example)
{% if interface.isEnabled %}
interface {{ interface.name }}
no shut
{% else %}
interface {{ interface.name }}
shutdown
{% endif %}
Comments
To include comments in your Jinja2 templates that won’t appear in the final output, use {# #}
. This is especially useful for adding notes or reminders within the template code that only the developer will see.
{# This is a comment and won't be included in the output #}
Providing Variables to Jinja2 Templates
In Jinja2, variables play an important role as they supply the dynamic content needed for template rendering. There are several methods to provide these variables to your templates, each suited for different scenarios.
Direct Assignment in Template (Less Common)
You can directly define variables within a Jinja2 template. This approach is straightforward but less flexible as the data is hardcoded into the template which kind of defeats the purpose of using templates.
{% set interface_name = 'Eth10' %}
interface {{ interface_name }}
Description Directly assigned
External Variables File
More commonly, variables are defined in an external file (often YAML or JSON), which Jinja2 imports at the time of rendering. This method is more scalable and manageable, especially for complex configurations.
interfaces:
- name: Eth1
description: SERVER-01
- name: Eth2
description: AD-01
You would then reference these variables in your template as follows.
{% for interface in interfaces %}
interface {{ interface.name }}
description {{ interface.description }}
{% endfor %}
Passing Variables at Runtime
You can also pass variables to the template at runtime. This can be done through a script in Python or Ansible, where you define the variables in your script and pass them to the template engine to be rendered.
from jinja2 import Template
template = Template('Hello {{ name }}!')
print(template.render(name='Alice'))
Understanding Template Engines
Template engines enable you to dynamically generate configurations based on predefined templates and variable data. Common tools used for this purpose include Ansible and Python, each providing robust support for template rendering.
Ansible
Here’s how you might use an Ansible playbook to render the template with the provided variable data.
template.j2
{% for interface in interfaces %}
interface {{ interface.name }}
description {{ interface.description }}
ip address {{ interface.p2p }}
no switchport
no shut
!
{% endfor %}
jinja2_play.yaml
---
- hosts: localhost
vars:
interfaces:
- name: Eth11
p2p: 10.10.10.1/30
description: TRANSIT-LINK
- name: Eth12
p2p: 10.10.20.1/30
description: CUSTOMER-A
- name: Eth13
p2p: 10.10.30.1/30
description: OUTSIDE
tasks:
- name: Generate interface configurations
template:
src: template.j2
dest: "cisco.cfg"
loop: "{{ interfaces }}"
.
├── cisco.cfg
├── jinja2_play.yaml
├── template.j2
In this playbook, we define the variables directly within the playbook under vars. The template module is then used to process each interface through a Jinja2 template named template.j2
. The output is saved to a file named cisco.cfg
in the same directory as the playbook.
Python Example
You can also use Python to render Jinja2 templates, which is particularly useful when you want to integrate Jinja2 with existing network management tools like Napalm and Netmiko. Similar to Ansible, with Python we simply provide both the template and the variable data, and Python handles the rest, producing the same end result.
vars.yaml
---
interfaces:
- name: Eth11
p2p: 10.10.10.1/30
description: TRANSIT-LINK
- name: Eth12
p2p: 10.10.20.1/30
description: CUSTOMER-A
- name: Eth13
p2p: 10.10.30.1/30
description: OUTSIDE
script.py
import yaml
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('.'),
trim_blocks=True,
lstrip_blocks=True)
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)
The script starts by importing the necessary libraries: yaml for reading YAML files where your data is stored and Environment and FileSystemLoader from the jinja2 module to manage templates.
The Jinja2 environment is set up to look for templates in the current directory, with parameters to control whitespace around Jinja2 control structures. The template named template.j2
is then loaded from the current directory.
Next, the script reads the variables from a file named vars.yaml
, converting its contents into a Python dictionary using yaml.safe_load(f)
. The template is processed with these variables using template.render(data)
, combining the template with the data to produce the final configuration output.
Lastly, this output is written to a file called config.txt
in the current directory.
.
├── config.txt
├── script.py
├── template.j2
└── vars.yaml
Closing Thoughts
We’ve just scratched the surface of what Jinja2 can do, particularly with tools like Ansible and Python. There’s a lot more to explore when it comes to automating network configurations and making your workflows more efficient. We’ll be diving deeper into these topics in future posts, so if you’re interested in learning more about automation and how to effectively apply it, make sure to subscribe and stay tuned for more content.