写这篇之前,我刚好又在一个新机房里把 sonic-mgmt 的测试床完整重搭了一遍。说实话,SONiC 生态里最劝退新人的不是编译 image,而是“拿到镜像之后怎么测”。很多人折腾半天 build 出来一个 SONiC 镜像,结果进系统一看,除了手工敲show命令,根本不知道该怎么系统性地验证功能,更别说把用例沉淀下来做回归了。这时候 sonic-mgmt 就是你要找的东西:它是 SONiC 官方的自动化测试与设备管理框架,负责把一台(或一群)SONiC 设备拉进一个可重复的测试环境里,跑 pytest 用例、收日志、出结论。这篇是系列第二篇,上一篇我拆过仓库本身的目录,这篇重点把它部署起来时的部署架构讲透,再完整走一遍从零到一跑通测试的测试流程。适合两类人:一是负责搭实验室和测试环境的网络工程师,二是想给自家 SONiC 衍生版本引入自动化测试的 DevOps。
1. 先搞清楚 sonic-mgmt 在整个 SONiC 生态里到底扮演什么角色
1.1 它不是“一个测试工具”,而是一整套测试工程体系
很多人第一次打开 sonic-mgmt 仓库会有点懵,因为它里面既有 Ansible 的 inventory 和 playbook,又有大量 pytest 用例,还有一些看起来像运维脚本的 testbed 工具。这种“混搭”其实是有意为之:sonic-mgmt 想解决的从来不是“某个功能有没有 bug”,而是“如何在一个可重复的物理或虚拟环境里,持续有效地验证 SONiC 系统”。
从职责边界来看,SONiC 整个生态大体分三层:buildimage 负责把源码编译成可安装的镜像,SONiC 本身是运行在交换机上的操作系统,而 sonic-mgmt 就是站在系统外面做黑盒验证的那层。它不关心某个进程内部是怎么实现的,只关心“配置下去之后,转发、协议、接口、ACL 这些行为是否符合预期”。所以你要理解它的架构,第一件事就是把“测试被测系统”和“被测系统本身”分开,sonic-mgmt 永远跑在 DUT(Device Under Test)之外。
这个仓库能做的事也远不止“跑一下 pytest”。它包含了完整的测试床建模能力:用 testbed.csv 描述测试床的逻辑拓扑,用 Ansible inventory 描述物理设备连接关系,用 topology 文件描述端口如何映射到邻居或 PTF 容器。它还负责环境部署,比如拉起 KVM 虚拟 SONiC、配置 fanout 交换机、启动模拟邻居的 vEOS/CeOS 虚机。测试执行完成后,它还能统一收集 DUT 日志、导出 junit 报告。换句话说,这是一套从“环境准备”到“结论判定”的完整流水线。
1.2 为什么最终是 Ansible + pytest 的组合,而不是搞个自研框架
有段时间我也在想:这套东西为什么不用 Robot Framework,或者干脆写一堆 shell 脚本?实际用下来,我理解了官方的选择,这个组合的合理性在于把两件性质不同的事情分开处理。
Ansible 负责所有“对设备的操作”:SSH 到 DUT 敲命令、通过 NETCONF 配置 fanout、给测试服务器写 hosts 解析、把 inventory 变量注入环境。它的优势是无代理,只要目标设备开了 SSH 就能管,这对要同时纳管 SONiC 交换机、Arista fanout、Linux 测试服务器的场景特别合适。而 pytest 负责的是“测试逻辑”:它的 fixture 机制天然适合做环境初始化和清理,parametrize 可以轻松对多个端口、多个 VLAN 做用例扩展,各种插件(比如 ptfadapter)把底层数据面操作封装成测试可以直接调用的对象。
如果全用 shell 脚本,你会发现每个用例都在重复处理“连不上设备”“命令超时”“日志没抓全”这类问题;如果全用 Ansible 写断言,那更是灾难,Ansible 本身不是为断言和报告设计的。所以这两者的分工其实很清晰:Ansible 解决“设备能不能连、环境对不对”,pytest 解决“功能好不好、哪里不对”。我第一次搭测试床时就是被这套分工救了命,环境问题全部靠 Ansible 排查,真正分析功能 bug 时才需要打开 pytest 日志。
1.3 和系列上一篇的关系
上一篇我把 sonic-mgmt 的目录结构和基本概念过了一遍,包括ansible/、tests/、testbed/这些顶层目录分别是干什么的。这篇会默认你已经把仓库 clone 到测试服务器上了,Python 依赖也装好了,所以我不再重复初始化步骤,直接讲“把这些文件变成一套能用的测试环境”的关键:部署架构是什么样,数据怎么走,测试流程有哪些环节,以及我在实际搭建和跑测过程中踩过的那些文档里找不到的坑。
2. 部署架构拆解:从测试服务器到 DUT 的完整链路
2.1 一台可用的测试床,至少需要哪几个角色
一个典型的 sonic-mgmt 测试床,不管 KVM 还是物理环境,通常都包含下表的角色。我第一次搭的时候以为“DUT + 一台电脑”就够了,后来才发现少算了 fanout 和模拟邻居,结果拓扑根本建不起来。
| 角色 | 常见数量 | 作用 | 典型形态 |
|---|---|---|---|
| 测试服务器 | 1 | 运行 Ansible/pytest,执行测试流程 | 高配 x86 服务器,装 Ubuntu/Debian |
| PTF 容器 | 1 | 数据面发包收包,模拟 DUT 的下游邻居 | Docker 容器,镜像为 docker-ptf |
| 邻居 VM | 0 ~ N | 模拟 T1/T2/spine 等上层邻居 | vEOS / CeOS 虚机,跑在测试服务器上 |
| Fanout 交换机 | 1 ~ 3 | 提供测试服务器与 DUT 之间的数据面物理连通 | Arista、思科等支持 VLAN 的交换机 |
| DUT | 1 ~ N | 被测的 SONiC 交换机 | 物理交换机,或 KVM 里的虚拟 SONiC |
| 管理交换机 | 可选 | 统一接入各设备管理口 | 普通千兆交换机即可 |
PTF 容器是整个架构里我最想强调的一个角色。它的全称是 Packet Test Framework,本质是一个带着一堆虚拟网口的 Linux 容器,这些网口通过 testbed 的拓扑映射,分别连接到 DUT 的不同数据口。pytest 用例里的ptfadapterfixture 就是帮你在容器里构造 Scapy 报文,从指定端口发出去,再从目标端口收回来验证。有了 PTF,你才能做“发一个带特定 VLAN tag 的包,确认 DUT 会不会按预期转发/丢弃”这类数据面测试。
2.2 控制面和数据面是两条完全不同的路
这是理解整个部署架构的 key point:SONiC 测试床把“管理控制”和“业务流量”物理隔离了。
管理面这条链路很简单:测试服务器 → 管理交换机 → DUT 的管理网口(通常是 eth0 或者 mgmt0)。Ansible 所有duthost.command()都走这条路,SSH 登录、下发 config、看日志,全部从这里走。这样做的好处是,你后面无论跑多重的流量测试,都不会因为控制面拥塞导致连不上设备。
数据面则是另一条路:测试服务器上的 PTF 容器网口 → 物理网线 → fanout 交换机 → DUT 的数据口。fanout 在这里起到的作用是“端口映射矩阵”。你不可能为了每个新拓扑都去物理拔插网线,而是把所有 PTF 口和 DUT 口都预先接到 fanout 上,再通过配置 fanout 的 VLAN,让某个 PTF 口的数据能到达指定的 DUT 口。换句话说,fanout 的转发规则就是那张“虚拟网线表”。testbed_connect.py这个脚本干的就是这件事:它读取 topology 文件,算出哪些端口要互通,然后自动把 VLAN 配置下发到 fanout 上。
我第一次部署时犯过一个典型错误:以为 PTF 容器起来就自动连通 DUT 了,结果怎么发包都不通,最后发现 fanout 上对应端口根本不在同一个 VLAN 里。所以记住:容器起来只代表端口存在,端口之间有没有桥接,取决于 fanout 的转发规则。
2.3 testbed.csv、inventory、拓扑变量是怎么串起来的
部署架构里最抽象的部分,就是“逻辑拓扑”和“物理设备”的映射关系。sonic-mgmt 用三个文件配合完成这件事。
testbed.csv定义“逻辑测试床”:给测试床起名字、指定拓扑类型(比如 t0、t1、ptf32)、PTF 容器的 IP、对应的测试服务器、DUT 的 hostname 等。典型一行的格式类似:
conf-name,group-name,topo,ptf-image-name,ptf,ptf_ip,server,vm_base,dut,inv_name,auto_recover vms-t0-1,sonic,t0,docker-ptf,ptf-1,10.0.0.101,server_1,[VM0001],sonic-1,sonic,Falseinventory文件定义“物理设备”:哪些机器是服务器、哪些 hostname 是 SONiC DUT、它们的 IP、SSH 用户名等。Ansible 跑 playbook 时,会根据 inventory 里的组找到对应设备。
拓扑变量文件(比如topo_t0.yml)定义“每个接口连到谁”:DUT 的 Ethernet4 是对接 PTF 端口 0 呢,还是对接某个 VM 的 veth?这些映射关系都在拓扑文件里。
三个文件的关系可以这样理解:testbed.csv告诉你“这套测试床叫什么、用的什么拓扑”,inventory 告诉你“物理上哪台机器是那个 DUT”,拓扑变量告诉你“数据平面每个口应该怎么接”。pytest 命令里的--testbed vms-t0-1和--inventory就是把它们绑到一起的胶水。所以部署排障时,凡是出现“找不到 DUT”“拓扑端口对不上”之类的问题,先按这个三角关系去查,通常很快能定位。
3. 测试流程:从空环境到拿到一份报告的完整链路
3.1 环境初始化和连通性验证是跑测前最后的保险
我养成的习惯是,不管昨天跑得多正常,今天开跑前一定先做一遍连通性验证,因为测试服务器重启、DUT 升级、fanout 配置被改动,任何一个环节断了,都会让后续几个小时的测试全部白跑。
第一步是建立在测试服务器到 DUT/PTF 的管理面连接。在 sonic-mgmt 的ansible/目录下,通常可以用testbed_cli.sh完成:
cd sonic-mgmt/ansible ./testbed_cli.sh connect-mgmt vms-t0-1 -i ../ansible/inventory -u admin这个命令做的事情因版本而异,有的版本是在测试服务器/etc/hosts里补上测试床相关设备的名字解析。执行后,先用 Ansible 自己验证一遍连通:
ansible -m ping sonic-1 -i inventoryDUT 侧,我至少会确认三件事:SSH 能登录、SONiC 核心进程起来了、接口状态正常。用sonic-ssh或者直接 ssh 上去执行:
show platform summary show interfaces status show ip interfaces如果走了 KVM 测试床,还要确认 PTF 容器和邻居 VM 都已经启动,通常用testbed_cli.sh start-vms拉起。容器起来后,我习惯进 PTF 容器里看一眼网口是否存在:
docker exec -it ptf-1 bash ip link show这一通操作看起来繁琐,但能省下后面大量排查时间。我甚至会把这几条命令写成一个precheck.sh,每次跑测之前先执行,输出有问题就不再往下走。
3.2 用例执行:run_tests.sh 和直接调 pytest
环境确认没问题之后,就是跑用例。sonic-mgmt 提供了run_tests.sh这个封装脚本,但不同分支之间参数略有差异,我建议先执行./run_tests.sh -h确认你手头版本的参数。常见用法大致是:
cd sonic-mgmt/tests ./run_tests.sh -i ../ansible/inventory -u admin -n vms-t0-1 -t t0 -s test_interfaces.py这里-n是测试床名字,-t是拓扑类型,-s指定测试脚本文件。这个脚本本质上是帮你在命令行里组装好一堆 pytest 参数,再透传给 pytest。如果你想更精细地控制,我也推荐直接用 pytest,这样能看到所有原生的过滤选项:
cd sonic-mgmt/tests pytest --inventory ../ansible/inventory --host-pattern sonic-1 \ --module-path ../ansible/library \ --testbed vms-t0-1 --testbed_file ../ansible/testbed.csv \ --user admin --show-capture=no --log-cli-level=info \ test_interfaces.py这里几个参数背后的含义要理解:--host-pattern指定 DUT 在 inventory 里的 hostname,--testbed_file指向testbed.csv,pytest 插件会从这个文件里解析出测试床的端口映射和设备信息。--module-path指向 Ansible 的 library 目录,因为很多 DUT 操作调用的不是 Ansible 官方模块,而是 sonic-mgmt 自带的扩展模块。
如果你只想跑某个用例,而不是整个文件,可以在脚本名后面加::用例名;想按关键字过滤用-k;想看有哪些用例但不确定能不能跑,用--collect-only先收集一遍。多 DUT 场景下,有些用例会依赖duthosts这个 fixture 遍历所有 DUT,这类用例在 t2/chassis 拓扑里很常见,单 DUT 测试床跑它们会直接被跳过,这是正常的。
3.3 日志采集和结果输出
测试跑完,很多人看一眼“passed/failed”就结束了,但这恰恰浪费了 sonic-mgmt 最大的价值。它对失败的定位能力,强在能把 DUT 侧的现场完整留给你。
pytest 有个--logs_path参数,可以把测试过程中的 Ansible 命令输出、日志文件统一收集到指定目录:
pytest ... --logs_path=/tmp/sonic-logs --junitxml=/tmp/result.xml test_bgp.pyDUT 本身则可以用show techsupport或者dumpstate打包系统状态,里面包括 syslog、各容器日志、config_db.json、show命令的快照等。排查问题时我通常先看这几个地方:怀疑协议问题看 fpmsyncd、bgpd 日志;怀疑配置问题直接对config_db.json;怀疑接口问题看show interfaces counters和show interface status。
另外,PTF 容器里的日志也容易被忽略。有些用例会直接把 PTF 侧收到的报文信息打到容器 stdout,所以失败了先去docker logs ptf-1里翻一翻,经常能看到“包没到预期端口”的线索。结果输出这块,我个人习惯固定加--junitxml,不管本地看还是接 CI 都方便,--log-cli-level=info也能让你在控制台看到用例每步在做什么,定位问题时比闷头跑一堆用例效率高得多。
4. 搭建和跑测时最容易翻车的几个点
4.1 PTF 端口和 DUT 端口映射错位,症状非常隐蔽
这是我最想提醒你的一件事。PTF 容器的端口编号和 DUT 的接口名不是天然一一对应的,它们之间靠拓扑文件映射。比如topo_t0.yml里可能会写ptf端口 0 对应 DUT 的 Ethernet0,但如果你换了 SKU,或者 DUT 侧的 breakout 模式不同,实际的端口顺序就可能错位。
典型的症状是:你用ptfadapter从端口 0 发包,在 DUT 上show interfaces counters却发现收到的包出现在 Ethernet4 上。更隐蔽的是,某些测试协议报文的目标 MAC 写的是广播地址,导致包根本看不出从哪个口进来,你会误以为 DUT 转发逻辑有 bug,查半天才发现是端口映射错了。
我的排查套路是:先在 PTF 容器里用ip link show确认容器端口数量,再到测试服务器上看 PTF 容器的映射参数,最后回到拓扑文件比对端口对应关系。平时搭建完测试床,我也会先跑一个最小探针用例:从 PTF 每个端口依次发一个带独特标识的包,到 DUT 上看从哪个接口收到,然后把这份实测映射表存下来,和拓扑文件做差异对比。这个习惯帮我避免了很多次“假失败”。
4.2 拓扑类型和用例不匹配,不是用例坏,是你没选对夹具
sonic-mgmt 的测试用例基本都在文件头部用 marker 声明了自己适合什么拓扑,例如:
pytestmark = [ pytest.mark.topology('t0', 't1') ]这意味着这个用例只在 t0 或 t1 拓扑下有效。如果你拿一个ptf32的纯 PTF 测试床去跑这种用例,框架会直接跳过,或者因为找不到对应 fixture 直接报错误。这不是代码坏了,而是“测试床的邻居模型”和“用例预期的环境”不匹配。
不同拓扑的关键差异在于邻居结构:t0 拓扑里 DUT 通常要对接上游 spine(用 VM 模拟)和下游服务器(用 PTF 模拟),t1 拓扑则是典型的汇聚层场景,dualtor 拓扑还会引入两台 DUT 做 mux 主备。你选拓扑的时候,不是在选“名字”,而是在选“一套网络环境”。所以我建议:拿到一批用例之前,先用pytest --collect-only收集一遍,看哪些用例被收集、哪些被 skip,心里有个数,别等跑了半小时才发现大量用例因为拓扑不匹配根本没执行。
4.3 KVM 测试床和物理测试床的差异比想象中更大
如果你刚接触 sonic-mgmt,大概率是从 KVM 虚拟测试床入手的,因为便宜、随时能重建。KVM 测试床的部署架构和物理环境基本一致,只是 DUT 变成了 QEMU 虚拟机,PTF 容器和邻居 VM 都跑在同一个宿主机上。但我要泼一盆冷水:KVM 适合跑“功能正确性”用例,不适合跑“时序敏感”用例。
我自己踩过最典型的是test_copp和 QoS 相关用例,在 KVM 上经常因为 CPU 调度抖动出现看似随机的失败,换到物理交换机上就稳定过了。原因是虚拟环境里 CPU 争抢、中断延迟都不稳定,控制面速率限制、队列调度这类跟时间强相关的测试,很难在 KVM 上得到可复现的结果。反过来,像接口 up/down、VLAN 转发、路由表项、show命令输出这类不依赖精确时序的用例,KVM 完全能胜任。
另外还有个实际经验:KVM 环境下 DUT 的“重启类”用例要格外小心,因为虚拟机的管理网口可能跟着重启而短暂消失,Ansible 的连接池容易超时。如果跑test_reboot这类用例,建议先确认框架里有没有针对 SSH 重连的等待机制,没有的话自己在用例里加轮询等待,不然大概率第一个 reboot 就把测试 session 搞挂了。
4.4 一个快速排查对照表
我把跑测过程中遇到频率最高的几个问题做了个简单对照,方便你遇到类似现象时快速定位:
| 现象 | 可能原因 | 优先排查项 |
|---|---|---|
| PTF 发包 DUT 收不到 | fanout VLAN 映射不对 | fanout 上的 vlan 配置、PTF 容器端口 link 状态 |
| 用例被跳过 | 拓扑 marker 不匹配 | pytest --collect-only 看 skip 原因 |
| DUT SSH 超时 | 管理网络链路异常 / DUT 重启中 | ping 管理口,检查告警日志 |
| 测试结果随机失败 | KVM 资源竞争 | 看失败用例是否属于时序敏感类 |
| 端口映射错位 | 拓扑文件与 SKU 不匹配 | 用探针用例实测端口对应关系 |
5. 二次开发:如何把你们自己的用例接进这套框架
5.1 先理解 tests 目录下的公共设施
sonic-mgmt 真正值钱的除了现成用例,还有底层的公共 fixture。我写新用例的第一步从来不是从零写,而是找一个最接近的场景,看它用了哪些 fixture,然后照着改。tests/common/下面有一堆现成的轮子:duthost是对 DUT 的封装,可以用它执行命令、读写文件、解析输出;ptfadapter是数据面的入口,可以发包收包;tbinfo能帮你拿到当前测试床的拓扑配置信息。
一个最简单的功能用例大概长这样:
import pytest pytestmark = [ pytest.mark.topology('t0', 't1') ] def test_demo_interface_status(duthost): output = duthost.command('show interfaces status') assert 'Ethernet0' in output['stdout']想要做数据面验证,就可以在此基础上引入 ptfadapter:
def test_demo_ptf_packet(ptfadapter, duthost): ptfadapter.dataplane.flush() src_port = 0 pkt = testutils.simple_tcp_packet( pktlen=64, eth_dst='00:11:22:33:44:55', eth_src='66:77:88:99:aa:bb', ip_src='10.0.0.1', ip_dst='10.0.0.2', tcp_sport=1234, tcp_dport=80, ) testutils.send_packet(ptfadapter, src_port, pkt) testutils.verify_packets(ptfadapter, pkt, [dst_port])这里dst_port的值取决于测试床拓扑里 DUT 哪个接口应该转发这个包。一般情况下,你可以借助tbinfo里的端口映射信息动态计算,而不是硬编码端口号,这样用例换到别的拓扑上也能跑。
5.2 我推荐的开发与调试顺序
写新用例时,我会按下面这个顺序走,能有效减少来回折腾:
- 先跑通一个最小探针。只写一条执行命令的用例,用 pytest 指定单个用例跑一遍,确认 fixture 能初始化、DUT 能连上。这一步一般是环境问题集中爆发的阶段,先把环境跑稳再写逻辑。
- 用 parametrize 展开覆盖范围。比如要对 4 个接口做测试,就参数化传入接口名,让 pytest 帮你生成多个测试点,而不是在用例里写 for 循环。这样单个样本失败时不会影响其他样本,报告也更清晰。
- 补上清理逻辑。很多 DUT 操作会改变系统状态,如果一个用例把配置改了却没恢复,下一个用例很可能失败。sonic-mgmt 的 fixture 通常会帮你做一定清理,但你自己改过的配置还是尽量在 teardown 里还原。
- 用 --collect-only 和 -k 验证选择逻辑。写完用例先收集一遍,确认它在你这套拓扑下会被正常收集,再真正执行。
实际开发中,我还会特意看一下现有用例的组织风格,比如tests/test_interfaces.py这一类文件,看它是怎么组织 fixture 作用域、怎么处理多 DUT、怎么把日志选项传给底层库的。跟着已有代码的风格走,比你自己另起炉灶要省力得多,也更容易在后续升级 sonic-mgmt 版本时把用例平滑迁移过来。
最后说个个人体会:我刚接触 sonic-mgmt 时,总想着“把框架完全搞懂再动手”,结果反而拖了很久。后来发现最快的路径是先跑通一套现成的 KVM 测试床和几个现成用例,对 pytest 的 fixture 和 testbed 下的目录结构有了体感,再回头看架构就通透多了。你在部署或跑测时遇到莫名其妙的失败,也先别急着怀疑代码逻辑,多半是端口映射、拓扑选择或者管理网络哪个环节悄悄出了问题。拿这篇里的排查思路照着走一遍,大部分问题都能收敛到具体原因。