Practice Questions

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.

Exam Topics Covered:
3.1 – Day-0 provisioning (controller-based)
3.2 – Python to manage and monitor configurations
3.5 – Security automation (policy, compliance, segmentation)
3.6 – Troubleshoot REST API automation solutions
4.3 – Manage device software versions
4.4 – Monitor network health

Table of Contents


1 – Architecture Overview

Catalyst Center (formerly DNA Center) exposes a REST-based Intent API that abstracts complex network operations into simple HTTP calls.

┌─────────────────────────────────────────────┐
│              Your Python Script              │
│  (requests library  OR  catalystcentersdk)   │
└──────────────────┬──────────────────────────┘
                   │ HTTPS (port 443)
                   ▼
┌─────────────────────────────────────────────┐
│          Catalyst Center Controller          │
│                                             │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐ │
│  │ Auth API │  │Intent API│  │ Event API │ │
│  │ /auth/   │  │ /dna/    │  │ /dna/     │ │
│  │ token    │  │ intent/  │  │ system/   │ │
│  └──────────┘  └──────────┘  └───────────┘ │
└──────────────────┬──────────────────────────┘
                   │ NETCONF / CLI / SNMP
                   ▼
┌─────────────────────────────────────────────┐
│         Managed Network Devices             │
│   Routers · Switches · APs · WLCs          │
└─────────────────────────────────────────────┘

Key API Base Paths

PurposeBase Path
Authentication/dna/system/api/v1/auth/token
Network Devices/dna/intent/api/v1/network-device
Site Health/dna/intent/api/v1/site-health
Client Health/dna/intent/api/v1/client-health
Command Runner/dna/intent/api/v1/network-device-poller/cli/read-request
Templates/dna/intent/api/v1/template-programmer/template
PnP (Day-0)/dna/intent/api/v1/onboarding/pnp-device

2 – Authentication with Python requests

Exam Topic: 3.2, 3.6

You must understand token-based auth — it is the foundation for every Catalyst Center API call.

How It Works

  1. Send a POST to /dna/system/api/v1/auth/token with HTTP Basic Auth (username:password).
  2. Receive a token string in the JSON response body.
  3. Pass that token as X-Auth-Token header in all subsequent requests.
  4. Token expires after ~60 minutes by default — plan for re-authentication.

Full Working Example

import requests
from requests.auth import HTTPBasicAuth
import urllib3
 
# ── Disable self-signed cert warnings (lab only!) ──────────────
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
 
# ── Connection Details ─────────────────────────────────────────
BASE_URL = "https://sandboxdnac.cisco.com"
USERNAME = "devnetuser"
PASSWORD = "Cisco123!"
 
# ── Step 1: Authenticate ──────────────────────────────────────
auth_url = f"{BASE_URL}/dna/system/api/v1/auth/token"
 
auth_response = requests.post(
    auth_url,
    auth=HTTPBasicAuth(USERNAME, PASSWORD),
    headers={"Content-Type": "application/json"},
    verify=False   # Only for lab/self-signed certs
)
 
# Always check the status code first
auth_response.raise_for_status()
 
token = auth_response.json()["Token"]
print(f"Token obtained: {token[:20]}...")
 
# ── Reusable headers for all future requests ──────────────────
HEADERS = {
    "X-Auth-Token": token,
    "Content-Type": "application/json",
    "Accept": "application/json"
}

Exam Trap: Authorization: Basic vs X-Auth-Token

  • The auth endpoint uses Authorization: Basic base64(user:pass) — Python’s HTTPBasicAuth handles this.
  • All other endpoints use X-Auth-Token: <token> — this is NOT a Bearer token, it’s a custom header.
  • Mixing these up is the most common exam-question pitfall.

What Happens on Failure?

Status CodeMeaningLikely Cause
401 UnauthorizedBad credentialsWrong username/password
403 ForbiddenValid auth but no permissionUser role lacks API access
404 Not FoundWrong URLTypo in /auth/token path
SSL ErrorCertificate issueMissing verify=False in lab

3 – Authentication with catalystcentersdk

Exam Topic: 3.2

The SDK simplifies authentication to a single constructor call — but understand what it does under the hood.

Installation

pip install catalystcentersdk

Full Working Example

from catalystcentersdk import CatalystCenterAPI
 
# The SDK handles authentication automatically on instantiation
api = CatalystCenterAPI(
    base_url="https://sandboxdnac.cisco.com",
    username="devnetuser",
    password="Cisco123!",
    verify=False    # Lab environment only
)
 
# Token is managed internally — auto-refreshes on expiry
print("Authenticated successfully via SDK")

SDK vs requests — When to Use What

Aspectrequestscatalystcentersdk
Token managementManual (you store & refresh)Automatic
PaginationManual (loop with offset/limit)Built-in iterators
Error handlingParse status codes yourselfRaises typed exceptions
Exam relevanceHigh — must understand raw flowHigh — must know the SDK exists
Production useMore control, more codeLess code, batteries included

4 – Get Network Devices (requests)

Exam Topics: 3.2, 4.4

Retrieving the device inventory is the single most common Catalyst Center API task on the exam.

Basic: Get All Devices

devices_url = f"{BASE_URL}/dna/intent/api/v1/network-device"
 
response = requests.get(devices_url, headers=HEADERS, verify=False)
response.raise_for_status()
 
devices = response.json()["response"]
 
for device in devices:
    print(f"{device['hostname']:<25} "
          f"{device['managementIpAddress']:<16} "
          f"{device['type']}")

Filtered: Search by Parameters

The API supports query parameters for filtering. Common filters:

# ── Filter by hostname (supports wildcards) ───────────────────
params = {"hostname": "cat_9k_1.abc.inc"}
response = requests.get(devices_url, headers=HEADERS,
                        params=params, verify=False)
 
# ── Filter by device family ───────────────────────────────────
params = {"family": "Switches and Hubs"}
response = requests.get(devices_url, headers=HEADERS,
                        params=params, verify=False)
 
# ── Filter by management IP ──────────────────────────────────
params = {"managementIpAddress": "10.10.22.66"}
response = requests.get(devices_url, headers=HEADERS,
                        params=params, verify=False)

Common Filter Parameters

ParameterExampleDescription
hostname"switch-floor1"Exact hostname match
managementIpAddress"10.0.0.1"Device management IP
serialNumber"FDO12345678"Serial number lookup
family"Routers"Device family grouping
platformId"C9300-48T"Platform/model filter
role"ACCESS"Network role assigned
limit50Max results per page (default 500)
offset1Starting record index

Pagination Pattern

def get_all_devices(base_url, headers):
    """Retrieve all devices handling pagination."""
    all_devices = []
    offset = 1
    limit = 50
 
    while True:
        params = {"offset": offset, "limit": limit}
        response = requests.get(
            f"{base_url}/dna/intent/api/v1/network-device",
            headers=headers, params=params, verify=False
        )
        response.raise_for_status()
        batch = response.json()["response"]
 
        if not batch:
            break
 
        all_devices.extend(batch)
        offset += limit
 
    return all_devices

Device Response Structure (Key Fields)

{
  "response": [
    {
      "id": "e5c7e849-3f12-4a5c-b3d2-abc123def456",
      "hostname": "cat_9k_1.abc.inc",
      "managementIpAddress": "10.10.22.66",
      "type": "Cisco Catalyst 9300 Switch",
      "family": "Switches and Hubs",
      "platformId": "C9300-48U",
      "softwareVersion": "17.9.4a",
      "role": "ACCESS",
      "serialNumber": "FDO12345678",
      "upTime": "45 days, 3:22:10.00",
      "reachabilityStatus": "Reachable",
      "lastUpdateTime": 1708097400000,
      "instanceTenantId": "abc123...",
      "instanceUuid": "e5c7e849-..."
    }
  ],
  "version": "1.0"
}

Exam Note: id vs instanceUuid

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 CatalystCenterAPI
 
api = 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.

Update the Network Role of a Device

update_url = f"{BASE_URL}/dna/intent/api/v1/network-device/brief"
 
payload = {
    "id": "e5c7e849-3f12-4a5c-b3d2-abc123def456",
    "role": "DISTRIBUTION",
    "roleSource": "MANUAL"
}
 
response = requests.put(
    update_url,
    headers=HEADERS,
    json=payload,
    verify=False
)
response.raise_for_status()
 
task_info = response.json()["response"]
print(f"Task ID: {task_info['taskId']}")

Asynchronous Task Pattern

Most write operations in Catalyst Center are asynchronous:

  1. You submit the request → get back a taskId
  2. Poll the task status endpoint until completion
  3. Only then is the change applied

This is a very common exam topic under 3.6 (troubleshooting REST APIs).

Polling a Task for Completion

import time
 
def wait_for_task(base_url, headers, task_id, timeout=60, interval=5):
    """Poll a Catalyst Center task until it completes or times out."""
    task_url = f"{base_url}/dna/intent/api/v1/task/{task_id}"
    elapsed = 0
 
    while elapsed < timeout:
        response = requests.get(task_url, headers=headers, verify=False)
        response.raise_for_status()
        task = response.json()["response"]
 
        # Check if task is complete
        if task.get("isError"):
            raise Exception(f"Task failed: {task.get('failureReason')}")
 
        if task.get("endTime"):
            print(f"Task completed: {task.get('progress')}")
            return task
 
        print(f"Task in progress: {task.get('progress')}...")
        time.sleep(interval)
        elapsed += interval
 
    raise TimeoutError(f"Task {task_id} did not complete within {timeout}s")
 
# ── Usage ──────────────────────────────────────────────────────
task_id = task_info["taskId"]
result = wait_for_task(BASE_URL, HEADERS, task_id)

Valid Device Roles

RoleDescription
ACCESSEdge/access layer device
DISTRIBUTIONDistribution layer device
CORECore layer device
BORDER ROUTERWAN border device
UNKNOWNRole not yet assigned

7 – Update Device Role (SDK)

Exam Topic: 3.2

# ── Update device role via SDK ────────────────────────────────
task = api.devices.update_device_role(
    id="e5c7e849-3f12-4a5c-b3d2-abc123def456",
    role="DISTRIBUTION",
    roleSource="MANUAL"
)
 
print(f"Task submitted: {task.response.taskId}")
 
# ── Check task status via SDK ─────────────────────────────────
task_result = api.task.get_task_by_id(task_id=task.response.taskId)
print(f"Status: {task_result.response.progress}")

8 – Day-0 PnP Provisioning

Exam Topic: 3.1 – Construct a controller-based network automation solution for Day-0 provisioning

PnP (Plug and Play) is how Catalyst Center automatically discovers, claims, and configures new devices without manual CLI access.

How PnP Works — The Full Flow

┌──────────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  New Device  │────►│ DHCP Server  │────►│  Catalyst    │────►│   Device     │
│  Powers on   │     │ Option 43    │     │  Center PnP  │     │  Configured  │
│  (factory    │     │ points to    │     │  Claims &    │     │  & managed   │
│   reset)     │     │  controller  │     │  provisions  │     │              │
└──────────────┘     └──────────────┘     └──────────────┘     └──────────────┘
    Step 1               Step 2               Step 3               Step 4
  (automatic)         (pre-configured)     (API-driven)         (automatic)

DHCP Option 43 or DNS

The new device needs to find Catalyst Center. Two common methods:

  • DHCP Option 43: Points device to controller IP (most common in enterprise)
  • DNS: Device queries pnpserver.<domain> → resolves to controller Without one of these, the device can’t start the PnP process.

Step 1: Get PnP Device List (Unclaimed Devices)

# ── List devices waiting in the PnP queue ─────────────────────
pnp_url = f"{BASE_URL}/dna/intent/api/v1/onboarding/pnp-device"
 
response = requests.get(pnp_url, headers=HEADERS, verify=False)
response.raise_for_status()
pnp_devices = response.json()
 
for device in pnp_devices:
    info = device.get("deviceInfo", {})
    print(f"Serial: {info.get('serialNumber'):<15} "
          f"PID: {info.get('pid', 'N/A'):<15} "
          f"State: {info.get('state', 'N/A')}")
 
# SDK
pnp_devices = api.device_onboarding_pnp.get_device_list()

Step 2: Add a Device to PnP (Pre-claim Before It Arrives)

# ── Pre-register a device so it's auto-claimed on connect ────
add_payload = {
    "deviceInfo": {
        "serialNumber": "FDO12345678",
        "name": "Branch-Switch-01",
        "pid": "C9300-48U",
        "sudiRequired": False,
        "stack": False
    }
}
 
response = requests.post(
    pnp_url,
    headers=HEADERS,
    json=add_payload,
    verify=False
)
response.raise_for_status()
device_id = response.json()["id"]
print(f"PnP device added: {device_id}")
 
# SDK
result = api.device_onboarding_pnp.add_device(
    deviceInfo={
        "serialNumber": "FDO12345678",
        "name": "Branch-Switch-01",
        "pid": "C9300-48U",
        "sudiRequired": False,
        "stack": False
    }
)

Step 3: Claim a Device to a Site with Template

# ── Claim the device and assign it to a site + template ──────
claim_payload = {
    "siteId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "deviceId": device_id,
    "type": "Default",
    "configInfo": {
        "configId": "template-uuid-here",
        "configParameters": [
            {"key": "HOSTNAME", "value": "Branch-Switch-01"},
            {"key": "MGMT_IP", "value": "10.10.50.10"},
            {"key": "MGMT_MASK", "value": "255.255.255.0"},
            {"key": "GATEWAY", "value": "10.10.50.1"}
        ]
    }
}
 
response = requests.post(
    f"{BASE_URL}/dna/intent/api/v1/onboarding/pnp-device/claim",
    headers=HEADERS,
    json=claim_payload,
    verify=False
)
response.raise_for_status()
print(f"Device claimed: {response.json()}")
 
# SDK
result = api.device_onboarding_pnp.claim_device(
    siteId="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    deviceId=device_id,
    type="Default",
    configInfo={
        "configId": "template-uuid-here",
        "configParameters": [
            {"key": "HOSTNAME", "value": "Branch-Switch-01"},
            {"key": "MGMT_IP", "value": "10.10.50.10"},
            {"key": "MGMT_MASK", "value": "255.255.255.0"},
            {"key": "GATEWAY", "value": "10.10.50.1"}
        ]
    }
)

Full Day-0 Automation Workflow

import requests
from requests.auth import HTTPBasicAuth
import time
import urllib3
urllib3.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

StateMeaning
UnclaimedDevice connected but not assigned to a site
PlannedDevice pre-registered, waiting to connect
OnboardingDevice is being provisioned
ProvisionedConfiguration applied successfully
ErrorProvisioning 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

How Templates Work in Catalyst Center

┌────────────────┐     ┌────────────────┐     ┌────────────────┐
│ Jinja2 Template│ ──► │  Catalyst      │ ──► │  Rendered      │
│ (with vars)    │     │  Center binds  │     │  Config pushed │
│                │     │  variables     │     │  to device     │
└────────────────┘     └────────────────┘     └────────────────┘

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

Conditionals (if/elif/else)

{% if DEVICE_ROLE == "ACCESS" %}
spanning-tree mode rapid-pvst
spanning-tree portfast default
{% elif DEVICE_ROLE == "DISTRIBUTION" %}
spanning-tree mode rapid-pvst
spanning-tree vlan 1-4094 priority 4096
{% else %}
spanning-tree mode rapid-pvst
spanning-tree vlan 1-4094 priority 0
{% endif %}

Loops (for)

{% 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 shutdown
 
ip 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 snooping
ip dhcp snooping vlan {{ DATA_VLAN }}
ip arp inspection vlan {{ DATA_VLAN }}
{% endif %}
 
line con 0
 logging synchronous
line vty 0 15
 transport input ssh
 login local
 
end

API: Create a Template via Python

# ── Create a new Day-N template in Catalyst Center ────────────
template_payload = {
    "name": "Branch-Switch-Day-N",
    "description": "Standard branch access switch template",
    "projectName": "Onboarding",
    "softwareType": "IOS-XE",
    "templateContent": """hostname {{ HOSTNAME }}
interface Vlan{{ MGMT_VLAN | default(1) }}
 ip address {{ MGMT_IP }} {{ MGMT_MASK }}
 no shutdown
ip default-gateway {{ GATEWAY }}
{% for vlan in VLANS %}
vlan {{ vlan.id }}
 name {{ vlan.name }}
{% endfor %}
""",
    "templateParams": [
        {"parameterName": "HOSTNAME", "dataType": "STRING",
         "required": True, "description": "Device hostname"},
        {"parameterName": "MGMT_VLAN", "dataType": "INTEGER",
         "required": False, "defaultValue": "1"},
        {"parameterName": "MGMT_IP", "dataType": "STRING",
         "required": True},
        {"parameterName": "MGMT_MASK", "dataType": "STRING",
         "required": True},
        {"parameterName": "GATEWAY", "dataType": "STRING",
         "required": True},
        {"parameterName": "VLANS", "dataType": "STRING",
         "required": False, "description": "List of VLAN objects"}
    ]
}
 
response = requests.post(
    f"{BASE_URL}/dna/intent/api/v1/template-programmer/project/"
    f"<project_id>/template",
    headers=HEADERS,
    json=template_payload,
    verify=False
)
response.raise_for_status()
task_id = response.json()["response"]["taskId"]
print(f"Template creation task: {task_id}")

API: Deploy (Push) a Template to a Device

# ── Deploy a versioned template to a target device ────────────
deploy_payload = {
    "forcePushTemplate": False,
    "targetInfo": [
        {
            "id": "10.10.22.66",           # Device IP or UUID
            "type": "MANAGED_DEVICE_IP",    # or MANAGED_DEVICE_UUID
            "params": {
                "HOSTNAME": "Branch-SW-Floor2",
                "MGMT_VLAN": "100",
                "MGMT_IP": "10.10.100.10",
                "MGMT_MASK": "255.255.255.0",
                "GATEWAY": "10.10.100.1"
            }
        }
    ],
    "templateId": "template-uuid-here"
}
 
response = requests.post(
    f"{BASE_URL}/dna/intent/api/v1/template-programmer/template/deploy",
    headers=HEADERS,
    json=deploy_payload,
    verify=False
)
deploy_task = response.json()
print(f"Deployment status: {deploy_task}")
 
# SDK
result = api.configuration_templates.deploy_template_v2(
    forcePushTemplate=False,
    templateId="template-uuid-here",
    targetInfo=[{
        "id": "10.10.22.66",
        "type": "MANAGED_DEVICE_IP",
        "params": {
            "HOSTNAME": "Branch-SW-Floor2",
            "MGMT_VLAN": "100",
            "MGMT_IP": "10.10.100.10",
            "MGMT_MASK": "255.255.255.0",
            "GATEWAY": "10.10.100.1"
        }
    }]
)

Jinja2 Exam Quick Reference

ConstructSyntaxExample
Variable{{ var }}{{ HOSTNAME }}
Conditional{% if %}...{% endif %}{% if ROLE == "ACCESS" %}
Loop{% for %}...{% endfor %}{% for v in VLANS %}
Filter{{ var | filter }}{{ NAME | upper }}
Default{{ var | default(val) }}{{ VLAN | default(1) }}
Comment{# text #}{# This is a comment #}
Whitespace{%- ... -%}Strips blank lines
Rangerange(start, end)range(1, 49) for ports 1–48

10 – Network Health Monitoring

Exam Topics: 4.4 – Monitor network health, 3.6 – Troubleshoot REST APIs

Site Health Overview

# ── 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")
 
# SDK
sites = 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')}%")
 
# SDK
health = api.clients.get_overall_client_health(timestamp=timestamp)

Device Health and Enrichment

# ── Get detailed info about a specific device ────────────────
# Device enrichment gives deep health details
enrich_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')}")
 
# SDK
images = 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

  1. Import an image to the Catalyst Center repository
  2. Assign the image as the “golden” image for a device family/site
  3. Distribute the image to target devices (copies to device flash)
  4. 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)

import requests
from requests.auth import HTTPBasicAuth
import time
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
 
BASE_URL = "https://sandboxdnac.cisco.com"
USERNAME = "devnetuser"
PASSWORD = "Cisco123!"
 
# ── 1. Authenticate ───────────────────────────────────────────
token = requests.post(
    f"{BASE_URL}/dna/system/api/v1/auth/token",
    auth=HTTPBasicAuth(USERNAME, PASSWORD),
    verify=False
).json()["Token"]
 
HEADERS = {
    "X-Auth-Token": token,
    "Content-Type": "application/json"
}
 
# ── 2. Find the target device ────────────────────────────────
devices = requests.get(
    f"{BASE_URL}/dna/intent/api/v1/network-device",
    headers=HEADERS,
    params={"hostname": "cat_9k_1.abc.inc"},
    verify=False
).json()["response"]
 
if not devices:
    print("Device not found!")
    exit()
 
device_id = devices[0]["id"]
current_role = devices[0]["role"]
print(f"Found device {devices[0]['hostname']} with role: {current_role}")
 
# ── 3. Update the device role ────────────────────────────────
task_response = requests.put(
    f"{BASE_URL}/dna/intent/api/v1/network-device/brief",
    headers=HEADERS,
    json={"id": device_id, "role": "CORE", "roleSource": "MANUAL"},
    verify=False
).json()
 
task_id = task_response["response"]["taskId"]
print(f"Update task submitted: {task_id}")
 
# ── 4. Wait for task completion ──────────────────────────────
for _ in range(12):
    time.sleep(5)
    task = requests.get(
        f"{BASE_URL}/dna/intent/api/v1/task/{task_id}",
        headers=HEADERS,
        verify=False
    ).json()["response"]
 
    if task.get("endTime"):
        print(f"Done: {task['progress']}")
        break
    if task.get("isError"):
        print(f"Failed: {task['failureReason']}")
        break
else:
    print("Task timed out")
 
# ── 5. Verify the change ─────────────────────────────────────
updated = requests.get(
    f"{BASE_URL}/dna/intent/api/v1/network-device/{device_id}",
    headers=HEADERS,
    verify=False
).json()["response"]
 
print(f"New role: {updated['role']}")

Gotchas for the Exam

Top Exam Pitfalls

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.


13 – Exam-Style Scenarios

Scenario 1: Troubleshooting Auth Failure (Topic 3.6)

You run the following code and get a 401 Unauthorized error on the device list call. What is wrong?

token_resp = requests.post(AUTH_URL, auth=HTTPBasicAuth(u, p), verify=False)
token = token_resp.json()["Token"]
headers = {"Authorization": f"Bearer {token}"}
devices = requests.get(DEVICES_URL, headers=headers, verify=False)

Scenario 2: Incomplete Device List (Topic 3.2)

Your script returns only 500 devices, but the network has 1200. What's the issue?

resp = requests.get(f"{BASE}/dna/intent/api/v1/network-device",
                    headers=headers, verify=False)
devices = resp.json()["response"]
print(f"Total: {len(devices)}")

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?


Scenario 4: SDK Authentication (Topic 3.2)

Fill in the blank to authenticate using the Python SDK:

from catalystcentersdk import CatalystCenterAPI
api = CatalystCenterAPI(
    base_url="https://catalyst.example.com",
    ______="admin",
    ______="Cisco123!",
    verify=False
)

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 }}
 switchport voice vlan {{ VOICE_VLAN }}

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 %}

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?


Scenario 8: Health Enrichment API (Topic 4.4)

You're trying to get enrichment details for a device but get an empty response. What's different about this API?

response = requests.get(
    f"{BASE_URL}/dna/intent/api/v1/network-device-enrichment-details",
    headers=HEADERS,
    params={"entity_type": "network_device", "entity_value": "10.10.22.66"},
    verify=False
)

Scenario 9: Template Deploy Failure (Topic 3.3, 3.6)

You deploy a template and the task completes with isError: true and message "Variable HOSTNAME not bound". What went wrong?


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...

CRUD Operations Map

OperationHTTP MethodEndpointReturns
List devicesGET/dna/intent/api/v1/network-deviceDevice list
Get one deviceGET/dna/intent/api/v1/network-device/{id}Single device
Update rolePUT/dna/intent/api/v1/network-device/briefTask ID
Delete deviceDELETE/dna/intent/api/v1/network-device/{id}Task ID
Check taskGET/dna/intent/api/v1/task/{taskId}Task status
PnP device listGET/dna/intent/api/v1/onboarding/pnp-devicePnP devices
PnP add devicePOST/dna/intent/api/v1/onboarding/pnp-deviceDevice ID
PnP claimPOST/dna/intent/api/v1/onboarding/pnp-device/claimTask ID
List templatesGET/dna/intent/api/v1/template-programmer/templateTemplates
Deploy templatePOST/dna/intent/api/v1/template-programmer/template/deployTask ID
Site healthGET/dna/intent/api/v1/site-healthHealth scores
Client healthGET/dna/intent/api/v1/client-healthClient scores
Device enrichmentGET/dna/intent/api/v1/network-device-enrichment-detailsDeep details
Software imagesGET/dna/intent/api/v1/image/importationImage list
ComplianceGET/dna/intent/api/v1/complianceStatus list

SDK Quick Reference

from catalystcentersdk import CatalystCenterAPI
 
api = CatalystCenterAPI(base_url=url, username=u, password=p, verify=False)
 
# Device management
api.devices.get_device_list()                          # All devices
api.devices.get_device_list(hostname="switch1")        # Filtered
api.devices.get_device_by_id(id="uuid-here")           # By ID
api.devices.get_device_by_serial_number(serial_number="FOC...")
api.devices.update_device_role(id="uuid", role="CORE", roleSource="MANUAL")
api.task.get_task_by_id(task_id="task-uuid")           # Check task
 
# Day-0 PnP
api.device_onboarding_pnp.get_device_list()            # PnP queue
api.device_onboarding_pnp.add_device(deviceInfo={...}) # Pre-register
api.device_onboarding_pnp.claim_device(...)            # Claim to site
 
# Templates
api.configuration_templates.get_projects()             # List projects
api.configuration_templates.gets_the_templates_available()
api.configuration_templates.deploy_template_v2(...)    # Push config
 
# Health monitoring
api.sites.get_site_health(timestamp=ts)                # Site health
api.clients.get_overall_client_health(timestamp=ts)    # Client health
 
# SWIM
api.software_image_management_swim.get_software_image_details()

Compare: Auth Methods Across Controllers

ControllerAuth MethodHeader/Mechanism
Catalyst CenterToken (Basic → Token)X-Auth-Token: <token>
SD-WAN (vManage)Session cookieCookie: JSESSIONID=<id>
MerakiAPI KeyX-Cisco-Meraki-API-Key: <key>
ISEBasic Auth (per request)Authorization: Basic <b64>

Memory Aid

Catalyst = Custom Token header SD-WAN = Session cookie Meraki = API Key header ISE = Basic auth every time


Further Study


enauto ccnp catalyst-center python api controller-based day0 pnp jinja2 health-monitoring swim

See Also