第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 的订阅机制,介绍如何配置和管理设备的数据推送。