第312篇:gNMI/gNOI 协议
关键词
gNMI、gNOI、gRPC、Protobuf、流式传输、网络管理接口、订阅、Get/Set/Capabilities、开放配置
一、gNMI/gNOI 概述
1.1 什么是 gNMI/gNOI
gNMI(gRPC Network Management Interface)和 gNOI(gRPC Network Operations Interface)是 Google 推出的基于 gRPC 的网络管理协议:
协议家族定位:
| gRPC 协议层 ┌────────────────────────────────────┐ └────────────────────────────────────┘ ┌────────────────────────────────────┐ └────────────────────────────────────┘ ┌────────────────────────────────────┐ | gNMI(网络管理接口) ├─ Capabilities:能力交换 ├─ Get:读取配置/状态 ├─ Set:修改配置 └─ Subscribe:数据订阅 gNOI(网络操作接口) ├─ System:系统操作(重启/升级) ├─ File:文件传输 ├─ Layer2/3:网络诊断 └─ Cert:证书管理 gRPC 传输层(HTTP/2) | |
|---|---|---|
gNMI vs NETCONF: ┌─ 编码:Protobuf(二进制) vs XML(文本) ├─ 传输:gRPC/HTTP2 vs SSH ├─ 流式:原生支持 vs 轮询 ├─ 性能:高(Protobuf 序列化快) vs 中 └─ 厂商支持:新兴 vs 成熟
1.2 核心优势
gNMI/gNOI 的四大优势:
1. 流式订阅(Streaming Telemetry)
┌─ 设备主动推送数据(Push 模式)
├─ 毫秒级采样间隔
├─ 减少轮询开销
└─ 实时监控网络状态
2. Protobuf 编码
┌─ 二进制编码,带宽效率高
├─ 序列化/反序列化极快
├─ 强类型 Schema
└─ 前后兼容(字段编号)
3. gRPC/HTTP2
┌─ 多路复用(一个连接多请求)
├─ 双向流(同时读写)
├─ 头部压缩
└─ 基于 HTTP2 的认证/加密
4. 统一接口
┌─ gNMI:配置管理 + 数据采集
├─ gNOI:运维操作
└─ 一套 gRPC 服务完成所有管理
二、gNMI 协议详解
2.1 gNMI RPC 接口
// gNMI Proto 定义(简化版)
// 源码:github.com/openconfig/gnmi/proto/gnmi/gnmi.proto
syntax = "proto3";
package gnmi;
// ===== gNMI 服务定义 =====
service gNMI {
// 能力交换:返回设备支持的模型
rpc Capabilities(CapabilityRequest)
returns (CapabilityResponse);
// 读取数据:获取配置或状态
rpc Get(GetRequest)
returns (GetResponse);
// 修改数据:创建/更新/删除
rpc Set(SetRequest)
returns (SetResponse);
// 订阅数据:流式推送
rpc Subscribe(stream SubscribeRequest)
returns (stream SubscribeResponse);
}
// ===== 路径定义 =====
message Path {
// 路径元素列表,例如:
// interfaces/interface[name=GE0/0/1]/state/counters
repeated string elem = 1;
// 带键的元素
message Elem {
string name = 1;
map<string, string> key = 2;
}
}
// ===== Get 请求 =====
message GetRequest {
enum DataType {
ALL = 0; // 配置 + 状态
CONFIG = 1; // 配置
STATE = 2; // 状态
OPERATIONAL = 3; // 操作数据
}
DataType type = 1;
repeated Path paths = 2;
}
// ===== Set 请求 =====
message SetRequest {
repeated Path delete = 1; // 删除路径
repeated Update replace = 2; // 替换
repeated Update update = 3; // 更新/合并
}
message Update {
Path path = 1;
TypedValue value = 2;
}
// ===== Subscribe 请求 =====
message SubscribeRequest {
SubscribeList subscribe = 1;
}
message SubscribeList {
enum Mode {
STREAM = 0; // 流式(持续推送)
ONCE = 1; // 一次采样
POLL = 2; // 轮询
}
Mode mode = 1;
enum Encoding {
PROTO = 0; // Protobuf
JSON = 1; // JSON
BYTES = 2; // Bytes
}
Encoding encoding = 3;
repeated Subscription subscription = 4;
}
message Subscription {
Path path = 1;
enum Mode {
TARGET_DEFINED = 0; // 设备默认
ON_CHANGE = 1; // 变化时推送
SAMPLE = 2; // 定时采样
}
Mode mode = 2;
uint64 sample_interval = 3; // 采样间隔(纳秒)
}
2.2 路径模型
gNMI 路径模型(类似文件系统):
YANG 模型 → gNMI 路径
YANG:
container interfaces {
list interface {
key "name";
leaf name { type string; }
leaf description { type string; }
container state {
leaf oper-status { type enumeration; }
leaf admin-status { type enumeration; }
}
}
}
gNMI 路径:
/interfaces
/interfaces/interface[name=GE0/0/1]
/interfaces/interface[name=GE0/0/1]/name
/interfaces/interface[name=GE0/0/1]/description
/interfaces/interface[name=GE0/0/1]/state
/interfaces/interface[name=GE0/0/1]/state/oper-status
路径对比:
┌─ NETCONF: <interfaces><interface><name>GE0/0/1</name>
├─ RESTCONF: /restconf/data/ietf-interfaces:interfaces/interface=GE0/0/1
└─ gNMI: /interfaces/interface[name=GE0/0/1]
三、Python gNMI 实战
3.1 环境准备
# 安装 gNMI Python 客户端
python -m pip install pygnmi
# 或者安装 gRPC 工具
python -m pip install grpcio grpcio-tools
# 下载 gNMI proto 文件
git clone https://github.com/openconfig/gnmi.git
3.2 连接与能力交换
#!/usr/bin/env python3
# gnmi_capabilities.py — gNMI 能力交换
from pygnmi.client import gNMIclient
import json
# gNMI 连接参数
GNMI_PARAMS = {
"target": ("192.168.1.1", 9339), # gNMI 默认端口 9339
"username": "admin",
"password": "admin123",
"insecure": True, # 生产环境使用 TLS 证书
"timeout": 30,
}
try:
with gNMIclient(**GNMI_PARAMS) as client:
print("✓ gNMI 连接成功")
# Capabilities:获取设备支持的模型
caps = client.capabilities()
print("\n=== gNMI Capabilities ===")
print(f"gNMI 版本: {caps.gNMI_version}")
print(f"\n支持的 YANG 模型 ({len(caps.supported_models)}):")
for model in caps.supported_models[:10]: # 只显示前10个
print(f" {model.name} v{model.organization}")
print(f" 版本: {model.version}")
print(f"\n支持的编码: {[str(e) for e in caps.supported_encodings]}")
except Exception as e:
print(f"✗ 错误: {e}")
3.3 Get 操作
#!/usr/bin/env python3
# gnmi_get.py — gNMI Get 操作
from pygnmi.client import gNMIclient
GNMI_PARAMS = {
"target": ("192.168.1.1", 9339),
"username": "admin",
"password": "admin123",
"insecure": True,
}
try:
with gNMIclient(**GNMI_PARAMS) as client:
# ===== Get: 读取配置 =====
print("=== Get: 接口配置 ===")
result = client.get(
path=["/interfaces"],
datatype="config", # CONFIG / STATE / ALL
encoding="json", # JSON / PROTO
)
if result and result.notification:
for notification in result.notification:
for update in notification.update:
print(f" 路径: {update.path}")
print(f" 值: {update.val.json_value[:200]}...")
# ===== Get: 读取状态 =====
print("\n=== Get: 接口状态 ===")
result = client.get(
path=["/interfaces/interface[name=GE0/0/1]/state"],
datatype="state",
)
if result and result.notification:
for notification in result.notification:
for update in notification.update:
print(f" 路径: {update.path}")
# ===== Get: 读取特定字段 =====
print("\n=== Get: 系统信息 ===")
result = client.get(
path=["/system/state"],
datatype="state",
)
if result and result.notification:
for notification in result.notification:
for update in notification.update:
print(f" {update.path}: {update.val}")
except Exception as e:
print(f"✗ 错误: {e}")
3.4 Set 操作
#!/usr/bin/env python3
# gnmi_set.py — gNMI Set 操作
from pygnmi.client import gNMIclient
import json
GNMI_PARAMS = {
"target": ("192.168.1.1", 9339),
"username": "admin",
"password": "admin123",
"insecure": True,
}
try:
with gNMIclient(**GNMI_PARAMS) as client:
# ===== Set: Update =====
# 更新接口描述
print("=== Set Update: 更新接口描述 ===")
updates = [
{
"path": "/interfaces/interface[name=GE0/0/1]/config/description",
"value": "Uplink to Core - Updated via gNMI",
}
]
result = client.set(update=updates)
print(f" 结果: {result.response}")
# ===== Set: Replace =====
# 替换整个接口配置
print("\n=== Set Replace: 创建 Loopback ===")
replace = [
{
"path": "/interfaces/interface[name=LoopBack100]",
"value": {
"name": "LoopBack100",
"config": {
"name": "LoopBack100",
"type": "iana-if-type:softwareLoopback",
"description": "Created via gNMI Set",
"enabled": True,
},
"subinterfaces": {
"subinterface": [
{
"index": 0,
"ipv4": {
"addresses": {
"address": [
{
"ip": "100.100.100.1",
"config": {
"ip": "100.100.100.1",
"prefix-length": 32,
},
}
]
}
},
}
]
},
},
}
]
result = client.set(replace=replace)
print(f" 结果: {result.response}")
# ===== Set: Delete =====
# 删除接口
print("\n=== Set Delete: 删除 LoopBack100 ===")
result = client.set(delete=["/interfaces/interface[name=LoopBack100]"])
print(f" 结果: {result.response}")
except Exception as e:
print(f"✗ 错误: {e}")
3.5 Subscribe 订阅
#!/usr/bin/env python3
# gnmi_subscribe.py — gNMI Subscribe 实时订阅
from pygnmi.client import gNMIclient
import time
GNMI_PARAMS = {
"target": ("192.168.1.1", 9339),
"username": "admin",
"password": "admin123",
"insecure": True,
}
def handle_subscribe_response(response):
"""处理订阅推送的数据"""
if response and response.notification:
for notification in response.notification:
timestamp = notification.timestamp
for update in notification.update:
path = update.path
val = update.val
# JSON 值
if hasattr(val, 'json_value'):
print(f"[{timestamp}] {path}: {val.json_value}")
else:
print(f"[{timestamp}] {path}: {val}")
try:
with gNMIclient(**GNMI_PARAMS) as client:
# ===== 订阅接口计数器(定时采样) =====
print("=== Subscribe: 接口计数器(5秒采样)===")
subscribe_paths = [
"/interfaces/interface[name=GE0/0/1]/state/counters",
]
# 启动订阅(持续 15 秒)
responses = client.subscribe(
subscribe=subscribe_paths,
mode="sample", # STREAM / ONCE / POLL
sample_interval=5_000_000_000, # 5秒(纳秒)
encoding="json",
)
print("正在接收订阅数据(15 秒后自动停止)...")
start_time = time.time()
for response in responses:
handle_subscribe_response(response)
if time.time() - start_time > 15:
print("订阅超时,自动停止")
break
except KeyboardInterrupt:
print("\n用户中断订阅")
except Exception as e:
print(f"✗ 错误: {e}")
四、gNOI 运维操作
4.1 gNOI 服务定义
// gNOI Proto 定义(简化版)
syntax = "proto3";
package gnoi;
// === System Service ===
service System {
// 设备重启
rpc Reboot(RebootRequest) returns (RebootResponse);
// 设备关闭
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
// 版本升级
rpc Upgrade(UpgradeRequest) returns (stream UpgradeResponse);
// Ping 诊断
rpc Ping(PingRequest) returns (stream PingResponse);
}
// === File Service ===
service File {
// 获取文件
rpc Get(GetRequest) returns (stream GetResponse);
// 上传文件
rpc Put(stream PutRequest) returns (PutResponse);
// 删除文件
rpc Remove(RemoveRequest) returns (RemoveResponse);
}
// === Certificate Service ===
service Certificate {
// 安装证书
rpc Install(stream InstallRequest)
returns (stream InstallResponse);
// 撤销证书
rpc Revoke(RevokeRequest) returns (RevokeResponse);
}
4.2 gNOI 操作示例
#!/usr/bin/env python3
# gnoi_operations.py — gNOI 运维操作
# 注意:gNOI Python 库需自行安装
# 或使用 gRPC 直接调用
import grpc
import time
# 模拟 gNOI 操作逻辑
def simulate_reboot(device, method="COLD"):
"""模拟设备重启"""
print(f"=== {device}: 发起 {method} 重启 ===")
print(f" 准备重启...")
time.sleep(0.5)
print(f" 正在关闭进程...")
time.sleep(0.5)
print(f" 系统重启中...")
time.sleep(1)
print(f"✓ {device}: 重启完成,启动时间: {time.strftime('%H:%M:%S')}")
return True
def simulate_ping(device, destination, count=4):
"""模拟 Ping 诊断"""
print(f"=== {device}: Ping {destination} ({count} 次) ===")
for i in range(count):
time.sleep(0.1)
print(f" ICMP seq={i+1}: time={10+i*2}ms TTL=64")
print(f"✓ Ping 完成,丢包率 0%")
return {
"sent": count,
"received": count,
"loss_percentage": 0,
"min_rtt": 10,
"avg_rtt": 13,
"max_rtt": 16,
}
def simulate_file_transfer(device, local_file, remote_path):
"""模拟文件传输"""
print(f"=== {device}: 传输 {local_file} → {remote_path} ===")
print(f" 连接建立...")
time.sleep(0.3)
print(f" 正在传输: 0%... 50%... 100%")
print(f"✓ 传输完成: 2.4 MB")
return {"bytes_transferred": 2400000, "duration_sec": 2.1}
# 使用示例
print("\n=== gNOI 运维操作模拟 ===\n")
simulate_reboot("192.168.1.1", "COLD")
print()
result = simulate_ping("192.168.1.1", "8.8.8.8", count=3)
print(f" 统计: {result['sent']} 发送, {result['received']} 接收, "
f"延迟 {result['avg_rtt']}ms")
print()
simulate_file_transfer("192.168.1.1", "image/NE20E-V800R022.cc", "/flash/")
五、gNMI 与 NETCONF/RESTCONF 对比
5.1 综合对比
| 特性 | gNMI | NETCONF | RESTCONF |
|---|---|---|---|
| 传输 编码 性能 流式订阅 事务 候选配置 回滚 运维操作 二进制 多路复用 厂商支持 | gRPC/HTTP2 Protobuf ★★★★★ 原生 部分 无 无 gNOI 是 是 新兴 | SSH XML ★★★ 需扩展 完善 有 有 RPC 否 否 成熟 | HTTP/HTTPS XML/JSON ★★★★ 无 无 无 无 无 否 是 较成熟 |
典型应用场景: ┌─ gNMI:Telemetry 数据采集 + 快速配置 ├─ NETCONF:核心网变更 + 事务性操作 └─ RESTCONF:Web 管理界面 + 简单查询
5.2 华为设备 gNMI 支持
华为设备 gNMI 配置:
1. 使能 gRPC 服务
system-view
grpc server enable
grpc server port 9339
2. 创建 gRPC 用户
aaa
local-user grpc_admin password cipher Admin@123
local-user grpc_admin service-type grpc
local-user grpc_admin privilege level 15
3. 配置 SSL 证书(可选,但推荐)
pki import certificate server certificate.cer
grpc server certificate server
4. 验证
display grpc server status
display grpc session
支持的功能:
┌─ gNMI Get/Set/Subscribe
├─ openconfig YANG 模型
├─ 华为扩展 YANG 模型
└─ JSON 编码(非 Protobuf)
六、gNMI 与 Telemetry 集成
6.1 gNMI 作为 Telemetry 采集协议
gNMI Subscribe 是 Telemetry 的推荐实现方式:
传统轮询(SNMP): ┌─ 管理站每 5 分钟轮询所有设备 ├─ 1000 设备 × 100 指标 = 10 万次/5 分钟 ├─ 带宽浪费(大部分指标未变化) └─ 时间精度差(5 分钟粒度)
gNMI Subscribe(流式推送): ┌─ 设备主动推送采样数据 ├─ 采样间隔:1 秒 - 10 分钟可配 ├─ 支持 On-Change:值变化时立即推送 └─ 带宽高效(只传有效数据)
Telemetry 采集架构: | 网络设备 ┌────────────┐ └────────────┘ 设备 1 设备 2 ──gNMI──→ 设备 N | 采集系统 gNMI Agent Subscribe Kafka ↓ InfluxDB ↓ Grafana | ───stream──→ | Telegraf ↓ | | --- | --- | --- | --- |
6.2 Telegraf + gNMI 采集配置
# telegraf.conf — gNMI 采集配置
[[inputs.gnmi]]
# 目标设备列表
addresses = [
"192.168.1.1:9339",
"192.168.1.2:9339",
]
# 认证
username = "admin"
password = "admin123"
# 不验证证书
insecure = true
# 订阅路径
[[inputs.gnmi.subscription]]
path = "/interfaces/interface/state/counters"
sample_interval = "10s"
[[inputs.gnmi.subscription]]
path = "/interfaces/interface/state/oper-status"
sample_interval = "30s"
[[inputs.gnmi.subscription]]
path = "/system/state"
sample_interval = "60s"
# 输出格式
# 定义字段重命名
[inputs.gnmi.tag]
interface = "name"
七、最佳实践
7.1 gNMI 使用规范
gNMI 使用最佳实践:
1. 路径设计
┌─ 使用标准 openconfig YANG 路径
├─ 精确路径减少数据传输量
├─ 批量路径合并到一次 Get
└─ 避免订阅整个模型树
2. Subscribe 配置
┌─ 采样间隔合理(不要 < 1 秒,除非必要)
├─ On-Change 适合状态变化(oper-status)
├─ SAMPLE 适合连续指标(计数器)
└─ 订阅路径数量控制(10-20 条/设备)
3. 连接管理
┌─ gRPC 连接复用(长连接)
├─ 设置合理的 keepalive
├─ 断线重连机制
└─ TLS 证书管理
4. 性能优化
┌─ Protobuf 编码优于 JSON
├─ 订阅按需,不要全量
├─ 采集系统水平扩展
└─ 数据缓冲处理背压
7.2 错误处理
gnmi 错误处理指南:
连接错误:
┌─ 检查端口(默认 9339)
├─ 确认 gRPC 服务已启用
├─ 防火墙放行
└─ 证书配置
认证错误:
┌─ 确认用户名密码
├─ 确认用户有 gRPC 权限
└─ 确认密码未过期
路径错误:
┌─ 确认 YANG 模型加载
├─ 使用 Capabilities 验证
└─ 路径大小写敏感
超时错误:
┌─ 减少单次请求的路径数量
├─ 增加 timeout
└─ 设备负载过高
八、总结
gNMI/gNOI 的核心价值:
gNMI — 新一代网络管理协议
┌─ 高效:Protobuf + gRPC → 带宽小、性能高
├─ 实时:流式订阅 → 毫秒级数据采集
├─ 统一:一套接口管配置 + 状态 + 订阅
└─ 开放:OpenConfig 标准模型
gNOI — 运维操作标准化
┌─ 重启/升级/Ping/Traceroute
├─ 文件传输
├─ 证书管理
└─ 统一运维 API
网络协议演进路径:
CLI / SNMP → NETCONF / RESTCONF → gNMI / gNOI
手工 结构化/API 流式/高性能
下篇预告:第313篇 — Telemetry 订阅配置,将深入网络 Telemetry 的订阅机制,介绍如何配置和管理设备的数据推送。