# Getting started with pyATS and Genie 

# Introduction

Most network automation projects focus on configuration deployment. Engineers use tools such as Python, Ansible, and NetBox to push changes across hundreds of devices.

However, one critical question remains:

**How do you verify that the network is operating correctly after a change?**

This is where **Cisco pyATS** and **Genie** become valuable.

**pyATS** (Python Automated Test Systems) is Cisco's open-source network testing and automation framework. At its core, it gives you a structured, programmatic way to connect to network devices, run commands, and validate results instead of manually SSH'ing in and reading the output.

**Genie** sits on top of pyATS and adds the parts that make it genuinely useful day-to-day :

*   **Parsers** — convert raw CLI text into structured Python dictionaries
    
*   **Models/Ops** — vendor-agnostic representations of network features (BGP, OSPF, interfaces, etc.), built by running several show commands and assembling them into one object
    
*   **Diff utilities** — compare two snapshots of state and show exactly what changed
    
*   **A test framework** (`aetest`) — for writing pass/fail validation scripts
    

# Why Traditional Validation Is Difficult ?

After a maintenance window, engineers typically perform manual checks such as:

*   Verify interfaces are up
    
*   Check routing adjacencies
    
*   Confirm BGP sessions
    
*   Verify MPLS LSPs
    
*   Check CPU and memory utilization
    

This approach has several challenges:

*   Time-consuming
    
*   Error-prone
    
*   Difficult to scale
    
*   No historical comparison
    
*   Inconsistent validation procedures
    

As networks grow, manual validation becomes increasingly unreliable.

# What Problem Does pyATS Solve?

If you've ever SSH'd into a router, run a show command, looked at the output, copy-pasted it into a spreadsheet and then repeated that for 20 routers .**pyATS** and **Genie** automate exactly that. The result comes back as structured Python data (dictionaries and lists) instead of plain text .

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Analogy: If SSH + CLI is like reading a paper map, Genie parsing is like using GPS coordinates. Same information, but a computer can use it directly.</div>
</div>

## Topology

![](https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/30cc0f35-2227-488f-8b98-e8c3a1fbfbf4.jpg align="center")

## Step 1 - Install pyATS

Use a virtual environment so this doesn't interfere with other Python projects.

```shell
python3 -m venv pyats-env
source pyats-env/bin/activate
pip install pyats[full]
```

Check if it worked:

```shell
pyats version check
```

You should see version numbers for pyATS and pyATS Library (Genie).

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Common issue: if installation fails with a build error, install Python dev headers first — <code>sudo dnf install python3-devel gcc</code> (Rocky/RHEL) or <code>sudo apt install python3-dev gcc</code> (Ubuntu/Debian).</div>
</div>

## Step 2 — Create a Testbed File

A testbed is a YAML file describing your device basically an inventory file. This is the only "config" pyATS needs.

Create `testbed.yaml`:

```yaml
testbed:
  name: my_first_lab

devices:
  cisco_xrv9000:
    os: iosxr
    type: router
    connections:
      defaults:
        class: unicon.Unicon
      cli:
        protocol: ssh
        ip: 172.20.20.15
        port: 22
    credentials:
      default:
        username: <username>
        password: <password>
```

## Step 3 — Connect to a Device

Your first script just connect and run a command, the "old fashioned" way:

```python
# 01_connect.py

from genie.testbed import load

tb = load("testbed.yaml")

router1 = tb.devices["cisco_xrv9000"]
router1.connect()

output = router1.execute("show version")
print(output)

router1.disconnect()
```

Execute it :

```python
python 01_connect.py
```

At this point, output is just a big string same as what you'd see in a terminal. Nothing magic yet. That changes in the next step.

## Step 4 — Your First Parse (The Magic Step)

This is the single most useful thing Genie does. Instead of `execute()`, use `parse()`:

```python
# 02_parse.py
from genie.testbed import load

tb = load("testbed.yaml")
router1 = tb.devices["cisco_xrv9000"]
router1.connect()

# The key difference: parse() instead of execute()
parsed = router1.parse("show ip interface brief")

print(parsed)

router1.disconnect()
```

Expected Output:

```python
2026-06-19 13:23:34,058: %UNICON-INFO: +++ cisco_xrv9000 with via 'cli': executing command 'show ip interface brief' +++
show ip interface brief
Fri Jun 19 01:24:13.557 UTC

Interface                      IP-Address      Status          Protocol Vrf-Name
Loopback0                      10.10.10.3      Up              Up       default
Loopback100                    172.165.11.1    Up              Up       multi-vendor
MgmtEth0/RP0/CPU0/0            10.0.0.15       Up              Up       clab-mgmt
GigabitEthernet0/0/0/0         192.168.1.3     Up              Up       default
GigabitEthernet0/0/0/1         unassigned      Shutdown        Down     default
GigabitEthernet0/0/0/2         10.57.254.3     Up              Up       multi-vendor

# Magic of parsing is instead of receiving a text you get structured data - python dictionary 

RP/0/RP0/CPU0:cisco_xrv9000#
{'interface': {'Loopback0': {'ip_address': '10.10.10.3', 'interface_status': 'Up', 'protocol_status': 'Up', 'vrf_name': 'default'}, 'Loopback100': {'ip_address': '172.165.11.1', 'interface_status': 'Up', 'protocol_status': 'Up', 'vrf_name': 'multi-vendor'}, 'MgmtEth0/RP0/CPU0/0': {'ip_address': '10.0.0.15', 'interface_status': 'Up', 'protocol_status': 'Up', 'vrf_name': 'clab-mgmt'}, 'GigabitEthernet0/0/0/0': {'ip_address': '192.168.1.3', 'interface_status': 'Up', 'protocol_status': 'Up', 'vrf_name': 'default'}, 'GigabitEthernet0/0/0/1': {'ip_address': 'unassigned', 'interface_status': 'Shutdown', 'protocol_status': 'Down', 'vrf_name': 'default'}, 'GigabitEthernet0/0/0/2': {'ip_address': '10.57.254.3', 'interface_status': 'Up', 'protocol_status': 'Up', 'vrf_name':
```

Now you can use it like any Python dict:

```python
for intf, details in parsed['interface'].items():
    if details['interface_status'] != 'Up':
        print(f"⚠️  {intf} is DOWN")
    else:
        print(f"✅ {intf} is UP — {details['ip_address']}")
```

Expected Output:

```python
2026-06-19 13:59:52,422: %UNICON-INFO: +++ cisco_xrv9000 with via 'cli': executing command 'show ip interface brief' +++
show ip interface brief
Fri Jun 19 02:00:31.950 UTC

Interface                      IP-Address      Status          Protocol Vrf-Name
Loopback0                      10.10.10.3      Up              Up       default
Loopback100                    172.165.11.1    Up              Up       multi-vendor
MgmtEth0/RP0/CPU0/0            10.0.0.15       Up              Up       clab-mgmt
GigabitEthernet0/0/0/0         192.168.1.3     Up              Up       default
GigabitEthernet0/0/0/1         unassigned      Shutdown        Down     default
GigabitEthernet0/0/0/2         10.57.254.3     Up              Up       multi-vendor

RP/0/RP0/CPU0:cisco_xrv9000#
✅ Loopback0 is UP — 10.10.10.3
✅ Loopback100 is UP — 172.165.11.1
✅ MgmtEth0/RP0/CPU0/0 is UP — 10.0.0.15
✅ GigabitEthernet0/0/0/0 is UP — 192.168.1.3
⚠️  GigabitEthernet0/0/0/1 is DOWN
✅ GigabitEthernet0/0/0/2 is UP — 10.57.254.3
```

**That's it.** You just turned CLI output into something you can filter, alert on, or feed into another system with no regex.

## Step 5 — Don't Know If a Parser Exists? Check First

Not every show command has a parser . Two ways to check before you write code:

### Option A — From the command line:

```python
genie parse "show ip interface brief" \
    --testbed-file testbed.yaml \
    --devices cisco_xrv9000
```

Expected Output:

```python
0%|                                                                                 | 0/1 [00:00<?, ?it/s]{
  "interface": {
    "GigabitEthernet0/0/0/0": {
      "interface_status": "Up",
      "ip_address": "192.168.1.3",
      "protocol_status": "Up",
      "vrf_name": "default"
    },
    "GigabitEthernet0/0/0/1": {
      "interface_status": "Shutdown",
      "ip_address": "unassigned",
      "protocol_status": "Down",
      "vrf_name": "default"
    },
    "GigabitEthernet0/0/0/2": {
      "interface_status": "Up",
      "ip_address": "10.57.254.3",
      "protocol_status": "Up",
      "vrf_name": "multi-vendor"
    },
    "Loopback0": {
      "interface_status": "Up",
      "ip_address": "10.10.10.3",
      "protocol_status": "Up",
      "vrf_name": "default"
    },
    "Loopback100": {
      "interface_status": "Up",
      "ip_address": "172.165.11.1",
      "protocol_status": "Up",
      "vrf_name": "multi-vendor"
    },
    "MgmtEth0/RP0/CPU0/0": {
      "interface_status": "Up",
      "ip_address": "10.0.0.15",
      "protocol_status": "Up",
      "vrf_name": "clab-mgmt"
    }
  }
}
100%|██████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00,  1.25it/s]
```

### Option B — Browse online :

Visit the Genie Feature Browser (pubhub.devnetcloud.com) and search for your platform + command.

![](https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/6b56da77-1119-4fe5-b3d8-bc0351652b31.jpg align="center")

## Step 6 — learn(): Snapshot an Entire Feature

`parse()` handles one command. `learn()` runs several commands and builds a complete model of a feature like OSPF, BGP, or interfaces all in one line.

```python
from genie.testbed import load

tb = load("testbed.yaml")
router1 = tb.devices["cisco_xrv9000"]
router1.connect()

# One line — runs multiple show commands behind the scenes
interfaces = router1.learn("interface")

for name, data in interfaces.info.items():
    print(name, "->", data.get("oper_status"))

router1.disconnect()
```

Expected output :

```python
2026-06-19 14:09:33,278: %UNICON-INFO: +++ cisco_xrv9000 with via 'cli': executing command 'show vrf all detail' +++
show vrf all detail
Fri Jun 19 02:10:12.809 UTC

VRF clab-mgmt; RD not set; VPN ID not set
VRF mode: Regular
Description Containerlab management VRF (DO NOT DELETE)
Interfaces:
  MgmtEth0/RP0/CPU0/0
Address family IPV4 Unicast
  No import VPN route-target communities
  No export VPN route-target communities
  No import route policy
  No export route policy
Address family IPV6 Unicast
  No import VPN route-target communities
  No export VPN route-target communities
  No import route policy
  No export route policy

2026-06-19 14:09:33,656: %UNICON-INFO: +++ cisco_xrv9000 with via 'cli': executing command 'show interface detail' +++
show interface detail
Fri Jun 19 02:10:13.162 UTC
Loopback0 is up, line protocol is up
  Interface state transitions: 1
  Hardware is Loopback interface(s)
  Internet address is 10.10.10.3/32
  MTU 1500 bytes, BW 0 Kbit
     reliability Unknown, txload Unknown, rxload Unknown
  Encapsulation Loopback,  loopback not set,
  Last link flapped 9w4d
  Last input Unknown, output Unknown
  Last clearing of "show interface" counters Unknown
  Input/output data rate is disabled.

Loopback100 is up, line protocol is up
  Interface state transitions: 1
  Hardware is Loopback interface(s)
  Description: VRF-TEST-LOOPBACK
  Internet address is 172.165.11.1/24
  MTU 1500 bytes, BW 0 Kbit
     reliability Unknown, txload Unknown, rxload Unknown
  Encapsulation Loopback,  loopback not set,
  Last link flapped 9w4d
  Last input Unknown, output Unknown
  Last clearing of "show interface" counters Unknown
  Input/output data rate is disabled.

2026-06-19 14:09:34,238: %UNICON-INFO: +++ cisco_xrv9000 with via 'cli': executing command 'show ethernet tags' +++
show ethernet tags
Fri Jun 19 02:10:13.743 UTC
St:    AD - Administratively Down, Dn - Down, Up - Up
Ly:    L2 - Switched layer 2 service, L3 = Terminated layer 3 service,
Xtra   C - Match on Cos, E  - Match on Ethertype, M - Match on source MAC
-,+:   Ingress rewrite operation; number of tags to pop and push respectively

2026-06-19 14:09:34,607: %UNICON-INFO: +++ cisco_xrv9000 with via 'cli': executing command 'show interfaces accounting' +++
show interfaces accounting
Fri Jun 19 02:10:14.110 UTC
No accounting statistics available for Loopback100

No accounting statistics available for Null0

GigabitEthernet0/0/0/0
  Protocol              Pkts In         Chars In     Pkts Out        Chars Out
  IPV4_UNICAST          3517881        270803858           27             2220
  IPV4_MULTICAST              0                0      1264246         88497220
  MPLS                    51762          3974532      3032839       1444721933
  ARP                       407            24420          407            17094
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Note</strong> : the above output was truncated. The<code> learn ()</code> function runs multiple commands</div>
</div>

## Step 7 — Before/After Diffs (Change Window Pattern)

This is the pattern that makes pyATS genuinely useful day-to-day: **snapshot**, **change**, **snapshot again**, **diff**.

There are two ways to do this a **no-code CLI** option (great for quick checks and change windows), and a **Python** option (better when this logic needs to live inside a larger script).

### Option A — No code: genie learn + genie diff

Collect a baseline before your change:

```python
 genie learn bgp \
    --testbed-file testbed.yaml \
    --output baseline
```

Expected Output:

```python
Learning '['bgp']' on devices '['cisco_xrv9000']'
100%|████████████████████████████████████| 1/1 [00:10<00:00, 10.03s/it]
+=============================================================+
| Genie Learn Summary for device cisco_xrv9000                                 |
+=============================================================+
|  Connected to cisco_xrv9000                                                  |
|  -   Log: baseline/connection_cisco_xrv9000.txt                              |
|-------------------------------------------------------------|
|  Learnt feature 'bgp'                                                        |
|  -  Ops structure:  baseline/bgp_iosxr_cisco_xrv9000_ops.txt                 |
|  -  Device Console: baseline/bgp_iosxr_cisco_xrv9000_console.txt                  |
|=============================================================|
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Simulated an issue on <strong>PE1</strong> by dropping the BGP session</div>
</div>

Collect the current state:

```python
 genie learn bgp \
    --testbed-file testbed.yaml \
    --output output
```

Expected Output:

```python
Learning '['bgp']' on devices '['cisco_xrv9000']'
100%|███████████████████████████████████████| 1/1 [00:10<00:00, 10.53s/it]
+=============================================================+
| Genie Learn Summary for device cisco_xrv9000                                 |
+=============================================================+
|  Connected to cisco_xrv9000                                                  |
|  -   Log: current/connection_cisco_xrv9000.txt                               |
|-------------------------------------------------------------|
|  Learnt feature 'bgp'                                                        |
|  -  Ops structure:  current/bgp_iosxr_cisco_xrv9000_ops.txt                  |
|  -  Device Console:   current/bgp_iosxr_cisco_xrv9000_console.txt                   |
|=============================================================|
```

Now diff the two snapshot folders:

```python
genie diff baseline current

1it [00:00, 441.13it/s]
+=============================================================+
| Genie Diff Summary between directories baseline/ and current/                |
+=============================================================+
|  File: bgp_iosxr_cisco_xrv9000_ops.txt                                       |
|   - Diff can be found at ./diff_bgp_iosxr_cisco_xrv9000_ops.txt              |
|-------------------------------------------------------------|
```

Expected Output:

```python
cat ./diff_bgp_iosxr_cisco_xrv9000_ops.txt

--- baseline/bgp_iosxr_cisco_xrv9000_ops.txt
+++ current/bgp_iosxr_cisco_xrv9000_ops.txt
 info:
  instance:
   default:
    vrf:
     default:
      neighbor:
       10.10.10.1:
-       session_state: established
+       session_state: idle
-       bgp_negotiated_capabilities:
-        four_octets_asn: advertised received
-        route_refresh: advertised received
-        vpnv4_unicast: advertised received
 routes_per_peer:
  instance:
   default:
    vrf:
     default:
      neighbor:
       10.10.10.1:
        address_family:
-        vpnv4 unicast RD 65000:10:
-         routes:
-          10.57.255.0/24:
-           index:
-            1:
-             next_hop: 10.10.10.1
-             origin_codes: i
-             status_codes: *>i
-          172.165.10.0/24:
-           index:
-            1:
-             next_hop: 10.10.10.1
-             origin_codes: i
-             status_codes: *>i
-        vpnv4 unicast RD 65000:30:
-         advertised:
-          10.57.254.0/24:
-           index:
-            1:
-             froms: Local
-             next_hop: 10.10.10.3
-             origin_code: ?
-          172.165.11.0/24:
-           index:
-            1:
-             froms: Local
-             next_hop: 10.10.10.3
-             origin_code: ?
 table:
  instance:
   default:
    vrf:
     default:
      address_family:
-      vpnv4 unicast RD 65000:10:
-       prefixes:
-        10.57.255.0/24:
-         index:
-          1:
-           locprf: 100
-           next_hop: 10.10.10.1
-           origin_codes: i
-           status_codes: *>i
-           weight: 0
-        172.165.10.0/24:
-         index:
-          1:
-           locprf: 100
-           next_hop: 10.10.10.1
-           origin_codes: i
-           status_codes: *>i
-           weight: 0
-       route_distinguisher: 65000:10


```

### Step 8 — A Simple Pass/Fail Test

pyATS has a built-in test framework called `aetest`. Here's the minimum useful structure: connect, check something, report pass/fail.

```python
from pyats import aetest
from genie.testbed import load


class CommonSetup(aetest.CommonSetup):
    @aetest.subsection
    def connect(self, testbed):
        for device in testbed.devices.values():
            device.connect()


class CheckBGP(aetest.Testcase):
    @aetest.test
    def all_neighbors_established(self, testbed):
        router1 = testbed.devices["cisco_xrv9000"]
        bgp = router1.parse("show bgp summary")

        for instance_name, instance_data in bgp["instance"].items():
            for vrf_name, vrf_data in instance_data.get("vrf", {}).items():
                for ip, neighbor_data in vrf_data.get("neighbor", {}).items():

                    # state_pfxrcd is nested under address_family on XR
                    af_data = neighbor_data.get("address_family", {})
                    established = False

                    for af_name, af_details in af_data.items():
                        pfxrcd = af_details.get("state_pfxrcd")
                        up_down = af_details.get("up_down")

                        # A numeric state_pfxrcd means session is Established
                        if pfxrcd is not None and str(pfxrcd).isdigit():
                            established = True
                            self.passed(
                                f"[{instance_name}/{vrf_name}] {ip} ({af_name}) "
                                f"is Established — up {up_down}, pfx received: {pfxrcd}"
                            )

                    if not established:
                        self.failed(
                            f"[{instance_name}/{vrf_name}] {ip} is DOWN"
                        )


class CommonCleanup(aetest.CommonCleanup):
    @aetest.subsection
    def disconnect(self, testbed):
        for device in testbed.devices.values():
            device.disconnect()


def main():
    tb = load("testbed.yaml")
    aetest.main(testbed=tb)


main()
```

Expected Output:

```python
RP/0/RP0/CPU0:cisco_xrv9000#
2026-06-22T14:10:47: %AETEST-INFO: Passed reason: [all/default] 10.10.10.1 (ipv4 unicast) is Established — up 2d22h, pfx received: 0
2026-06-22T14:10:47: %AETEST-INFO: The result of section all_neighbors_established is => PASSED
2026-06-22T14:10:47: %AETEST-INFO: The result of testcase CheckBGP is => PASSED
2026-06-22T14:10:47: %AETEST-INFO: +--------------------------+
2026-06-22T14:10:47: %AETEST-INFO: |                          Starting common cleanup            |
2026-06-22T14:10:47: %AETEST-INFO: +--------------------------+
2026-06-22T14:10:47: %AETEST-INFO: +--------------------------+
2026-06-22T14:10:47: %AETEST-INFO: |                        Starting subsection disconnect     |
2026-06-22T14:10:47: %AETEST-INFO: +--------------------------+
2026-06-22T14:10:58: %AETEST-INFO: The result of subsection disconnect is => PASSED
2026-06-22T14:10:58: %AETEST-INFO: The result of common cleanup is => PASSED
2026-06-22T14:10:58: %AETEST-INFO: +--------------------------+
2026-06-22T14:10:58: %AETEST-INFO: |                               Detailed Results                               |
2026-06-22T14:10:58: %AETEST-INFO: +--------------------------+
2026-06-22T14:10:58: %AETEST-INFO:  SECTIONS/TESTCASES                                                      RESULT
2026-06-22T14:10:58: %AETEST-INFO: ----------------------------
2026-06-22T14:10:58: %AETEST-INFO: .
2026-06-22T14:10:58: %AETEST-INFO: |--common_setup                                                        PASSED
2026-06-22T14:10:58: %AETEST-INFO: |-- connect                                                           PASSED
2026-06-22T14:10:58: %AETEST-INFO: |-- CheckBGP                                                              PASSED
2026-06-22T14:10:58: %AETEST-INFO: |-- all_neighbors_established                                       PASSED
2026-06-22T14:10:58: %AETEST-INFO: -- common_cleanup                                                        PASSED
2026-06-22T14:10:58: %AETEST-INFO -- disconnect                                                        PASSED
2026-06-22T14:10:58: %AETEST-INFO: +--------------------------+
2026-06-22T14:10:58: %AETEST-INFO: |                                   Summary                            |
2026-06-22T14:10:58: %AETEST-INFO: +--------------------------+
2026-06-22T14:10:58: %AETEST-INFO:  Number of ABORTED                                                            0
2026-06-22T14:10:58: %AETEST-INFO:  Number of BLOCKED                                                            0
2026-06-22T14:10:58: %AETEST-INFO:  Number of ERRORED                                                            0
2026-06-22T14:10:58: %AETEST-INFO:  Number of FAILED                                                             0
2026-06-22T14:10:58: %AETEST-INFO:  Number of PASSED                                                             3
2026-06-22T14:10:58: %AETEST-INFO:  Number of PASSX                                                              0
2026-06-22T14:10:58: %AETEST-INFO:  Number of SKIPPED                                                            0
2026-06-22T14:10:58: %AETEST-INFO:  Total Number                                                                 3
2026-06-22T14:10:58: %AETEST-INFO:  Success Rate                                                            100.0%
2026-06-22T14:10:58: %AETEST-INFO: ----------------------------
```

# Quick Reference Cheat Sheet

| What you want to do | Code |
| --- | --- |
| Load your inventory | tb = load("testbed.yaml") |
| Connect to a device | device.connect() |
| Run a raw command | device.execute("show ...") |
| Get structured data | device.parse("show ...") |
| Snapshot a whole feature | device.learn("bgp") |
| Compare two snapshots | Diff(before, after) diff.findDiff() |
| Check available parsers | genie parsers show --os |

# Further Reading

*   pyATS Documentation — developer.cisco.com/docs/pyats
    
*   Genie Feature Browser — pubhub.devnetcloud.com/media/genie-feature-browser
    
*   pyATS GitHub — github.com/CiscoTestAutomation/pyats
    
*   Cisco DevNet pyATS Learning Labs — developer.cisco.com/learning
