第307篇:YAML/JSON 配置数据格式
关键词
YAML、JSON、数据格式、结构化数据、序列化、配置管理、数据驱动、自动化数据定义
一、为什么需要数据格式
1.1 从硬编码到数据驱动
自动化演进的三个阶段:
L1:硬编码脚本
┌─ 所有值写在 Python 代码里
├─ 改一个 IP 就要改代码
└─ 脚本不可复用
L2:配置文件驱动
┌─ 数据写在 YAML/JSON 文件中
├─ Python 代码只负责逻辑
├─ 改数据不改代码
└─ 脚本可复用于不同场景
L3:数据库/CMDB 驱动
┌─ 数据存储在 CMDB/数据库中
├─ 自动化流程自动读取
├─ 实时更新
└─ 全流程自动化
JSON 和 YAML 就是 L2 阶段的核心数据载体
1.2 JSON vs YAML 对比
| 特性 | JSON | YAML |
|---|---|---|
| 语法 可读性 注释 多行字符串 类型 解析速度 Python 集成 典型用途 Ansible | 严格 一般 ❌ 不支持 ❌ 不方便 自动 快 标准库 json API 数据交换 部分支持 | 简洁 优秀 ✅ 支持 # ✅ 支持 |
选择建议: ┌─ 写配置文件/设备数据 → YAML(更可读) ├─ API 交互/数据交换 → JSON(更快、通用) └─ Ansible 项目 → YAML(默认格式)
二、JSON 基础
2.1 JSON 语法
JSON 支持的四种基本结构:
1. 对象(字典)
{
"name": "ACC-SW01",
"role": "access",
"location": "B1-Floor01"
}
2. 数组(列表)
["ACC-SW01", "ACC-SW02", "ACC-SW03"]
3. 基本类型
┌─ 字符串:"hello"
├─ 数字:100, 3.14
├─ 布尔:true, false
└─ 空值:null
4. 嵌套
{
"device": {
"name": "ACC-SW01",
"interfaces": [
{"name": "GE0/0/1", "ip": "10.0.1.1"}
]
}
}
JSON 规则:
┌─ 键必须用双引号
├─ 字符串必须用双引号
├─ 不能有注释
└─ 最后一个元素不能有逗号
2.2 Python 操作 JSON
#!/usr/bin/env python3
# json_basic.py — JSON 读写操作
import json
# ===== 写入 JSON =====
device = {
"hostname": "CORE-RT01",
"vendor": "Huawei",
"model": "NE20E-S4",
"os_version": "V800R022C00",
"interfaces": [
{
"name": "GigabitEthernet0/0/1",
"ip": "10.0.12.1",
"mask": 30,
"status": "up",
},
{
"name": "GigabitEthernet0/0/2",
"ip": "10.0.13.1",
"mask": 30,
"status": "up",
},
],
"vlans": [10, 20, 30],
"features": {
"ospf": True,
"bgp": True,
"mpls": True,
"segment_routing": False,
},
}
# 写入文件(格式化输出)
with open("device.json", "w", encoding="utf-8") as f:
json.dump(device, f, indent=2, ensure_ascii=False)
print("✓ 已写入 device.json")
# 写入文件(压缩、省空间)
with open("device_min.json", "w") as f:
json.dump(device, f, separators=(",", ":"))
print("✓ 已写入 device_min.json")
# ===== 读取 JSON =====
with open("device.json", "r", encoding="utf-8") as f:
loaded = json.load(f)
print(f"\n设备名称: {loaded['hostname']}")
print(f"接口数量: {len(loaded['interfaces'])}")
print(f"启用特性: {[k for k, v in loaded['features'].items() if v]}")
# ===== JSON 转字符串 =====
json_str = json.dumps(device, indent=2, ensure_ascii=False)
print(f"\nJSON 字符串(前 200 字符):")
print(json_str[:200])
# ===== 字符串转 JSON =====
json_input = '{"name": "SW01", "vlans": [10, 20]}'
parsed = json.loads(json_input)
print(f"\n解析结果: {parsed}")
print(f"名称: {parsed['name']}, VLAN 数: {len(parsed['vlans'])}")
# ===== 网络设备批量数据 =====
devices = [
{"hostname": "CORE-SW01", "role": "core", "mgmt_ip": "10.0.0.1"},
{"hostname": "CORE-SW02", "role": "core", "mgmt_ip": "10.0.0.2"},
{"hostname": "ACC-SW01", "role": "access", "mgmt_ip": "10.0.1.1"},
{"hostname": "ACC-SW02", "role": "access", "mgmt_ip": "10.0.1.2"},
]
# 写入数组
with open("devices.json", "w") as f:
json.dump(devices, f, indent=2)
# 只读 access 交换机
access_sw = [d for d in devices if d["role"] == "access"]
print(f"\n接入交换机: {[d['hostname'] for d in access_sw]}")
2.3 JSON 在网络自动化中的典型应用
{
"_comment": "网络设备清单(设备信息表)",
"devices": [
{
"hostname": "CORE-RT01",
"mgmt_ip": "10.255.1.1",
"device_type": "huawei",
"role": "core-router",
"location": "DC-A-R01",
"vendor": "Huawei",
"model": "NE20E-S4"
},
{
"hostname": "CORE-SW01",
"mgmt_ip": "10.255.2.1",
"device_type": "huawei",
"role": "core-switch",
"location": "DC-A-R02",
"vendor": "Huawei",
"model": "CE12808"
}
]
}
{
"_comment": "配置参数(VLAN/接口规划)",
"global": {
"ntp_server": "203.0.113.1",
"domain": "corp.local",
"snmp_community": "public-ro"
},
"vlans": [
{"id": 10, "name": "MGMT", "subnet": "10.0.10.0/24"},
{"id": 20, "name": "OFFICE", "subnet": "10.0.20.0/24"},
{"id": 100, "name": "SERVER", "subnet": "10.0.100.0/24"}
],
"interfaces": [
{"device": "CORE-SW01", "name": "GE0/0/1",
"desc": "UPLINK-TO-RT01", "mode": "trunk",
"vlans": "10 20 100"},
{"device": "CORE-SW01", "name": "GE0/0/2",
"desc": "ACCESS-FLOOR1", "mode": "access", "vlan": 20}
]
}
三、YAML 基础
3.1 YAML 语法
YAML 核心语法:
# 注释以 # 开头
# 键值对(字典)
hostname: CORE-RT01
role: core-router
location: "DC-A-R01" # 引号可选
# 数组(列表)
vlans:
- 10
- 20
- 30
# 或者行内格式
vlans: [10, 20, 30]
# 嵌套结构
interfaces:
- name: GE0/0/1
ip: 10.0.12.1
mask: 30
status: up
- name: GE0/0/2
ip: 10.0.13.1
mask: 30
status: up
# 多行字符串(保留换行)
config: |
interface GE0/0/1
ip address 10.0.1.1 255.255.255.0
undo shutdown
# 多行字符串(折叠空格)
description: >
This is a long description
that will be folded into
a single line.
# 布尔值
ospf_enabled: true
bgp_enabled: false
debug_mode: yes # 等同于 true
verbose_mode: no # 等同于 false
YAML 规则:
┌─ 缩进用空格(不能用 Tab)
├─ 缩进层级表示嵌套
├─ - 表示列表元素
├─ : 后面必须有空格
└─ # 开头是注释
3.2 Python 操作 YAML
#!/usr/bin/env python3
# yaml_basic.py — YAML 读写操作
# 安装:python -m pip install pyyaml
import yaml
import json
# ===== YAML 数据(直接写在代码中) =====
yaml_data = """
# 网络设备清单
devices:
- hostname: CORE-RT01
vendor: Huawei
model: NE20E-S4
mgmt_ip: 10.255.1.1
role: core
interfaces:
- name: GigabitEthernet0/0/0
ip: 10.0.0.1
mask: 30
description: "Uplink to Core-SW"
- name: GigabitEthernet0/0/1
ip: 10.0.1.1
mask: 24
description: "Access network"
- hostname: ACC-SW01
vendor: Huawei
model: S5735-L48T4XE
mgmt_ip: 10.255.2.1
role: access
vlans: [10, 20, 30, 100]
interfaces:
- name: GigabitEthernet0/0/1
mode: trunk
allowed_vlans: "10 20 30 100"
- name: GigabitEthernet0/0/2
mode: access
vlan: 10
- name: GigabitEthernet0/0/3
mode: access
vlan: 20
# 全局参数
global:
ntp_server: 203.0.113.1
syslog_server: 10.0.0.100
snmp:
community: public
version: v2c
location: "DC-A-R01"
"""
# ===== 解析 YAML =====
data = yaml.safe_load(yaml_data)
print("=== YAML 解析结果 ===")
print(f"设备数量: {len(data['devices'])}")
for dev in data["devices"]:
print(f"\n设备: {dev['hostname']}")
print(f" 厂商: {dev['vendor']} {dev['model']}")
print(f" 管理 IP: {dev['mgmt_ip']}")
print(f" 接口数: {len(dev.get('interfaces', []))}")
print(f" VLAN: {dev.get('vlans', 'N/A')}")
print(f"\n全局 NTP: {data['global']['ntp_server']}")
print(f"SNMP 版本: {data['global']['snmp']['version']}")
# ===== 写入 YAML =====
with open("network.yml", "w", encoding="utf-8") as f:
yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
print("\n✓ 已写入 network.yml")
# ===== YAML 与 JSON 互转 =====
json_str = json.dumps(data, indent=2, ensure_ascii=False)
with open("network.json", "w", encoding="utf-8") as f:
f.write(json_str)
print("✓ 已写入 network.json(YAML → JSON 转换)")
# ===== 从 YAML 文件读取 =====
with open("network.yml", "r", encoding="utf-8") as f:
loaded = yaml.safe_load(f)
core_devices = [d for d in loaded["devices"] if d["role"] == "core"]
print(f"\n核心设备: {[d['hostname'] for d in core_devices]}")
access_devices = [d for d in loaded["devices"] if d["role"] == "access"]
print(f"接入设备: {[d['hostname'] for d in access_devices]}")
四、实战:数据驱动的配置生成
4.1 完整的数据驱动流程
#!/usr/bin/env python3
# data_driven_config.py — 数据驱动的配置生成
import yaml
import json
from jinja2 import Environment, FileSystemLoader
from netmiko import ConnectHandler
import os
# ===== 1. YAML 数据文件 =====
yaml_device_data = """
# 网络设备数据定义
site_info:
name: "Corp-Office-B1"
location: "Building 1, Floor 2"
ntp:
server: 203.0.113.1
timezone: "CST+08:00"
devices:
- hostname: ACC-SW01
mgmt_ip: 10.0.1.1
device_type: huawei
vlans:
- {id: 10, name: MGMT}
- {id: 20, name: OFFICE}
- {id: 30, name: GUEST}
interfaces:
- name: GigabitEthernet0/0/1
description: "UPLINK-TO-CORE"
mode: trunk
allowed_vlans: "10 20 30"
- name: GigabitEthernet0/0/2
description: "OFFICE-FLOOR1"
mode: access
default_vlan: 20
- name: GigabitEthernet0/0/3
description: "GUEST-WIFI"
mode: access
default_vlan: 30
- hostname: ACC-SW02
mgmt_ip: 10.0.1.2
device_type: huawei
vlans:
- {id: 10, name: MGMT}
- {id: 40, name: PROD}
- {id: 50, name: DEV}
interfaces:
- name: GigabitEthernet0/0/1
description: "UPLINK-TO-CORE"
mode: trunk
allowed_vlans: "10 40 50"
- name: GigabitEthernet0/0/2
description: "PROD-SERVER01"
mode: access
default_vlan: 40
- name: GigabitEthernet0/0/3
description: "DEV-LAB01"
mode: access
default_vlan: 50
"""
# ===== 2. 读取数据 =====
data = yaml.safe_load(yaml_device_data)
print(f"站点: {data['site_info']['name']}")
print(f"设备数: {len(data['devices'])}\n")
# ===== 3. 生成 Inventory 数据 =====
inventory = {
"all": {
"hosts": {},
"vars": {
"ntp_server": data["ntp"]["server"],
"timezone": data["ntp"]["timezone"],
},
}
}
for dev in data["devices"]:
inventory["all"]["hosts"][dev["hostname"]] = {
"ansible_host": dev["mgmt_ip"],
"device_type": dev["device_type"],
"vlans": dev["vlans"],
"interfaces": dev["interfaces"],
}
# 保存为 JSON(给外部工具用)
with open("inventory.json", "w") as f:
json.dump(inventory, f, indent=2)
print("✓ Inventory 已保存到 inventory.json")
# ===== 4. 生成每个设备的配置 =====
os.makedirs("output_configs", exist_ok=True)
os.makedirs("templates", exist_ok=True)
# 模板
config_template = """\
! ============================================
! 站点: {{ site_name }}
! 设备: {{ device.hostname }}
! 管理 IP: {{ device.mgmt_ip }}
! 生成时间: {{ gen_time }}
! ============================================
#
sysname {{ device.hostname }}
#
{% for vlan in device.vlans %}
vlan {{ vlan.id }}
name {{ vlan.name }}
#
{% endfor %}
#
{% for iface in device.interfaces %}
interface {{ iface.name }}
description {{ iface.description }}
port link-type {{ iface.mode }}
{% if iface.mode == "trunk" %}
port trunk allow-pass vlan {{ iface.allowed_vlans }}
{% else %}
port default vlan {{ iface.default_vlan }}
{% endif %}
undo shutdown
#
{% endfor %}
#
ntp-service unicast-server {{ ntp_server }}
#
return
"""
with open("templates/device_config.j2", "w") as f:
f.write(config_template)
from datetime import datetime
env = Environment(loader=FileSystemLoader("templates"))
template = env.get_template("device_config.j2")
for dev in data["devices"]:
output = template.render(
site_name=data["site_info"]["name"],
device=dev,
ntp_server=data["ntp"]["server"],
gen_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
)
filename = f"output_configs/{dev['hostname']}_config.txt"
with open(filename, "w", encoding="utf-8") as f:
f.write(output)
print(f"✓ 已生成: {filename} ({len(output.splitlines())} 行)")
# ===== 5. 生成部署脚本 =====
deploy_script = """#!/usr/bin/env python3
# 自动生成的部署脚本
from netmiko import ConnectHandler
from netmiko.exceptions import NetmikoTimeoutException
import yaml
DEVICES = %s
def deploy_device(dev_name, dev_info):
\"\"\"部署设备配置\"\"\"
device = {
"device_type": dev_info["device_type"],
"host": dev_info["ansible_host"],
"username": input(f"输入 {dev_name} 用户名: "),
"password": input(f"输入 {dev_name} 密码: "),
}
try:
conn = ConnectHandler(**device)
except NetmikoTimeoutException:
print(f" [失败] {dev_name} 连接超时")
return
# 读取生成的配置
with open(f"output_configs/{dev_name}_config.txt") as f:
config = f.read()
conn.send_config_set(config.split("\\n"))
conn.save()
conn.disconnect()
print(f" [成功] {dev_name} 配置已下发")
if __name__ == "__main__":
for name, info in DEVICES.items():
print(f"部署 {name}...")
deploy_device(name, info)
"""
# 提取 devices 字典用于部署脚本
deploy_devices = {}
for dev in data["devices"]:
deploy_devices[dev["hostname"]] = {
"ansible_host": dev["mgmt_ip"],
"device_type": dev["device_type"],
}
with open("deploy_configs.py", "w", encoding="utf-8") as f:
f.write(deploy_script % json.dumps(deploy_devices, indent=4))
print("\n✓ 部署脚本已生成: deploy_configs.py")
print("\n=== 数据驱动配置生成流程 ===")
print("YAML 数据 → Jinja2 模板 → 配置文件 → 部署脚本")
print(" ↓ ↓ ↓")
print(" 人工维护数据 自动渲染生成 自动下发执行")
4.2 从 Excel 生成 YAML 数据
#!/usr/bin/env python3
# excel_to_yaml.py — Excel 转 YAML 配置数据
"""
适用场景:
网络规划人员在 Excel 中维护设备清单/VLAN 规划
运维人员通过脚本转为 YAML,再生成配置
"""
import yaml
import json
# 模拟从 Excel 读取的数据
# 实际使用 openpyxl 或 pandas 读取
excel_data = {
"devices": [
{
"hostname": "ACC-SW01",
"mgmt_ip": "10.0.1.1",
"role": "access",
"location": "B1-F1",
"model": "S5735-L48T4XE",
},
{
"hostname": "ACC-SW02",
"mgmt_ip": "10.0.1.2",
"role": "access",
"location": "B1-F2",
"model": "S5735-L48T4XE",
},
{
"hostname": "CORE-SW01",
"mgmt_ip": "10.0.0.1",
"role": "core",
"location": "DC-A",
"model": "CE12808",
},
],
"vlans": [
{"id": 10, "name": "MGMT", "subnet": "10.0.10.0/24"},
{"id": 20, "name": "DATA", "subnet": "10.0.20.0/24"},
{"id": 100, "name": "SERVER", "subnet": "10.0.100.0/24"},
],
"global": {
"ntp": "203.0.113.1",
"snmp_location": "Beijing DC",
},
}
# 转换为 YAML
with open("network_data.yml", "w", encoding="utf-8") as f:
yaml.dump(excel_data, f, default_flow_style=False, allow_unicode=True)
print("✓ Excel 数据已转换为 network_data.yml")
# 验证
with open("network_data.yml", "r") as f:
loaded = yaml.safe_load(f)
print(f"设备数: {len(loaded['devices'])}")
print(f"VLAN 数: {len(loaded['vlans'])}")
print(f"NTP: {loaded['global']['ntp']}")
五、数据验证
5.1 数据完整性检查
#!/usr/bin/env python3
# validate_data.py — 数据验证
import yaml
import json
import sys
def validate_device_data(data):
"""验证设备数据的完整性"""
errors = []
warnings = []
if "devices" not in data:
errors.append("缺少 devices 字段")
for i, dev in enumerate(data.get("devices", [])):
prefix = f"设备 #{i+1}"
# 必填字段
for field in ["hostname", "mgmt_ip"]:
if field not in dev:
errors.append(f"{prefix}: 缺少 {field}")
# IP 格式检查
if "mgmt_ip" in dev:
parts = dev["mgmt_ip"].split(".")
if len(parts) != 4 or not all(
p.isdigit() and 0 <= int(p) <= 255 for p in parts
):
errors.append(f"{prefix}: mgmt_ip 格式错误 {dev['mgmt_ip']}")
# 主机名校验
if "hostname" in dev:
if not dev["hostname"].replace("-", "").isalnum():
warnings.append(
f"{prefix}: hostname 包含特殊字符 {dev['hostname']}"
)
# 接口校验
for j, iface in enumerate(dev.get("interfaces", [])):
iface_prefix = f"{prefix} 接口 #{j+1}"
if "mode" in iface and iface["mode"] not in ["access", "trunk", "hybrid"]:
errors.append(f"{iface_prefix}: 未知 mode '{iface['mode']}'")
return errors, warnings
# 测试
test_data = yaml.safe_load("""
devices:
- hostname: "ACC-SW01"
mgmt_ip: "10.0.1.1"
interfaces:
- name: GE0/0/1
mode: trunk
- hostname: "INVALID_IP_SW"
mgmt_ip: "10.0." # 错误 IP
interfaces:
- name: GE0/0/1
mode: invalid_mode # 错误模式
- hostname: "" # 空主机名
mgmt_ip: "10.0.2.1"
""")
errors, warnings = validate_device_data(test_data)
if errors:
print("=== 错误 ===")
for e in errors:
print(f" ✗ {e}")
if warnings:
print("=== 警告 ===")
for w in warnings:
print(f" △ {w}")
if not errors and not warnings:
print("✓ 数据校验通过")
六、最佳实践
6.1 数据组织规范
推荐的数据组织方式:
1. 分层次
┌─ site: 站点/项目信息
├─ global: 全局配置参数
├─ devices: 设备清单
├─ vlans: VLAN 规划
├─ subnets: IP 地址规划
└─ links: 链路信息
2. 独立文件
┌─ site.yml — 站点信息
├─ devices.yml — 设备清单
├─ vlans.yml — VLAN 规划
├─ network.yml — 网络参数
└─ secrets.yml — 敏感信息(加密)
3. 版本管理
┌─ 所有数据文件纳入 Git 管理
├─ 每次变更 commit + PR review
├─ 标签对应发布版本
└─ 回滚历史版本
4. 数据校验
┌─ 提交前自动校验
├─ 定义 JSON Schema
├─ 检查必填字段
└─ IP 格式/唯一性检查
6.2 敏感信息处理
#!/usr/bin/env python3
# secrets_handling.py — 凭证管理
"""
安全准则:
1. 永远不要将密码硬编码在 YAML/JSON 中
2. 使用环境变量或专用密钥管理工具
3. 使用 ansible-vault 加密敏感数据
"""
import os
import yaml
# 方式 1:环境变量
username = os.environ.get("NET_USER", "")
password = os.environ.get("NET_PASSWORD", "")
if not username or not password:
# 交互式输入
import getpass
username = input("用户名: ")
password = getpass.getpass("密码: ")
# 方式 2:独立的加密凭证文件
# secrets.yml(不上传到 Git)
# username: admin
# password: YourRealPassword123
# 在 .gitignore 中添加 secrets.yml
# 方式 3:使用 keyring 系统
try:
import keyring
password = keyring.get_password("network_automation", username)
except ImportError:
pass
# 方式 4:Ansible Vault 加密
# ansible-vault encrypt secrets.yml
# ansible-vault decrypt secrets.yml
七、总结
JSON 和 YAML 在网络自动化中的角色:
JSON — 程序间数据交换 ┌─ API 请求/响应 ├─ 数据库存储 ├─ 配置文件备份 └─ Python/JavaScript 互操作
YAML — 人类可读的配置 ┌─ Ansible Playbook ├─ 设备数据定义 ├─ 网络规划文档 └─ 自动化输入数据
数据驱动自动化工作流: | YAML数据 定义 | → | Jinja2 渲染 | → | Netmiko 下发 | → | 设备配置 生效 | | --- | --- | --- | --- | --- | --- | --- | 人工维护 自动生成 自动执行 验证确认
下篇预告:第308篇 — Ansible Network Modules 实战,将深入 Ansible 的网络自动化模块,介绍如何通过 Playbook 批量管理网络设备。