第306篇:Jinja2 模板引擎:配置模板化
关键词
Jinja2、模板引擎、配置模板化、模板渲染、变量替换、for 循环、if 判断、模板继承、网络自动化
一、Jinja2 简介
1.1 为什么要用模板
网络配置的痛点:
手工编写配置:
┌─ 100 台接入交换机,每台 50 行配置
├─ 类似配置复制粘贴 100 次
├─ 改一个 IP 段 → 改 100 个文件
└─ 出错率:3-5%(IP 写错、VLAN 漏配)
模板化配置:
┌─ 1 个模板文件 + 1 个数据文件
├─ 一次渲染生成 100 份配置
├─ 改数据 → 重新渲染即可
└─ 出错率趋于 0
Jinja2 是 Python 生态最流行的 模板引擎,最初为 Django 设计,后被 Ansible 采用成为网络自动化标配:
Jinja2 的工作模式:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 模板文件 │ + │ 数据变量 │ → │ 渲染结果 │
│ .j2 │ │ YAML/JSON│ │ 配置文件 │
└──────────┘ └──────────┘ └──────────┘
模板 = 固定结构 + 占位变量
数据 = 实际值
结果 = 生成的具体配置
示例:
模板:interface {{ ifname }}
ip address {{ ip }}/{{ mask }}
数据:{"ifname": "GigabitEthernet0/0/1",
"ip": "10.0.1.1", "mask": 24}
结果:interface GigabitEthernet0/0/1
ip address 10.0.1.1/24
安装:
python -m pip install jinja2
1.2 模板语法基础
Jinja2 三种标记:
1. {{ variable }} — 变量输出
┌─ 输出变量的值
├─ 可用过滤器:{{ name | upper }}
└─ 可用表达式:{{ count + 1 }}
2. {% statement %} — 控制结构
┌─ {% for item in items %} ... {% endfor %}
├─ {% if condition %} ... {% endif %}
└─ {% set name = value %}
3. {# comment #} — 注释
└─ 不会出现在渲染结果中
常用过滤器(Filter):
┌─ {{ name | upper }} → 大写
├─ {{ name | lower }} → 小写
├─ {{ name | replace("-","_") }} → 替换
├─ {{ ip | ipaddr }} → IP 处理(Ansible)
├─ {{ list | join(", ") }} → 列表合并
└─ {{ value | default("N/A") }} → 默认值
二、基础模板实战
2.1 第一个模板
#!/usr/bin/env python3
# jinja2_basic.py — Jinja2 基础渲染
from jinja2 import Template
# 1. 简单变量替换
template_str = """
interface {{ interface_name }}
description {{ description }}
port link-type {{ port_type }}
port default vlan {{ vlan_id }}
undo shutdown
"""
# 数据
data = {
"interface_name": "GigabitEthernet0/0/1",
"description": "ACCESS_TO_OFFICE",
"port_type": "access",
"vlan_id": 100,
}
# 渲染
template = Template(template_str)
output = template.render(data)
print(output)
输出:
interface GigabitEthernet0/0/1
description ACCESS_TO_OFFICE
port link-type access
port default vlan 100
undo shutdown
2.2 使用文件模板
#!/usr/bin/env python3
# jinja2_file.py — 从文件加载模板
from jinja2 import Environment, FileSystemLoader
import os
# 1. 创建模板目录
os.makedirs("templates", exist_ok=True)
# 2. 编写模板文件 templates/access_port.j2
template_content = """
{# 接入端口配置模板 #}
interface {{ if.name }}
description {{ if.description | default("TO_" + if.name) }}
port link-type {{ if.mode | default("access") }}
{% if if.mode == "trunk" %}
port trunk allow-pass vlan {{ if.vlans | join(" ") }}
{% else %}
port default vlan {{ if.vlan_id }}
{% endif %}
undo shutdown
{% if if.port_security %}
port-security enable
port-security max-mac-num {{ if.max_mac | default(5) }}
{% endif %}
"""
with open("templates/access_port.j2", "w") as f:
f.write(template_content)
print("模板已创建")
# 3. 加载模板环境
env = Environment(
loader=FileSystemLoader("templates"),
trim_blocks=True, # 去掉行首空白
lstrip_blocks=True, # 去掉行末空白
)
template = env.get_template("access_port.j2")
# 4. 数据
interface_data = {
"name": "GigabitEthernet0/0/2",
"description": "Link_to_Server_Room",
"mode": "access",
"vlan_id": 200,
"port_security": True,
"max_mac": 10,
}
output = template.render(if=interface_data)
print(output)
输出:
interface GigabitEthernet0/0/2
description Link_to_Server_Room
port link-type access
port default vlan 200
undo shutdown
port-security enable
port-security max-mac-num 10
三、高级模板技术
3.1 批量设备配置生成
#!/usr/bin/env python3
# jinja2_batch.py — 批量生成多设备配置
from jinja2 import Environment, FileSystemLoader
import os
import json
os.makedirs("templates", exist_ok=True)
os.makedirs("output", exist_ok=True)
# ===== 模板文件:接入交换机完整配置 =====
switch_template = """
{# 接入交换机配置模板 #}
sysname {{ device.hostname }}
#
{% for vlan in device.vlans %}
vlan batch {{ vlan.id }}
name {{ vlan.name | default("VLAN" ~ vlan.id) }}
#
{% endfor %}
{% for iface in device.interfaces %}
interface {{ iface.name }}
description {{ iface.desc }}
port link-type {{ iface.mode }}
{% if iface.mode == "trunk" %}
port trunk allow-pass vlan {{ iface.allowed_vlans | join(" ") }}
{% else %}
port default vlan {{ iface.vlan }}
{% endif %}
undo shutdown
#
{% endfor %}
{% if device.mgmt_ip %}
interface Vlanif{{ device.mgmt_vlan | default(1) }}
ip address {{ device.mgmt_ip }} {{ device.mgmt_mask }}
#
{% endif %}
{% if device.ntp_server %}
ntp-service unicast-server {{ device.ntp_server }}
#
{% endif %}
return
"""
with open("templates/switch_config.j2", "w") as f:
f.write(switch_template)
# ===== 数据定义 =====
SWITCHES = [
{
"hostname": "ACC-SW01",
"mgmt_ip": "10.0.1.1",
"mgmt_mask": "255.255.255.0",
"mgmt_vlan": 10,
"ntp_server": "203.0.113.1",
"vlans": [
{"id": 10, "name": "MGMT"},
{"id": 20, "name": "OFFICE"},
{"id": 30, "name": "GUEST"},
{"id": 100, "name": "SERVER"},
],
"interfaces": [
{"name": "GigabitEthernet0/0/1", "desc": "UPLINK-TO-CORE",
"mode": "trunk", "allowed_vlans": [10, 20, 30, 100]},
{"name": "GigabitEthernet0/0/2", "desc": "OFFICE-FLOOR1",
"mode": "access", "vlan": 20},
{"name": "GigabitEthernet0/0/3", "desc": "GUEST-WIFI",
"mode": "access", "vlan": 30},
{"name": "GigabitEthernet0/0/4", "desc": "SERVER-RACK01",
"mode": "access", "vlan": 100},
],
},
{
"hostname": "ACC-SW02",
"mgmt_ip": "10.0.1.2",
"mgmt_mask": "255.255.255.0",
"mgmt_vlan": 10,
"ntp_server": "203.0.113.1",
"vlans": [
{"id": 10, "name": "MGMT"},
{"id": 40, "name": "PROD"},
{"id": 50, "name": "DEV"},
],
"interfaces": [
{"name": "GigabitEthernet0/0/1", "desc": "UPLINK-TO-CORE",
"mode": "trunk", "allowed_vlans": [10, 40, 50]},
{"name": "GigabitEthernet0/0/2", "desc": "PROD-LINE01",
"mode": "access", "vlan": 40},
{"name": "GigabitEthernet0/0/3", "desc": "DEV-LAB01",
"mode": "access", "vlan": 50},
],
},
]
# ===== 渲染 =====
env = Environment(
loader=FileSystemLoader("templates"),
trim_blocks=True,
lstrip_blocks=True,
)
template = env.get_template("switch_config.j2")
for sw in SWITCHES:
output = template.render(device=sw)
filename = f"output/{sw['hostname']}_config.txt"
with open(filename, "w", encoding="utf-8") as f:
f.write(output)
print(f"✓ 已生成: {filename}")
print(f"\n共生成 {len(SWITCHES)} 台设备配置")
3.2 条件与循环进阶
#!/usr/bin/env python3
# jinja2_advanced.py — 高级模板技巧
from jinja2 import Environment, FileSystemLoader
import os
os.makedirs("templates", exist_ok=True)
# 复杂模板:OSPF 配置生成
ospf_template = """
{# OSPF 配置模板 #}
ospf {{ ospf.process_id }} router-id {{ ospf.router_id }}
{% if ospf.bfd %}
bfd enable
bfd all-interfaces enable
{% endif %}
{% if ospf.area is iterable and ospf.area is not string %}
{# 多区域配置 #}
{% for area in ospf.area %}
area {{ area.id }}
{% for network in area.networks %}
network {{ network.network }} {{ network.wildcard }}
{% endfor %}
{% if area.stub %}
stub
{% endif %}
{% if area.nssa %}
nssa
{% endif %}
{% endfor %}
{% else %}
{# 单区域配置 #}
area {{ ospf.area }}
{# 这里会自动展开 networks #}
{% for network in ospf.networks %}
network {{ network.network }} {{ network.wildcard }}
{% endfor %}
{% endif %}
#
{% set total_networks = ospf.area is mapping
? ospf.networks | length
: ospf.area | sum(attribute='networks') | length %}
{# 注释:上面是宏的用法,简化版如下 #}
{% if ospf.silent_interfaces %}
{% for iface in ospf.silent_interfaces %}
silent-interface {{ iface }}
{% endfor %}
{% endif %}
return
"""
with open("templates/ospf_config.j2", "w") as f:
f.write(ospf_template)
# 多个 OSPF 配置数据
OSPF_CONFIGS = [
{
"process_id": 1,
"router_id": "1.1.1.1",
"bfd": True,
"area": [
{
"id": "0.0.0.0",
"networks": [
{"network": "10.0.0.0", "wildcard": "0.0.0.255"},
{"network": "10.0.1.0", "wildcard": "0.0.0.255"},
],
},
{
"id": "0.0.0.1",
"networks": [
{"network": "172.16.0.0", "wildcard": "0.0.255.255"},
],
"stub": True,
},
],
"silent_interfaces": ["LoopBack0"],
},
{
"process_id": 2,
"router_id": "2.2.2.2",
"bfd": False,
"area": "0.0.0.0",
"networks": [
{"network": "192.168.0.0", "wildcard": "0.0.0.255"},
],
},
]
env = Environment(
loader=FileSystemLoader("templates"),
trim_blocks=True,
lstrip_blocks=True,
)
template = env.get_template("ospf_config.j2")
for i, config in enumerate(OSPF_CONFIGS):
output = template.render(ospf=config)
print(f"\n=== OSPF 配置 {i+1} ===")
print(output)
四、模板继承与宏
4.1 模板继承
#!/usr/bin/env python3
# jinja2_extends.py — 模板继承
"""
模板继承思想:
基础模板(base.j2): 定义设备配置的通用结构
子模板(device.j2): 继承基础模板,填充具体内容
"""
from jinja2 import Environment, FileSystemLoader
import os
os.makedirs("templates", exist_ok=True)
# === 基础模板 ===
base_template = """\
{# base.j2 — 设备配置基础模板 #}
! ============================================
! 设备: {{ device.hostname }}
! 模板: {{ device.template_version | default("1.0") }}
! 生成时间: {{ generation_time }}
! ============================================
{% block system %}{% endblock %}
{% block interfaces %}{% endblock %}
{% block routing %}{% endblock %}
{% block security %}{% endblock %}
! ============================================
! 配置结束
! ============================================
"""
# === 路由器子模板 ===
router_template = """\
{% extends "base.j2" %}
{% block system %}
sysname {{ device.hostname }}
#
{% if device.ntp_server %}
ntp-service unicast-server {{ device.ntp_server }}
#
{% endif %}
{% endblock %}
{% block interfaces %}
{% for iface in device.interfaces %}
interface {{ iface.name }}
description {{ iface.desc }}
ip address {{ iface.ip }} {{ iface.mask }}
undo shutdown
{% if iface.ospf %}
ospf enable {{ iface.ospf }} area {{ iface.ospf_area }}
{% endif %}
#
{% endfor %}
{% endblock %}
{% block routing %}
ospf {{ device.ospf.process }}
router-id {{ device.ospf.router_id }}
area {{ device.ospf.area }}
#
{% endblock %}
"""
# === 交换机子模板 ===
switch_template = """\
{% extends "base.j2" %}
{% block system %}
sysname {{ device.hostname }}
#
{% for vlan in device.vlans %}
vlan batch {{ vlan.id }}
name {{ vlan.name }}
#
{% endfor %}
{% endblock %}
{% block interfaces %}
{% for iface in device.interfaces %}
interface {{ iface.name }}
description {{ iface.desc }}
port link-type {{ iface.mode }}
{% if iface.mode == "trunk" %}
port trunk allow-pass vlan {{ iface.allowed_vlans | join(" ") }}
{% else %}
port default vlan {{ iface.vlan }}
{% endif %}
undo shutdown
#
{% endfor %}
{% endblock %}
{% block routing %}
{% endblock %}
"""
with open("templates/base.j2", "w") as f:
f.write(base_template)
with open("templates/router.j2", "w") as f:
f.write(router_template)
with open("templates/switch.j2", "w") as f:
f.write(switch_template)
# === 渲染 ===
env = Environment(
loader=FileSystemLoader("templates"),
trim_blocks=True,
lstrip_blocks=True,
)
# 添加当前时间变量
from datetime import datetime
# 路由器
router_data = {
"hostname": "CORE-RT01",
"template_version": "2.0",
"ntp_server": "203.0.113.1",
"interfaces": [
{"name": "GigabitEthernet0/0/1", "desc": "TO-CORE-SW01",
"ip": "10.0.12.1", "mask": "255.255.255.252",
"ospf": 1, "ospf_area": "0.0.0.0"},
{"name": "GigabitEthernet0/0/2", "desc": "TO-BORDER-FW",
"ip": "10.0.13.1", "mask": "255.255.255.252"},
],
"ospf": {"process": 1, "router_id": "1.1.1.1", "area": "0.0.0.0"},
}
output = env.get_template("router.j2").render(
device=router_data,
generation_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
)
print("=== 路由器配置 ===")
print(output)
# 交换机
switch_data = {
"hostname": "ACC-SW01",
"template_version": "2.0",
"vlans": [
{"id": 10, "name": "MGMT"},
{"id": 20, "name": "OFFICE"},
],
"interfaces": [
{"name": "GigabitEthernet0/0/1", "desc": "UPLINK",
"mode": "trunk", "allowed_vlans": [10, 20]},
{"name": "GigabitEthernet0/0/2", "desc": "PC-FLOOR1",
"mode": "access", "vlan": 20},
],
}
output = env.get_template("switch.j2").render(
device=switch_data,
generation_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
)
print("\n=== 交换机配置 ===")
print(output)
4.2 宏(Macro)
#!/usr/bin/env python3
# jinja2_macro.py — 宏的使用
from jinja2 import Environment, FileSystemLoader
import os
os.makedirs("templates", exist_ok=True)
# 宏定义文件
macro_template = """\
{# macros.j2 — 通用宏定义 #}
{% macro interface_config(name, desc, mode, vlan) -%}
interface {{ name }}
description {{ desc }}
port link-type {{ mode }}
{% if mode == "trunk" %}
port trunk allow-pass vlan {{ vlan }}
{% else %}
port default vlan {{ vlan }}
{% endif %}
undo shutdown
{%- endmacro %}
{% macro acl_rule(number, action, src, dst) -%}
acl number {{ number }}
rule {{ loop.index }} permit {{ action }} source {{ src }} destination {{ dst }}
{%- endmacro %}
{% macro ospf_network(pid, network, wildcard, area) -%}
ospf {{ pid }}
area {{ area }}
network {{ network }} {{ wildcard }}
{%- endmacro %}
"""
config_template = """\
{% from "macros.j2" import interface_config, ospf_network %}
sysname {{ hostname }}
#
{{ interface_config("GigabitEthernet0/0/1", "UPLINK", "trunk", "10 20 30") }}
{{ interface_config("GigabitEthernet0/0/2", "SERVER", "access", 100) }}
{{ interface_config("GigabitEthernet0/0/3", "GUEST", "access", 200) }}
#
{{ ospf_network(1, "10.0.0.0", "0.0.0.255", "0.0.0.0") }}
{{ ospf_network(1, "192.168.0.0", "0.0.0.255", "0.0.0.1") }}
#
return
"""
with open("templates/macros.j2", "w") as f:
f.write(macro_template)
with open("templates/config_with_macro.j2", "w") as f:
f.write(config_template)
env = Environment(
loader=FileSystemLoader("templates"),
trim_blocks=True,
lstrip_blocks=True,
)
output = env.get_template("config_with_macro.j2").render(hostname="CORP-SW01")
print(output)
五、Jinja2 + Netmiko 完整自动化
5.1 模板渲染 + 自动下发
#!/usr/bin/env python3
# jinja2_netmiko_deploy.py — 完整自动化流程
from jinja2 import Environment, FileSystemLoader
from netmiko import ConnectHandler
import json
import os
# ===== 1. 定义数据 =====
DEVICES_DATA = [
{
"hostname": "ACC-SW01",
"mgmt_ip": "10.0.1.1",
"username": "admin",
"password": "admin123",
"device_type": "huawei",
"vlans": [10, 20, 30],
"interfaces": [
{"name": "GE0/0/1", "desc": "UPLINK", "mode": "trunk", "vlans": "10 20 30"},
{"name": "GE0/0/2", "desc": "PC-F1", "mode": "access", "vlan": 10},
],
},
]
# ===== 2. 模板 =====
os.makedirs("templates", exist_ok=True)
VLAN_TEMPLATE = """\
{% for vlan in device.vlans %}
vlan {{ vlan }}
{% endfor %}
"""
INTF_TEMPLATE = """\
{% for iface in device.interfaces %}
interface {{ iface.name }}
description {{ iface.desc }}
port link-type {{ iface.mode }}
{% if iface.mode == "trunk" %}
port trunk allow-pass vlan {{ iface.vlans }}
{% else %}
port default vlan {{ iface.vlan }}
{% endif %}
quit
{% endfor %}
"""
with open("templates/vlan.j2", "w") as f:
f.write(VLAN_TEMPLATE)
with open("templates/interface.j2", "w") as f:
f.write(INTF_TEMPLATE)
# ===== 3. 渲染 + 下发 =====
env = Environment(
loader=FileSystemLoader("templates"),
trim_blocks=True,
lstrip_blocks=True,
)
for device_data in DEVICES_DATA:
print(f"\n=== 处理 {device_data['hostname']} ===")
# 渲染 VLAN 配置
vlan_config = env.get_template("vlan.j2").render(device=device_data)
print("VLAN 配置:")
print(vlan_config.strip())
# 渲染接口配置
intf_config = env.get_template("interface.j2").render(device=device_data)
print("接口配置:")
print(intf_config.strip())
# 通过 Netmiko 下发
print("正在连接到设备...")
conn = ConnectHandler(
device_type=device_data["device_type"],
host=device_data["mgmt_ip"],
username=device_data["username"],
password=device_data["password"],
)
try:
# 下发 VLAN
conn.send_config_set(vlan_config.strip().split("\n"))
print("✓ VLAN 配置已下发")
# 下发接口
conn.send_config_set(intf_config.strip().split("\n"))
print("✓ 接口配置已下发")
# 保存
conn.save()
print("✓ 配置已保存")
# 验证
verify = conn.send_command("display vlan summary")
print(f"VLAN 数量验证: {verify[:100]}...")
except Exception as e:
print(f"✗ 错误: {e}")
finally:
conn.disconnect()
六、Jinja2 最佳实践
6.1 模板组织
推荐的项目结构:
config_project/
├── templates/ # 模板目录
│ ├── base.j2 # 基础模板
│ ├── router.j2 # 路由器配置模板
│ ├── switch.j2 # 交换机配置模板
│ ├── firewall.j2 # 防火墙配置模板
│ ├── macros.j2 # 宏定义
│ └── fragments/ # 片段模板
│ ├── ospf.j2
│ ├── bgp.j2
│ └── snmp.j2
├── data/ # 数据文件
│ ├── devices.yaml # 设备列表
│ └── network.yaml # 网络参数
├── output/ # 生成的配置
└── deploy.py # 渲染 + 下发脚本
6.2 模板规范
Jinja2 模板编写规范:
1. 命名规范
┌─ 模板名:设备类型_功能.j2
│ 如:huawei_switch_vlan.j2
├─ 变量名:snake_case
└─ 块名:有意义(system/interfaces/routing)
2. 代码风格
┌─ 缩进与最终配置对齐
├─ 使用 {# #} 注释复杂逻辑
├─ 变量用 {{ }} 保持清晰
└─ 控制结构 {% %} 单独一行
3. 可维护性
┌─ 数据和模板分离(从 YAML/JSON 读取)
├─ 使用模板继承减少重复
├─ 复杂逻辑用宏封装
└─ 模板版本标记
4. 测试
┌─ 渲染后人工审核差异
├─ 先测试环境验证
├─ 使用 --check 模式预览
└─ 保留历史版本
6.3 常见陷阱
陷阱 1:变量不存在
{{ undefined_var }} → 渲染为 ''
解决:用 default 过滤器
{{ var | default("") }}
陷阱 2:空白控制
模板中的空行会影响配置格式
解决:trim_blocks + lstrip_blocks
陷阱 3:列表循环
{% for iface in interfaces %}
如果 interfaces 是 None → 报错
解决:用 default([])
{% for iface in interfaces | default([]) %}
陷阱 4:字典遍历
{% for key, value in dict.items() %}
注意:Jinja2 中必须写 .items()
七、总结
Jinja2 在网络自动化中的价值:
数据与视图分离
┌─ 网络拓扑数据(JSON/YAML)
├─ 配置模板(.j2 文件)
└─ 渲染引擎 → 配置文件
一次编写,到处使用
┌─ 一套模板支持所有类似设备
├─ 改数据不改模板
└─ Ansible/Netmiko/NAPALM 都支持
模板驱动自动化的完整链路:
数据定义 → 模板渲染 → 配置下发 → 验证确认
(YAML) (Jinja2) (Netmiko) (Diff/Check)
下篇预告:第307篇 — YAML/JSON 配置数据格式,将介绍网络自动化中数据的结构化表示,以及 YAML 和 JSON 在配置管理中的应用。