Post

Handling Jinja with Python

Handling Jinja with Python

Introduction

Configuring network devices can be a repetitive and error-prone task, especially when similar settings need to be applied across multiple devices. Whether you’re working with Cisco, Juniper, or even custom systems, generating configuration files manually is not only tedious—it’s a prime candidate for automation. With Jinja templating and Python, you can simplify your workflow, reduce mistakes, and ensure consistency across your environment. In this post, we’ll explore how to create dynamic configuration templates that can adapt to various scenarios, making your network automation smoother and more efficient.


1. Getting Started with Jinja in Network Engineering

First, install Jinja2 via pip if you haven’t already:

1
pip install Jinja2

Then, let’s create a basic template for a network device configuration.

Basic Config Template

Imagine a simple configuration where you set the hostname and configure a single interface:

1
2
3
4
5
6
hostname {{ hostname }}
!
interface {{ interface }}
 ip address {{ ip_address }} {{ subnet_mask }}
 no shutdown
!

Render the template with Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from jinja2 import Template

config_template = """
hostname {{ hostname }}
!
interface {{ interface }}
 ip address {{ ip_address }} {{ subnet_mask }}
 no shutdown
!
"""

template = Template(config_template)
rendered_config = template.render(
    hostname="Device1",
    interface="GigabitEthernet0/1",
    ip_address="192.168.1.1",
    subnet_mask="255.255.255.0"
)
print(rendered_config)

This script outputs a configuration with your specified variables.


2. Dynamic Configurations with Loops

Often, network devices have multiple interfaces. You can use Jinja loops to generate configuration blocks for each interface dynamically.

Example: Configuring Multiple Interfaces

Create a Jinja template that loops over a list of interface dictionaries:

1
2
3
4
5
6
7
8
hostname {{ hostname }}
!
{% for intf in interfaces %}
interface {{ intf.name }}
 ip address {{ intf.ip_address }} {{ intf.subnet_mask }}
 no shutdown
!
{% endfor %}

And render it with Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from jinja2 import Template

multi_intf_template = """
hostname {{ hostname }}
!
{% for intf in interfaces %}
interface {{ intf.name }}
 ip address {{ intf.ip_address }} {{ intf.subnet_mask }}
 no shutdown
!
{% endfor %}
"""

template = Template(multi_intf_template)
config = template.render(
    hostname="Device2",
    interfaces=[
        {'name': 'GigabitEthernet0/1', 'ip_address': '10.0.0.1', 'subnet_mask': '255.255.255.0'},
        {'name': 'GigabitEthernet0/2', 'ip_address': '10.0.1.1', 'subnet_mask': '255.255.255.0'}
    ]
)
print(config)

This produces a configuration for each interface defined in the list.


3. Using File-Based Templates for Complex Configurations

For real-world applications, store your config templates as separate files. Use Jinja’s FileSystemLoader to manage these templates.

Example: File-Based Template

Suppose you save your template as config_template.txt in a folder called templates:

1
2
3
4
5
6
7
8
hostname {{ hostname }}
!
{% for intf in interfaces %}
interface {{ intf.name }}
 ip address {{ intf.ip_address }} {{ intf.subnet_mask }}
 no shutdown
!
{% endfor %}

Load and render the template using Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template("config_template.txt")

rendered_config = template.render(
    hostname="Device3",
    interfaces=[
        {'name': 'GigabitEthernet0/1', 'ip_address': '172.16.0.1', 'subnet_mask': '255.255.0.0'},
        {'name': 'GigabitEthernet0/2', 'ip_address': '172.16.1.1', 'subnet_mask': '255.255.0.0'}
    ]
)
print(rendered_config)

This approach makes your configuration management scalable and maintainable.


4. Advanced Templating Techniques

Custom Filters for Data Transformation

You can create custom filters for scenarios like formatting IP addresses or converting numerical values.

1
2
3
4
5
6
7
8
def format_ip(ip):
    # For example, add a simple transformation if needed.
    return ip.strip()

env.filters['format_ip'] = format_ip

template = env.from_string("Interface IP: {{ ip_address | format_ip }}")
print(template.render(ip_address=" 192.168.100.1 "))

Template Inheritance for Consistency

If you manage many similar devices, use template inheritance to define a base configuration and extend it for device-specific settings.

base_config.txt:

1
2
3
4
hostname {{ hostname }}
!
{% block interfaces %}
{% endblock %}

device_config.txt:

1
2
3
4
5
6
7
8
9
{% extends "base_config.txt" %}
{% block interfaces %}
{% for intf in interfaces %}
interface {{ intf.name }}
 ip address {{ intf.ip_address }} {{ intf.subnet_mask }}
 no shutdown
!
{% endfor %}
{% endblock %}

Render this extended template with Python to keep your configurations DRY (Don’t Repeat Yourself).


5. Real-World Applications in Network Automation

By integrating Jinja with Python, you can automate various network tasks, such as:

  • Bulk configuration deployment: Generate config files for multiple devices in your network.
  • Automated updates: Seamlessly update configurations when IP addresses or other parameters change.
  • Audit and compliance: Quickly regenerate configurations for audit purposes.

6. Security Considerations

When working with network configurations, ensure that sensitive data is handled securely:

  • Sanitize Inputs: Always validate and sanitize variables passed into templates.
  • Access Controls: Limit who can generate and deploy configuration files.
  • Version Control: Maintain version control over your templates to track changes and audit modifications.
1
env = Environment(loader=FileSystemLoader("templates"), autoescape=True)

This snippet demonstrates enabling autoescaping—even though it’s more relevant for web applications, it’s a good practice when handling user-supplied input.

This post is licensed under CC BY 4.0 by the author.