第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 在配置管理中的应用。