第307篇:YAML 与 JSON——结构化数据格式
关键词:YAML、JSON、数据序列化、设备清单、配置变量、数据验证、schema
一、引言
网络自动化的数据流可以概括为:读取数据 → 处理数据 → 输出配置 → 下发设备。其中"数据"需要一个标准化的载体格式,既要方便人类编写和阅读,又要被程序高效解析。
YAML和JSON是网络自动化领域最常用的两种结构化数据格式:YAML以其简洁的语法成为Ansible、Kubernetes、Jinja2变量的首选格式;JSON则因其普适性成为API接口、采集结果存储、配置文件交换的通用标准。
二、YAML 语法详解
2.1 为什么选择 YAML
| 特性 | YAML | JSON |
|---|---|---|
| 注释 | ✅ # 注释 |
❌ 不支持 |
| 多行文本 | ✅ | 和 > 折叠 |
❌ 需\n转义 |
| 重复引用 | ✅ &alias / *ref |
❌ 需重复定义 |
| 人类可读性 | ⭐⭐⭐ | ⭐⭐ |
| 解析性能 | ⭐⭐ | ⭐⭐⭐ |
| API通用性 | ⭐⭐ | ⭐⭐⭐ |
2.2 基本数据类型
# === 标量类型 ===
# 字符串(无需引号,包含空格时需引号)
hostname: Core-SW-01
description: "核心交换机 01号"
# 整数
vlan_id: 100
port_speed: 10000
# 浮点数
utilization: 78.5
# 布尔值
interface_enabled: true
gr_enabled: false
# 空值
optional_field: null
# === 列表(数组)===
upstream_peers:
- 10.255.0.2
- 10.255.0.3
- 10.255.0.4
# 紧凑写法
vlan_list: [10, 20, 30, 100, 200]
# === 字典(映射)===
device:
hostname: Core-SW-01
vendor: Huawei
model: CE6865
management_ip: 10.255.0.1
# 紧凑写法
ip_info: {address: "10.255.0.1", mask: "/24"}
2.3 嵌套结构
# 多级嵌套:设备 → 接口 → 配置
network:
site:
name: DC-BJ-01
location: "北京·回龙观数据中心"
devices:
- hostname: Core-SW-01
role: core
interfaces:
- name: GigabitEthernet0/0/1
description: "To-Core-SW-02(互联)"
type: trunk
allowed_vlans:
- 10
- 20
- 100
mtu: 9216
- name: GigabitEthernet0/0/2
description: "To-Core-SW-03(互联)"
type: trunk
allowed_vlans:
- 10
- 20
- 100
mtu: 9216
bgp:
local_as: 65001
router_id: "10.255.0.1"
neighbors:
- ip: "10.255.0.2"
remote_as: 65001
description: "iBGP to Core-SW-02"
- ip: "203.0.113.1"
remote_as: 65100
description: "eBGP to ISP-A"
2.4 锚点与引用(去重利器)
# 定义锚点
common_settings: &common_settings
username: admin
password: "{{ vault_password }}"
device_type: huawei
timeout: 30
delay_factor: 1
devices:
core_switch_01:
host: 192.168.1.1
<<: *common_settings # 引用锚点,继承所有属性
core_switch_02:
host: 192.168.1.2
<<: *common_settings # 引用锚点
access_switch_01:
host: 192.168.1.3
<<: *common_settings
delay_factor: 2 # 覆盖锚点中的值
2.5 多行文本
# 保留换行符(literal block)
startup_script: |
display version
display ip interface brief
display vlan brief
display bgp peer
# 折叠换行符(folded block)
device_description: >
此设备为北京回龙观数据中心
核心交换机01号,型号CE6865,
承担业务路由转发与BGP会话维护。
三、JSON 语法详解
3.1 JSON 基本格式
{
"hostname": "Core-SW-01",
"vendor": "Huawei",
"model": "CE6865",
"management_ip": "10.255.0.1",
"role": "core",
"vlans": [10, 20, 30, 100, 200],
"interfaces": [
{
"name": "GigabitEthernet0/0/1",
"description": "To-Core-SW-02",
"type": "trunk",
"mtu": 9216,
"enabled": true
},
{
"name": "GigabitEthernet0/0/2",
"description": "To-Core-SW-03",
"type": "trunk",
"mtu": 9216,
"enabled": true
}
],
"bgp": {
"local_as": 65001,
"router_id": "10.255.0.1",
"graceful_restart": true
}
}
3.2 JSON vs YAML 对比
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| 设备清单(Inventory) | YAML | 可读性好、支持注释、锚点去重 |
| Ansible变量文件 | YAML | Ansible原生支持 |
| API响应数据 | JSON | 通用标准、浏览器原生支持 |
| 采集结果存储 | JSON | 易被各类工具解析 |
| 配置文件 | JSON | 结构严格、易于校验 |
| Kubernetes配置 | YAML | K8s原生使用YAML |
四、Python 中的 YAML 与 JSON 操作
4.1 YAML 读写
import yaml
# === 读取YAML ===
with open("devices.yaml", "r", encoding="utf-8") as f:
data = yaml.safe_load(f) # safe_load更安全,不执行任意代码
print(data["network"]["site"]["name"])
# === 写入YAML ===
config_data = {
"devices": [
{
"hostname": "Core-SW-01",
"ip": "192.168.1.1",
"role": "core",
"vlans": [10, 20, 100],
},
{
"hostname": "Access-SW-01",
"ip": "192.168.1.3",
"role": "access",
"vlans": [10, 20],
},
]
}
with open("output_devices.yaml", "w", encoding="utf-8") as f:
yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True)
4.2 JSON 读写
import json
# === 读取JSON ===
with open("device_config.json", "r", encoding="utf-8") as f:
config = json.load(f)
print(config["hostname"])
print(config["bgp"]["router_id"])
# === 写入JSON ===
collection_result = {
"timestamp": "2025-01-15T10:30:00",
"device": "Core-SW-01",
"interfaces": {
"GigabitEthernet0/0/1": {
"status": "up",
"speed": 1000,
"duplex": "full",
},
"GigabitEthernet0/0/2": {
"status": "up",
"speed": 1000,
"duplex": "full",
},
},
"route_count": 15234,
"bgp_peers": 8,
}
with open("collection_result.json", "w", encoding="utf-8") as f:
json.dump(collection_result, f, indent=2, ensure_ascii=False)
4.3 YAML 与 JSON 互转
import yaml
import json
# YAML → JSON
with open("devices.yaml", "r", encoding="utf-8") as f:
yaml_data = yaml.safe_load(f)
with open("devices.json", "w", encoding="utf-8") as f:
json.dump(yaml_data, f, indent=2, ensure_ascii=False)
# JSON → YAML
with open("devices.json", "r", encoding="utf-8") as f:
json_data = json.load(f)
with open("devices_converted.yaml", "w", encoding="utf-8") as f:
yaml.dump(json_data, f, default_flow_style=False, allow_unicode=True)
五、设备清单(Inventory)设计
5.1 标准设备清单模板
# inventory/devices.yaml
# 全局默认值
all:
children:
core:
vars:
device_type: huawei
username: admin
password: "{{ vault_password }}"
role: core
bgp_enabled: true
ospf_area: "0.0.0.0"
hosts:
core-sw-01:
ansible_host: 192.168.1.1
mgmt_ip: 10.255.0.1
router_id: "10.255.0.1"
bgp_local_as: 65001
uplinks:
- remote_device: core-sw-02
local_port: GigabitEthernet0/0/1
remote_port: GigabitEthernet0/0/1
- remote_device: core-sw-03
local_port: GigabitEthernet0/0/2
remote_port: GigabitEthernet0/0/1
core-sw-02:
ansible_host: 192.168.1.2
mgmt_ip: 10.255.0.2
router_id: "10.255.0.2"
bgp_local_as: 65001
core-sw-03:
ansible_host: 192.168.1.3
mgmt_ip: 10.255.0.3
router_id: "10.255.0.3"
bgp_local_as: 65001
aggregation:
vars:
device_type: huawei
username: admin
password: "{{ vault_password }}"
role: aggregation
bgp_enabled: true
ospf_area: "0.0.0.1"
hosts:
agg-sw-01:
ansible_host: 192.168.1.10
mgmt_ip: 10.255.1.1
router_id: "10.255.1.1"
bgp_local_as: 65001
agg-sw-02:
ansible_host: 192.168.1.11
mgmt_ip: 10.255.1.2
router_id: "10.255.1.2"
bgp_local_as: 65001
access:
vars:
device_type: huawei
username: admin
password: "{{ vault_password }}"
role: access
bgp_enabled: false
hosts:
access-sw-01:
ansible_host: 192.168.1.20
mgmt_ip: 10.255.2.1
stp_priority: 4096
5.2 设备清单验证脚本
import yaml
import json
import sys
def validate_inventory(inventory_path):
"""验证设备清单的完整性和一致性"""
with open(inventory_path, "r", encoding="utf-8") as f:
inventory = yaml.safe_load(f)
errors = []
warnings = []
hosts = []
# 遍历所有分组
for group_name, group_data in inventory.get("all", {}).get("children", {}).items():
group_vars = group_data.get("vars", {})
for host_name, host_vars in group_data.get("hosts", {}).items():
host_entry = {
"name": host_name,
"group": group_name,
"vars": {**group_vars, **host_vars} # 合并组变量与主机变量
}
hosts.append(host_entry)
# 验证必需字段
required_fields = ["ansible_host"]
for field in required_fields:
if field not in host_vars and field not in group_vars:
errors.append(f"[{host_name}] 缺少必需字段: {field}")
# 验证IP格式
ip = host_vars.get("ansible_host", "")
parts = ip.split(".")
if len(parts) != 4:
errors.append(f"[{host_name}] IP格式无效: {ip}")
else:
try:
for p in parts:
val = int(p)
if val < 0 or val > 255:
errors.append(f"[{host_name}] IP段超出范围: {ip}")
break
except ValueError:
errors.append(f"[{host_name}] IP段非数字: {ip}")
# 验证role字段
role = host_vars.get("role", group_vars.get("role"))
valid_roles = ["core", "aggregation", "access"]
if role not in valid_roles:
errors.append(f"[{host_name}] 无效角色: {role}")
# 检查router_id(核心和汇聚设备必须有)
if role in ("core", "aggregation"):
if "router_id" not in host_vars and "router_id" not in group_vars:
errors.append(f"[{host_name}] {role}设备缺少router_id")
# 检查IP唯一性(稍后统一检查)
# 检查IP唯一性
ips = [h["vars"].get("ansible_host") for h in hosts]
duplicates = [ip for ip in ips if ips.count(ip) > 1]
if duplicates:
errors.append(f"发现重复IP: {set(duplicates)}")
# 打印结果
print(f"\n{'='*60}")
print(f"设备清单验证结果")
print(f"{'='*60}")
print(f"设备总数: {len(hosts)}")
if errors:
print(f"\n❌ 错误 ({len(errors)}):")
for err in errors:
print(f" {err}")
if warnings:
print(f"\n⚠️ 警告 ({len(warnings)}):")
for warn in warnings:
print(f" {warn}")
if not errors and not warnings:
print("\n✅ 验证通过,无错误和警告")
return len(errors) == 0
# 使用
if __name__ == "__main__":
success = validate_inventory("inventory/devices.yaml")
sys.exit(0 if success else 1)
六、数据采集结果JSON设计
6.1 标准化采集结果格式
{
"metadata": {
"collector": "napalm",
"version": "4.1.0",
"timestamp": "2025-01-15T10:30:00Z",
"device": {
"hostname": "Core-SW-01",
"ip": "192.168.1.1"
}
},
"facts": {
"vendor": "Huawei",
"model": "CE6865",
"os_version": "V200R019C10",
"serial_number": "210235Q8D210BXXXX",
"uptime_seconds": 1234567
},
"interfaces": {
"GigabitEthernet0/0/1": {
"is_up": true,
"is_enabled": true,
"speed": 1000,
"mac_address": "00:1e:10:aa:bb:cc",
"mtu": 9216,
"description": "To-Core-SW-02"
},
"GigabitEthernet0/0/2": {
"is_up": true,
"is_enabled": true,
"speed": 1000,
"mac_address": "00:1e:10:dd:ee:ff",
"mtu": 9216,
"description": "To-Core-SW-03"
}
},
"summary": {
"total_interfaces": 48,
"up_interfaces": 12,
"down_interfaces": 36,
"total_routes": 15234,
"bgp_peer_count": 8,
"bgp_up_peers": 8,
"vlan_count": 15
}
}
6.2 JSON Schema 验证
import json
from jsonschema import validate, ValidationError
# 定义JSON Schema
inventory_schema = {
"type": "object",
"required": ["metadata", "facts", "interfaces", "summary"],
"properties": {
"metadata": {
"type": "object",
"required": ["timestamp", "device"],
"properties": {
"collector": {"type": "string"},
"timestamp": {"type": "string", "format": "date-time"},
"device": {
"type": "object",
"required": ["hostname", "ip"],
"properties": {
"hostname": {"type": "string"},
"ip": {"type": "string", "format": "ipv4"},
}
}
}
},
"facts": {
"type": "object",
"required": ["vendor", "model", "os_version"],
"properties": {
"vendor": {"type": "string"},
"model": {"type": "string"},
"os_version": {"type": "string"},
"uptime_seconds": {"type": "integer", "minimum": 0},
}
},
"summary": {
"type": "object",
"properties": {
"total_interfaces": {"type": "integer", "minimum": 0},
"up_interfaces": {"type": "integer", "minimum": 0},
"down_interfaces": {"type": "integer", "minimum": 0},
"total_routes": {"type": "integer", "minimum": 0},
"bgp_peer_count": {"type": "integer", "minimum": 0},
}
}
}
}
# 验证采集结果
with open("collection_result.json", "r", encoding="utf-8") as f:
data = json.load(f)
try:
validate(instance=data, schema=inventory_schema)
print("✅ 采集结果格式验证通过")
except ValidationError as e:
print(f"❌ 验证失败: {e.message}")
七、故障案例:YAML 缩进错误引发连锁故障
7.1 故障现象
Ansible Playbook在部署VLAN配置时,所有接入交换机的VLAN ID变成了字符串而非整数,导致配置下发失败:
fatal: [access-sw-01]: FAILED! => {
"msg": "VLAN ID must be an integer, got '10 20 30 100 200'"
}
7.2 排查过程
查看变量文件:
# 错误的YAML
access_vlans: [10, 20, 30, 100, 200]
# 在某些场景下,如果缩进不正确
access_vlans: [10, 20, 30, 100, 200]
问题在于文件中的混合缩进(空格+Tab)。YAML对缩进极为敏感,Tab与空格的混用会导致解析器产生意外行为。在某些YAML解析器版本中,这会把列表解析为单个字符串。
7.3 解决方案
# 安装yamllint进行语法检查
pip install yamllint
# 运行检查
yamllint -d relaxed inventory/devices.yaml
配置.yamllint规则:
# .yamllint
rules:
indentation:
spaces: 2
indent-sequences: whatever
check-multi-line-strings: true
line-length:
max: 200
truthy:
allowed-values: ['true', 'false']
八(续)、高级主题
8.1 YAML 高级特性:合并键与复杂锚点
在实际网络自动化项目中,YAML 的锚点与合并键(<<)可大幅减少设备清单的重复定义:
# inventories/advanced_inventory.yaml
# 网络级全局默认值
network_defaults: &net_defaults
device_type: huawei
connection: network_cli
timeout: 30
auth_pass: "{{ vault_net_password }}"
authorize: true
authorize_pass: "{{ vault_enable_password }}"
# 厂商特定覆盖
huawei_defaults: &huawei_defaults
<<: *net_defaults
platform: vrpc
terminal_length: 0
juniper_defaults: &juniper_defaults
<<: *net_defaults
host_key_check: false
port: 22
# 站点级分组
sites:
DC-BJ-01:
location: "北京·回龙观"
dc_manager: 10.255.0.254
devices:
- hostname: Core-SW-BJ-01
ansible_host: 192.168.1.1
role: spine
<<: *huawei_defaults
bgp_as: 65001
ospf_area: "0.0.0.0"
- hostname: Core-SW-BJ-02
ansible_host: 192.168.1.2
role: spine
<<: *huawei_defaults
bgp_as: 65001
ospf_area: "0.0.0.0"
8.2 JSON Patch(RFC 6902)在配置变更中的应用
JSON Patch 允许以"操作数组"的形式描述配置变更,非常适合 RESTCONF/NETCONF 的场景:
import json
# 原始配置
original = {
"interfaces": {
"GigabitEthernet1/0/1": {
"description": "Old-Description",
"enabled": True,
"mtu": 1500
}
}
}
# JSON Patch 操作序列(RFC 6902)
patch_operations = [
{"op": "replace", "path": "/interfaces/GigabitEthernet1/0/1/description",
"value": "To-Core-SW-02-Uplink"},
{"op": "replace", "path": "/interfaces/GigabitEthernet1/0/1/mtu", "value": 9216},
{"op": "add", "path": "/interfaces/GigabitEthernet1/0/1/ipv6_enabled", "value": True},
]
# 应用 Patch
import jsonpatch
modified = jsonpatch.JsonPatch(patch_operations).apply(original)
print(json.dumps(modified, indent=2, ensure_ascii=False))
8.3 YAML 与 JSON 的序列化陷阱
| 陷阱 | YAML 表现 | JSON 表现 | 规避方法 |
|---|---|---|---|
| 布尔值歧义 | yes/no/on/off 被解析为布尔 |
仅 true/false |
YAML 中用 true/false |
| 数字前缀 | 0755 被解析为八进制 |
仅十进制 | 用字符串 "0755" |
| 大整数精度 | 依赖解析器 | 依赖 JS 实现(超出 2^53 丢失精度) | 大整数存为字符串 |
| 空值 | null/~/空 |
仅 null |
统一用 null |
| Unicode | 原生支持 | 需要 ensure_ascii=False |
写入 JSON 时设置参数 |
8.4 设备配置变更请求 JSON Schema
from jsonschema import validate, ValidationError
# 网络变更请求 Schema
change_request_schema = {
"type": "object",
"required": ["request_id", "device", "operation", "target", "reason", "approver"],
"properties": {
"request_id": {"type": "string", "pattern": "^CR-\\d{4}-\\d{4}$"},
"device": {"type": "string"},
"operation": {
"type": "string",
"enum": ["add_vlan", "delete_vlan", "modify_interface", "shutdown", "no_shutdown"]
},
"target": {
"type": "object",
"properties": {
"interface": {"type": "string"},
"vlan_id": {"type": "integer", "minimum": 1, "maximum": 4094},
"description": {"type": "string"},
"mtu": {"type": "integer", "minimum": 68, "maximum": 9216},
},
"additionalProperties": True
},
"reason": {"type": "string", "minLength": 10},
"approver": {"type": "string"},
"scheduled_time": {"type": "string", "format": "date-time"},
"rollback_plan": {"type": "string"},
"test_verified": {"type": "boolean"}
}
}
# 示例变更请求
change_request = {
"request_id": "CR-2026-0901",
"device": "Core-SW-01",
"operation": "modify_interface",
"target": {
"interface": "GigabitEthernet1/0/1",
"description": "To-PE-BJ-02-MPLS",
"mtu": 9216
},
"reason": "扩容 MPLS LSP 需要支持 jumbo frame",
"approver": "张工",
"scheduled_time": "2026-09-07T02:00:00Z",
"rollback_plan": "恢复 MTU 为 1500",
"test_verified": True
}
try:
validate(instance=change_request, schema=change_request_schema)
print("✅ 变更请求 Schema 验证通过")
except ValidationError as e:
print(f"❌ 变更请求验证失败: {e.message}")
八、总结
| 知识点 | 要点 |
|---|---|
| YAML优势 | 人类可读、支持注释、锚点引用、多行文本 |
| JSON优势 | 通用性强、解析速度快、API标准格式 |
| 格式选择 | 人类编写用YAML,机器交换用JSON |
| Python操作 | yaml.safe_load/dump、json.load/dump |
| 设备清单 | 分层结构(all → group → host)、变量继承 |
| 数据验证 | YAML用yamllint,JSON用jsonschema |
| 互转 | YAML和JSON可无损互转(字典/列表结构) |
| 避坑 | 严格使用空格缩进(禁止Tab)、布尔值写true/false |
思考题
- YAML中的
true/True/yes/on都会被解析为什么类型?如果在设备清单中不小心写了enabled: yes,Python读取后会是什么值? - 如果设备清单中有200台设备,其中50台共享相同的SSH凭据和超时设置,如何用YAML最简洁地表达?
- 设计一个JSON Schema来验证"网络接口配置变更请求",需包含接口名、操作类型(up/down/configure)、目标参数、变更理由和审批人字段。
下篇预告
第308篇:Ansible 网络模块——Playbook 管理设备。学习Ansible如何将设备清单、Jinja2模板、SSH连接整合为一个声明式自动化框架。