第310篇:YANG 数据建模语言
关键词
YANG、数据建模、NETCONF、RESTCONF、数据结构、模块、容器、列表、叶子、数据模型
一、YANG 简介
1.1 什么是 YANG
YANG(Yet Another Next Generation)是一种 数据建模语言,用于定义网络设备配置和状态数据的结构:
YANG 在网络自动化中的位置:
| YANG Model(数据模型定义) ┌─ interface:name, type, ip, status ├─ vlan:id, name, tagged-ports └─ ospf:process-id, area, networks ↓ 建模 ┌────────────────────────────────────┐ └────────────────────────────────────┘ ↑ 协议 ┌────────────────────────────────────┐ | 网络设备 配置 = YANG 模型的数据实例 状态 = YANG 模型的数据实例 NETCONF / RESTCONF / gNMI 基于 YANG 模型读写设备数据 | |
|---|---|---|
为什么需要 YANG: ┌─ CLI 命令是非结构化的文本 ├─ SNMP MIB 结构老旧,扩展困难 ├─ 不同厂商相同功能的数据模型不同 └─ YANG 提供统一、结构化的数据模型
1.2 YANG 与 SNMP MIB 对比
| 特性 | SNMP MIB | YANG |
|---|---|---|
| 定义语言 可读性 配置操作 状态数据 列表操作 约束定义 厂商扩展 RPC 操作 通知 | SMIv2(ASN.1) 差 set(复杂) 只读 不支持 简单 私有 MIB 不支持 Trap | YANG 好 自然支持 读写 增删改查 丰富 模块继承 原生支持 Notification |
二、YANG 语法基础
2.1 模块结构
// 第310篇示例:YANG 模块定义
// 文件名:example-interface.yang
module example-interface {
// 模块标识
yang-version 1.1;
namespace "http://example.com/ns/interface";
prefix intf;
// 导入其他模块
import ietf-inet-types {
prefix inet;
}
// 模块描述
organization "Example Corp";
contact "support@example.com";
description
"This module defines a simple interface model.";
revision 2025-01-01 {
description "Initial revision";
reference "RFC 8344";
}
// ===== 容器(Container) =====
container interfaces {
description
"Interface configuration and state.";
// ===== 列表(List) =====
list interface {
key "name"; // 主键
description
"A network interface.";
// ---- 叶子(Leaf) ----
leaf name {
type string;
description "Interface name.";
}
leaf type {
type enumeration {
enum ethernet;
enum loopback;
enum vlan;
enum tunnel;
}
mandatory true;
description "Interface type.";
}
leaf enabled {
type boolean;
default true;
description
"Whether the interface is enabled.";
}
// ---- 叶子列表(Leaf-List) ----
leaf-list ip-address {
type inet:ipv4-address;
description
"List of IPv4 addresses.";
}
// ---- 嵌套容器 ----
container statistics {
config false; // 只读状态
description
"Interface statistics.";
leaf in-octets {
type uint64;
description
"Total input octets.";
}
leaf out-octets {
type uint64;
description
"Total output octets.";
}
leaf in-errors {
type uint32;
description
"Input errors.";
}
}
// ---- 选择(Choice) ----
choice connection-type {
description
"How this interface connects.";
case direct {
leaf peer-interface {
type leafref {
path "/interfaces/interface/name";
}
}
}
case vlan {
leaf vlan-id {
type uint16 {
range "1..4094";
}
}
}
}
}
}
}
2.2 YANG 核心语句
YANG 核心语法元素:
1. 容器(Container)
┌─ 类似 JSON 的对象 {}
├─ 包含子节点
└─ 自身没有值
2. 列表(List)
┌─ 类似数组 []
├─ key 定义主键唯一标识
├─ 支持 CRUD 操作
└─ 有序/无序列表
3. 叶子(Leaf)
┌─ 单个值
├─ 有类型约束
└─ 可设默认值
4. 叶子列表(Leaf-List)
┌─ 同一类型的多个值
├─ 类似数组
└─ 无嵌套结构
5. 类型(Types)
┌─ 内置:string, uint32, boolean, enumeration
├─ 派生:range, pattern(正则)
├─ 引用:leafref
├─ 联合:union
└─ 自定义:typedef
6. 约束
┌─ mandatory:必填
├─ default:默认值
├─ config false:只读状态
├─ must:XPath 条件
└─ when:条件约束
2.3 类型定义
// 自定义类型示例
module example-types {
yang-version 1.1;
namespace "http://example.com/ns/types";
prefix ex-type;
// ---- 自定义类型 ----
typedef vlan-id {
type uint16 {
range "1..4094";
}
description "VLAN identifier.";
}
typedef ipv4-prefix {
type string {
pattern
'([0-9]{1,3}\.){3}[0-9]{1,3}'
+ '/([0-9]|[12][0-9]|3[0-2])';
}
description "IPv4 prefix in CIDR notation.";
}
typedef admin-state {
type enumeration {
enum up {
value 1;
description "Administratively up.";
}
enum down {
value 2;
description "Administratively down.";
}
}
description "Administrative state.";
}
typedef interface-ref {
type leafref {
path "/if:interfaces/if:interface/if:name";
}
description
"Reference to an existing interface.";
}
// ---- 使用自定义类型 ----
container vlan-config {
leaf id {
type vlan-id;
mandatory true;
}
leaf name {
type string {
length "1..32";
}
}
leaf admin-status {
type admin-state;
default up;
}
leaf-list member-interfaces {
type interface-ref;
}
}
}
三、YANG 模块的构建块
3.1 模型分层结构
YANG 数据模型的分层结构:
模块(Module): ┌─ 最高层次 ├─ 类似 Python 模块 ├─ namespace + prefix └─ 可被其他模块 import
子模块(Submodule): ┌─ 属于某个模块 ├─ 可以拆分大模块 └─ 使用 belongs-to 关联父模块
增强(Augment): ┌─ 向已有模块添加新节点 ├─ 厂商扩展标准模型 └─ 保持向前兼容
偏差(Deviation): ┌─ 声明设备与标准模型的差异 ├─ 不支持某些节点 └─ 约束条件不同
YANG 模块之间的关系: | ietf-interfaces(IETF 标准) ↑ augment huawei-if-extension(华为扩展) ├─ huawei-if-vlan └─ huawei-if-ospf | | | --- | --- |
3.2 标准 YANG 模型
常用的标准 YANG 模型:
基础模型(RFC):
┌─ ietf-interfaces(RFC 8344):接口模型
├─ ietf-ip(RFC 8344):IP 地址模型
├─ ietf-routing(RFC 8349):路由模型
├─ ietf-ospf:OSPF 模型
├─ ietf-bgp:BGP 模型
├─ ietf-yang-library:模块清单
└─ ietf-netconf-monitoring:NETCONF 监控
华为 YANG 模型:
┌─ huawei-ifm:接口管理
├─ huawei-vlan:VLAN 配置
├─ huawei-ospf:OSPF 配置
├─ huawei-bgp:BGP 配置
├─ huawei-lldp:LLDP 配置
└─ huawei-ntp:NTP 配置
查看设备支持的 YANG 模型:
NETCONF: <get-schema>
RESTCONF: GET /restconf/data/ietf-yang-library
四、YANG 与 Python 交互
4.1 使用 pyang 工具
# 安装 pyang(YANG 工具集)
python -m pip install pyang
# 验证 YANG 模块语法
pyang --check example-interface.yang
# 导出为树形结构
pyang -f tree example-interface.yang
# 导出为 YIN(XML 格式)
pyang -f yin example-interface.yang
# 导出为 UML 图
pyang -f uml example-interface.yang -o model.uml
# pyang -f tree 输出示例
module: example-interface
+--rw interfaces
+--rw interface* [name]
+--rw name string
+--rw type enumeration
+--rw enabled? boolean
+--rw ip-address* inet:ipv4-address
+--ro statistics
| +--ro in-octets? uint64
| +--ro out-octets? uint64
| +--ro in-errors? uint32
+--rw (connection-type)?
+--:(direct)
| +--rw peer-interface? leafref
+--:(vlan)
+--rw vlan-id? uint16
4.2 Python 操作 YANG 数据
#!/usr/bin/env python3
# yang_data_operations.py — YANG 数据操作
# 安装:python -m pip install pyangbind
# 或使用标准 XML/JSON 处理 YANG 编码的数据
import json
import xml.etree.ElementTree as ET
# ===== YANG 数据实例(JSON 编码) =====
# 对应 YANG 模型 example-interface 的数据实例
interface_data = {
"ietf-interfaces:interfaces": {
"interface": [
{
"name": "GigabitEthernet0/0/1",
"type": "ethernet",
"enabled": True,
"ip-address": ["10.0.1.1", "10.0.1.2"],
"statistics": {
"in-octets": 1024000,
"out-octets": 512000,
"in-errors": 0,
},
},
{
"name": "LoopBack0",
"type": "loopback",
"enabled": True,
"ip-address": ["1.1.1.1"],
},
]
}
}
print("=== YANG 数据实例(JSON)===")
for intf in interface_data["ietf-interfaces:interfaces"]["interface"]:
print(f"\n接口: {intf['name']}")
print(f" 类型: {intf['type']}")
print(f" 启用: {intf['enabled']}")
print(f" IP: {', '.join(intf.get('ip-address', []))}")
if "statistics" in intf:
s = intf["statistics"]
print(f" 入流量: {s['in-octets']} bytes")
print(f" 出流量: {s['out-octets']} bytes")
print(f" 入错误: {s['in-errors']}")
# ===== XML 编码(NETCONF 格式) =====
def yang_data_to_xml(data_dict, namespace="urn:ietf:params:xml:ns:yang:ietf-interfaces"):
"""将 YANG 数据字典转为 XML(简化版)"""
root = ET.Element("interfaces", xmlns=namespace)
for intf in data_dict.get("interface", []):
iface_elem = ET.SubElement(root, "interface")
name = ET.SubElement(iface_elem, "name")
name.text = intf["name"]
type_elem = ET.SubElement(iface_elem, "type")
type_elem.text = intf["type"]
enabled = ET.SubElement(iface_elem, "enabled")
enabled.text = str(intf.get("enabled", True)).lower()
for ip in intf.get("ip-address", []):
ip_elem = ET.SubElement(iface_elem, "ip-address")
ip_elem.text = ip
return ET.tostring(root, encoding="unicode", short_empty_elements=False)
xml_output = yang_data_to_xml(interface_data["ietf-interfaces:interfaces"])
print("\n=== XML 编码(NETCONF 格式)===")
print(xml_output)
# ===== 验证数据是否符合 YANG 约束 =====
def validate_vlan_id(vlan_id):
"""验证 VLAN ID 是否符合 YANG 约束"""
if not isinstance(vlan_id, int):
return False, "VLAN ID 必须是整数"
if vlan_id < 1 or vlan_id > 4094:
return False, "VLAN ID 必须在 1-4094 范围内"
return True, "OK"
def validate_ip_prefix(prefix):
"""验证 IP 前缀格式"""
import re
pattern = r'^(\d{1,3}\.){3}\d{1,3}/(\d|[12]\d|3[0-2])$'
if not re.match(pattern, prefix):
return False, "IP 前缀格式无效"
# 验证每个 octet
ip_part = prefix.split("/")[0]
for octet in ip_part.split("."):
if int(octet) > 255:
return False, f"IP 段 {octet} 超出 0-255"
return True, "OK"
# 测试
test_cases = [100, 0, 4095, "abc"]
for tc in test_cases:
ok, msg = validate_vlan_id(tc)
print(f"VLAN ID {tc}: {'✓' if ok else '✗'} {msg}")
test_prefixes = ["10.0.1.0/24", "256.0.0.0/8", "10.0.0.0/33"]
for tp in test_prefixes:
ok, msg = validate_ip_prefix(tp)
print(f"前缀 {tp}: {'✓' if ok else '✗'} {msg}")
五、YANG 模型的增删改查
5.1 操作模型数据
#!/usr/bin/env python3
# yang_crud.py — YANG 模型数据的增删改查
import json
from copy import deepcopy
class YangDataStore:
"""简单的 YANG 数据存储(模拟设备)"""
def __init__(self):
self.data = {
"interfaces": {
"interface": []
}
}
def get_interface(self, name):
"""查询接口(Read)"""
for intf in self.data["interfaces"]["interface"]:
if intf["name"] == name:
return deepcopy(intf)
return None
def list_interfaces(self):
"""列出所有接口(List)"""
return deepcopy(self.data["interfaces"]["interface"])
def create_interface(self, interface):
"""创建接口(Create)"""
# 检查是否已存在
existing = self.get_interface(interface["name"])
if existing:
raise ValueError(f"接口 {interface['name']} 已存在")
# 验证必填字段
required = ["name", "type"]
for field in required:
if field not in interface:
raise ValueError(f"缺少必填字段: {field}")
self.data["interfaces"]["interface"].append(deepcopy(interface))
return True
def update_interface(self, name, updates):
"""更新接口(Update)"""
for intf in self.data["interfaces"]["interface"]:
if intf["name"] == name:
intf.update(updates)
return True
raise ValueError(f"接口 {name} 不存在")
def delete_interface(self, name):
"""删除接口(Delete)"""
original_len = len(self.data["interfaces"]["interface"])
self.data["interfaces"]["interface"] = [
i for i in self.data["interfaces"]["interface"]
if i["name"] != name
]
if len(self.data["interfaces"]["interface"]) == original_len:
raise ValueError(f"接口 {name} 不存在")
return True
# ===== 测试 =====
store = YangDataStore()
# Create
store.create_interface({
"name": "GigabitEthernet0/0/1",
"type": "ethernet",
"enabled": True,
"ip-address": ["10.0.1.1"],
})
print("✓ 创建接口 GE0/0/1")
# Read
intf = store.get_interface("GigabitEthernet0/0/1")
print(f" 查询结果: {intf['name']}, {intf['type']}")
# Update
store.update_interface("GigabitEthernet0/0/1", {
"enabled": False,
"description": "Maintenance"
})
updated = store.get_interface("GigabitEthernet0/0/1")
print(f" 更新状态: enabled={updated['enabled']}")
# List
print(f" 接口列表: {[i['name'] for i in store.list_interfaces()]}")
# Delete
store.delete_interface("GigabitEthernet0/0/1")
print(f" 删除后数量: {len(store.list_interfaces())}")
5.2 YANG 数据过滤与查询
#!/usr/bin/env python3
# yang_filter.py — YANG 数据过滤与查询
# 模拟 YANG 模型数据
devices_data = {
"device": [
{
"name": "CORE-RT01",
"role": "core",
"interfaces": {
"interface": [
{"name": "GE0/0/1", "type": "ethernet", "ip": "10.0.12.1/30"},
{"name": "LoopBack0", "type": "loopback", "ip": "1.1.1.1/32"},
]
},
},
{
"name": "ACC-SW01",
"role": "access",
"interfaces": {
"interface": [
{"name": "GE0/0/1", "type": "ethernet", "mode": "trunk"},
{"name": "GE0/0/2", "type": "ethernet", "mode": "access"},
]
},
},
]
}
# XPath 风格的过滤查询
def xpath_filter(data, path, condition=None):
"""模拟 XPath 查询"""
parts = path.strip("/").split("/")
current = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
elif isinstance(current, list):
# 遍历列表
results = []
for item in current:
if isinstance(item, dict):
item_result = item.get(part)
if item_result:
if isinstance(item_result, list):
results.extend(item_result)
else:
results.append(item_result)
current = results
else:
return []
# 应用条件过滤
if condition and isinstance(current, list):
return [item for item in current
if all(item.get(k) == v for k, v in condition.items())]
return current
# 查询示例
# 1. 查询所有设备
all_devices = xpath_filter(devices_data, "/device")
print("=== 所有设备 ===")
for d in all_devices:
print(f" {d['name']} ({d['role']})")
# 2. 查询核心设备
core_devices = xpath_filter(devices_data, "/device", {"role": "core"})
print("\n=== 核心设备 ===")
for d in core_devices:
print(f" {d['name']}")
# 3. 查询所有 type=ethernet 的接口
all_ifs = []
for dev in devices_data["device"]:
for intf in dev["interfaces"]["interface"]:
if intf.get("type") == "ethernet":
all_ifs.append({**intf, "device": dev["name"]})
print("\n=== 所有以太网接口 ===")
for i in all_ifs:
print(f" {i['device']}: {i['name']}")
六、YANG 模型的实际应用
6.1 标准 IETF 接口模型示例
// RFC 8344: ietf-interfaces 简化版
module ietf-interfaces {
yang-version 1.1;
namespace "urn:ietf:params:xml:ns:yang:ietf-interfaces";
prefix if;
import ietf-yang-types {
prefix yang;
}
container interfaces {
list interface {
key "name";
leaf name {
type string;
}
leaf description {
type string;
}
leaf type {
type identityref {
base interface-type;
}
mandatory true;
}
leaf enabled {
type boolean;
default true;
}
leaf link-up-down-trap-enable {
type enumeration {
enum enabled;
enum disabled;
}
}
container ipv4 {
leaf enabled {
type boolean;
default true;
}
leaf mtu {
type uint16 {
range "68..1500";
}
}
list address {
key "ip";
leaf ip {
type yang:ipv4-address;
}
leaf prefix-length {
type uint8 {
range "0..32";
}
mandatory true;
}
}
}
container statistics {
config false;
leaf discontinuity-time {
type yang:date-and-time;
}
leaf in-octets {
type yang:counter64;
}
leaf in-errors {
type yang:counter32;
}
leaf out-octets {
type yang:counter64;
}
}
}
}
}
6.2 基于 YANG 的配置操作
#!/usr/bin/env python3
# yang_config_ops.py — 基于 YANG 模型的配置操作
from copy import deepcopy
# 模拟设备数据库
device_db = {}
def yang_validate(data, schema):
"""YANG 数据校验"""
errors = []
if not isinstance(data, dict):
errors.append("数据必须是字典")
for field, rules in schema.items():
value = data.get(field)
if rules.get("mandatory") and value is None:
errors.append(f"缺少必填字段: {field}")
continue
if value is not None:
# 类型检查
expected_type = rules["type"]
if not isinstance(value, expected_type):
errors.append(
f"{field}: 期望 {expected_type.__name__}, "
f"实际 {type(value).__name__}"
)
# 范围检查
if "range" in rules and isinstance(value, (int, float)):
min_v, max_v = rules["range"]
if value < min_v or value > max_v:
errors.append(
f"{field}: 值 {value} 超出范围 [{min_v}, {max_v}]"
)
# 模式检查
if "pattern" in rules and isinstance(value, str):
import re
if not re.match(rules["pattern"], value):
errors.append(f"{field}: 格式不匹配 {rules['pattern']}")
return errors
# 接口 YANG 模式的简化表示
INTERFACE_YANG_SCHEMA = {
"name": {"type": str, "mandatory": True, "pattern": r"^[A-Za-z0-9/]+$"},
"type": {"type": str, "mandatory": True},
"enabled": {"type": bool, "mandatory": False},
"mtu": {"type": int, "mandatory": False, "range": (68, 9216)},
"description": {"type": str, "mandatory": False, "pattern": r"^[\w\-\s./]+$"},
"ip_address": {"type": str, "mandatory": False},
}
# 测试数据
test_configs = [
{"name": "GE0/0/1", "type": "ethernet", "enabled": True, "mtu": 1500},
{"name": "Invalid/Name!", "type": "ethernet"}, # 名称格式错误
{"name": "GE0/0/2", "type": "ethernet", "mtu": 99999}, # MTU 超范围
{"name": "LoopBack0", "type": "loopback", "ip_address": "10.0.0.1"},
]
for config in test_configs:
errors = yang_validate(config, INTERFACE_YANG_SCHEMA)
if errors:
print(f"✗ 校验失败 {config.get('name', '?')}:")
for err in errors:
print(f" - {err}")
else:
print(f"✓ 校验通过: {config['name']}")
device_db[config["name"]] = deepcopy(config)
七、YANG 最佳实践
7.1 模型设计原则
YANG 模型设计原则:
1. 单一职责
┌─ 一个模块只负责一个功能域
├─ 接口模型只管接口
├─ 路由模型只管路由
└─ 避免大而全的模块
2. 扩展性
┌─ 使用 augment 扩展现有模型
├─ 不修改标准模型
├─ 厂商扩展加独立前缀
└─ 保持向后兼容
3. 命名规范
┌─ 模块名:ietf/huawei/example- + 功能名
├─ 容器名:名词复数
├─ 列表名:名词单数
├─ 叶子名:小写+连字符
└─ 类型名:名词+描述
4. 文档
┌─ 每个模块写 description
├─ 每个容器/list/leaf 写 description
├─ revision 记录变更历史
└─ reference 引用标准文档
7.2 YANG 工具链
YANG 工具链:
开发工具:
┌─ pyang:YANG 语法检查 + 格式转换
├─ yanglint:YANG 数据验证
├─ YANG Designer:可视化建模
└─ VS Code YANG 插件
代码生成:
┌─ pyangbind:YANG → Python 绑定的类
├─ yang2dsdl:YANG → XML Schema
├─ sysrepo:YANG → C 语言 API
└─ yangc:YANG → C 语言结构体
测试工具:
┌─ yangvalidator:在线验证
├─ Netopeer2:NETCONF 模拟器
├─ confd:YANG 基础服务器
└─ yang-explorer:YANG 浏览器
八、总结
YANG 在网络自动化中的核心地位:
统一的配置数据模型 ┌─ 跨厂商接口抽象 ├─ 配置与状态统一建模 ├─ 支持 CRUD 操作 └─ 类型约束保证数据质量
自动化协议的基础 ┌─ NETCONF 操作基于 YANG 路径 ├─ RESTCONF URL 映射 YANG 节点 ├─ gNMI Path 引用 YANG 路径 └─ Telemetry 数据按 YANG 编码
YANG 驱动的自动化链路: | YANG 模型 定义 | → | 数据实例 (XML/JSON) 生成 | → | NETCONF/ RESTCONF 写入设备 | | --- | --- | --- | --- | --- | 标准/厂商 Python 脚本 API 交互
下篇预告:第311篇 — NETCONF/RESTCONF 协议,将介绍如何通过 NETCONF 和 RESTCONF 协议基于 YANG 模型对网络设备进行编程化配置管理。