Jinja2 Configuration Templates
Jinja2 Configuration Templates for Network Automation¶
"From Copy-Paste Configs to Data-Driven Generation β Template Everything"¶
You've mastered YAML for data modelling and JSON for API interactions. Now it's time to learn Jinja2βthe template engine that transforms your structured data into actual device configurations.
Why Jinja2 is essential for network automation:
- β Generate configurations from data β Write template once, apply to 1000 devices
- β Eliminate copy-paste errors β No more manual find-and-replace
- β Consistent standards β Every device gets the same template (with device-specific values)
- β Version controlled templates β Track configuration standards over time
- β Conditional logic β Different config blocks based on device type, role, location
- β Python-native β Integrates seamlessly with your automation workflows
Real-world impact: A single Jinja2 template can replace hundreds of static configuration files. Change the template once, regenerate configs for your entire network.
π― What You'll Learn¶
By the end of this tutorial, you'll understand:
- β Jinja2 syntax fundamentals (variables, conditionals, loops, filters)
- β Rendering templates with Python
- β Template inheritance and reusability
- β Advanced filters for network automation
- β Whitespace control for clean configurations
- β Error handling and debugging templates
- β Real-world patterns: VLAN configs, interface templates, ACLs
- β Integration with YAML data sources
- β Production deployment workflows
π Prerequisites¶
Required Knowledge¶
- β Completed YAML Data Modelling Tutorial β Understanding structured data
- β Completed JSON Data Handling Tutorial β Data serialization
- β Familiarity with Cisco IOS configuration syntax
Required Software¶
# Create a virtual environment
python -m venv jinja2_venv
source jinja2_venv/bin/activate
# Windows PowerShell: .\jinja2_venv\Scripts\Activate.ps1
# Windows CMD: jinja2_venv\Scripts\activate.bat
# Install required packages
pip install jinja2 pyyaml netmiko
Required Access¶
- No device access required for basic concepts
- Optional: SSH access to test device for deployment examples
π Jinja2 Fundamentals: Understanding Templates¶
What is Jinja2?¶
Jinja2 is a templating engineβit takes templates (text with placeholders) and renders them with actual data.
Example:
Template (config_template.j2):
hostname {{ hostname }}
!
interface {{ interface_name }}
ip address {{ ip_address }} {{ subnet_mask }}
no shutdown
Data (Python dict):
data = {
'hostname': 'router1',
'interface_name': 'GigabitEthernet0/0',
'ip_address': '10.1.1.1',
'subnet_mask': '255.255.255.0'
}
Rendered Output:
hostname router1
!
interface GigabitEthernet0/0
ip address 10.1.1.1 255.255.255.0
no shutdown
Key concept: {{ variable }} gets replaced with actual values from your data.
π§ Jinja2 Syntax: Core Concepts¶
1. Variables¶
Variables are enclosed in double curly braces {{ }}.
hostname {{ hostname }}
ip domain-name {{ domain_name }}
Data:
{
'hostname': 'core-router-01',
'domain_name': 'example.com'
}
Output:
hostname core-router-01
ip domain-name example.com
2. Accessing Nested Data¶
Access nested dictionary values with dot notation or brackets.
hostname {{ device.hostname }}
!
interface {{ device.interfaces.management.name }}
ip address {{ device.interfaces.management.ip }}
Data:
{
'device': {
'hostname': 'router1',
'interfaces': {
'management': {
'name': 'GigabitEthernet0/0',
'ip': '192.168.1.1'
}
}
}
}
3. Conditionals¶
Use {% if %} for conditional logic.
hostname {{ hostname }}
!
{% if enable_ssh %}
ip ssh version 2
{% endif %}
!
{% if enable_logging %}
logging buffered {{ log_size }}
logging host {{ syslog_server }}
{% endif %}
Data:
{
'hostname': 'router1',
'enable_ssh': True,
'enable_logging': True,
'log_size': 32000,
'syslog_server': '10.10.10.10'
}
Output:
hostname router1
!
ip ssh version 2
!
logging buffered 32000
logging host 10.10.10.10
4. Loops¶
Use {% for %} to iterate over lists.
hostname {{ hostname }}
!
{% for vlan in vlans %}
vlan {{ vlan.id }}
name {{ vlan.name }}
{% endfor %}
Data:
{
'hostname': 'switch1',
'vlans': [
{'id': 10, 'name': 'DATA'},
{'id': 20, 'name': 'VOICE'},
{'id': 30, 'name': 'GUEST'}
]
}
Output:
hostname switch1
!
vlan 10
name DATA
vlan 20
name VOICE
vlan 30
name GUEST
5. Filters¶
Filters modify variables using the pipe | operator.
hostname {{ hostname | upper }}
!
interface {{ interface }}
description {{ description | default('No description') }}
Common filters:
upperβ Convert to uppercaselowerβ Convert to lowercasedefault(value)β Provide default if variable is undefinedlengthβ Get length of list/stringjoin(separator)β Join list items
Example:
hostname {{ hostname | upper }}
!
{% for vlan in vlans %}
vlan {{ vlan }}
{% endfor %}
!
! Total VLANs: {{ vlans | length }}
! VLAN list: {{ vlans | join(',') }}
Data:
{
'hostname': 'router1',
'vlans': [10, 20, 30, 40]
}
Output:
hostname ROUTER1
!
vlan 10
vlan 20
vlan 30
vlan 40
!
! Total VLANs: 4
! VLAN list: 10,20,30,40
π Rendering Templates in Python¶
Basic Template Rendering¶
#!/usr/bin/env python3
"""
Render Jinja2 template with Python
"""
from jinja2 import Template
# Define template
template_string = """
hostname {{ hostname }}
!
interface {{ interface }}
ip address {{ ip }} {{ mask }}
no shutdown
"""
# Create template object
template = Template(template_string)
# Data to render
data = {
'hostname': 'router1',
'interface': 'GigabitEthernet0/0',
'ip': '10.1.1.1',
'mask': '255.255.255.0'
}
# Render template
config = template.render(data)
print(config)
Output:
hostname router1
!
interface GigabitEthernet0/0
ip address 10.1.1.1 255.255.255.0
no shutdown
Loading Templates from Files¶
#!/usr/bin/env python3
"""
Load and render Jinja2 templates from files
"""
from jinja2 import Environment, FileSystemLoader
import yaml
# Create Jinja2 environment
env = Environment(
loader=FileSystemLoader('templates'), # Template directory
trim_blocks=True, # Remove first newline after block
lstrip_blocks=True # Strip leading spaces from blocks
)
# Load template from file
template = env.get_template('router_config.j2')
# Load data from YAML
with open('device_data.yaml') as f:
data = yaml.safe_load(f)
# Render template
config = template.render(data)
# Save to file
with open(f"configs/{data['hostname']}_config.txt", 'w') as f:
f.write(config)
print(f"β Configuration generated for {data['hostname']}")
Project structure:
project/
βββ templates/
β βββ router_config.j2
βββ device_data.yaml
βββ configs/
βββ generate_config.py
ποΈ Real-World Pattern 1: VLAN Configuration¶
VLAN Template (templates/vlan_config.j2)¶
!
! VLAN Configuration
! Generated: {{ generation_date }}
! Device: {{ hostname }}
!
hostname {{ hostname }}
!
{% for vlan in vlans %}
vlan {{ vlan.id }}
name {{ vlan.name }}
{% if vlan.description %}
description {{ vlan.description }}
{% endif %}
{% endfor %}
!
{% for interface, config in interfaces.items() %}
interface {{ interface }}
{% if config.mode == 'access' %}
switchport mode access
switchport access vlan {{ config.vlan }}
{% elif config.mode == 'trunk' %}
switchport mode trunk
switchport trunk allowed vlan {{ config.allowed_vlans | join(',') }}
{% endif %}
description {{ config.description }}
no shutdown
!
{% endfor %}
VLAN Data (vlan_data.yaml)¶
---
hostname: access-switch-01
generation_date: "2026-03-12"
vlans:
- id: 10
name: DATA
description: "Corporate data network"
- id: 20
name: VOICE
description: "VoIP phones"
- id: 30
name: GUEST
description: "Guest wireless"
- id: 99
name: MGMT
description: "Management"
interfaces:
GigabitEthernet0/1:
mode: access
vlan: 10
description: "Workstation port"
GigabitEthernet0/2:
mode: access
vlan: 20
description: "IP Phone"
GigabitEthernet0/24:
mode: trunk
allowed_vlans: [10, 20, 30, 99]
description: "Trunk to distribution"
Generate Script (generate_vlan_config.py)¶
#!/usr/bin/env python3
"""
Generate VLAN configuration from template and data
"""
from jinja2 import Environment, FileSystemLoader
import yaml
from datetime import datetime
def generate_vlan_config(data_file, template_file, output_file):
"""
Generate VLAN configuration
"""
# Setup Jinja2 environment
env = Environment(
loader=FileSystemLoader('templates'),
trim_blocks=True,
lstrip_blocks=True
)
# Load data
with open(data_file) as f:
data = yaml.safe_load(f)
# Add generation timestamp
data['generation_date'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Load and render template
template = env.get_template(template_file)
config = template.render(data)
# Save configuration
with open(output_file, 'w') as f:
f.write(config)
print(f"β Configuration generated: {output_file}")
print(f" Device: {data['hostname']}")
print(f" VLANs: {len(data['vlans'])}")
print(f" Interfaces: {len(data['interfaces'])}")
return config
if __name__ == "__main__":
config = generate_vlan_config(
'vlan_data.yaml',
'vlan_config.j2',
'configs/access-switch-01_config.txt'
)
print("\nGenerated Configuration Preview:")
print("=" * 70)
print(config[:500]) # Show first 500 characters
Generated Output:
!
! VLAN Configuration
! Generated: 2026-03-12 11:45:30
! Device: access-switch-01
!
hostname access-switch-01
!
vlan 10
name DATA
description Corporate data network
vlan 20
name VOICE
description VoIP phones
vlan 30
name GUEST
description Guest wireless
vlan 99
name MGMT
description Management
!
interface GigabitEthernet0/1
switchport mode access
switchport access vlan 10
description Workstation port
no shutdown
!
interface GigabitEthernet0/2
switchport mode access
switchport access vlan 20
description IP Phone
no shutdown
!
interface GigabitEthernet0/24
switchport mode trunk
switchport trunk allowed vlan 10,20,30,99
description Trunk to distribution
no shutdown
!
π Real-World Pattern 2: Access Control Lists¶
ACL Template (templates/acl_config.j2)¶
!
! Access Control Lists
! Device: {{ hostname }}
!
{% for acl in acls %}
{% if acl.type == 'standard' %}
ip access-list standard {{ acl.name }}
{% for rule in acl.rules %}
{{ rule.action }} {{ rule.source }}
{% endfor %}
{% elif acl.type == 'extended' %}
ip access-list extended {{ acl.name }}
{% for rule in acl.rules %}
{{ rule.action }} {{ rule.protocol }} {{ rule.source }} {{ rule.destination }}{% if rule.port %} eq {{ rule.port }}{% endif %}
{% endfor %}
{% endif %}
!
{% endfor %}
!
! Apply ACLs to interfaces
{% for interface, config in interface_acls.items() %}
interface {{ interface }}
{% if config.inbound %}
ip access-group {{ config.inbound }} in
{% endif %}
{% if config.outbound %}
ip access-group {{ config.outbound }} out
{% endif %}
!
{% endfor %}
ACL Data (acl_data.yaml)¶
---
hostname: edge-router-01
acls:
- name: ALLOW_MANAGEMENT
type: standard
rules:
- action: permit
source: 10.10.0.0 0.0.255.255
- action: deny
source: any
- name: ALLOW_WEB_TRAFFIC
type: extended
rules:
- action: permit
protocol: tcp
source: any
destination: any
port: 80
- action: permit
protocol: tcp
source: any
destination: any
port: 443
- action: deny
protocol: ip
source: any
destination: any
interface_acls:
GigabitEthernet0/0:
inbound: ALLOW_MANAGEMENT
GigabitEthernet0/1:
outbound: ALLOW_WEB_TRAFFIC
βοΈ Advanced Jinja2 Features¶
1. Template Inheritance¶
Base template (templates/base_config.j2):
!
! Base Configuration
! Device: {{ hostname }}
! Template: {{ template_name }}
! Generated: {{ timestamp }}
!
hostname {{ hostname }}
!
{% block common_settings %}
ip domain-name {{ domain_name }}
ip name-server {{ dns_servers | join(' ') }}
{% endblock %}
!
{% block device_specific %}
! Device-specific configuration goes here
{% endblock %}
!
{% block interfaces %}
! Interface configuration
{% endblock %}
!
end
Child template (templates/router_config.j2):
{% extends "base_config.j2" %}
{% block device_specific %}
! Router-specific settings
ip routing
router ospf {{ ospf_process_id }}
network {{ ospf_network }} {{ ospf_wildcard }} area {{ ospf_area }}
{% endblock %}
{% block interfaces %}
{% for interface in interfaces %}
interface {{ interface.name }}
ip address {{ interface.ip }} {{ interface.mask }}
no shutdown
!
{% endfor %}
{% endblock %}
2. Macros (Reusable Blocks)¶
{% macro interface_config(name, ip, mask, description='') %}
interface {{ name }}
ip address {{ ip }} {{ mask }}
{% if description %}
description {{ description }}
{% endif %}
no shutdown
!
{% endmacro %}
!
! Using macros
{{ interface_config('GigabitEthernet0/0', '10.1.1.1', '255.255.255.0', 'WAN Link') }}
{{ interface_config('GigabitEthernet0/1', '192.168.1.1', '255.255.255.0') }}
3. Whitespace Control¶
Control how Jinja2 handles whitespace and newlines.
{# Remove whitespace before/after block #}
{% for vlan in vlans -%}
vlan {{ vlan }}
{% endfor -%}
{# The minus sign (-) removes whitespace #}
{%- for interface in interfaces -%}
interface {{ interface }}
{% endfor -%}
Whitespace modifiers:
{%-β Strip whitespace before-%}β Strip whitespace after{#β Comment (not rendered)
4. Custom Filters¶
Create custom filters for network automation.
#!/usr/bin/env python3
"""
Custom Jinja2 filters for network automation
"""
from jinja2 import Environment, FileSystemLoader
import ipaddress
def subnet_mask_to_cidr(mask):
"""
Convert subnet mask to CIDR notation
Example: '255.255.255.0' -> '/24'
"""
return '/' + str(ipaddress.IPv4Network(f'0.0.0.0/{mask}').prefixlen)
def wildcard_mask(subnet_mask):
"""
Convert subnet mask to wildcard mask
Example: '255.255.255.0' -> '0.0.0.255'
"""
mask_int = int(ipaddress.IPv4Address(subnet_mask))
wildcard_int = 0xFFFFFFFF ^ mask_int
return str(ipaddress.IPv4Address(wildcard_int))
def interface_short_name(interface):
"""
Convert interface name to short form
Example: 'GigabitEthernet0/0' -> 'Gi0/0'
"""
replacements = {
'GigabitEthernet': 'Gi',
'FastEthernet': 'Fa',
'TenGigabitEthernet': 'Te',
'Ethernet': 'Et'
}
for long, short in replacements.items():
if interface.startswith(long):
return interface.replace(long, short)
return interface
# Setup Jinja2 environment with custom filters
env = Environment(loader=FileSystemLoader('templates'))
env.filters['subnet_to_cidr'] = subnet_mask_to_cidr
env.filters['wildcard'] = wildcard_mask
env.filters['short_name'] = interface_short_name
# Example template using custom filters
template_string = """
interface {{ interface }}
ip address {{ ip }} {{ mask }} {# {{ mask | subnet_to_cidr }} #}
!
router ospf 1
network {{ network }} {{ mask | wildcard }} area 0
!
! Short name: {{ interface | short_name }}
"""
template = env.from_string(template_string)
data = {
'interface': 'GigabitEthernet0/0',
'ip': '10.1.1.1',
'mask': '255.255.255.0',
'network': '10.1.1.0'
}
print(template.render(data))
Output:
interface GigabitEthernet0/0
ip address 10.1.1.1 255.255.255.0
!
router ospf 1
network 10.1.1.0 0.0.0.255 area 0
!
! Short name: Gi0/0
π Real-World Pattern 3: Multi-Device Configuration Generation¶
Complete Automation Workflow¶
Project Structure:
network-config-automation/
βββ templates/
β βββ base_config.j2
β βββ router_config.j2
β βββ switch_config.j2
βββ data/
β βββ devices.yaml
β βββ global_settings.yaml
βββ configs/
β βββ (generated configs)
βββ generate_configs.py
βββ deploy_configs.py
devices.yaml¶
---
devices:
- hostname: core-router-01
device_type: router
template: router_config.j2
management_ip: 10.1.1.1
interfaces:
- name: GigabitEthernet0/0
ip: 192.168.1.1
mask: 255.255.255.0
description: "WAN Link"
- name: GigabitEthernet0/1
ip: 10.10.1.1
mask: 255.255.255.0
description: "LAN Link"
ospf:
process_id: 1
networks:
- network: 192.168.1.0
wildcard: 0.0.0.255
area: 0
- hostname: access-switch-01
device_type: switch
template: switch_config.j2
management_ip: 10.1.1.10
vlans:
- id: 10
name: DATA
- id: 20
name: VOICE
interfaces:
GigabitEthernet0/1:
mode: access
vlan: 10
description: "User port"
global_settings.yaml¶
---
domain_name: example.com
dns_servers:
- 8.8.8.8
- 8.8.4.4
ntp_servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
syslog_server: 10.10.10.10
snmp_community: public
timezone: EST -5 0
generate_configs.py¶
#!/usr/bin/env python3
"""
Generate configurations for all devices from templates and data
"""
from jinja2 import Environment, FileSystemLoader
import yaml
from datetime import datetime
import os
def load_data():
"""Load device and global configuration data"""
with open('data/devices.yaml') as f:
devices = yaml.safe_load(f)
with open('data/global_settings.yaml') as f:
global_settings = yaml.safe_load(f)
return devices['devices'], global_settings
def generate_device_config(device, global_settings, env):
"""
Generate configuration for a single device
"""
# Combine device-specific and global settings
data = {**global_settings, **device}
data['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Load template
template = env.get_template(device['template'])
# Render configuration
config = template.render(data)
return config
def save_config(hostname, config):
"""Save configuration to file"""
os.makedirs('configs', exist_ok=True)
filename = f"configs/{hostname}_config.txt"
with open(filename, 'w') as f:
f.write(config)
return filename
def main():
"""Main function"""
print("=" * 70)
print("NETWORK CONFIGURATION GENERATOR")
print("=" * 70)
# Setup Jinja2 environment
env = Environment(
loader=FileSystemLoader('templates'),
trim_blocks=True,
lstrip_blocks=True
)
# Load data
devices, global_settings = load_data()
print(f"\nβ Loaded {len(devices)} device(s)")
print(f"β Loaded global settings")
# Generate configs
print(f"\n{'=' * 70}")
print("GENERATING CONFIGURATIONS")
print("=" * 70)
generated_configs = []
for device in devices:
print(f"\nπ Generating config for {device['hostname']}...")
# Generate config
config = generate_device_config(device, global_settings, env)
# Save config
filename = save_config(device['hostname'], config)
# Track generated files
generated_configs.append({
'hostname': device['hostname'],
'filename': filename,
'size': len(config)
})
print(f" β Saved to {filename}")
print(f" β Size: {len(config):,} bytes")
# Summary
print(f"\n{'=' * 70}")
print("GENERATION SUMMARY")
print("=" * 70)
print(f"Total devices: {len(generated_configs)}")
print(f"Total config size: {sum(c['size'] for c in generated_configs):,} bytes")
print(f"\n{'=' * 70}")
print("GENERATED FILES:")
print("=" * 70)
for config in generated_configs:
print(f" {config['filename']}")
print()
if __name__ == "__main__":
main()
Run the generator:
python generate_configs.py
Output:
======================================================================
NETWORK CONFIGURATION GENERATOR
======================================================================
β Loaded 2 device(s)
β Loaded global settings
======================================================================
GENERATING CONFIGURATIONS
======================================================================
π Generating config for core-router-01...
β Saved to configs/core-router-01_config.txt
β Size: 1,245 bytes
π Generating config for access-switch-01...
β Saved to configs/access-switch-01_config.txt
β Size: 987 bytes
======================================================================
GENERATION SUMMARY
======================================================================
Total devices: 2
Total config size: 2,232 bytes
======================================================================
GENERATED FILES:
======================================================================
configs/core-router-01_config.txt
configs/access-switch-01_config.txt
π§ͺ Testing and Validating Templates¶
Template Testing Script¶
#!/usr/bin/env python3
"""
Test Jinja2 templates with sample data
Catch errors before deployment
"""
from jinja2 import Environment, FileSystemLoader, TemplateError
import yaml
def test_template(template_name, test_data_file):
"""
Test template with sample data
"""
print(f"\nTesting: {template_name}")
print("-" * 50)
# Setup environment
env = Environment(
loader=FileSystemLoader('templates'),
trim_blocks=True,
lstrip_blocks=True
)
try:
# Load template
template = env.get_template(template_name)
# Load test data
with open(test_data_file) as f:
data = yaml.safe_load(f)
# Render template
config = template.render(data)
# Basic validation
if len(config) == 0:
print("β FAIL: Generated config is empty")
return False
# Check for common Cisco config patterns
if 'hostname' not in config:
print("β WARNING: Config doesn't contain hostname")
print(f"β PASS: Generated {len(config):,} bytes")
print("\nPreview (first 300 characters):")
print(config[:300])
return True
except TemplateError as e:
print(f"β FAIL: Template error")
print(f" Error: {e}")
return False
except Exception as e:
print(f"β FAIL: Unexpected error")
print(f" Error: {e}")
return False
def main():
"""Main function"""
print("=" * 70)
print("TEMPLATE TESTING")
print("=" * 70)
tests = [
('router_config.j2', 'test_data/router_test.yaml'),
('switch_config.j2', 'test_data/switch_test.yaml'),
('vlan_config.j2', 'test_data/vlan_test.yaml')
]
results = []
for template, test_data in tests:
result = test_template(template, test_data)
results.append((template, result))
# Summary
print("\n" + "=" * 70)
print("TEST SUMMARY")
print("=" * 70)
passed = sum(1 for _, result in results if result)
failed = len(results) - passed
for template, result in results:
status = "β PASS" if result else "β FAIL"
print(f"{status}: {template}")
print(f"\nTotal: {len(results)} | Passed: {passed} | Failed: {failed}")
if __name__ == "__main__":
main()
π Common Jinja2 Pitfalls & Solutions¶
Pitfall 1: Undefined Variables¶
Problem:
hostname {{ hostname }}
interface {{ undefined_variable }}
Error: UndefinedError: 'undefined_variable' is undefined
Solutions:
{# Option 1: Use default filter #}
interface {{ interface_name | default('GigabitEthernet0/0') }}
{# Option 2: Check if defined #}
{% if interface_name is defined %}
interface {{ interface_name }}
{% endif %}
{# Option 3: Configure Jinja2 to ignore undefined #}
from jinja2 import Environment, Undefined
class SilentUndefined(Undefined):
def _fail_with_undefined_error(self, *args, **kwargs):
return ''
env = Environment(undefined=SilentUndefined)
Pitfall 2: Whitespace Issues¶
Problem:
{% for vlan in vlans %}
vlan {{ vlan }}
{% endfor %}
Output (extra blank lines):
vlan 10
vlan 20
vlan 30
Solution:
{% for vlan in vlans -%}
vlan {{ vlan }}
{% endfor -%}
Output (clean):
vlan 10
vlan 20
vlan 30
Pitfall 3: Type Errors¶
Problem:
switchport trunk allowed vlan {{ vlans }}
Data:
{'vlans': [10, 20, 30]} # List, not string
Output (wrong):
switchport trunk allowed vlan [10, 20, 30]
Solution:
switchport trunk allowed vlan {{ vlans | join(',') }}
Output (correct):
switchport trunk allowed vlan 10,20,30
π Best Practices for Production¶
1. Separate Data from Templates¶
Good structure:
project/
βββ templates/ # Templates (version controlled)
βββ data/ # Data files (version controlled)
βββ configs/ # Generated configs (excluded from git)
βββ scripts/ # Generation scripts
2. Version Control Everything¶
git add templates/ data/ scripts/
git commit -m "Updated VLAN template for new standard"
git push
Exclude generated configs:
# .gitignore
configs/
*.pyc
__pycache__/
3. Use Template Inheritance¶
Don't repeat yourselfβuse base templates:
{# base_config.j2 #}
{% block header %}
! Base configuration
{% endblock %}
{% block device_config %}
! Override this block
{% endblock %}
4. Document Your Templates¶
{#
Template: router_config.j2
Purpose: Generate router configurations for WAN edge devices
Required variables:
- hostname (string)
- interfaces (list of dicts)
- ospf (dict with process_id, networks)
Optional variables:
- enable_ssh (boolean, default: true)
Author: Network Team
Last updated: 2026-03-12
#}
hostname {{ hostname }}
...
5. Test Before Deploy¶
# Always test templates before deploying
def validate_config(config):
"""Basic config validation"""
checks = [
('hostname' in config, "Missing hostname"),
('end' in config, "Missing end statement"),
(len(config) > 100, "Config too short")
]
for check, error_msg in checks:
if not check:
raise ValueError(f"Validation failed: {error_msg}")
return True
π Additional Resources¶
- Jinja2 Official Documentation β Complete Jinja2 reference
- Jinja2 Template Designer Documentation β Template syntax guide
- Network to Code Jinja2 Blog β Real-world Jinja2 examples
- PyYAML Documentation β For loading YAML data
π― Key Takeaways¶
- β Jinja2 transforms data into configurations β Write template once, use everywhere
- β Separation of concerns β Data (YAML) + Logic (Python) + Templates (Jinja2)
- β Version control templates β Track configuration standards over time
- β Eliminate manual errors β No more copy-paste mistakes
- β Conditional logic β Generate different configs based on device type, role
- β Production-ready β Used by enterprises for large-scale automation
π Next Steps¶
You've mastered the data modelling trilogy (YAML + JSON + Jinja2)! Now apply these skills:
- Nornir Fundamentals (Recommended Next)
- Use YAML inventories and Jinja2 templates
-
Deploy configs at scale with parallel execution
- Validate generated configs with PyATS
-
Ensure templates produce correct output
- Secure your automation workflows
-
Manage credentials for template deployment
- Compare generated configs vs running configs
- Detect configuration drift
Remember: Templates + Data = Scalable Configuration Management. Master Jinja2, eliminate manual configuration errors forever.
β Back to JSON Tutorial | Continue to Nornir Fundamentals β
Need help applying this in a live Cisco environment?
If you want this pattern implemented, governed, or adapted for your estate, use the contact page to start a discovery conversation or review how Nautomation Prime delivers engagements.