📑 课程信息
- 领域:网络自动化 / NetDevOps / Python 测试框架

📖知识精讲
本课程系统介绍了 Cisco 开源的 pyATS 自动化测试框架,涵盖其起源、核心功能、分层架构、典型应用场景以及 testbed 配置文件的创建方法,帮助学习者掌握利用 pyATS 实现网络自动化测试与状态校验的核心能力。
pyATS 基础概念与命名规范
-
名称与发音:pyATS 全称为 Python Automated Test System,发音为 "pie-A-T-S",是 Cisco 内部开发后于 2017 年开源的自动化框架。
-
🚩重点:不要将 pyATS 局限理解为仅用于测试,它是一套功能完整的全栈网络自动化框架,Cisco 内部每日运行数百万次测试任务。
-
补充说明:pyATS 采用 Apache 2.0 开源协议,完全免费,支持多厂商设备,不仅限于 Cisco 操作系统,还可测试服务器与 Linux 主机。
-
-
课程学习目标:掌握 pyATS 自动化网络测试方法、部署 pyATS 并创建 testbed 管理设备、利用 pyATS 完成网络状态校验。
pyATS 核心能力与特色功能
-
CLI 输出结构化解析:将传统非结构化的 CLI 命令输出自动转换为 JSON 格式的键值对数据,可快速定位如 CRC 错误、接口双工状态等关键信息。
- 🚩重点:仅需少量 Python 代码即可遍历全网所有设备的所有接口,批量完成指定指标的自动化校验,大幅提升测试效率。
-
状态快照与差异对比:通过 learn 功能采集网络设备的完整运行状态,使用 diff 功能对比变更前后的状态差异,直观展示配置增减内容。
-
跨厂商语法适配:内置多操作系统解析器,自动根据设备类型生成对应语法的配置命令,无需工程师记忆所有设备的命令行语法。
pyATS 典型创新应用场景
-
业务化数据输出:将采集到的结构化网络状态数据转换为 CSV、Excel、HTML、思维导图甚至音频文件,让非技术人员也能直观查看网络状态。
-
CI/CD 流水线集成:完整融入 NetDevOps 流程,依次完成状态采集、自动化测试、配置推送、变更后重校验、失败自动回滚的全流程闭环。
pyATS 解决的传统网络运维痛点
-
复杂网络监控难题:传统人工排查故障难以定位大规模网络中的故障点,pyATS 通过设备健康检查功能,将配置状态与运行状态结合生成网络快照,快速识别异常。
- 🚩重点:可预先定义"黄金基准配置",每日自动对比全网快照与基准的差异,确保网络配置始终符合合规要求。
-
网络变更风险管控:硬件迁移、系统升级等场景下,人工执行变更前后检查极易遗漏细节,pyATS 可自动化完成全量状态快照与对比,大幅降低人为错误。
pyATS 分层架构体系
-
三层架构设计:最底层为 pyATS Core Test Infrastructure,负责设备连接、命令执行与基础报告生成;中间层为 pyATS Libraries(原 Genie),提供解析器、拓扑管理等自动化工具;最上层为自定义业务逻辑层,可集成 Xpresso 可视化仪表盘、Jenkins CI/CD 工具。
-
架构图示:
业务逻辑层 → 集成(Xpresso、Jenkins) 库层 → pyATS Libraries (Formerly Genie)、pyATS Library Framework (Formerly Genie) 工具层 → pyATS Core Test Infrastructure
-
-
无关性设计:所有操作系统、平台、协议的支持均通过插件扩展实现,原生支持第三方厂商设备与协议。
pyATS Testbed 配置文件详解
-
YAML 格式定义:testbed 文件是 pyATS 描述网络拓扑的核心载体,通过 YAML 格式定义所有设备的连接信息、硬件属性、互联关系。
- 🚩重点:YAML 是严格依赖缩进的标记语言,编写时需特别注意缩进规范,避免语法错误;设备名称必须与设备主机名完全匹配,否则会导致连接挂起。
-
三大核心配置块:testbed 全局块定义通用属性与全局凭证;devices 块定义每台设备的连接参数、操作系统类型、登录凭证;topology 块定义设备接口的 IP 地址与互联链路关系。
-
多种创建方式:支持手动使用文本编辑器编写、通过 Excel/CSV 导入转换生成、直接用 Python 字典/JSON 数据动态创建三种方式,适配不同技术水平的用户。
🖍️ 重点速览
🚩 考点重点
-
pyATS 定位认知:不能将 pyATS 仅理解为测试工具,它是全功能 Python 网络自动化框架,支持配置、文档生成、状态对比等全场景操作。
-
结构化解析能力:pyATS 核心魔法是将非结构化 CLI 输出转换为 JSON 键值对,实现批量自动化校验。
-
设备健康检查公式:设备健康状态 = 配置状态 + 运行状态,是 pyATS 最常用的场景。
-
黄金基准配置:预先定义合规基准快照,每日自动对比全网状态与基准的差异,确保配置合规。
-
testbed 注意事项:YAML 严格依赖缩进,设备名称必须与主机名完全一致,否则会导致连接异常。
💡 核心概念
-
pyATS:Cisco 开源的 Python Automated Test System,发音为 pie-A-T-S,基于 Apache 2.0 协议,是全栈网络自动化框架。
-
testbed:pyATS 中用于描述网络设备、连接信息、拓扑关系的 YAML 格式配置文件,是自动化测试的基础载体。
-
learn 功能:自动采集网络设备特定功能或协议的完整运行状态,生成结构化状态快照。
-
diff 功能:对比两次网络状态快照的差异,直观展示配置变更的增减内容。
-
pyATS 三层架构:底层 Core 负责设备连接与基础管理,中间层 Libraries 提供解析器等自动化工具,上层支持自定义业务逻辑与第三方工具集成。
✨ 课堂金句
-
"当我接触到自动化,它彻底改变了我日常工作的一切。我更享受工作,热爱写代码,热爱解决问题,而这就是自动化带给我们的价值。"
-
"只要你能把 pyATS 的名字读对,你就已经解决了采用这个框架一半的难题。"
-
"不要只把 pyATS 用来做测试,你可以用它把网络状态变成任何人都能看懂的业务文档,甚至音频文件,自动化可以玩出很多花样。"
📝 待办事项
-
官方资源访问 :访问 https://developer.cisco.com/pyATS 或 cs.co/pyATS 获取 pyATS 官方文档、学习实验与社区支持资源。
-
拓展学习:浏览 pyATS Genie 功能浏览器,了解官方提供的各类解析器与 API 能力。
-
后续实践:尝试使用三种不同方式创建 pyATS testbed 文件,熟悉 YAML 缩进规范与配置结构。
🎯 课程总结
🔍 网络自动化测试背景与挑战
-
网络复杂性驱动需求:随着网络规模扩大,人工测试面临效率低、易出错、一致性差等问题,企业对网络可用性和稳定性的高要求促使自动化测试策略的发展。
-
核心痛点:网络变更导致的中断、 downtime 和性能下降可能造成组织功能障碍及财务损失,需通过自动化测试验证网络健康状态和变更影响。
📌 pyATS 概述
-
定义与发音 :pyATS(Python Automated Test System,发音 "pie-A-T-S")是 Cisco 开发的开源网络测试框架,基于 Python 和 CPython,遵循 Apache 2.0 许可协议。
-
核心组件:
-
Genie SDK(pyATS 库):提供 CLI 接口和低代码门槛,支持解析设备输出、配置管理和状态验证。
-
测试床(Testbed):以 YAML 文件定义网络拓扑,包含设备连接信息(IP、端口、协议)和凭据,实现对物理/虚拟设备的抽象。
-
-
关键特性:模块化架构、可扩展性强、厂商中立(支持多厂商设备和操作系统)、支持复杂测试场景和报告生成。
✨ 培训与课程目标
-
培训价值:学习 Cisco 如何开发和实施 pyATS,重新定义网络测试流程,简化自动化测试工作流。
-
课程内容:涵盖 pyATS 生态系统、核心库、测试床创建、设备连接管理、CLI 输出解析(parse 功能)、设备特性学习(learn 功能)、变更检测(diff 功能)及 XPRESSO 仪表板使用。
-
能力目标:
-
描述 pyATS 在网络环境自动化测试中的应用
-
部署 pyATS 并创建包含设备的测试床
-
管理设备连接并使用 pyATS 进行网络测试
-
🔧 pyATS 解决的核心挑战
| 挑战场景 | 传统方法痛点 | pyATS 解决方案 |
|---|---|---|
| 网络监控 | 人工检查复杂,依赖日志和报告 | 通过 API 和解析器采集配置状态与运行状态,生成网络快照,对比基线(Golden Config)检测异常 |
| 配置变更 | 变更前后检查耗时,易漏检 | 自动化执行变更前后快照(pre-check/post-check),通过 diff 功能对比差异,验证变更效果 |
📊 pyATS 框架架构
-
分层结构(自底向上):
-
核心测试基础设施:处理设备连接、命令执行、测试床管理和报告生成
-
pyATS 库(原 Genie):提供解析 CLI 输出、配置管理、拓扑测试的标准化库
-
集成层:支持与 Jenkins 等 CI/CD 工具集成,实现持续测试
-
业务逻辑层:允许用户构建自定义设备模型和测试场景,支持 XPRESSO 图形化界面
-
🚀 应用场景与测试类型
-
NetDevOps 管道集成:在 Infrastructure as Code (IaC) 流程中自动化测试阶段,替代人工干预。
-
支持的测试类型:
-
白盒测试:验证网络基础设施的内部配置、运行状态及外部集成
-
黑盒测试:聚焦网络行为,对比实际结果与预期资产
-
特性测试:评估新功能或改进功能的有效性
-
性能与扩展性测试:测量网络响应时间、带宽、速度及扩展能力
-
📝 关键功能与工作流
-
设备健康检查:整合配置状态与运行状态,生成健康报告。
-
变更验证流程:
-
变更前捕获网络快照
-
执行变更操作
-
变更后再次捕获快照
-
使用 diff 功能分析差异,确认变更符合预期
-
课堂相关实验------Lab-1:





















bash
# Common Setup Section
#
class common_setup(aetest.CommonSetup):
'''Common Setup Section
Defines subsections that performs configuration common to the entire script.
'''
@aetest.subsection
def check_topology(self, testbed, ios_names):
'''
check that we have at least two devices and a link between the devices
If so, mark the next subsection for looping.
'''
# abort/fail the testscript if no testbed was provided
if not testbed or not testbed.devices:
self.failed('No testbed was provided to script launch',
goto = ['exit'])
for ios_name in ios_names:
if ios_name not in testbed:
# abort/fail the testscript if no matching device was provided
self.failed('testbed needs to contain device {ios_name}'.format(ios_name=ios_name,),goto = ['exit'])
# add them to testscript parameters
ios_device = testbed.devices[ios_name]
# get corresponding links
links = ios_device.links
# save link and ios_device as parameter
self.parent.parameters[ios_name] = {'ios': ios_device, 'links': links}
assert len(links) >= 1, 'require one link or more between devices'
. . .
@aetest.subsection
def establish_connections(self, steps, ios_names):
'''
establish connection to both devices
'''
for ios_name in ios_names:
with steps.start('Connecting to ios device: %s'%(ios_name)):
self.parent.parameters[ios_name]['ios'].connect()
# abort/fail the testscript if any device isn't connected
if not self.parent.parameters[ios_name]['ios'].connected:
self.failed('One of the devices could not be connected to',goto = ['exit'])
@aetest.subsection
def marking_interface_count_testcases(self, testbed):
'''
mark the VerifyInterfaceCountTestcase for looping.
'''
# ignore CML terminal_server
devices = [d for d in testbed.devices.keys() if 'terminal_server' not in d]
logger.info(banner('Looping VerifyInterfaceCountTestcase'
' for {}'.format(devices)))
# dynamic loop marking on testcase
aetest.loop.mark(VerifyInterfaceCountTestcase, device = devices)
Continued -



bash
@aetest.loop(ios_name = ('R1', 'R2'))
class PingTestcase(aetest.Testcase):
'''Ping test'''
groups = ('basic', 'looping')
@aetest.setup
def setup(self, ios_name):
destination = []
for link in self.parent.parameters[ios_name]['links']:
# To get the link interfaces ip
for intf in link.interfaces:
parsed_dict = self.parent.parameters[intf.device.name]['ios'].\
parse('show ip interface brief')
intf_ip = parsed_dict['interface'][intf.name]['ip_address']
destination.append(intf_ip)
# apply loop to next section
aetest.loop.mark(self.ping, destination = destination)
. . .
@aetest.test
def ping(self, ios_name, destination):
'''
ping destination ip address from device
Sample of ping command result:
ping
Protocol [ip]:
Target IP address: 10.10.10.2
Repeat count [5]:
Datagram size [100]:
Timeout in seconds [2]:
Extended commands [n]: n
Sweep range of sizes [n]: n
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 10.10.10.2, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/1 ms
'''
try:
# store command result for later usage
result = self.parameters[ios_name]['ios'].ping(destination)
# result = self.parameters[device].ping(destination)
. . .
except Exception as e:
# abort/fail the testscript if ping command returns any exception
# such as connection timeout or command failure
self.failed('Ping {} from device {} failed with error: {}'.format(
destination,
device,
str(e),
),
goto = ['exit'])
else:
# extract success rate from ping result with regular expression
match = re.search(r'Success rate is (?P<rate>\d+) percent', result)
success_rate = match.group('rate')
# log the success rate
logger.info(banner('Ping {} with success rate of {}%'.format(
destination,
success_rate,
)
)
)
Continued -


bash
#
# Verify Interface Count Testcase
#
class VerifyInterfaceCountTestcase(aetest.Testcase):
'''Verify interface count test'''
groups = ('basic', 'looping')
@aetest.test
def extract_interface_count(self, device):
'''
extract interface counts from `show version`
Sample of show version command result:
show version
Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(2)T, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2016 by Cisco Systems, Inc.
Compiled Tue 22-Mar-16 16:19 by prod_rel_team
ROM: Bootstrap program is IOSv
ios2 uptime is 1 hour, 17 minutes
System returned to ROM by reload
System image file is "flash0:/vios-adventerprisek9-m"
Last reload reason: Unknown reason
<....>
. . .
Cisco IOSv (revision 1.0) with with 484609K/37888K bytes of memory.
Processor board ID 9QTSICFAZS7Q2I61N8WNZ
2 Gigabit Ethernet interfaces
DRAM configuration is 72 bits wide with parity disabled.
256K bytes of non-volatile configuration memory.
2097152K bytes of ATA System CompactFlash 0 (Read/Write)
0K bytes of ATA CompactFlash 1 (Read/Write)
0K bytes of ATA CompactFlash 2 (Read/Write)
10080K bytes of ATA CompactFlash 3 (Read/Write)
Configuration register is 0x0
'''
try:
# store execution result for later usage
result = self.parameters[device]['ios'].execute('show version')
except Exception as e:
# abort/fail the testscript if show version command returns any
# exception such as connection timeout or command failure
self.failed('Device {} \'show version\' failed: {}'.format(device,
str(e)),
goto = ['exit'])
else:
# extract interfaces counts from `show version`
match = re.search(r'(?P<ethernet>\d+) Gigabit Ethernet interfaces\r\n', result)
ethernet_intf_count = int(match.group('ethernet'))
# log the interface counts
logger.info(banner('\'show version\' returns {} ethernet interfaces'
.format(
ethernet_intf_count
)
)
)
# add them to testcase parameters
self.parameters.update(ethernet_intf_count = ethernet_intf_count,
serial_intf_count = 0)
@aetest.test
def verify_interface_count(self,
device,
ethernet_intf_count = 0,
serial_intf_count = 0):
'''
verify interface counts with `show ip interface brief`
Sample of show ip interface brief command result:
show ip interface brief
Interface IP-Address OK? Method Status Protocol
GigabitEthernet0/0 unassigned YES unset administratively down down
GigabitEthernet0/1 10.10.10.2 YES manual up up
'''
try:
# store execution result for later usage
result = self.parameters[device]['ios'].execute('show ip interface brief')
except Exception as e:
# abort/fail the testscript if show ip interface brief command
# returns any exception such as connection timeout or command
# failure
self.failed('Device {} \'show ip interface brief\' failed: '
'{}'.format(device, str(e)),
goto = ['exit'])
else:
# extract ethernet interfaces
ethernet_interfaces = re.finditer(r'\r\nGigabitEthernet\d+\s+', result)
# total number of ethernet interface
len_ethernet_interfaces = len(tuple(ethernet_interfaces))
# log the ethernet interface counts
logger.info(banner('\'show ip interface brief\' returns {} ethernet'
' interfaces'.format(len_ethernet_interfaces)))
# compare the ethernet interface count between
# `show ip interface brief` and `show version`
assert len_ethernet_interfaces == et
Continued -





课堂相关实验------Lab-2:

































