Practice Questions

Test your knowledge on this topic in the ENAUTO Exam Trainer — 186 questions across 5 interactive modes.

Operations – Deep Dive for ENAUTO v2.0

Exam Relevance

Topic 4.0 Operations (20%) covers testing, simulations, software management, health monitoring, telemetry, and webhooks. This guide covers the gap topics: 4.1 (testing/validation), 4.2 (simulations), and 4.5 (telemetry). For covered topics, see: Catalyst_Center_Deep_Dive (SWIM 4.3, Health 4.4), Meraki_Deep_Dive (Webhooks 4.6), SDWAN_Deep_Dive (SWIM 4.3, Health 4.4).

Exam Topics Covered:
4.1 – Testing and validation with pyATS/Genie
4.2 – Network topology simulations (CML)
4.5 – Model-Driven Telemetry (MDT)

Table of Contents


1 – pyATS and Genie for Testing and Validation

Exam Topic: 4.1 — Construct automated testing and validation of network state with pyATS/Genie

What is pyATS?

pyATS (Python Automated Test System) is Cisco’s open-source test automation framework. Originally an internal Cisco tool, it was released to the community and is now the standard for programmatic network testing and validation.

┌─────────────────────────────────────────────┐
│              pyATS Framework                │
│                                             │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐ │
│  │ Testbed  │  │ aetest   │  │ Reporting │ │
│  │ (YAML)   │  │ (Tests)  │  │ (Logs)    │ │
│  └──────────┘  └──────────┘  └───────────┘ │
│                                             │
│  ┌─────────────────────────────────────────┐│
│  │         Genie Libraries                 ││
│  │  • learn()  — operational state models  ││
│  │  • parse()  — structured show commands  ││
│  │  • Diff     — pre/post comparison       ││
│  │  • Triggers — automated actions         ││
│  │  • Verifications — automated checks     ││
│  └─────────────────────────────────────────┘│
│                                             │
│  ┌─────────────────────────────────────────┐│
│  │         Unicon (Connection Library)     ││
│  │  SSH · Telnet · Console · REST          ││
│  └─────────────────────────────────────────┘│
└─────────────────────────────────────────────┘

What is Genie?

Genie is the library layer built on top of pyATS. While pyATS provides the test framework (test structure, reporting, topology), Genie provides the network-specific intelligence: device models, parsers for show commands, and the Diff engine for state comparison.

pyATS vs Genie — Exam Distinction

ComponentPurposeExample
pyATSTest framework (testbed, test runner, reporting)aetest.test, loader.load()
GenieNetwork libraries (parsers, models, Diff)device.learn(), device.parse(), Diff()
UniconConnection library for device interactionSSH, Telnet, console connections

In practice, you install them together. The exam may test whether you know which layer does what.

Installation

pip install pyats[full]

This installs pyATS core, Genie libraries, Unicon connection library, and all supported parser packages. Requires Python 3.8+.

Testbed File (YAML)

The testbed file defines your network topology — devices, credentials, and connection details. This is the entry point for all pyATS operations.

testbed:
  name: enauto-lab
 
devices:
  csr1000v:
    os: iosxe
    type: router
    connections:
      defaults:
        class: unicon.Unicon
      cli:
        protocol: ssh
        ip: 10.10.20.48
    credentials:
      default:
        username: developer
        password: C1sco12345
 
  nx-osv:
    os: nxos
    type: switch
    connections:
      defaults:
        class: unicon.Unicon
      cli:
        protocol: ssh
        ip: 10.10.20.58
    credentials:
      default:
        username: admin
        password: Admin_1234!

Testbed Keys to Remember

  • os: determines which parser library to use (iosxe, nxos, iosxr, etc.)
  • type: is informational only (router, switch, firewall)
  • connections.defaults.class: unicon.Unicon is required for SSH/Telnet
  • Credentials can be encrypted using pyats secret for production use

Connecting to Devices

from pyats.topology import loader
 
# Load testbed from YAML
testbed = loader.load('testbed.yaml')
 
# Get a specific device
device = testbed.devices['csr1000v']
 
# Connect (establishes SSH session)
device.connect()
 
# Execute a command (raw string output)
output = device.execute('show version')
print(output)
 
# Disconnect
device.disconnect()

Connection Options

# Connect without logging to console
device.connect(log_stdout=False)
 
# Connect and learn device state automatically
device.connect(learn_hostname=True)

Learning Structured Data with device.learn()

learn() creates a comprehensive model of a feature across all relevant show commands. The result is a structured Python object (dictionary-like) — not raw CLI text.

from pyats.topology import loader
 
testbed = loader.load('testbed.yaml')
device = testbed.devices['csr1000v']
device.connect(log_stdout=False)
 
# Learn all interface data (runs multiple show commands internally)
interfaces = device.learn('interface')
 
# Access structured data
for name, data in interfaces.info.items():
    status = data.get('oper_status', 'unknown')
    print(f"{name}: {status}")
 
# Available features to learn:
# interface, ospf, bgp, vrf, routing, acl, arp, platform, etc.

Parsing Show Commands with device.parse()

parse() takes a single show command and returns structured output as a Python dictionary. Unlike learn(), it maps 1:1 to a specific CLI command.

# Parse a specific show command into structured data
routes = device.parse('show ip route')
 
# Access parsed data
for protocol, data in routes.get('vrf', {}).get('default', {}).get('address_family', {}).get('ipv4', {}).get('routes', {}).items():
    print(f"Route: {protocol} via {data.get('next_hop', {})}")
 
# Parse show ip interface brief
brief = device.parse('show ip interface brief')
for intf, details in brief.get('interface', {}).items():
    print(f"{intf}: {details['status']}/{details['protocol']}")

parse() vs learn() — When to Use Which

MethodScopeUse Case
parse()Single show commandQuick check of specific output
learn()Entire feature (multiple commands)Comprehensive state capture
execute()Raw commandUnstructured output, custom commands

Pre/Post Change Validation with Genie Diff

This is a core exam topic. The Diff engine compares two snapshots of device state and reports what changed.

from pyats.topology import loader
from genie.utils.diff import Diff
 
testbed = loader.load('testbed.yaml')
device = testbed.devices['csr1000v']
device.connect(log_stdout=False)
 
# STEP 1: Capture pre-change state
pre_snapshot = device.learn('interface')
 
# STEP 2: Make a change (e.g., shut down an interface)
device.configure('''
interface GigabitEthernet3
 shutdown
''')
 
# STEP 3: Capture post-change state
post_snapshot = device.learn('interface')
 
# STEP 4: Compare with Diff
diff = Diff(pre_snapshot, post_snapshot)
diff.findDiff()
 
# Print what changed
print(diff)
# Output shows:
#  GigabitEthernet3:
#   enabled: True -> False
#   oper_status: up -> down

Diff Requires Same Object Types

Both arguments to Diff() must be the same type — both from learn() or both from parse(). Mixing them will produce incorrect or empty results.

pyATS Test Scripts (aetest)

pyATS test scripts use the aetest module to define structured, reportable test cases. The structure follows a three-phase pattern.

"""
Verify all interfaces are operationally UP.
Exam Topic: 4.1
"""
import logging
from pyats import aetest
from pyats.topology import loader
 
logger = logging.getLogger(__name__)
 
# ─── PHASE 1: Common Setup ───────────────────────────────────
class CommonSetup(aetest.CommonSetup):
    """Connect to all devices in the testbed."""
 
    @aetest.subsection
    def connect_to_devices(self, testbed):
        for device_name, device in testbed.devices.items():
            device.connect(log_stdout=False)
            self.parent.parameters[device_name] = device
 
    @aetest.subsection
    def mark_tests(self, testbed):
        """Dynamically mark the test to loop over each device."""
        aetest.loop.mark(
            InterfaceStatusCheck,
            device_name=list(testbed.devices.keys())
        )
 
# ─── PHASE 2: Test Cases ─────────────────────────────────────
class InterfaceStatusCheck(aetest.Testcase):
    """Verify that all interfaces are UP on a device."""
 
    @aetest.setup
    def learn_interfaces(self, device_name):
        device = self.parent.parameters[device_name]
        self.interfaces = device.learn('interface')
 
    @aetest.test
    def check_oper_status(self, device_name):
        failed_intfs = []
        for name, data in self.interfaces.info.items():
            # Skip management and loopback interfaces
            if 'Loopback' in name or 'Mgmt' in name:
                continue
            oper_status = data.get('oper_status', 'unknown')
            if oper_status != 'up':
                failed_intfs.append(f"{name}: {oper_status}")
 
        if failed_intfs:
            self.failed(f"Interfaces down: {', '.join(failed_intfs)}")
        else:
            self.passed("All interfaces are UP")
 
# ─── PHASE 3: Common Cleanup ─────────────────────────────────
class CommonCleanup(aetest.CommonCleanup):
    """Disconnect from all devices."""
 
    @aetest.subsection
    def disconnect(self, testbed):
        for device in testbed.devices.values():
            device.disconnect()
 
# ─── Run standalone ──────────────────────────────────────────
if __name__ == '__main__':
    testbed = loader.load('testbed.yaml')
    aetest.main(testbed=testbed)

aetest Result Methods

MethodMeaning
self.passed()Test passed
self.failed()Test failed (continues to next test)
self.errored()Test encountered an unexpected error
self.skipped()Test was intentionally skipped
self.blocked()Test blocked by a dependency failure

Running pyATS from the Command Line

# Run a test script
pyats run job my_test.py --testbed testbed.yaml
 
# Generate HTML report
pyats logs view
 
# Run Genie learn from CLI (no script needed)
genie learn interface --testbed testbed.yaml --output pre_snapshot/
 
# Compare two snapshots from CLI
genie diff pre_snapshot/ post_snapshot/ --output diff_report/

Robot Framework Integration

pyATS integrates with Robot Framework for keyword-driven testing (brief mention for exam awareness):

*** Settings ***
Library    pyats.robot.pyATSRobot
 
*** Test Cases ***
Connect To Device
    use testbed "testbed.yaml"
    connect to device "csr1000v"
 
Verify Interface Count
    ${output}=    parse "show ip interface brief" on device "csr1000v"
    # Keyword-driven assertions on parsed output

pyATS for CI/CD Integration

pyATS scripts can be integrated into CI/CD pipelines for automated network validation:

# .gitlab-ci.yml
stages:
  - validate
 
network-validation:
  stage: validate
  image: ciscotestautomation/pyats:latest
  script:
    - pyats run job interface_check.py --testbed testbed.yaml
    - pyats logs view --report
  artifacts:
    paths:
      - logs/

Triggers and Verifications in Genie

Genie provides built-in Triggers (actions) and Verifications (checks) that can be combined into test profiles without writing custom code.

# Verifications: Check device state
# Run all built-in verifications for a device
# CLI approach:
# genie run --trigger-uids="" --verification-uids="Verify_Interfaces" --testbed testbed.yaml

Built-in Genie Verifications (Exam Awareness)

  • Verify_Interfaces — All interfaces UP
  • Verify_BgpAllNexthopReachable — BGP next-hops reachable
  • Verify_NtpAssociations — NTP associations valid
  • Verify_Hsrp — HSRP states correct

Triggers include TriggerShutNoShutInterface, TriggerClearBgp, etc. These test device resilience.


2 – Network Topology Simulations

Exam Topic: 4.2 — Construct network simulations with CML / VIRL

Cisco Modeling Labs (CML)

CML (formerly VIRL) is Cisco’s network simulation platform for creating virtual network topologies. It runs virtual instances of IOS XE, NX-OS, IOS XR, and third-party images.

┌─────────────────────────────────────────────┐
│              CML Server                     │
│         (Bare Metal or VM)                  │
│                                             │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐ │
│  │ REST API │  │ Web UI   │  │ Image     │ │
│  │ /api/v0/ │  │ (Cockpit)│  │ Manager   │ │
│  └──────────┘  └──────────┘  └───────────┘ │
│                                             │
│  ┌─────────────────────────────────────────┐│
│  │         Lab Topology Engine             ││
│  │  Nodes · Interfaces · Links · Configs   ││
│  └─────────────────────────────────────────┘│
│                                             │
│  ┌─────────────────────────────────────────┐│
│  │         Virtual Network Devices         ││
│  │  IOSv · CSR1000v · NXOSv · ASAv · etc  ││
│  └─────────────────────────────────────────┘│
└─────────────────────────────────────────────┘

CML REST API

The CML API allows you to programmatically create, manage, and tear down network topologies.

Authentication

import requests
 
CML_HOST = "https://10.10.20.161"
USERNAME = "developer"
PASSWORD = "C1sco12345"
 
# Authenticate — returns a JWT token as a plain string
auth_response = requests.post(
    f"{CML_HOST}/api/v0/authenticate",
    json={"username": USERNAME, "password": PASSWORD},
    verify=False
)
token = auth_response.json()  # Returns a plain string token (not a dict)
 
headers = {"Authorization": f"Bearer {token}"}

CML Auth Returns a Plain String

Unlike Catalyst Center (which returns {"Token": "..."} in a dict), CML authenticate returns the token as a raw JSON string. So auth_response.json() gives you the token directly — no key lookup needed.

Core API Operations

# List all labs
labs = requests.get(f"{CML_HOST}/api/v0/labs", headers=headers, verify=False)
print(labs.json())  # List of lab IDs
 
# Create a new lab
new_lab = requests.post(
    f"{CML_HOST}/api/v0/labs",
    headers=headers,
    json={"title": "automation-test"},
    verify=False
)
lab_id = new_lab.json()  # Returns lab ID string
 
# Get lab details
lab_detail = requests.get(
    f"{CML_HOST}/api/v0/labs/{lab_id}",
    headers=headers,
    verify=False
)
 
# Add a node to the lab
node = requests.post(
    f"{CML_HOST}/api/v0/labs/{lab_id}/nodes",
    headers=headers,
    json={
        "label": "router1",
        "node_definition": "iosv",
        "x": 100,
        "y": 200
    },
    verify=False
)
node_id = node.json()  # Returns node ID
 
# Start the entire lab
requests.put(
    f"{CML_HOST}/api/v0/labs/{lab_id}/state/start",
    headers=headers,
    verify=False
)
 
# Stop and wipe the lab
requests.put(
    f"{CML_HOST}/api/v0/labs/{lab_id}/state/stop",
    headers=headers,
    verify=False
)
 
# Delete the lab
requests.delete(
    f"{CML_HOST}/api/v0/labs/{lab_id}",
    headers=headers,
    verify=False
)

Python virl2-client SDK

The virl2-client package provides a higher-level Python interface for CML.

pip install virl2-client
from virl2_client import ClientLibrary
 
# Connect to CML server
client = ClientLibrary(
    "https://10.10.20.161",
    "developer",
    "C1sco12345",
    ssl_verify=False
)
 
# Create a new lab
lab = client.create_lab("enauto-test")
 
# Add nodes
r1 = lab.create_node("R1", "iosv", 100, 200)
r2 = lab.create_node("R2", "iosv", 300, 200)
sw1 = lab.create_node("SW1", "iosvl2", 200, 400)
 
# Create links between interfaces
# Get first available interface on each node
intf_r1 = r1.get_interface_by_slot(0)
intf_r2 = r2.get_interface_by_slot(0)
lab.create_link(intf_r1, intf_r2)
 
# Connect switch to routers
intf_r1_sw = r1.get_interface_by_slot(1)
intf_sw1_r1 = sw1.get_interface_by_slot(0)
lab.create_link(intf_r1_sw, intf_sw1_r1)
 
# Start the lab
lab.start()
 
# Wait until all nodes are booted
lab.wait_until_lab_converged()
 
# Check node states
for node in lab.nodes():
    print(f"{node.label}: {node.state}")
 
# Get console access URL
print(r1.console_url)
 
# Stop and remove the lab
lab.stop()
lab.wipe()
lab.remove()

Topology Export and Import

CML topologies can be exported as YAML and re-imported — useful for versioning lab environments in Git.

# Export topology to YAML
topology_yaml = lab.download()
with open("topology.yaml", "w") as f:
    f.write(topology_yaml)
 
# Import topology from YAML
with open("topology.yaml", "r") as f:
    topology_data = f.read()
imported_lab = client.import_lab(topology_data, title="imported-lab")
imported_lab.start()

Integration with CI/CD

Combine CML with pyATS for automated pre-deployment testing:

# .gitlab-ci.yml — Spin up topology, test, tear down
stages:
  - simulate
  - test
  - cleanup
 
spin-up-lab:
  stage: simulate
  script:
    - python create_lab.py   # Uses virl2-client to create + start topology
    - sleep 120              # Wait for nodes to boot
 
run-pyats-tests:
  stage: test
  script:
    - pyats run job validate_network.py --testbed cml_testbed.yaml
  artifacts:
    paths:
      - logs/
 
teardown-lab:
  stage: cleanup
  when: always
  script:
    - python destroy_lab.py  # Stop, wipe, and remove lab

Use Cases for Network Simulations

Use CaseDescription
Pre-deployment testingValidate configs before pushing to production
Automation developmentDevelop and test scripts safely
CI/CD pipeline validationAutomated topology + test in pipelines
Training and certificationPractice labs without physical hardware
Failure scenario testingSimulate link failures, convergence events

EVE-NG as an Alternative

EVE-NG (Emulated Virtual Environment - Next Generation) is a community/commercial alternative to CML. While the exam focuses on CML, EVE-NG supports similar use cases and also offers a REST API for programmatic control. The exam does not test EVE-NG specifics.


3 – Model-Driven Telemetry

Exam Topic: 4.5 — Construct and implement model-driven telemetry

What is Model-Driven Telemetry (MDT)?

Model-Driven Telemetry is a push-based data collection mechanism where network devices proactively stream operational data to collectors. This is fundamentally different from traditional SNMP polling.

Traditional SNMP Polling:
  Collector ──GET──> Device    (pull every N seconds)
  Collector <──Response── Device

Model-Driven Telemetry:
  Device ──STREAM──> Collector  (push at configured interval)
  Device ──STREAM──> Collector
  Device ──STREAM──> Collector
  (continuous, no polling overhead on the device)

Key characteristics:

  • Push-based: device sends data without being asked
  • YANG model-based: uses the same YANG models as NETCONF/RESTCONF
  • High-frequency: can stream data every second (or faster)
  • Efficient encoding: uses Protocol Buffers (protobuf) for compact transport
  • Scalable: no SNMP polling overhead on the device CPU

Telemetry Subscription Modes

There are two subscription modes that define who initiates the telemetry session:

Dial-Out (Device-Initiated)

The device is configured to push data to a remote collector. The device opens the connection.

  • Configured on the device via CLI, NETCONF, or RESTCONF
  • Device connects outbound to the collector
  • Persistent — survives collector restarts (device retries)
  • Most common in production deployments

Dial-In (Collector-Initiated)

The collector subscribes to the device dynamically. No device CLI configuration is needed.

  • Collector initiates a subscription via NETCONF or gRPC
  • Dynamic — subscription exists only while the session is active
  • No persistent device configuration needed
  • Good for ad-hoc monitoring and troubleshooting

Dial-In vs Dial-Out — Comparison Table

AspectDial-OutDial-In
InitiatorDevice pushes to collectorCollector subscribes to device
ConfigurationOn the device (CLI/NETCONF)On the collector (dynamic)
PersistenceSurvives restartsLost when session drops
Use caseProduction monitoringAd-hoc troubleshooting
ProtocolsgRPC, TCP, HTTPNETCONF, gRPC
Exam keyword”configured subscription""dynamic subscription”

Transport Protocols

TransportDescriptionEncoding Support
gRPCHTTP/2-based RPC framework, efficient binary encodingKVGPB, GPB, JSON
TCPSimple TCP stream, lightweightKVGPB, GPB, JSON
HTTPWebhook-style delivery over HTTPSJSON

gRPC is the Recommended Transport

For the exam, gRPC is the preferred transport for model-driven telemetry. It uses HTTP/2 for multiplexing, supports bidirectional streaming, and is the most efficient option.

Encoding Formats

EncodingFull NameSizeReadabilityExam Notes
KVGPBKey-Value Google Protocol BuffersSmallestMachine onlyMost efficient, self-describing keys
GPBGoogle Protocol Buffers (compact)SmallMachine onlyRequires compiled .proto files to decode
JSONJavaScript Object NotationLargestHuman-readableEasy to debug, higher bandwidth

IOS XE CLI Configuration (Dial-Out)

Configure telemetry subscriptions directly on the device:

! Define a telemetry subscription
telemetry ietf subscription 101
 encoding encode-kvgpb
 filter xpath /process-cpu-ios-xe-oper:cpu-usage/cpu-utilization/five-seconds
 source-address 10.10.20.48
 stream yang-push
 update-policy periodic 3000
 receiver ip address 10.10.20.50 57000
 receiver protocol grpc-tcp

telemetry ietf subscription 102
 encoding encode-kvgpb
 filter xpath /interfaces-ios-xe-oper:interfaces/interface/statistics
 source-address 10.10.20.48
 stream yang-push
 update-policy periodic 5000
 receiver ip address 10.10.20.50 57000
 receiver protocol grpc-tcp

telemetry ietf subscription 103
 encoding encode-kvgpb
 filter xpath /memory-ios-xe-oper:memory-statistics/memory-statistic
 source-address 10.10.20.48
 stream yang-push
 update-policy periodic 10000
 receiver ip address 10.10.20.50 57000
 receiver protocol grpc-tcp

CLI Configuration Breakdown

FieldPurposeExample
subscription 101Unique subscription IDInteger
encodingData formatencode-kvgpb, encode-json
filter xpathYANG path to the dataXPath to YANG leaf/container
source-addressDevice IP to source fromManagement IP
streamData stream typeyang-push (standard)
update-policyPush triggerperiodic <centiseconds> or on-change
receiver ipCollector IP and portTelegraf/pipeline endpoint
receiver protocolTransportgrpc-tcp, tcp, native

update-policy Units

The periodic value is in centiseconds (1/100th of a second).

  • 3000 = 30 seconds
  • 1000 = 10 seconds
  • 100 = 1 second

This is a common exam trick question.

NETCONF Subscription (Dial-In)

Subscribe dynamically via NETCONF — no device configuration needed:

"""
Dial-in telemetry subscription via NETCONF.
Exam Topic: 4.5
"""
from ncclient import manager
 
# Establish-subscription RPC
subscribe_rpc = """
<establish-subscription xmlns="urn:ietf:params:xml:ns:yang:ietf-event-notifications"
                        xmlns:yp="urn:ietf:params:xml:ns:yang:ietf-yang-push">
  <stream>yp:yang-push</stream>
  <yp:xpath-filter>/interfaces-ios-xe-oper:interfaces/interface/statistics</yp:xpath-filter>
  <yp:period>1000</yp:period>
</establish-subscription>
"""
 
with manager.connect(
    host="10.10.20.48",
    port=830,
    username="developer",
    password="C1sco12345",
    hostkey_verify=False
) as m:
    # Send subscription request
    response = m.dispatch(subscribe_rpc)
    print(response)
 
    # Receive telemetry notifications
    while True:
        notification = m.take_notification()
        if notification:
            print(notification.notification_xml)

RESTCONF Subscription Setup

You can also configure telemetry subscriptions via RESTCONF by pushing the configuration to the device:

"""
Configure a telemetry subscription via RESTCONF.
Exam Topic: 4.5
"""
import requests
from requests.auth import HTTPBasicAuth
 
BASE_URL = "https://10.10.20.48/restconf"
HEADERS = {
    "Accept": "application/yang-data+json",
    "Content-Type": "application/yang-data+json"
}
AUTH = HTTPBasicAuth("developer", "C1sco12345")
 
# Configure a telemetry subscription via RESTCONF
subscription = {
    "ietf-event-notifications:subscription": {
        "subscription-id": 201,
        "stream": "yang-push",
        "encoding": "encode-kvgpb",
        "receiver": {
            "address": "10.10.20.50",
            "port": 57000,
            "protocol": "grpc-tcp"
        },
        "source-address": "10.10.20.48",
        "filter": {
            "xpath-filter": "/process-cpu-ios-xe-oper:cpu-usage/cpu-utilization"
        },
        "update-trigger": {
            "periodic": {
                "period": 3000
            }
        }
    }
}
 
response = requests.put(
    f"{BASE_URL}/data/ietf-event-notifications:subscriptions/subscription=201",
    headers=HEADERS,
    auth=AUTH,
    json=subscription,
    verify=False
)
print(f"Status: {response.status_code}")

TIG Stack (Telegraf + InfluxDB + Grafana)

The TIG stack is the most common collector pipeline for model-driven telemetry:

┌──────────────┐     gRPC/TCP      ┌──────────────┐
│  IOS XE      │ ──────────────>   │  Telegraf     │
│  Device      │  (push telemetry) │  (Collector)  │
│              │                   │              │
│  subscription│                   │  cisco_mdtd  │
│  101, 102... │                   │  input plugin │
└──────────────┘                   └──────┬───────┘
                                          │ write to DB
                                          ▼
                                   ┌──────────────┐
                                   │  InfluxDB     │
                                   │  (Time-Series │
                                   │   Database)   │
                                   └──────┬───────┘
                                          │ query
                                          ▼
                                   ┌──────────────┐
                                   │  Grafana      │
                                   │  (Dashboards  │
                                   │   & Alerts)   │
                                   └──────────────┘
ComponentRoleDefault Port
TelegrafReceives gRPC/TCP telemetry streams, transforms data57000 (gRPC input)
InfluxDBStores time-series telemetry data8086
GrafanaVisualizes data with dashboards and alerting3000

Telegraf configuration snippet for receiving Cisco MDT:

# /etc/telegraf/telegraf.conf
 
# Input: Cisco Model-Driven Telemetry
[[inputs.cisco_telemetry_mdt]]
  transport = "grpc"
  service_address = ":57000"
 
# Output: InfluxDB
[[outputs.influxdb_v2]]
  urls = ["http://localhost:8086"]
  token = "my-influx-token"
  organization = "my-org"
  bucket = "telemetry"

Common YANG Paths for Telemetry

These are the most frequently referenced YANG model paths on IOS XE:

MetricYANG XPathUse Case
CPU (5s)/process-cpu-ios-xe-oper:cpu-usage/cpu-utilization/five-secondsCPU monitoring
Memory/memory-ios-xe-oper:memory-statistics/memory-statisticMemory utilization
Interfaces/interfaces-ios-xe-oper:interfaces/interface/statisticsInterface counters
Interface state/ietf-interfaces:interfaces-state/interfaceOper status (IETF model)
BGP neighbors/bgp-ios-xe-oper:bgp-state-data/neighborsBGP peer monitoring
OSPF neighbors/ospf-ios-xe-oper:ospf-oper-dataOSPF adjacency tracking
Environment/environment-ios-xe-oper:environment-sensorsTemperature, fan, power

IETF vs Cisco-Native YANG Models

  • IETF models (e.g., ietf-interfaces) are vendor-neutral but have fewer features
  • Cisco-native models (e.g., Cisco-IOS-XE-interfaces-oper) have richer data
  • The exam may show either — recognize the namespace prefix to identify which

Verifying Telemetry on IOS XE

! Show all active subscriptions
show telemetry ietf subscription all

! Show details for a specific subscription
show telemetry ietf subscription 101 detail

! Show receiver connection status
show telemetry receiver all

! Show telemetry statistics
show telemetry internal subscription all stats

Example output:

csr1000v# show telemetry ietf subscription all
  Telemetry subscription brief

  ID     Type     State    Filter type
  -----------------------------------------------
  101    Configured  Valid  xpath
  102    Configured  Valid  xpath
  103    Configured  Valid  xpath

csr1000v# show telemetry ietf subscription 101 detail
  Subscription ID: 101
  Type: Configured
  State: Valid
  Stream: yang-push
  Filter:
    Filter type: xpath
    XPath: /process-cpu-ios-xe-oper:cpu-usage/cpu-utilization/five-seconds
  Update policy:
    Update Trigger: periodic
    Period: 3000
  Encoding: encode-kvgpb
  Source Address: 10.10.20.48
  Receiver:
    Address: 10.10.20.50
    Port: 57000
    Protocol: grpc-tcp
    State: Connected

Subscription States

StateMeaning
ValidSubscription is active and streaming
InvalidXPath filter is wrong or YANG model not supported
SuspendedReceiver unreachable, device retrying

4 – Health Monitoring with Controller APIs (Topic 4.4)

Exam Topic: 4.4 — Monitor network health using controller APIs

Both Catalyst Center and SD-WAN Manager expose dedicated health endpoints, but they differ significantly in authentication and data structure.

Controller Comparison

AspectCatalyst CenterSD-WAN Manager
AuthenticationBasic Auth → Token (X-Auth-Token)Session-based (JSESSIONID + X-XSRF-TOKEN)
Primary Health Endpoint/dna/intent/api/v1/network-health/dataservice/device
API DesignIntent API (outcome-based)Data Service API (device-centric)
Time ParametersstartTime and endTime (epoch ms)startTime and endTime (epoch ms)

Catalyst Center Health Endpoints

The Intent API provides three health categories:

MethodEndpointPurpose
POST/dna/system/api/v1/auth/tokenObtain authorization token (Basic Auth)
GET/dna/intent/api/v1/site-healthHealth per area/building (network + client scores)
GET/dna/intent/api/v1/network-healthDevice health by category (Access, Core, Distribution, Router, Wireless)
GET/dna/intent/api/v1/client-healthWired/wireless clients by health state

Intent API vs General API

The “Intent API” (/dna/intent/...) focuses on business outcomes — health, compliance, status. The general system API (/dna/system/...) handles management tasks like authentication. Both live under the same base URL.

Python Workflow: Health Monitoring Script

Step 1 — Authenticate and obtain token:

import requests
from requests.auth import HTTPBasicAuth
import urllib3
 
urllib3.disable_warnings()
 
BASE_URL = "https://10.10.20.85"
AUTH_URL = "/dna/system/api/v1/auth/token"
 
response = requests.post(
    BASE_URL + AUTH_URL,
    auth=HTTPBasicAuth("admin", "password"),
    verify=False
)
token = response.json()["Token"]
headers = {"X-Auth-Token": token, "Content-Type": "application/json"}

Step 2 — Query network health (device categories):

response = requests.get(
    BASE_URL + "/dna/intent/api/v1/network-health",
    headers=headers, verify=False
)
health = response.json()["response"]
print(f"Good: {health[0]['goodCount']}, Bad: {health[0]['badCount']}, "
      f"Score: {health[0]['healthScore']}")

Step 3 — Query site health (per location):

for site in requests.get(
    BASE_URL + "/dna/intent/api/v1/site-health",
    headers=headers, verify=False
).json()["response"]:
    print(f"Site: {site['siteName']}, Health: {site['networkHealthAverage']}")

Step 4 — Analyze client health:

clients = requests.get(
    BASE_URL + "/dna/intent/api/v1/client-health",
    headers=headers, verify=False
).json()["response"]
 
for score in clients[0]["scoreDetail"]:
    print(f"{score['scoreCategory']['value']}: "
          f"{score['clientCount']} clients, score={score['scoreValue']}")

Client Health Categories

CategoryMeaning
GOODConnected and performing well
FAIRConnected with minor issues
POORSignificant connectivity or performance problems
IDLEConnected but no active traffic
NODATANo health data available
NEWRecently connected, insufficient data collected

Time-Based Health Queries

Exam Question

Which parameter is commonly used with API endpoints to specify the time range for health or issue data?

import time
 
# Query health for a specific point in time
timestamp = int(time.time() * 1000)
response = requests.get(
    f"{BASE_URL}/dna/intent/api/v1/site-health",
    headers=headers,
    params={"timestamp": timestamp},
    verify=False
)

5 – Cross-Reference to Covered Topics

The following Domain 4.0 topics are also covered in controller-specific deep dives:

TopicDescriptionWhere to Find
4.3Manage device software versions (SWIM)Catalyst_Center_Deep_Dive, SDWAN_Deep_Dive
4.4Monitor network health (controller-specific)Catalyst_Center_Deep_Dive, Meraki_Deep_Dive, SDWAN_Deep_Dive
4.6Webhook-based monitoringMeraki_Deep_Dive

6 – Common Patterns and Gotchas

pyATS vs Manual Testing

AspectManual TestingpyATS Automation
SpeedMinutes per deviceSeconds per device
ConsistencyHuman error possibleRepeatable every time
ScalabilityDoes not scaleScales to thousands of devices
EvidenceScreenshots, notesStructured logs and reports
Pre/post diffVisual comparisonProgrammatic Diff()
CI/CD integrationNot possibleNative pipeline integration

Telemetry vs SNMP

AspectSNMP PollingModel-Driven Telemetry
Data flowPull (collector polls device)Push (device streams to collector)
FrequencyTypically 5-minute intervalsSub-second possible
Device CPU impactHigh (processes each poll)Low (scheduled push)
Data modelMIBs (flat, hard to parse)YANG (hierarchical, structured)
EncodingBER (ASN.1)Protobuf (KVGPB) or JSON
ScalabilityPoor at high frequencyExcellent (push-based)
On-change supportNo (must poll)Yes (on-change update policy)
TransportUDP (unreliable)gRPC/TCP (reliable)

CML API Gotchas

Common CML API Mistakes

  1. Auth token is a plain stringresponse.json() returns the token directly, not inside a dict
  2. Lab state transitions take time — after calling start, nodes need minutes to boot; use wait_until_lab_converged() with the SDK
  3. Node definitions are case-sensitiveiosv not IOSv; check available definitions with GET /api/v0/node_definitions
  4. Labs must be stopped before deletion — cannot delete a running lab; stop first, then wipe, then delete
  5. Interface slots start at 0get_interface_by_slot(0) is the first interface

pyATS Gotchas

Common pyATS Mistakes

  1. Wrong os: in testbed — must match exactly (iosxe, not ios-xe or IOS-XE)
  2. Parser not found — not all show commands have parsers; use genie parse to check availability
  3. learn() returns object, not dict — access data via .info attribute, not direct dict access
  4. Testbed credentialspassword must be a string, not an integer (YAML gotcha: quote "12345")
  5. Diff() on different features — both snapshots must be the same feature type

7 – Exam-Style Scenarios

Scenario 1: pyATS Diff Interpretation (Topic 4.1)

You run the following pyATS code and it prints nothing. What does this mean?

pre = device.learn('interface')
# ... network change applied ...
post = device.learn('interface')
diff = Diff(pre, post)
diff.findDiff()
print(diff)

Scenario 2: Telemetry Subscription Invalid (Topic 4.5)

You configure a telemetry subscription on IOS XE and show telemetry ietf subscription all shows the state as Invalid. What are the most likely causes?

telemetry ietf subscription 201
 encoding encode-kvgpb
 filter xpath /cpu-usage/cpu-utilization/five-seconds
 stream yang-push
 update-policy periodic 3000
 receiver ip address 10.10.20.50 57000
 receiver protocol grpc-tcp

Scenario 3: CML Lab Lifecycle (Topic 4.2)

You need to automate the following workflow using the CML API: create a lab, add two routers, connect them, start the lab, and clean up after testing. Put the following API calls in the correct order:

  1. DELETE /api/v0/labs/{id}
  2. POST /api/v0/authenticate
  3. POST /api/v0/labs/{id}/links
  4. PUT /api/v0/labs/{id}/state/stop
  5. POST /api/v0/labs
  6. PUT /api/v0/labs/{id}/state/start
  7. POST /api/v0/labs/{id}/nodes (x2)

Scenario 4: Dial-In vs Dial-Out (Topic 4.5)

Your company wants to monitor CPU utilization on 500 routers. The monitoring team wants subscriptions to persist even if the collector is restarted. Should they use dial-in or dial-out telemetry, and why?


Scenario 5: pyATS Testbed Error (Topic 4.1)

You run a pyATS test and get ConnectionError: Failed to connect to device 'router1'. The device is reachable via SSH from the terminal. What should you check in your testbed YAML?


Scenario 6: Telemetry Update Policy (Topic 4.5)

You configure a telemetry subscription with update-policy periodic 100. How often will the device send updates?

telemetry ietf subscription 301
 filter xpath /interfaces-ios-xe-oper:interfaces/interface/statistics
 update-policy periodic 100

8 – Quick Reference Cheat Sheet

pyATS / Genie

# Install
pip install pyats[full]

# CLI tools
pyats run job <script>.py --testbed testbed.yaml
genie learn interface --testbed testbed.yaml --output snapshot/
genie diff snapshot1/ snapshot2/ --output diff_report/
genie parse "show ip route" --testbed testbed.yaml --device csr1000v

# Python essentials
from pyats.topology import loader
from genie.utils.diff import Diff

testbed = loader.load('testbed.yaml')
device = testbed.devices['name']
device.connect()

device.execute('show version')         # raw string
device.parse('show ip route')          # structured dict
device.learn('interface')              # comprehensive model
device.configure('interface Gi1\n shutdown')  # push config

diff = Diff(pre, post)
diff.findDiff()

CML API

# Auth
POST /api/v0/authenticate              → token (plain string)

# Labs
POST   /api/v0/labs                     → create lab
GET    /api/v0/labs                     → list labs
GET    /api/v0/labs/{id}               → lab details
DELETE /api/v0/labs/{id}               → delete lab

# Nodes
POST   /api/v0/labs/{id}/nodes         → add node
GET    /api/v0/labs/{id}/nodes         → list nodes

# Links
POST   /api/v0/labs/{id}/links         → create link

# State
PUT    /api/v0/labs/{id}/state/start   → start lab
PUT    /api/v0/labs/{id}/state/stop    → stop lab

# SDK: pip install virl2-client
from virl2_client import ClientLibrary

Model-Driven Telemetry

# Dial-out (device-initiated, persistent)
telemetry ietf subscription <id>
 encoding encode-kvgpb
 filter xpath <yang-xpath>
 stream yang-push
 update-policy periodic <centiseconds>
 receiver ip address <collector-ip> <port>
 receiver protocol grpc-tcp

# Dial-in (collector-initiated, dynamic)
→ NETCONF establish-subscription RPC
→ No device config needed

# Verify
show telemetry ietf subscription all
show telemetry ietf subscription <id> detail
show telemetry receiver all

# Key YANG paths (IOS XE)
CPU:        /process-cpu-ios-xe-oper:cpu-usage/cpu-utilization/five-seconds
Memory:     /memory-ios-xe-oper:memory-statistics/memory-statistic
Interfaces: /interfaces-ios-xe-oper:interfaces/interface/statistics
BGP:        /bgp-ios-xe-oper:bgp-state-data/neighbors
Environment:/environment-ios-xe-oper:environment-sensors

# TIG Stack: Telegraf (collector:57000) → InfluxDB (store:8086) → Grafana (viz:3000)
# Update-policy units: centiseconds (3000 = 30 seconds)

Study Priority for Domain 4.0

  1. 4.1 pyATS — Know testbed YAML, learn() vs parse(), Diff(), aetest structure
  2. 4.5 Telemetry — Know dial-in vs dial-out, YANG paths, centisecond units, TIG stack
  3. 4.2 CML — Know API lifecycle (auth → create → nodes → links → start → stop → delete)
  4. Review 4.3 SWIM, 4.4 Health, 4.6 Webhooks in their respective deep dives

See Also