网络自动化Python课程:Cisco PyATS网络自动化测试框架

📑 课程信息

  • 领域:网络自动化 / 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/pyATScs.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 框架架构
  • 分层结构(自底向上):

    1. 核心测试基础设施:处理设备连接、命令执行、测试床管理和报告生成

    2. pyATS 库(原 Genie):提供解析 CLI 输出、配置管理、拓扑测试的标准化库

    3. 集成层:支持与 Jenkins 等 CI/CD 工具集成,实现持续测试

    4. 业务逻辑层:允许用户构建自定义设备模型和测试场景,支持 XPRESSO 图形化界面

🚀 应用场景与测试类型
  • NetDevOps 管道集成:在 Infrastructure as Code (IaC) 流程中自动化测试阶段,替代人工干预。

  • 支持的测试类型

    • 白盒测试:验证网络基础设施的内部配置、运行状态及外部集成

    • 黑盒测试:聚焦网络行为,对比实际结果与预期资产

    • 特性测试:评估新功能或改进功能的有效性

    • 性能与扩展性测试:测量网络响应时间、带宽、速度及扩展能力

📝 关键功能与工作流
  • 设备健康检查:整合配置状态与运行状态,生成健康报告。

  • 变更验证流程

    1. 变更前捕获网络快照

    2. 执行变更操作

    3. 变更后再次捕获快照

    4. 使用 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:

相关推荐
小袁拒绝摆烂2 小时前
Jenkins部署经验
运维·jenkins
刚入门的大一新生2 小时前
Linux-进程控制
linux·运维·服务器·c++
上火的金鱼妹3 小时前
K8S基础组件作用和关系整理
linux·运维·服务器·kubernetes
Vcaker3 小时前
Linux学习25-harbor私有仓库部署
linux·运维·学习
醉颜凉3 小时前
网络安全必学:粘性MAC地址(Sticky MAC)原理与应用全解析
运维·服务器·网络·安全·web安全
不懂的浪漫4 小时前
ToDesk 连接 Linux 后分辨率过低的解决方法
linux·运维·数据库
姚不倒4 小时前
etcd 学习系列(七):排障指南 —— 常见问题与解决方案
运维·etcd
广州灵眸科技有限公司5 小时前
从手动三步到上电即连:4G模组systemd开机自连方案
linux·运维·服务器·开发语言·网络·人工智能·php
Lear5 小时前
Docker 私有仓库搭建与使用
运维·自动化运维