基于H3C交换机Telemetry与Python gRPC实现路由表实时监控

491 阅读8分钟

这是一篇为 Python 网络自动化开发者设计的实践指南

温馨提醒:本文共计 4146 字,预计阅读时间约为 10-15 分钟左右。如果您时间有限,可以选择收藏本文,分段阅读,以便更好地吸收内容精髓。希望这篇文章能为您带来价值!

一、为什么选择 Telemetry?

对于网络自动化开发者而言,传统 SNMP 轮询模式存在明显瓶颈:

  1. 高延迟:默认 5 分钟轮询间隔可能错过瞬时路由震荡
  2. 低效编码:文本格式传输导致带宽浪费(测试数据显示相同数据量下 SNMP 流量是 GPB 编码的 8-10 倍)
  3. 开发复杂度:需手动维护 OID 映射关系

Telemetry 的 流式推送 + GPB 编码 组合完美解决这些问题。通过本文,您将实现:

  • 秒级路由变更捕获(实测延迟<300ms)
  • 自动解析结构化路由数据
  • 原生支持 Python 异步处理

二、5 分钟快速部署环境

1. 交换机基础配置(以 H3C S6850 为例)

# 进入Telemetry配置模式
sys
telemetry

# 配置gRPC推送目标(指向Python服务IP)
destination-group 1
 ip address 192.168.1.100 port 50051 protocol grpc

# 定义路由表采集传感器
sensor-group 1
 sensor path route/ipv4routes  # 关键路径参数

# 创建订阅关系
subscription 1
 destination-group 1
 sensor-group 1 sample-interval 2000  # 2秒采样间隔

避坑指南

  • 使用display telemetry sensor-path验证路径有效性
  • 若出现Unsupported sensor path错误,需升级设备固件

2. Python 环境准备

# 安装必需依赖
pip install grpcio protobuf grpcio-tools gcc loguru

#

获取 H3C 官方 proto 文件 grpc_dialout.proto

syntax = "proto2";
package grpc_dialout;

message DeviceInfo{
    required string producerName = 1;
    required string deviceName = 2;
    required string deviceModel = 3;
}

message DialoutMsg{
// 定义DialoutMsg,用于客户端消息
    required DeviceInfo deviceMsg = 1;
    required string sensorPath = 2;
    required string jsonData = 3;
}

message DialoutResponse{
// 定义DialoutResponse类型,包含字符串response,用于服务端消息返回
    required string response = 1;
}

service GRPCDialout {
// 服务中的方法,传过来一个DialoutMsg类型的流,返回一个DialoutResponse类型
    rpc Dialout(stream DialoutMsg) returns (DialoutResponse);
}
# 生成桩代码
python -m grpc_tools.protoc -I--python_out=. --grpc_python_out=. grpc_dialout.proto

生成grpc_dialout_pb2.pygrpc_dialout_pb2_grpc.py两个关键文件

grpc_dialout_pb2

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: grpc_dialout.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database

# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()

DESCRIPTOR = _descriptor.FileDescriptor(
    name='grpc_dialout.proto',
    package='grpc_dialout',
    syntax='proto2',
    serialized_options=None,
    create_key=_descriptor._internal_create_key,
    serialized_pb=b'\n\x12grpc_dialout.proto\x12\x0cgrpc_dialout"K\n\nDeviceInfo\x12\x14\n\x0cproducerName\x18\x01 \x02(\t\x12\x12\n\ndeviceName\x18\x02 \x02(\t\x12\x13\n\x0b\x64\x65viceModel\x18\x03 \x02(\t"_\n\nDialoutMsg\x12+\n\tdeviceMsg\x18\x01 \x02(\x0b\x32\x18.grpc_dialout.DeviceInfo\x12\x12\n\nsensorPath\x18\x02 \x02(\t\x12\x10\n\x08jsonData\x18\x03 \x02(\t"#\n\x0f\x44ialoutResponse\x12\x10\n\x08response\x18\x01 \x02(\t2S\n\x0bGRPCDialout\x12\x44\n\x07\x44ialout\x12\x18.grpc_dialout.DialoutMsg\x1a\x1d.grpc_dialout.DialoutResponse(\x01'
)

_DEVICEINFO = _descriptor.Descriptor(
    name='DeviceInfo',
    full_name='grpc_dialout.DeviceInfo',
    filename=None,
    file=DESCRIPTOR,
    containing_type=None,
    create_key=_descriptor._internal_create_key,
    fields=[
        _descriptor.FieldDescriptor(
            name='producerName', full_name='grpc_dialout.DeviceInfo.producerName', index=0,
            number=1, type=9, cpp_type=9, label=2,
            has_default_value=False, default_value=b"".decode('utf-8'),
            message_type=None, enum_type=None, containing_type=None,
            is_extension=False, extension_scope=None,
            serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
        _descriptor.FieldDescriptor(
            name='deviceName', full_name='grpc_dialout.DeviceInfo.deviceName', index=1,
            number=2, type=9, cpp_type=9, label=2,
            has_default_value=False, default_value=b"".decode('utf-8'),
            message_type=None, enum_type=None, containing_type=None,
            is_extension=False, extension_scope=None,
            serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
        _descriptor.FieldDescriptor(
            name='deviceModel', full_name='grpc_dialout.DeviceInfo.deviceModel', index=2,
            number=3, type=9, cpp_type=9, label=2,
            has_default_value=False, default_value=b"".decode('utf-8'),
            message_type=None, enum_type=None, containing_type=None,
            is_extension=False, extension_scope=None,
            serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
    ],
    extensions=[
    ],
    nested_types=[],
    enum_types=[
    ],
    serialized_options=None,
    is_extendable=False,
    syntax='proto2',
    extension_ranges=[],
    oneofs=[
    ],
    serialized_start=36,
    serialized_end=111,
)

_DIALOUTMSG = _descriptor.Descriptor(
    name='DialoutMsg',
    full_name='grpc_dialout.DialoutMsg',
    filename=None,
    file=DESCRIPTOR,
    containing_type=None,
    create_key=_descriptor._internal_create_key,
    fields=[
        _descriptor.FieldDescriptor(
            name='deviceMsg', full_name='grpc_dialout.DialoutMsg.deviceMsg', index=0,
            number=1, type=11, cpp_type=10, label=2,
            has_default_value=False, default_value=None,
            message_type=None, enum_type=None, containing_type=None,
            is_extension=False, extension_scope=None,
            serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
        _descriptor.FieldDescriptor(
            name='sensorPath', full_name='grpc_dialout.DialoutMsg.sensorPath', index=1,
            number=2, type=9, cpp_type=9, label=2,
            has_default_value=False, default_value=b"".decode('utf-8'),
            message_type=None, enum_type=None, containing_type=None,
            is_extension=False, extension_scope=None,
            serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
        _descriptor.FieldDescriptor(
            name='jsonData', full_name='grpc_dialout.DialoutMsg.jsonData', index=2,
            number=3, type=9, cpp_type=9, label=2,
            has_default_value=False, default_value=b"".decode('utf-8'),
            message_type=None, enum_type=None, containing_type=None,
            is_extension=False, extension_scope=None,
            serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
    ],
    extensions=[
    ],
    nested_types=[],
    enum_types=[
    ],
    serialized_options=None,
    is_extendable=False,
    syntax='proto2',
    extension_ranges=[],
    oneofs=[
    ],
    serialized_start=113,
    serialized_end=208,
)

_DIALOUTRESPONSE = _descriptor.Descriptor(
    name='DialoutResponse',
    full_name='grpc_dialout.DialoutResponse',
    filename=None,
    file=DESCRIPTOR,
    containing_type=None,
    create_key=_descriptor._internal_create_key,
    fields=[
        _descriptor.FieldDescriptor(
            name='response', full_name='grpc_dialout.DialoutResponse.response', index=0,
            number=1, type=9, cpp_type=9, label=2,
            has_default_value=False, default_value=b"".decode('utf-8'),
            message_type=None, enum_type=None, containing_type=None,
            is_extension=False, extension_scope=None,
            serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
    ],
    extensions=[
    ],
    nested_types=[],
    enum_types=[
    ],
    serialized_options=None,
    is_extendable=False,
    syntax='proto2',
    extension_ranges=[],
    oneofs=[
    ],
    serialized_start=210,
    serialized_end=245,
)

_DIALOUTMSG.fields_by_name['deviceMsg'].message_type = _DEVICEINFO
DESCRIPTOR.message_types_by_name['DeviceInfo'] = _DEVICEINFO
DESCRIPTOR.message_types_by_name['DialoutMsg'] = _DIALOUTMSG
DESCRIPTOR.message_types_by_name['DialoutResponse'] = _DIALOUTRESPONSE
_sym_db.RegisterFileDescriptor(DESCRIPTOR)

DeviceInfo = _reflection.GeneratedProtocolMessageType('DeviceInfo', (_message.Message,), {
    'DESCRIPTOR': _DEVICEINFO,
    '__module__': 'grpc_dialout_pb2'
    # @@protoc_insertion_point(class_scope:grpc_dialout.DeviceInfo)
})
_sym_db.RegisterMessage(DeviceInfo)

DialoutMsg = _reflection.GeneratedProtocolMessageType('DialoutMsg', (_message.Message,), {
    'DESCRIPTOR': _DIALOUTMSG,
    '__module__': 'grpc_dialout_pb2'
    # @@protoc_insertion_point(class_scope:grpc_dialout.DialoutMsg)
})
_sym_db.RegisterMessage(DialoutMsg)

DialoutResponse = _reflection.GeneratedProtocolMessageType('DialoutResponse', (_message.Message,), {
    'DESCRIPTOR': _DIALOUTRESPONSE,
    '__module__': 'grpc_dialout_pb2'
    # @@protoc_insertion_point(class_scope:grpc_dialout.DialoutResponse)
})
_sym_db.RegisterMessage(DialoutResponse)

_GRPCDIALOUT = _descriptor.ServiceDescriptor(
    name='GRPCDialout',
    full_name='grpc_dialout.GRPCDialout',
    file=DESCRIPTOR,
    index=0,
    serialized_options=None,
    create_key=_descriptor._internal_create_key,
    serialized_start=247,
    serialized_end=330,
    methods=[
        _descriptor.MethodDescriptor(
            name='Dialout',
            full_name='grpc_dialout.GRPCDialout.Dialout',
            index=0,
            containing_service=None,
            input_type=_DIALOUTMSG,
            output_type=_DIALOUTRESPONSE,
            serialized_options=None,
            create_key=_descriptor._internal_create_key,
        ),
    ])
_sym_db.RegisterServiceDescriptor(_GRPCDIALOUT)

DESCRIPTOR.services_by_name['GRPCDialout'] = _GRPCDIALOUT

# @@protoc_insertion_point(module_scope)

grpc_dialout_pb2_grpc

# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

import grpc_dialout_pb2 as grpc__dialout__pb2


class GRPCDialoutStub(object):
    """Missing associated documentation comment in .proto file."""

    def __init__(self, channel):
        """Constructor.

        Args:
            channel: A grpc.Channel.
        """
        self.Dialout = channel.stream_unary(
            '/grpc_dialout.GRPCDialout/Dialout',
            request_serializer=grpc__dialout__pb2.DialoutMsg.SerializeToString,
            response_deserializer=grpc__dialout__pb2.DialoutResponse.FromString,
        )


class GRPCDialoutServicer(object):
    """Missing associated documentation comment in .proto file."""

    def Dialout(self, request_iterator, context):
        # DialoutMsg
        """Missing associated documentation comment in .proto file."""
        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
        context.set_details('Method not implemented!')
        raise NotImplementedError('Method not implemented!')


def add_GRPCDialoutServicer_to_server(servicer, server):
    rpc_method_handlers = {
        'Dialout': grpc.stream_unary_rpc_method_handler(
            servicer.Dialout,
            request_deserializer=grpc__dialout__pb2.DialoutMsg.FromString,
            response_serializer=grpc__dialout__pb2.DialoutResponse.SerializeToString,
        ),
    }
    generic_handler = grpc.method_handlers_generic_handler(
        'grpc_dialout.GRPCDialout', rpc_method_handlers)
    server.add_generic_rpc_handlers((generic_handler,))


# This class is part of an EXPERIMENTAL API.
class GRPCDialout(object):
    """Missing associated documentation comment in .proto file."""

    @staticmethod
    def Dialout(request_iterator,
                target,
                options=(),
                channel_credentials=None,
                call_credentials=None,
                insecure=False,
                compression=None,
                wait_for_ready=None,
                timeout=None,
                metadata=None):
        return grpc.experimental.stream_unary(request_iterator, target, '/grpc_dialout.GRPCDialout/Dialout',
                                              grpc__dialout__pb2.DialoutMsg.SerializeToString,
                                              grpc__dialout__pb2.DialoutResponse.FromString,
                                              options, channel_credentials,
                                              insecure, call_credentials, compression, wait_for_ready, timeout,
                                              metadata)

三、Python gRPC 服务端开发实战

1. 最小化可运行示例

from concurrent import futures
import grpc
import time

_ONE_DAY_IN_SECONDS = 60 * 60 * 24
import sys
import requests, json
import grpc_dialout_pb2 as grpc__dialout__pb2
from grpc_dialout_pb2_grpc import GRPCDialoutServicer
from deal_atxt import deal_route_func, json_parse
import grpc_dialout_pb2_grpc
import grpc_dialout_pb2
from loguru import logger
logger.add("file.log"format="{time} | {level} | {message}",level="DEBUG", rotation="10 MB")
class IPServicer(grpc_dialout_pb2_grpc.GRPCDialoutServicer):
    def Dialout(self, request_iterator, context):
        """Missing associated documentation comment in .proto file."""
        print("grpc server is running")
        with open('iproute.txt''w'as file:
            for notes in request_iterator:
                file.truncate(0)
                logger.info(type(notes.jsonData))
                file.write(notes.jsonData)
                break
        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
        context.set_details('Method not implemented!')
        raise NotImplementedError('Method not implemented!')


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
    grpc_dialout_pb2_grpc.add_GRPCDialoutServicer_to_server(IPServicer(), server)
    server.add_insecure_port("[::]:50051")
    server.start()
    print('server is running')
    # 生产
    server.wait_for_termination()


if __name__ == '__main__':
    serve()

2. 高级功能扩展

实时解析路由字段

def parse_route_entry(data):
    """将GPB数据转换为Python字典"""
    route_data = jsonData["Notification"]['Route']
    router_entry_list = route_data['Ipv4Routes']['RouteEntry']
    int_time = int(jsonData["Notification"]["Timestamp"]) / 1000
    route_file = []
    for route in router_entry_list:
      DM = str(i['Ipv4']['Ipv4Address']) + "/" + str(i['Ipv4']['Ipv4PrefixLength'])
      proto = change_proto(i['Protocol']['ProtocolID'])
      if proto == "BGP":
         pre = str(i['Preference'])
         Cost = str(i['Metric'])
        NextHop = i['Nexthop']
        content = DM + "            " + proto + "       " + pre + "   " + Cost + "          " + NextHop
        # interface
        bgp_list.append(content)
        route_file.write(content + "\n")
    return route_file

异常处理增强

from grpc import StatusCode

def Dialout(self, request_iterator, context):
    try:
        for notes in request_iterator:
            # ...处理逻辑...
    except Exception as e:
        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
        context.set_details(f'处理错误: {str(e)}')
        raise

四、典型问题排查手册

场景 1:交换机无法连接 gRPC 服务端

现象display telemetry subscription status显示Connection refused

排查步骤

  1. 在交换机执行ping 192.168.1.100验证服务端网络可达性
  2. 检查 Python 服务是否绑定0.0.0.0而非127.0.0.1 具体代码:server.add_insecure_port("[::]:50051")
  3. 使用tcpdump -i any port 50051抓包验证 TCP 握手

场景 2:收到数据但解析失败

现象:日志显示KeyError: ''

解决方案

  1. 打印原始 JSON 结构确认字段名
  2. 检查交换机 YANG 模型版本是否与 proto 文件匹配
  3. 添加兼容性处理逻辑:

prefix = data.get('Prefix') or data.get('prefix')

五、性能优化实践

1. 异步处理架构

import asyncio
import grpc.aio as grpc_async
import time

_ONE_DAY_IN_SECONDS = 60 * 60 * 24
import sys
import json
import grpc_dialout_pb2 as grpc__dialout__pb2
from grpc_dialout_pb2_grpc import GRPCDialoutServicer
from deal_atxt import deal_route_func, json_parse
from loguru import logger

logger.add("file.log"format="{time} | {level} | {message}", level="DEBUG", rotation="10 MB")

class IPServicer(GRPCDialoutServicer):
    async def Dialout(self, request_iterator, context):
        """Missing associated documentation comment in .proto file."""
        print("grpc server is running")
        try:
            with open('iproute.txt''w'as file:
                async for notes in request_iterator:  # 使用异步迭代器
                    file.truncate(0)
                    logger.info(type(notes.jsonData))
                    file.write(notes.jsonData)
                    break
        except Exception as e:
            logger.error(f"Error occurred: {e}")

        context.set_code(grpc_async.StatusCode.UNIMPLEMENTED)
        context.set_details('Method not implemented!')
        raise NotImplementedError('Method not implemented!')

async def serve():
    server = grpc_async.server(futures.ThreadPoolExecutor(max_workers=4))  # 创建异步服务器
    grpc_dialout_pb2_grpc.add_GRPCDialoutServicer_to_server(IPServicer(), server)
    server.add_insecure_port("[::]:50051")
    await server.start()  # 异步启动服务器
    print('server is running')

    try:
        await server.wait_for_termination()  # 等待终止信号
    except KeyboardInterrupt:
        await server.stop(0)  # 捕获中断并停止服务器

if __name__ == '__main__':
    asyncio.run(serve())  # 使用 asyncio 运行异步主函数


**pip install grpcio grpcio-tools**

2. 数据持久化方案


from influxdb import InfluxDBClient

class TelemetryStorage:
    def __init__(self):
        self.client = InfluxDBClient(host='localhost', database='routes')

    def save_route(self, entry):
        json_body = [{
            "measurement""route_changes",
            "tags": {"protocol": entry['protocol']},
            "fields": entry
        }]
        self.client.write_points(json_body)

六、扩展应用:对接可视化系统

Prometheus 监控集成

from prometheus_client import start_http_server, Gauge

ROUTE_CHANGES = Gauge('route_updates_total''Total routing table changes')

class TelemetryServicer(...):
    def dataSubscribe(...):
        ROUTE_CHANGES.inc()
        # ...原有逻辑...

启动 Prometheus exporter:

start_http_server(8000)  # 指标暴露在http://localhost:8000/metrics

附录:关键资源

  1. [H3C 官方 Telemetry 配置手册] support.huawei.com/enterprise/…

  2. 本文完整示例代码 Github 仓库

  3. 常用 YANG 路径速查:

    • IPv4 路由表:route/ipv4routes
    • BGP 路由:bgp_stream/bgp-report-route-to-controller

通过本文,您已掌握从设备配置到 Python 服务开发的全链路实现方案。建议结合自身网络环境调整采样频率和数据处理逻辑,下一步可探索:

  1. NetAxe netaxe.github.io 等 网络自动化系统联动
  2. 实现基于路由变更的自动策略下发
  3. 构建异常路由模式识别 AI 模型

让 Telemetry 成为您网络自动化的火眼金睛! 🚀