pyATS and Genie Tutorial: Parse Show Commands and Test State - 夜莺博客

pyATS and Genie Tutorial: Parse Show Commands and Test State

Grep and eyeballs do not scale to a thousand devices, and regex parses break every time a vendor adds a column. pyATS with the Genie library solves that by shipping vendor-aware parsers that turn show interfaces output into Python dictionaries, plus a "learn" feature that snapshots protocol state and diffs it against a baseline later. This guide covers installation, the testbed file, parsing from Python and the CLI, building your own parsers, and how to wire a state-diff check into a pipeline so it fails loudly when the network changes underneath you.

pyATS (Python Automated Test Systems) began life inside Cisco as the internal framework for regression testing IOS, NX-OS and IOS XR images. Cisco eventually open-sourced it, and the Genie library that rides alongside it is what makes it useful to network engineers outside the vendor: a growing catalog of hundreds of parsers, learn features and triggers for the platforms you already run. The mental model to hold onto is that pyATS is a test framework first and a parser second. That distinction matters because it pushes you toward assertions with exit codes, not pretty printouts you have to read manually, which is exactly what an automated workflow needs.

Install and build a testbed

python3 -m venv ~/pyats-env && source ~/pyats-env/bin/activate
pip install pyats genie

genie create testbed interactive --output testbed.yml
pyats validate testbed testbed.yml

The testbed is a YAML description of your devices - credentials, OS, connection type and topology. Validate it before anything else: a testbed typo produces a confusing parser error rather than a connection error.

A minimal hand-edited testbed entry looks like this, and it is worth understanding each key because every one of them has bitten somebody:

devices:
  core-sw1:
    os: iosxe
    type: switch
    connections:
      cli:
        protocol: ssh
        ip: 10.10.10.11
        port: 22
        credentials:
          default:
            username: netops
            password: '%ENV{PYATS_PW}'

The os key selects which Genie parser family is loaded, so a wrong value sends you hunting for a bug in code that is actually fine. The %ENV{} syntax pulls the password from an environment variable rather than hard-coding it, which keeps secrets out of the repository - the same discipline described for Netmiko automation. Keep the testbed in version control and treat a change to it as a code change.

Parse show command output

from genie.testbed import load

testbed = load('testbed.yml')
dev = testbed.devices['core-sw1']
dev.connect(log_stdout=False)

parsed = dev.parse('show interfaces')
for name, i in parsed.items():
    print(name, i['oper_status'], i.get('counters', {}).get('in_crc_errors'))

# one-liner from the shell
# pyats parse "show ip route" --testbed-file testbed.yml --device core-sw1

The returned structure is a nested dictionary you can index directly - no regex, no fragile column offsets. The same parser handles IOS, IOS XE, IOS XR, NX-OS and Junos, so the same script works across platforms when you key on the fields you care about.

Two practical notes from production use. First, parse is a thin wrapper around connect and disconnect: if you call it repeatedly on the same device object you pay for a fresh SSH session each time, so connect once, then call dev.parse() many times and disconnect at the end. Second, the parser output is only as good as the CLI command it wraps - abbreviated commands sometimes map to different parsers than the long form, so always write the full command in your scripts to avoid surprises during an upgrade.

Learn state and diff against a baseline

# capture a snapshot
genie learn ospf --testbed-file testbed.yml --output baseline/

# ... hours later, after a change window ...
genie learn ospf --testbed-file testbed.yml --output after/

# compare
pyats diff baseline/ after/

This is the most valuable three-command workflow in the toolkit: it tells you exactly what changed in OSPF, BGP, interfaces, LLDP or spanning tree. Run genie learn all for a full snapshot, then diff after every maintenance window - it catches the neighbour-adjacency and route changes a human reviewer misses.

The difference between parse and learn is worth stating plainly. Parse runs a single show command and hands you the structured result; it is stateless. Learn runs a defined bundle of commands, assembles them into a coherent model of a protocol or subsystem, and writes that model to disk as JSON. That model is what makes diffing possible, because you snapshot it before the window and after, and pyats diff walks the two trees for you. A learn feature for OSPF, for example, captures neighbour states, interface assignments, area configuration and timers in one artifact, so a change to a timer that re-converges cleanly still shows up as a difference.

Store baselines with a date and change-ticket reference. When something breaks a week later, a labelled baseline is the difference between "we think this is the config that was there" and "here is the JSON proof of what was there".

Write assertions, not reports

from genie.testbed import load
from pyats.aetest import Testcase, test

class NetworkHealth(Testcase):
    @test
    def bgp_established(self):
        dev = load('testbed.yml').devices['core-r1']
        dev.connect(log_stdout=False)
        bgp = dev.parse('show bgp summary')
        for peer, data in bgp['vrf']['default']['neighbor'].items():
            state = data['state']
            self.assertEqual(state, 'established', f'{peer} is {state}')
        dev.disconnect()

Save and run it with pyats run job or pyats run genie, and emit --json-report output that your CI system or orchestrator can consume. Because this is a test framework rather than a script collection, failures are first-class results with exit codes.

pyats run job NetworkHealth_job.py --testbed-file testbed.yml \
  --html-logs ./logs --json-report ./report

When a test fails, pyATS exits non-zero, which is the single behavior that lets a merge gate or a post-change job reject a bad deployment automatically. Pair the assertion with a trigger - genie ships triggers such as InterfaceUtilization or CpuUtilization - to collect extra context automatically at failure time instead of debugging blind hours later.

Custom parsers for anything missing

pyats shell --testbed-file testbed.yml
from genie.libs.parser.utils import get_parser_help
get_parser_help('show interfaces')

# structured output uses an XML control file per platform
# genie/libs/parser/iosxe/show_interface.py + show_interface.xml

Inspect an existing parser before writing one - nine times out of ten the command you need already exists, sometimes under a different platform directory. When you must extend, keep the parser's schema stable so downstream consumers do not break.

A custom parser is a Python class plus an XML control file that describes the schema (types, keys, list nesting). The class inherits from the platform parser base, you implement a cli() that returns the command string, and a p1() regex that captures the interesting blocks. Because the schema lives in XML, you can validate the shape of the output with genie.libs.parser.utils helpers before shipping, which catches the classic mistake of returning a dict where a list was expected.

Scaling to many devices

from genie.testbed import load
from concurrent.futures import ThreadPoolExecutor

testbed = load('testbed.yml')

def snapshot(name):
    d = testbed.devices[name]
    d.connect(log_stdout=False)
    out = d.parse('show version')
    d.disconnect()
    return name, out['version']['version']

with ThreadPoolExecutor(max_workers=16) as pool:
    for name, ver in pool.map(snapshot, testbed.devices):
        print(name, ver)

pyATS connects per device, so wall-clock time is dominated by SSH setup and per-command latency. Running devices in a thread pool cuts a fleet sweep from hours to minutes, but keep the worker count sane - network devices enforce SSH connection limits, and a TACACS server that rate-limits logins will lock you out faster than any router will. Twenty to thirty workers against a few hundred devices is a reasonable starting point; measure before you raise it.

Fitting pyATS into a pipeline

The natural place for pyATS is the validation stage of a change pipeline, sitting between "config pushed" and "window closed". A typical flow is: Ansible templates and pushes the intended config, pyATS connects and asserts the resulting state (interfaces up, BGP established, no new CRCs), and a learn-and-diff step produces the evidence artifact that attaches to the change ticket. If any assertion fails, the pipeline stops and a human looks before anything else moves. That is change control with proof rather than change control with hope, and it plugs cleanly into the Ansible/Netmiko toolchains covered in NAPALM getters and config diff and Oxidized config backup.

Design the assertions around what the change was supposed to accomplish, not around the commands you happened to type. If the window is "add a new VLAN to the core", the test should assert that the VLAN exists on every expected trunk, that the SVIs are up, and that no other VLAN was disturbed - the last one is what catches a fat-fingered trunk list. Keep the tests small and named after the intent so that a failure in a 2 a.m. report tells an on-call engineer what broke without having to open the script. A useful pattern is one test case per change type (add VLAN, add BGP peer, change an ACL), reused across windows, so the validation library grows with your operational experience instead of being rewritten every time.

Debugging common pyATS errors

# dump everything during connection setup
pyats parse "show interfaces" --testbed-file testbed.yml \
  --device core-sw1 --log-level debug

# is the parser the problem, or the connection?
dev.parse('show interfaces', timeout=60)

Three failures account for most wasted afternoons. SchemaEmptyParserError means the parser ran but matched nothing - usually a device output format the parser does not know, or a command that returned an error string. ConnectionError is transport: bad credentials, an ACL blocking your jump host, or the wrong os in the testbed. And a hang with no output is almost always a paging prompt or a login banner the framework is not expecting - disable terminal length 0 interaction issues by confirming the device's cli connection block has the right protocol. Reading the parser source for the exact command it sends is the fastest way to tell which of the three you have.

Where pyATS fits beside your other tools

pyATS is strongest for validation after change, Netmiko for straight-line CLI execution, NAPALM for getters and config diff at the API layer, and Ansible for orchestration and templating. Layering them - Ansible to push, pyATS to prove, Oxidized to archive - gives you change control with evidence. See also: Netmiko automation, NAPALM getters, Oxidized config backup and model-driven APIs compared.

原文链接:https://developer.cisco.com/docs/pyats/parsing-device-output