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 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.
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
Component
Purpose
Example
pyATS
Test framework (testbed, test runner, reporting)
aetest.test, loader.load()
Genie
Network libraries (parsers, models, Diff)
device.learn(), device.parse(), Diff()
Unicon
Connection library for device interaction
SSH, 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.
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 YAMLtestbed = loader.load('testbed.yaml')# Get a specific devicedevice = testbed.devices['csr1000v']# Connect (establishes SSH session)device.connect()# Execute a command (raw string output)output = device.execute('show version')print(output)# Disconnectdevice.disconnect()
Connection Options
# Connect without logging to consoledevice.connect(log_stdout=False)# Connect and learn device state automaticallydevice.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 loadertestbed = 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 datafor 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 dataroutes = device.parse('show ip route')# Access parsed datafor 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 briefbrief = 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
Method
Scope
Use Case
parse()
Single show command
Quick check of specific output
learn()
Entire feature (multiple commands)
Comprehensive state capture
execute()
Raw command
Unstructured 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 loaderfrom genie.utils.diff import Difftestbed = loader.load('testbed.yaml')device = testbed.devices['csr1000v']device.connect(log_stdout=False)# STEP 1: Capture pre-change statepre_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 statepost_snapshot = device.learn('interface')# STEP 4: Compare with Diffdiff = Diff(pre_snapshot, post_snapshot)diff.findDiff()# Print what changedprint(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 loggingfrom pyats import aetestfrom pyats.topology import loaderlogger = 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
Method
Meaning
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 scriptpyats run job my_test.py --testbed testbed.yaml# Generate HTML reportpyats logs view# Run Genie learn from CLI (no script needed)genie learn interface --testbed testbed.yaml --output pre_snapshot/# Compare two snapshots from CLIgenie 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:
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
The CML API allows you to programmatically create, manage, and tear down network topologies.
Authentication
import requestsCML_HOST = "https://10.10.20.161"USERNAME = "developer"PASSWORD = "C1sco12345"# Authenticate — returns a JWT token as a plain stringauth_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 labslabs = requests.get(f"{CML_HOST}/api/v0/labs", headers=headers, verify=False)print(labs.json()) # List of lab IDs# Create a new labnew_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 detailslab_detail = requests.get( f"{CML_HOST}/api/v0/labs/{lab_id}", headers=headers, verify=False)# Add a node to the labnode = 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 labrequests.put( f"{CML_HOST}/api/v0/labs/{lab_id}/state/start", headers=headers, verify=False)# Stop and wipe the labrequests.put( f"{CML_HOST}/api/v0/labs/{lab_id}/state/stop", headers=headers, verify=False)# Delete the labrequests.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 serverclient = ClientLibrary( "https://10.10.20.161", "developer", "C1sco12345", ssl_verify=False)# Create a new lablab = client.create_lab("enauto-test")# Add nodesr1 = 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 nodeintf_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 routersintf_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 lablab.start()# Wait until all nodes are bootedlab.wait_until_lab_converged()# Check node statesfor node in lab.nodes(): print(f"{node.label}: {node.state}")# Get console access URLprint(r1.console_url)# Stop and remove the lablab.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 YAMLtopology_yaml = lab.download()with open("topology.yaml", "w") as f: f.write(topology_yaml)# Import topology from YAMLwith 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 downstages: - simulate - test - cleanupspin-up-lab: stage: simulate script: - python create_lab.py # Uses virl2-client to create + start topology - sleep 120 # Wait for nodes to bootrun-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 Case
Description
Pre-deployment testing
Validate configs before pushing to production
Automation development
Develop and test scripts safely
CI/CD pipeline validation
Automated topology + test in pipelines
Training and certification
Practice labs without physical hardware
Failure scenario testing
Simulate 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
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
Encoding
Full Name
Size
Readability
Exam Notes
KVGPB
Key-Value Google Protocol Buffers
Smallest
Machine only
Most efficient, self-describing keys
GPB
Google Protocol Buffers (compact)
Small
Machine only
Requires compiled .proto files to decode
JSON
JavaScript Object Notation
Largest
Human-readable
Easy to debug, higher bandwidth
IOS XE CLI Configuration (Dial-Out)
Configure telemetry subscriptions directly on the device:
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
State
Meaning
Valid
Subscription is active and streaming
Invalid
XPath filter is wrong or YANG model not supported
Suspended
Receiver 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
Aspect
Catalyst Center
SD-WAN Manager
Authentication
Basic Auth → Token (X-Auth-Token)
Session-based (JSESSIONID + X-XSRF-TOKEN)
Primary Health Endpoint
/dna/intent/api/v1/network-health
/dataservice/device
API Design
Intent API (outcome-based)
Data Service API (device-centric)
Time Parameters
startTime and endTime (epoch ms)
startTime and endTime (epoch ms)
Catalyst Center Health Endpoints
The Intent API provides three health categories:
Method
Endpoint
Purpose
POST
/dna/system/api/v1/auth/token
Obtain authorization token (Basic Auth)
GET
/dna/intent/api/v1/site-health
Health per area/building (network + client scores)
GET
/dna/intent/api/v1/network-health
Device health by category (Access, Core, Distribution, Router, Wireless)
GET
/dna/intent/api/v1/client-health
Wired/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.
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']}")
Which parameter is commonly used with API endpoints to specify the time range for health or issue data?
Answer startTime and endTime query parameters (epoch milliseconds). For Catalyst Center health endpoints, a single timestamp parameter can also be used to query a point-in-time snapshot.
import time# Query health for a specific point in timetimestamp = 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:
The Invalid state means the device cannot resolve the XPath to a valid YANG model node. Always use the full namespace-prefixed path.
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:
DELETE /api/v0/labs/{id}
POST /api/v0/authenticate
POST /api/v0/labs/{id}/links
PUT /api/v0/labs/{id}/state/stop
POST /api/v0/labs
PUT /api/v0/labs/{id}/state/start
POST /api/v0/labs/{id}/nodes (x2)
Answer
Correct order: 2 → 5 → 7 → 3 → 6 → 4 → 1
Authenticate (POST /authenticate) — get token
Create lab (POST /labs) — get lab ID
Add nodes (POST /labs/{id}/nodes) — twice, for both routers
Create link (POST /labs/{id}/links) — connect the two nodes
Start lab (PUT /labs/{id}/state/start) — boot the topology
… run tests …
Stop lab (PUT /labs/{id}/state/stop) — must stop before deleting
Delete lab (DELETE /labs/{id}) — clean up
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?
Answer
Dial-out is the correct choice. Reasons:
Persistence — Dial-out subscriptions are configured on the device and survive collector restarts. The device will reconnect automatically.
Scale — With 500 routers, configuring subscriptions on each device (via automation) is more reliable than maintaining 500 active gRPC sessions from the collector.
Dial-in subscriptions are dynamic and lost when the session drops — requiring re-subscription after every collector restart.
Deploy using Ansible or RESTCONF to push the telemetry configuration to all 500 devices.
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?
Answer
Common testbed issues when SSH works manually but pyATS fails:
os: field — must be correct (iosxe, nxos, iosxr). Wrong OS causes Unicon to send the wrong login sequence.
connections.defaults.class — must be unicon.Unicon (capitalization matters).
protocol: — must match (ssh, not SSH or telnet).
credentials: — password must be a string in quotes if it looks numeric.
ip: — verify the IP matches and the port (default 22) is correct.
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?
The device will send updates every 1 second. The periodic value is in centiseconds (1/100th of a second), so 100 centiseconds = 1 second. Common values:
# 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