Test your knowledge on this topic in the ENAUTO Exam Trainer — 186 questions across 5 interactive modes.
Cisco Catalyst Center – Deep Dive for ENAUTO v2.0
Exam Relevance
Topic 3.0 Controller-Based Network Automation (30%) is the highest-weighted section.
This guide maps every code example to a specific exam objective so you know exactly what you’re studying.
Both fields contain the same UUID value. When other API calls ask for a deviceId, use this value. The exam may use either field name.
5 – Get Network Devices (SDK)
Exam Topic: 3.2
from catalystcentersdk import CatalystCenterAPIapi = CatalystCenterAPI( base_url="https://sandboxdnac.cisco.com", username="devnetuser", password="Cisco123!", verify=False)# ── Get all devices (SDK handles pagination) ──────────────────devices = api.devices.get_device_list()for device in devices.response: print(f"{device.hostname:<25} " f"{device.managementIpAddress:<16} " f"{device.softwareVersion}")# ── Get device by ID ──────────────────────────────────────────device = api.devices.get_device_by_id( id="e5c7e849-3f12-4a5c-b3d2-abc123def456")print(f"Device: {device.response.hostname}")# ── Get device by serial number ───────────────────────────────device = api.devices.get_device_by_serial_number( serial_number="FDO12345678")
SDK Method Naming Convention
The SDK mirrors the API structure. Methods live under api.devices, api.sites, api.template_programmer, etc. The method names are snake_case versions of the API operation names.
6 – Update Device Role (requests)
Exam Topics: 3.2, 3.5
Updating device roles is a common configuration management task — and a likely exam scenario.
import requestsfrom requests.auth import HTTPBasicAuthimport timeimport urllib3urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)BASE_URL = "https://sandboxdnac.cisco.com"# ── 1. Authenticate ───────────────────────────────────────────token = requests.post( f"{BASE_URL}/dna/system/api/v1/auth/token", auth=HTTPBasicAuth("devnetuser", "Cisco123!"), verify=False).json()["Token"]HEADERS = {"X-Auth-Token": token, "Content-Type": "application/json"}# ── 2. Get the target site ID ────────────────────────────────sites = requests.get( f"{BASE_URL}/dna/intent/api/v1/site", headers=HEADERS, verify=False).json()["response"]target_site = next( (s for s in sites if "Branch" in s.get("name", "")), None)# ── 3. Get the template ID ───────────────────────────────────templates = requests.get( f"{BASE_URL}/dna/intent/api/v1/template-programmer/template", headers=HEADERS, verify=False).json()target_template = next( (t for t in templates if "Day0" in t.get("name", "")), None)# ── 4. Pre-register new devices from inventory list ──────────new_devices = [ {"serial": "FDO11111111", "name": "Branch-SW-01", "pid": "C9300-48U"}, {"serial": "FDO22222222", "name": "Branch-SW-02", "pid": "C9300-24T"},]for device in new_devices: # Add to PnP result = requests.post( f"{BASE_URL}/dna/intent/api/v1/onboarding/pnp-device", headers=HEADERS, json={"deviceInfo": { "serialNumber": device["serial"], "name": device["name"], "pid": device["pid"] }}, verify=False ).json() # Claim with template requests.post( f"{BASE_URL}/dna/intent/api/v1/onboarding/pnp-device/claim", headers=HEADERS, json={ "siteId": target_site["id"], "deviceId": result["id"], "type": "Default", "configInfo": { "configId": target_template["templateId"], "configParameters": [ {"key": "HOSTNAME", "value": device["name"]} ] } }, verify=False ) print(f"Pre-registered & claimed: {device['name']} ({device['serial']})")
PnP Device States
State
Meaning
Unclaimed
Device connected but not assigned to a site
Planned
Device pre-registered, waiting to connect
Onboarding
Device is being provisioned
Provisioned
Configuration applied successfully
Error
Provisioning failed — check task/logs
9 – Jinja2 Configuration Templates
Exam Topic: 3.3 – Construct advanced network configuration templates using Jinja2 constructs such as loops, conditionals, output modifiers, and filters
Catalyst Center uses Jinja2 (a Python-based templating engine) for Day-N configuration templates. These templates are stored in the Template Programmer and deployed to devices.
Jinja2 Syntax Essentials for the Exam
Variables
hostname {{ HOSTNAME }}interface Vlan{{ MGMT_VLAN }} ip address {{ MGMT_IP }} {{ MGMT_MASK }} no shutdown
{% for vlan_id, vlan_name in VLANS %}vlan {{ vlan_id }} name {{ vlan_name }}{% endfor %}{% for port in range(1, ACCESS_PORTS + 1) %}interface GigabitEthernet1/0/{{ port }} switchport mode access switchport access vlan {{ DATA_VLAN }} spanning-tree portfast{% endfor %}
Filters (transform data)
{# Common Jinja2 filters tested on the exam #}hostname {{ HOSTNAME | upper }}description {{ DESCRIPTION | truncate(50) }}ip address {{ MGMT_IP | default("10.0.0.1") }} {{ MASK }}{# Join a list into a comma-separated string #}banner motd ^ Authorized VLANs: {{ VLAN_LIST | join(", ") }} ^
Output Modifiers (whitespace control)
{#- The minus sign strips whitespace -#}{%- for intf in INTERFACES %}interface {{ intf.name }} description {{ intf.description }}{%- endfor %}{# Without the -, you get blank lines between blocks #}
Complete Template Example: Branch Switch Day-N
! ── Generated by Catalyst Center Template Programmer ──! Template: Branch-Switch-Day-N! Device: {{ HOSTNAME }}hostname {{ HOSTNAME }}{% if ENABLE_SECRET is defined %}enable secret {{ ENABLE_SECRET }}{% endif %}! ── Management Interface ──interface Vlan{{ MGMT_VLAN | default(1) }} ip address {{ MGMT_IP }} {{ MGMT_MASK }} no shutdownip default-gateway {{ GATEWAY }}! ── VLANs ──{% for vlan in VLANS %}vlan {{ vlan.id }} name {{ vlan.name }}{% endfor %}! ── Access Ports ──{% for port in range(1, ACCESS_PORT_COUNT + 1) %}interface GigabitEthernet1/0/{{ port }} switchport mode access switchport access vlan {{ DATA_VLAN }}{%- if VOICE_VLAN is defined %} switchport voice vlan {{ VOICE_VLAN }}{%- endif %} spanning-tree portfast no shutdown{% endfor %}! ── Uplink Ports ──{% for uplink in UPLINKS %}interface {{ uplink.interface }} switchport mode trunk switchport trunk allowed vlan {{ uplink.allowed_vlans | join(",") }} channel-group {{ uplink.port_channel }} mode active no shutdown{% endfor %}! ── NTP ──{% for server in NTP_SERVERS | default(["10.0.0.100"]) %}ntp server {{ server }}{% endfor %}! ── Security Hardening ──{% if DEVICE_ROLE == "ACCESS" %}ip dhcp snoopingip dhcp snooping vlan {{ DATA_VLAN }}ip arp inspection vlan {{ DATA_VLAN }}{% endif %}line con 0 logging synchronousline vty 0 15 transport input ssh login localend
# ── Get health summary for all sites ─────────────────────────import time# timestamp must be in milliseconds (epoch)timestamp = int(time.time() * 1000)response = requests.get( f"{BASE_URL}/dna/intent/api/v1/site-health", headers=HEADERS, params={"timestamp": timestamp}, verify=False)response.raise_for_status()sites = response.json()["response"]for site in sites: print(f"\nSite: {site['siteName']}") print(f" Network Health: {site.get('networkHealthAverage', 'N/A')}%") print(f" Client Health: {site.get('clientHealthWired', 'N/A')}% (wired) " f"/ {site.get('clientHealthWireless', 'N/A')}% (wireless)") print(f" Devices: {site.get('numberOfNetworkDevice', 0)} total, " f"{site.get('numberOfGoodNetworkDevice', 0)} healthy")# SDKsites = api.sites.get_site_health(timestamp=timestamp)
Client Health
# ── Get overall client health scores ─────────────────────────timestamp = int(time.time() * 1000)response = requests.get( f"{BASE_URL}/dna/intent/api/v1/client-health", headers=HEADERS, params={"timestamp": timestamp}, verify=False)response.raise_for_status()health = response.json()["response"]for entry in health: print(f"\nSite: {entry.get('siteId', 'Global')}") for score in entry.get("scoreDetail", []): print(f" {score['scoreCategory']['value']}: " f"{score.get('clientCount', 0)} clients, " f"score={score.get('scoreValue', 'N/A')}%")# SDKhealth = api.clients.get_overall_client_health(timestamp=timestamp)
Device Health and Enrichment
# ── Get detailed info about a specific device ────────────────# Device enrichment gives deep health detailsenrich_headers = { **HEADERS, "entity_type": "network_device", "entity_value": "10.10.22.66" # Device IP for enrichment}response = requests.get( f"{BASE_URL}/dna/intent/api/v1/network-device-enrichment-details", headers=enrich_headers, verify=False)response.raise_for_status()details = response.json()for device in details: info = device.get("deviceDetails", {}) print(f"Device: {info.get('hostname')}") print(f" Overall Health: {info.get('overallHealth')}%") print(f" CPU: {info.get('cpuHealth')}%") print(f" Memory: {info.get('memoryHealth')}%") print(f" Reachability: {info.get('reachabilityStatus')}")
Enrichment Headers Are Special
The enrichment API uses custom request headers (entity_type and entity_value) instead of URL parameters or request body. This is unique in the Catalyst Center API and a common exam question.
Network Health Dashboard Script
def print_health_dashboard(base_url, headers): """Print a summary health dashboard for all monitored sites.""" timestamp = int(time.time() * 1000) # Get site health sites = requests.get( f"{base_url}/dna/intent/api/v1/site-health", headers=headers, params={"timestamp": timestamp}, verify=False ).json()["response"] print("=" * 60) print(f"{'SITE':<25} {'NETWORK':>8} {'WIRED':>8} {'WIRELESS':>8}") print("=" * 60) for site in sorted(sites, key=lambda s: s.get("siteName", "")): net = site.get("networkHealthAverage") wired = site.get("clientHealthWired") wireless = site.get("clientHealthWireless") print(f"{site['siteName']:<25} " f"{f'{net}%' if net else 'N/A':>8} " f"{f'{wired}%' if wired else 'N/A':>8} " f"{f'{wireless}%' if wireless else 'N/A':>8}") # Count problem devices devices = requests.get( f"{base_url}/dna/intent/api/v1/network-device", headers=headers, verify=False ).json()["response"] unreachable = [d for d in devices if d.get("reachabilityStatus") != "Reachable"] if unreachable: print(f"\n⚠ {len(unreachable)} unreachable device(s):") for d in unreachable: print(f" - {d['hostname']} ({d['managementIpAddress']})")print_health_dashboard(BASE_URL, HEADERS)
11 – Software Image Management (SWIM)
Exam Topic: 4.3 – Construct a controller-based automation solution to manage device software versions
Get Available Software Images
# ── List software images in the repository ────────────────────response = requests.get( f"{BASE_URL}/dna/intent/api/v1/image/importation", headers=HEADERS, verify=False)response.raise_for_status()images = response.json()["response"]for img in images: print(f"{img.get('name'):<40} " f"v{img.get('version', 'N/A'):<15} " f"{img.get('family', 'N/A')}")# SDKimages = api.software_image_management_swim.get_software_image_details()
Check Device Software Compliance
# ── Get device compliance status for software versions ────────response = requests.get( f"{BASE_URL}/dna/intent/api/v1/compliance", headers=HEADERS, params={"complianceStatus": "NON_COMPLIANT"}, verify=False)response.raise_for_status()non_compliant = response.json()["response"]for device in non_compliant: print(f"Non-compliant: {device.get('deviceName')} " f"(ID: {device.get('deviceUuid')})")
SWIM Workflow
Import an image to the Catalyst Center repository
Assign the image as the “golden” image for a device family/site
Distribute the image to target devices (copies to device flash)
Activate the image (triggers reboot with new software)
Each step is an async task — you must poll the task endpoint.
12 – Common Patterns and Gotchas
Pattern: Complete Workflow (Auth → Get → Update → Verify)
1. verify=False is lab-only
In production, always use proper CA certificates. The exam may ask about this in security contexts (topic 3.5).
2. Token goes in X-Auth-Token, NOT Authorization: Bearer
Catalyst Center uses a custom header. Other Cisco platforms use different schemes — don’t mix them up.
3. POST for auth, GET for read, PUT for update
The auth endpoint is POST. Device retrieval is GET. Updates are PUT. The exam tests whether you know which HTTP method to use.
4. Response is always wrapped in {"response": ..., "version": "..."}
You must access .json()["response"] — not .json() directly.
5. Write operations return a Task, not the result
Never assume a PUT/POST has completed immediately. Always check the task status.
6. offset starts at 1, not 0
Unlike most APIs, Catalyst Center pagination starts at 1.
params = {"offset": 1, "limit": 500}# Loop incrementing offset by limit until empty response
Scenario 3: Update Appears to Fail (Topic 3.6)
You call PUT to update a device role. The response status is 202 Accepted and you print the response — but the role hasn't changed when you check. Why?
Answer
Write operations are asynchronous. A 202 means the task was accepted, not completed. You need to poll /dna/intent/api/v1/task/{taskId} until endTime is set or isError is true.
Scenario 4: SDK Authentication (Topic 3.2)
Fill in the blank to authenticate using the Python SDK:
from catalystcentersdk import CatalystCenterAPIapi = CatalystCenterAPI( base_url="https://catalyst.example.com", ______="admin", ______="Cisco123!", verify=False)
Answer
username="admin",password="Cisco123!",
Scenario 5: Jinja2 Template Error (Topic 3.3)
The following Jinja2 template produces an error when VOICE_VLAN is not defined. How do you fix it?
interface GigabitEthernet1/0/1 switchport mode access switchport access vlan {{ DATA_VLAN }}{% if VOICE_VLAN is defined %} switchport voice vlan {{ VOICE_VLAN }}{% endif %}
Or with a default filter: {{ VOICE_VLAN | default("") }}
Scenario 6: Jinja2 Loop Output (Topic 3.3)
What configuration does this template produce when VLANS = [{"id": 10, "name": "DATA"}, {"id": 20, "name": "VOICE"}]?
{% for vlan in VLANS %}vlan {{ vlan.id }} name {{ vlan.name | upper }}{% endfor %}
Answer
vlan 10
name DATA
vlan 20
name VOICE
The | upper filter converts the name to uppercase. Since both were already uppercase, it appears unchanged — but if "name": "data", the output would be DATA.
Scenario 7: PnP Device Not Claiming (Topic 3.1)
You've pre-registered a device in PnP and connected it to the network, but it stays in Unclaimed state. What are the most likely causes?
Answer
DHCP Option 43 not configured — device can’t discover Catalyst Center
Serial number mismatch — the pre-registered serial doesn’t match the physical device
Device not in factory default state — PnP requires a clean device
You deploy a template and the task completes with isError: true and message "Variable HOSTNAME not bound". What went wrong?
Answer
The template has a {{ HOSTNAME }} variable, but the deploy payload’s configParameters didn’t include a value for it. Every required template variable must be provided in the params dict:
"params": { "HOSTNAME": "Branch-SW-01" # This was missing}
14 – Quick Reference Cheat Sheet
Authentication Flow
POST /dna/system/api/v1/auth/token
├── Header: Authorization: Basic base64(user:pass)
├── Response: {"Token": "eyJ..."}
└── Use token as: X-Auth-Token: eyJ...