这是一篇为 Python 网络自动化开发者设计的实践指南
温馨提醒:本文共计 4146 字,预计阅读时间约为 10-15 分钟左右。如果您时间有限,可以选择收藏本文,分段阅读,以便更好地吸收内容精髓。希望这篇文章能为您带来价值!
一、为什么选择 Telemetry?
对于网络自动化开发者而言,传统 SNMP 轮询模式存在明显瓶颈:
- 高延迟:默认 5 分钟轮询间隔可能错过瞬时路由震荡
- 低效编码:文本格式传输导致带宽浪费(测试数据显示相同数据量下 SNMP 流量是 GPB 编码的 8-10 倍)
- 开发复杂度:需手动维护 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.py和grpc_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
排查步骤:
- 在交换机执行
ping 192.168.1.100验证服务端网络可达性 - 检查 Python 服务是否绑定
0.0.0.0而非127.0.0.1具体代码:server.add_insecure_port("[::]:50051") - 使用
tcpdump -i any port 50051抓包验证 TCP 握手
场景 2:收到数据但解析失败
现象:日志显示KeyError: ''
解决方案:
- 打印原始 JSON 结构确认字段名
- 检查交换机 YANG 模型版本是否与 proto 文件匹配
- 添加兼容性处理逻辑:
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
附录:关键资源
-
[H3C 官方 Telemetry 配置手册] support.huawei.com/enterprise/…
-
常用 YANG 路径速查:
- IPv4 路由表:
route/ipv4routes - BGP 路由:
bgp_stream/bgp-report-route-to-controller
- IPv4 路由表:
通过本文,您已掌握从设备配置到 Python 服务开发的全链路实现方案。建议结合自身网络环境调整采样频率和数据处理逻辑,下一步可探索:
- 与 NetAxe netaxe.github.io 等 网络自动化系统联动
- 实现基于路由变更的自动策略下发
- 构建异常路由模式识别 AI 模型
让 Telemetry 成为您网络自动化的火眼金睛! 🚀