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.