第310篇:YANG 数据模型基础
关键词:YANG、数据模型、NETCONF、RESTCONF、模型驱动、节点类型、容器、列表、配置数据、状态数据
一、引言
在前几篇中,我们通过CLI命令、SSH连接和文本模板管理网络设备。CLI的本质是"自由文本协议"——不同厂商使用不同的命令语法、不同的输出格式。即便Netmiko和NAPALM做了大量适配工作,底层仍然依赖正则表达式解析文本。
YANG(Yet Another Next Generation)是IETF定义的网络管理数据建模语言(RFC 7950)。它为网络设备的数据(配置和状态)提供了一套标准化的、结构化的描述方式。有了YANG模型,控制器和设备之间交换的不再是"文本字符串",而是"结构化数据"——如同数据库中表与字段的关系,明确、可验证、可编程。
YANG是NETCONF和RESTCONF协议的基石。不理解YANG,就无法深入模型驱动的网络管理。
二、YANG 的历史与定位
2.1 从 NETMOD 到 YANG 1.1
| 标准 | RFC | 年份 | 说明 |
|---|---|---|---|
| NETMOD | RFC 6020 | 2010 | YANG 1.0前身,奠定了基本概念 |
| YANG 1.0 | RFC 6020 | 2010 | 初始版本,存在设计缺陷 |
| YANG 1.1 | RFC 7950 | 2016 | 主流版本,修复1.0问题,新增特性 |
| YANG 1.2 | 草案阶段 | 2024+ | 增强语义约束 |
2.2 模型驱动管理架构

2.3 YANG 在协议栈中的位置
| 层级 | 协议/模型 | 职责 |
|---|---|---|
| 数据建模层 | YANG | 定义数据的结构、类型、约束、关系 |
| 数据编码层 | XML / JSON | 序列化YANG模型中的数据 |
| 传输协议层 | NETCONF / RESTCONF | RPC调用、数据读写的事务机制 |
| 传输层 | SSH / HTTPS | 安全传输通道 |
| 链路层 | TCP/IP | 网络传输 |
三、YANG 基础语法
3.1 模块结构
module huawei-system {
yang-version 1.1;
namespace "urn:huawei:yang:system";
prefix "hw-sys";
organization "Huawei Technologies Co., Ltd.";
contact "support@huawei.com";
description
"华为设备系统管理模块";
revision 2023-06-15 {
description "初始版本";
reference "RFC 7950";
}
// 容器节点开始
container system {
config true;
leaf hostname {
type string {
length "1..63";
}
description "设备主机名";
}
leaf domain-name {
type string {
length "1..255";
}
description "域名";
}
}
}
3.2 关键字对照
| YANG关键字 | 说明 | 类比 |
|---|---|---|
module |
YANG模块定义 | Python的module |
submodule |
子模块(拆分大模块) | Python的package |
container |
容器节点(分组) | JSON对象 {} |
leaf |
叶子节点(单值) | JSON字段 "key": "value" |
leaf-list |
叶子列表(多值) | JSON数组 ["a", "b"] |
list |
列表节点(有序重复) | JSON数组对象 [{}, {}] |
choice |
互斥选择 | Python的enum |
anyxml |
任意XML数据 | 动态字段 |
rpc |
远程过程调用 | 设备操作(重启、保存等) |
notification |
异步通知 | 设备推送事件 |
uses |
引用节点组(复用) | Python的import |
augment |
扩展已有节点 | 继承与扩展 |
四、数据类型详解
4.1 内置数据类型
module data-types-demo {
yang-version 1.1;
namespace "urn:demo:data-types";
prefix "dt";
container demo {
config true;
// 基本类型
leaf name {
type string;
}
leaf age {
type uint8;
}
leaf temperature {
type decimal64 {
fraction-digits 2;
}
}
leaf enabled {
type boolean;
}
// 枚举类型
leaf status {
type enumeration {
enum "up";
enum "down";
enum "testing";
enum "dormant";
}
}
// 标识符类型
leaf protocol {
type identityref {
base "routing-protocol";
}
}
identity routing-protocol {
description "路由协议基类";
}
identity ospf {
base "routing-protocol";
}
identity isis {
base "routing-protocol";
}
identity bgp {
base "routing-protocol";
}
}
}
4.2 约束修饰符
leaf interface-mtu {
type uint16 {
range "68..9216"; // 数值范围
}
default 1500; // 默认值
mandatory true; // 是否必须
description "接口MTU值";
}
leaf description {
type string {
length "0..223"; // 字符串长度
pattern '[a-zA-Z0-9 _-]+'; // 正则表达式
}
description "接口描述";
}
leaf-list allowed-vlans {
type uint16 {
range "1..4094";
}
min-elements 1; // 最少元素
max-elements 4094; // 最多元素
ordered-by user; // 由用户维护顺序
description "Trunk允许通过的VLAN列表";
}
五、容器、列表与复杂结构
5.1 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"; // 列表的key
leaf name {
type string;
description "接口名称";
}
leaf description {
type string;
description "接口描述";
}
leaf type {
type identityref {
base "if:interface-type";
}
mandatory true;
}
leaf enabled {
type boolean;
default "true";
description "接口管理状态";
}
leaf admin-status {
type enumeration {
enum up;
enum down;
enum testing;
}
}
leaf oper-status {
type enumeration {
enum up;
enum down;
enum testing;
enum unknown;
}
config false; // 状态数据,不可配置
}
leaf mtu {
type uint16;
description "接口MTU";
}
leaf if-index {
type int32;
config false; // 只读状态
}
leaf phys-address {
type yang:phys-address;
config false; // MAC地址,只读
}
leaf-list supported-statistic-types {
type identityref {
base "if:statistic-type";
}
config false;
}
// 嵌套容器:子接口
container subinterfaces {
list subinterface {
key "index";
leaf index {
type uint32;
}
leaf ipv4-address {
type yang:dotted-quad;
}
}
}
// 统计信息(只读)
container statistics {
config false;
leaf in-octets {
type yang:counter32;
}
leaf out-octets {
type yang:counter32;
}
leaf in-unicast-pkts {
type yang:counter32;
}
leaf in-discards {
type yang:counter32;
}
}
}
}
}
5.2 对应的 JSON 数据
上述YANG模型对应的一台设备接口数据的JSON表示:
{
"ietf-interfaces:interfaces": {
"interface": [
{
"name": "GigabitEthernet0/0/1",
"description": "To-Core-SW-02",
"type": "iana-if-type:ethernetCsmacd",
"enabled": true,
"admin-status": "up",
"oper-status": "up",
"mtu": 9216,
"if-index": 1,
"phys-address": "00:1e:10:aa:bb:cc",
"statistics": {
"in-octets": 1234567890,
"out-octets": 987654321,
"in-unicast-pkts": 1234567,
"in-discards": 0
}
},
{
"name": "GigabitEthernet0/0/2",
"description": "To-Core-SW-03",
"type": "iana-if-type:ethernetCsmacd",
"enabled": true,
"admin-status": "up",
"oper-status": "up",
"mtu": 9216,
"if-index": 2,
"phys-address": "00:1e:10:dd:ee:ff",
"statistics": {
"in-octets": 2345678901,
"out-octets": 876543210,
"in-unicast-pkts": 2345678,
"in-discards": 3
}
}
]
}
}
5.3 config true vs config false
这是YANG模型中最重要的概念之一:
| 属性 | config true | config false |
|---|---|---|
| 语义 | 配置数据 | 状态数据 |
| 可写 | ✅ 可通过PUT/POST修改 | ❌ 只读 |
| 可删 | ✅ 可通过DELETE移除 | ❌ 由设备维护 |
| 持久化 | 保存到启动配置 | 不保存(设备运行时生成) |
| 示例 | hostname、VLAN、接口IP | 接口Up/Down状态、流量统计、BGP会话状态 |
container bgp {
// 配置数据:管理员可以修改
config true;
leaf local-as {
type uint32;
}
leaf router-id {
type yang:dotted-quad;
}
// 嵌套的状态数据
container neighbors {
config false; // 状态数据
list neighbor {
key "address";
leaf address {
type yang:dotted-quad;
}
leaf state {
type enumeration {
enum idle;
enum connect;
enum active;
enum opensent;
enum openconfirm;
enum established;
}
}
leaf uptime {
type yang:timeticks;
}
leaf messages-received {
type yang:counter32;
}
}
}
}
六、RPC 操作与 Notification
6.1 RPC(远程过程调用)
// 定义RPC:保存配置
rpc save-configuration {
description "将运行配置保存为启动配置";
input {
leaf copy-run-to-startup {
type boolean;
default true;
description "是否将running-config保存为startup-config";
}
}
output {
leaf success {
type boolean;
}
leaf message {
type string;
}
}
}
// 定义RPC:重启设备
rpc reboot-device {
description "重启网络设备";
input {
leaf delay-seconds {
type uint16;
default 60;
description "延迟重启时间(秒)";
}
leaf comment {
type string;
description "重启原因说明";
}
}
output {
leaf scheduled {
type boolean;
}
}
}
6.2 Notification(异步通知)
// 定义通知:接口状态变化
notification interface-status-change {
description "接口状态发生变化时推送";
leaf interface-name {
type string;
}
leaf previous-state {
type enumeration {
enum up;
enum down;
}
}
leaf new-state {
type enumeration {
enum up;
enum down;
}
}
leaf timestamp {
type yang:date-and-time;
}
}
// 定义通知:BGP会话状态变化
notification bgp-session-change {
description "BGP邻居会话状态变化";
leaf neighbor-ip {
type yang:dotted-quad;
}
leaf previous-state {
type string;
}
leaf new-state {
type string;
}
leaf reason {
type string;
}
leaf timestamp {
type yang:date-and-time;
}
}
七、YANG 工具链
7.1 YANG 验证与转换工具
# pyang:最流行的YANG验证工具
pip install pyang
# 验证YANG文件语法
pyang --lint my_model.yang
# 转换为XML Schema
pyang -f rfc my_model.yang > my_model_rfc.txt
# 生成Python类
pip install yangson
pyang -f tree my_model.yang
# 查看树形结构
pyang -f tree ietf-interfaces.yang
# 输出:
# +--rw interfaces
# +--rw interface* [name]
# +--rw name? string
# +--rw description? string
# +--rw type? identityref
# +--rw enabled? boolean
# +--ro oper-status? enumeration
# +--ro statistics
# +--ro in-octets? counter32
# +--ro out-octets? counter32
7.2 YANG 在线资源
| 资源 | 地址 | 说明 |
|---|---|---|
| IETF YANG Registry | https://datatracker.ietf.org/wg/netmod/documents/ | 官方标准模型库 |
| Open YANG Models | https://github.com/YangModels/yang | 社区贡献的厂商模型 |
| pyang | https://github.com/mbj4668/pyang | YANG验证工具 |
| YANG Catalog | https://yangcatalog.org/ | YANG模型在线目录 |
八、网络自动化测试框架与验证脚本
8.1 测试框架架构

8.2 YANG 模型单元测试
# tests/test_yang_models.py
import pytest
import subprocess
import yaml
# 测试YANG模型语法正确性
def test_yang_syntax_valid(yang_file):
"""使用pyang验证YANG文件语法"""
result = subprocess.run(
["pyang", "--lint", yang_file],
capture_output=True,
text=True
)
assert result.returncode == 0, f"YANG语法错误: {result.stderr}"
def test_yang_no_warnings(yang_file):
"""检查YANG模型无警告"""
result = subprocess.run(
["pyang", "--lint", "--ignore-errors", yang_file],
capture_output=True,
text=True
)
warnings = [line for line in result.stdout.split('\n')
if 'warning' in line.lower()]
assert len(warnings) == 0, f"存在警告: {warnings}"
# 参数化测试:验证多个YANG文件
yang_files = [
"models/ietf-interfaces.yang",
"models/huawei-system.yang",
"models/custom-vlan.yang",
]
@pytest.mark.parametrize("yang_file", yang_files)
def test_all_yang_files(yang_file):
"""批量验证所有YANG文件"""
test_yang_syntax_valid(yang_file)
# 测试YANG模型树形结构
def test_yang_tree_structure(yang_file):
"""验证YANG模型的树形结构"""
result = subprocess.run(
["pyang", "-f", "tree", yang_file],
capture_output=True,
text=True
)
tree_output = result.stdout
# 验证树中包含预期的节点
assert "container" in tree_output, "缺少container节点"
assert "leaf" in tree_output, "缺少leaf节点"
8.3 NETCONF 集成测试
# tests/test_netconf_operations.py
import pytest
import ncclient
@pytest.fixture
def netconf_session():
"""NETCONF会话fixture"""
options = {
"host": "192.168.1.1",
"port": 830,
"username": "admin",
"password": "H@wa123!",
"hostkey_verify": False,
"device_params": {"name": "huawei"},
}
device_handler = ncclient.manager.get_device_handler(options)
session = ncclient.manager.connect(
host=options["host"],
port=options["port"],
username=options["username"],
password=options["password"],
hostkey_verify=False,
device_params=options["device_params"],
)
yield session
session.close_channel()
def test_get_config(netconf_session):
"""测试get-config操作"""
reply = netconf_session.get_config(
source="running",
filter=("subtree", "<interfaces xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\"/>")
)
assert reply is not None
assert b"<interfaces>" in reply.xml
def test_edit_config(netconf_session):
"""测试edit-config操作"""
config = """
<config>
<interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
<interface>
<name>TestInterface</name>
<description>Test by pytest</description>
<enabled>true</enabled>
</interface>
</interfaces>
</config>
"""
reply = netconf_session.edit_config(target="running", config=config)
assert reply.errors is None or len(reply.errors) == 0
def test_rpc_operation(netconf_session):
"""测试RPC操作"""
reply = netconf_session.dispatch(
"""<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
<get>
<filter type="subtree">
<interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"/>
</filter>
</get>
</rpc>"""
)
assert reply is not None
8.4 配置验证脚本
# scripts/config_validator.py
import yaml
import json
import re
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class ValidationResult:
"""验证结果"""
device: str
check_name: str
passed: bool
message: str
expected: str = ""
actual: str = ""
class ConfigValidator:
"""配置验证器"""
def __init__(self, baseline_file: str):
with open(baseline_file, 'r', encoding='utf-8') as f:
self.baseline = yaml.safe_load(f)
def validate_hostname(self, device_config: dict, hostname: str) -> ValidationResult:
"""验证设备主机名"""
expected = self.baseline.get("hostname", hostname)
actual = device_config.get("hostname", "")
passed = expected == actual
return ValidationResult(
device=hostname,
check_name="hostname",
passed=passed,
message=f"主机名{'匹配' if passed else '不匹配'}",
expected=expected,
actual=actual
)
def validate_vlan_list(self, device_config: dict, hostname: str) -> ValidationResult:
"""验证VLAN列表"""
expected_vlans = set(self.baseline.get("vlans", []))
actual_vlans = set(device_config.get("vlans", []))
missing = expected_vlans - actual_vlans
extra = actual_vlans - expected_vlans
passed = not missing and not extra
message = "VLAN列表匹配"
if missing:
message += f", 缺少: {missing}"
if extra:
message += f", 多余: {extra}"
return ValidationResult(
device=hostname,
check_name="vlan_list",
passed=passed,
message=message,
expected=str(sorted(expected_vlans)),
actual=str(sorted(actual_vlans))
)
def validate_interface_description(self, device_config: dict,
hostname: str) -> ValidationResult:
"""验证接口描述规范性"""
pattern = r'^[A-Za-z0-9_\-]+(To-|From-)[A-Za-z0-9_\-]+$'
interfaces = device_config.get("interfaces", {})
failed_ifaces = []
for iface_name, iface_data in interfaces.items():
desc = iface_data.get("description", "")
if desc and not re.match(pattern, desc):
failed_ifaces.append(f"{iface_name}: {desc}")
passed = not failed_ifaces
return ValidationResult(
device=hostname,
check_name="interface_description",
passed=passed,
message=f"接口描述{'全部' if passed else '部分'}规范",
actual="; ".join(failed_ifaces) if failed_ifaces else ""
)
def run_all_validations(self, device_configs: dict) -> List[ValidationResult]:
"""运行所有验证"""
results = []
for hostname, config in device_configs.items():
results.append(self.validate_hostname(config, hostname))
results.append(self.validate_vlan_list(config, hostname))
results.append(self.validate_interface_description(config, hostname))
return results
def generate_report(self, results: List[ValidationResult]) -> str:
"""生成验证报告"""
total = len(results)
passed = sum(1 for r in results if r.passed)
failed = total - passed
report = f"""
=== 配置验证报告 ===
总检查项: {total}
通过: {passed}
失败: {failed}
通过率: {passed/total*100:.1f}%
"""
for r in results:
status = "✓" if r.passed else "✗"
report += f"{status} [{r.device}] {r.check_name}: {r.message}\n"
if not r.passed and r.actual:
report += f" 期望: {r.expected}\n"
report += f" 实际: {r.actual}\n"
return report
8.5 pytest 测试组织
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
unit: 单元测试标记
integration: 集成测试标记
slow: 耗时测试标记
gnmi: gNMI相关测试
netconf: NETCONF相关测试
addopts = -v --tb=short
# tests/conftest.py
import pytest
import yaml
@pytest.fixture
def test_devices():
"""测试设备清单fixture"""
with open("tests/fixtures/test_devices.yaml", 'r') as f:
return yaml.safe_load(f)
@pytest.fixture
def baseline_config():
"""基线配置fixture"""
with open("tests/fixtures/baseline.yaml", 'r') as f:
return yaml.safe_load(f)
@pytest.fixture
def sample_yang_models():
"""示例YANG模型fixture"""
return [
"models/test-iface.yang",
"models/test-system.yang",
]
九、总结
8.1 故障现象
使用NETCONF从控制器获取华为CE6865的接口信息时,设备返回bad-element错误:
<rpc-error>
<error-type>protocol</error-type>
<error-tag>bad-element</error-tag>
<error-path>/interfaces/interface[name='GE0/0/1']/hw-if:if-extender</error-path>
<error-message>Unknown element</error-message>
</rpc-error>
8.2 排查过程
步骤1:检查控制器使用的YANG模型版本
控制器使用 ietf-interfaces@2014-05-08(旧版)
步骤2:检查设备支持的YANG模型
<!-- 通过 get-schema 获取设备支持的模型 -->
<rpc message-id="101">
<get-schema xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-library">
<yang-module>ietf-interfaces</yang-module>
</get-schema>
</rpc>
<!-- 设备返回: ietf-interfaces@2018-02-20(新版) -->
步骤3:对比新旧版本的差异
| 特性 | 2014-05-08 | 2018-02-20 |
|---|---|---|
admin-status |
存在 | 移除(合并到enabled) |
oper-status |
存在 | 存在 |
phys-address |
if:phys-address |
移至子模块 |
| 统计计数器 | counter32 |
新增counter64 |
8.3 根因与解决
控制器的NETCONF请求中包含了旧版模型特有的if-extender扩展节点,而设备的新版YANG模型已移除此节点。
解决方案:
1. 更新控制器的YANG模型库至2018-02-20版本
2. 在控制器中移除对if-extender节点的引用
3. 建立YANG模型版本管理机制,确保控制器与设备模型版本对齐
九、总结
| 知识点 | 要点 |
|---|---|
| YANG定位 | IETF标准数据建模语言,NETCONF/RESTCONF的数据层 |
| 核心节点 | container(分组)、leaf(单值)、list(列表)、leaf-list(多值) |
| 数据类型 | string、int、boolean、enumeration、identityref、union等 |
| 约束 | range、length、pattern、min/max-elements、mandatory、default |
| config语义 | config true=配置数据(可读写),config false=状态数据(只读) |
| RPC | 定义设备操作(保存、重启等),有input/output |
| Notification | 设备主动推送事件(接口变化、BGP状态变化) |
| 工具链 | pyang验证、yangcatalog查模型、Open YANG Models社区库 |
思考题
- YANG模型中
list和leaf-list的本质区别是什么?如果用一个列表存储接口名,另一个存储VLAN ID列表,分别该用哪种? config true和config false的划分对NETCONF的操作(get-config vs get)有什么影响?为什么get-config拿不到状态数据?- 设计一个YANG模型来管理VLAN:需包含VLAN ID(1-4094)、名称(最长32字符)、描述、成员接口列表,且VLAN ID作为列表key。
下篇预告
第311篇:NETCONF与RESTCONF协议。学习如何通过NETCONF(XML+SSH)和RESTCONF(JSON+HTTPS)读写YANG模型数据,实现真正的模型驱动网络管理。