An Introduction to Jinja2 for Network Automation

An Introduction to Jinja2 for Network Automation

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.

Please note that when specifically discussing the action of merging template and data, it may also simply be referred to as the “rendering process” within the context of a template engine.

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.

Subscribe to our newsletter to keep updated.

Don't miss anything. Get all the latest posts delivered straight to your inbox.
Great! Check your inbox and click the link to confirm your subscription.
Error! Please enter a valid email address!