第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

思考题

  1. YAML中的true/True/yes/on都会被解析为什么类型?如果在设备清单中不小心写了enabled: yes,Python读取后会是什么值?
  2. 如果设备清单中有200台设备,其中50台共享相同的SSH凭据和超时设置,如何用YAML最简洁地表达?
  3. 设计一个JSON Schema来验证"网络接口配置变更请求",需包含接口名、操作类型(up/down/configure)、目标参数、变更理由和审批人字段。

下篇预告

第308篇:Ansible 网络模块——Playbook 管理设备。学习Ansible如何将设备清单、Jinja2模板、SSH连接整合为一个声明式自动化框架。