Practice Questions

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

Device-Level Network Automation – Deep Dive for ENAUTO v2.0

Exam Relevance

Topic 2.0 Device-Level Network Automation (25%) is the second-highest weighted domain. This guide covers direct device interaction — SSH-based automation (Netmiko), model-driven programmability (NETCONF/RESTCONF), Ansible device modules, Day-0 provisioning, and on-box automation (EEM, Guest Shell). Every code example maps to a specific exam objective.

Exam Topics Covered:
2.1 – Netmiko for configuration management
2.2 – ncclient for NETCONF operations
2.3 – RESTCONF with Python requests
2.4 – Ansible for device-level automation
2.5 – Day-0 provisioning (device-level)
2.6 – Troubleshoot RESTCONF, NETCONF, YANG
2.7 – On-box automation (EEM, guest shell, on-box Python)

Table of Contents


1 – Netmiko for Configuration Management

Exam Topic: 2.1

Netmiko is the go-to library for SSH-based device automation. Understand the abstraction layers, the key methods, and how TextFSM provides structured output.

Architecture: SSH Abstraction Layers

┌─────────────────────────────────────────────┐
│              Your Python Script              │
│          (import netmiko)                    │
└──────────────────┬──────────────────────────┘
                   │ High-level methods
                   ▼
┌─────────────────────────────────────────────┐
│               Netmiko                        │
│   ConnectHandler, send_command,              │
│   send_config_set, device_type mapping       │
└──────────────────┬──────────────────────────┘
                   │ SSH session management
                   ▼
┌─────────────────────────────────────────────┐
│               Paramiko                       │
│   Low-level SSH2 protocol implementation     │
└──────────────────┬──────────────────────────┘
                   │ TCP/22
                   ▼
┌─────────────────────────────────────────────┐
│           Network Device (IOS XE)           │
└─────────────────────────────────────────────┘

Why Netmiko over Paramiko?

Paramiko handles raw SSH. Netmiko adds:

  • Automatic prompt detection (no manual recv() loops)
  • Device-type awareness (cisco_ios, cisco_xe, cisco_nxos, arista_eos, etc.)
  • Built-in enable() for privilege escalation
  • send_config_set() that enters and exits config mode automatically
  • TextFSM integration for parsing show commands into structured data

Key Methods

MethodPurposeMode
ConnectHandler(**device)Create SSH connectionN/A
send_command(cmd)Execute show command, return outputExec/Priv
send_command(cmd, expect_string=r"confirm")Wait for custom promptExec
send_config_set(cmds)Push list of config commandsGlobal Config
send_config_from_file(file)Push commands from a fileGlobal Config
enable()Enter enable/privileged modeUser Priv
save_config()Execute write memory / copy run startPriv
disconnect()Close SSH sessionN/A
find_prompt()Return the current device promptAny

ConnectHandler Parameters

from netmiko import ConnectHandler
 
device = {
    "device_type": "cisco_ios",       # Required: netmiko device type
    "host": "sandbox-iosxe-latest-1.cisco.com",
    "username": "admin",
    "password": "C1sco12345",
    "port": 22,                        # Default: 22
    "secret": "enable_password",       # For enable() if needed
    "timeout": 30,                     # Connection timeout in seconds
    "session_log": "session.log",      # Optional: log all I/O to file
}
 
net_connect = ConnectHandler(**device)

Common device_type Values for the Exam

device_typePlatform
cisco_iosIOS / IOS XE (SSH)
cisco_xeIOS XE (alternative)
cisco_nxosNX-OS
cisco_xrIOS XR
arista_eosArista EOS

Using the wrong device_type causes prompt detection failures and timeouts.

TextFSM Integration for Structured Output

Netmiko integrates with NTC Templates (TextFSM) to parse CLI output into structured data (list of dicts).

# Unstructured output (raw string)
output_raw = net_connect.send_command("show ip interface brief")
print(type(output_raw))  # <class 'str'>
 
# Structured output via TextFSM (requires ntc-templates installed)
output_parsed = net_connect.send_command(
    "show ip interface brief",
    use_textfsm=True
)
print(type(output_parsed))  # <class 'list'>
# [{'interface': 'GigabitEthernet1', 'ip_address': '10.10.20.48',
#   'status': 'up', 'proto': 'up'}, ...]

Install NTC Templates

pip install ntc-templates

TextFSM templates are matched automatically by device_type + command.

Full Working Example

from netmiko import ConnectHandler
 
# ── DevNet IOS XE Always-On Sandbox ─────────────────────────────
device = {
    "device_type": "cisco_ios",
    "host": "sandbox-iosxe-latest-1.cisco.com",
    "username": "admin",
    "password": "C1sco12345",
}
 
# ── Connect ─────────────────────────────────────────────────────
net_connect = ConnectHandler(**device)
print(f"Connected to: {net_connect.find_prompt()}")
 
# ── Show command ────────────────────────────────────────────────
version_output = net_connect.send_command("show version")
print(version_output[:200])  # First 200 chars
 
# ── Structured show command (TextFSM) ──────────────────────────
interfaces = net_connect.send_command(
    "show ip interface brief",
    use_textfsm=True
)
for intf in interfaces:
    print(f"  {intf['interface']:30s} {intf['ip_address']:15s} {intf['status']}")
 
# ── Configure a loopback interface ──────────────────────────────
config_commands = [
    "interface Loopback99",
    "description Configured by Netmiko",
    "ip address 10.99.99.1 255.255.255.0",
    "no shutdown",
]
output = net_connect.send_config_set(config_commands)
print(output)
 
# ── Save configuration ─────────────────────────────────────────
net_connect.save_config()
 
# ── Verify ──────────────────────────────────────────────────────
verify = net_connect.send_command("show ip interface brief | include Loopback99")
print(f"Verification: {verify}")
 
# ── Disconnect ──────────────────────────────────────────────────
net_connect.disconnect()
print("Session closed.")

send_command() Advanced Parameters

# Wait for a non-standard prompt (e.g., confirmation dialog)
output = net_connect.send_command(
    "reload",
    expect_string=r"confirm",   # regex pattern to wait for
)
 
# Increase delay_factor for slow devices
output = net_connect.send_command(
    "show tech-support",
    delay_factor=4,             # multiplier for all internal delays
    read_timeout=120,           # max seconds to wait for output
)

2 – ncclient for NETCONF Operations

Exam Topic: 2.2

NETCONF is a model-driven, XML-based protocol. ncclient is the Python library for NETCONF operations. Know the protocol stack, XML filters, and edit_config operations.

NETCONF Protocol Stack

┌─────────────────────────────────────────────┐
│            Content Layer                     │
│   Configuration data (XML-encoded YANG)      │
├─────────────────────────────────────────────┤
│           Operations Layer                   │
│   get, get-config, edit-config, copy-config, │
│   lock, unlock, commit, validate, close      │
├─────────────────────────────────────────────┤
│            Messages Layer                    │
│   <rpc>, <rpc-reply>, <notification>         │
├─────────────────────────────────────────────┤
│           Transport Layer                    │
│   SSH (TCP port 830) — mandatory             │
└─────────────────────────────────────────────┘

NETCONF vs CLI

AspectCLI (Netmiko)NETCONF (ncclient)
ProtocolSSH (port 22)SSH subsystem (port 830)
Data formatUnstructured textStructured XML
Data modelScreen-scrapingYANG models
Transaction supportNoYes (lock/unlock/commit)
Error handlingParse output stringsStructured <rpc-error>
Partial configManualSubtree/xpath filters

manager.connect() — Establishing a Session

from ncclient import manager
 
m = manager.connect(
    host="sandbox-iosxe-latest-1.cisco.com",
    port=830,                      # NETCONF default port
    username="admin",
    password="C1sco12345",
    hostkey_verify=False,          # Skip host key check (lab only!)
    device_params={"name": "iosxe"},  # Optional: platform hint
    timeout=30,
)
 
print(f"Session ID: {m.session_id}")
print(f"Connected: {m.connected}")

Server Capabilities

After connecting, check which YANG models the device supports:

for capability in m.server_capabilities:
    if "ietf-interfaces" in capability:
        print(capability)
# Output: urn:ietf:params:xml:ns:yang:ietf-interfaces?module=ietf-interfaces&revision=2014-05-08

get_config() — Retrieve Configuration

# ── Full running config (XML) ──────────────────────────────────
full_config = m.get_config(source="running")
print(full_config.xml[:500])
 
# ── Filtered: only interfaces (subtree filter) ─────────────────
filter_xml = """
<filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"/>
</filter>
"""
interfaces = m.get_config(source="running", filter=filter_xml)
print(interfaces)

Subtree Filter vs XPath Filter

Filter TypeSyntaxSupport
Subtree<filter type="subtree">...</filter>All NETCONF devices
XPath<filter type="xpath" select="/interfaces"/>Not all devices support it

For the exam, default to subtree filters — they are universally supported.

edit_config() — Modify Configuration

# ── Create a new loopback interface ─────────────────────────────
config_payload = """
<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
    <interface>
      <name>Loopback100</name>
      <description>Created via NETCONF</description>
      <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">
        ianaift:softwareLoopback
      </type>
      <enabled>true</enabled>
      <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
        <address>
          <ip>10.100.100.1</ip>
          <netmask>255.255.255.0</netmask>
        </address>
      </ipv4>
    </interface>
  </interfaces>
</config>
"""
 
result = m.edit_config(target="running", config=config_payload)
print(f"edit_config result: {result.ok}")  # True if successful

edit_config Operations (default-operation)

# default-operation controls how edit_config handles existing data
# Options: "merge" (default), "replace", "none"
 
# Merge (default) — adds/updates, leaves everything else alone
m.edit_config(target="running", config=config_payload, default_operation="merge")
 
# Replace — replaces the targeted subtree entirely
m.edit_config(target="running", config=config_payload, default_operation="replace")

operation="delete" in XML

To remove a specific element, add operation="delete" in the XML:

<interface operation="delete">
  <name>Loopback100</name>
</interface>

lock() / unlock() — Safe Editing

# Lock prevents other NETCONF sessions from making changes
m.lock(target="running")
 
try:
    m.edit_config(target="running", config=config_payload)
finally:
    m.unlock(target="running")

commit() — For Candidate Datastore Devices

# Some devices (IOS XR, Junos) use a candidate datastore
# IOS XE typically operates on the running datastore directly
 
# Workflow for candidate datastore:
m.lock(target="candidate")
m.edit_config(target="candidate", config=config_payload)
m.commit()             # Apply candidate → running
m.unlock(target="candidate")

Common XML Namespaces

NamespaceURIUsage
NETCONF baseurn:ietf:params:xml:ns:netconf:base:1.0<config>, <filter> wrappers
ietf-interfacesurn:ietf:params:xml:ns:yang:ietf-interfacesInterface config/state
ietf-ipurn:ietf:params:xml:ns:yang:ietf-ipIPv4/IPv6 addressing
iana-if-typeurn:ietf:params:xml:ns:yang:iana-if-typeInterface type identities
Cisco IOS XE nativehttp://cisco.com/ns/yang/Cisco-IOS-XE-nativeIOS XE native model

Full Working Example

from ncclient import manager
import xml.dom.minidom
 
# ── Connect ─────────────────────────────────────────────────────
m = manager.connect(
    host="sandbox-iosxe-latest-1.cisco.com",
    port=830,
    username="admin",
    password="C1sco12345",
    hostkey_verify=False,
)
 
# ── Get interfaces (filtered) ──────────────────────────────────
filter_xml = """
<filter>
  <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"/>
</filter>
"""
result = m.get_config(source="running", filter=filter_xml)
print(xml.dom.minidom.parseString(result.xml).toprettyxml(indent="  ")[:1000])
 
# ── Create Loopback100 via edit_config ──────────────────────────
new_loopback = """
<config>
  <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
    <interface>
      <name>Loopback100</name>
      <description>NETCONF Automated</description>
      <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">
        ianaift:softwareLoopback
      </type>
      <enabled>true</enabled>
      <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
        <address>
          <ip>10.100.100.1</ip>
          <netmask>255.255.255.0</netmask>
        </address>
      </ipv4>
    </interface>
  </interfaces>
</config>
"""
 
edit_result = m.edit_config(target="running", config=new_loopback)
print(f"Config applied: {edit_result.ok}")
 
# ── Verify ──────────────────────────────────────────────────────
verify = m.get_config(source="running", filter="""
<filter>
  <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
    <interface>
      <name>Loopback100</name>
    </interface>
  </interfaces>
</filter>
""")
print(xml.dom.minidom.parseString(verify.xml).toprettyxml(indent="  "))
 
# ── Close session ───────────────────────────────────────────────
m.close_session()

3 – RESTCONF with Python requests

Exam Topic: 2.3

RESTCONF maps YANG models to RESTful HTTP operations. Know the URL structure, content types, and HTTP method mapping.

Protocol Basics

┌─────────────────────────────────────────────┐
│              Your Python Script              │
│         import requests (HTTPS)              │
└──────────────────┬──────────────────────────┘
                   │ HTTPS (port 443)
                   ▼
┌─────────────────────────────────────────────┐
│           IOS XE Device (RESTCONF)          │
│                                             │
│   /.well-known/host-meta → discover root   │
│   /restconf/data/...     → YANG data       │
│   /restconf/operations/...→ YANG RPCs      │
└─────────────────────────────────────────────┘

RESTCONF vs NETCONF

AspectNETCONFRESTCONF
TransportSSH (port 830)HTTPS (port 443)
Data encodingXML onlyJSON or XML
ProtocolRPC-basedRESTful (HTTP verbs)
Python libraryncclientrequests (standard HTTP)
URL addressingXPath/subtree filterURI path to YANG node
Datastore selectionget_config(source=“running”)Always running (or intended)
Streaming telemetryYes (notifications)No

Content Types

Content-Type / AcceptFormat
application/yang-data+jsonJSON encoding (preferred for Python)
application/yang-data+xmlXML encoding

Use yang-data+json, NOT application/json

RESTCONF has its own media types. Using plain application/json may work on some platforms but is technically incorrect and may cause 415 Unsupported Media Type errors.

URL Structure

https://{host}/restconf/data/{module}:{container}/{list}={key}

Examples:
https://10.10.20.48/restconf/data/ietf-interfaces:interfaces
https://10.10.20.48/restconf/data/ietf-interfaces:interfaces/interface=Loopback100
https://10.10.20.48/restconf/data/Cisco-IOS-XE-native:native/hostname
https://10.10.20.48/restconf/data/openconfig-interfaces:interfaces

HTTP Method Mapping

HTTP MethodRESTCONF OperationNETCONF Equivalent
GETRead dataget-config / get
POSTCreate new resourceedit-config (create)
PUTCreate or replace resourceedit-config (replace)
PATCHMerge/update resourceedit-config (merge)
DELETERemove resourceedit-config (delete)

POST vs PUT vs PATCH

  • POST — Creates a new resource. Fails with 409 Conflict if it already exists.
  • PUTReplaces the entire resource. Creates if it does not exist.
  • PATCHMerges changes into the existing resource. Safest for updates.

For the exam: PATCH is the safest method for updating existing configuration.

Well-Known URI Discovery

import requests
requests.packages.urllib3.disable_warnings()
 
HOST = "sandbox-iosxe-latest-1.cisco.com"
response = requests.get(
    f"https://{HOST}/.well-known/host-meta",
    auth=("admin", "C1sco12345"),
    verify=False,
)
print(response.text)
# Returns: <XRD><Link rel="restconf" href="/restconf"/></XRD>

Headers Setup

import requests
import urllib3
 
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
 
HOST = "sandbox-iosxe-latest-1.cisco.com"
BASE_URL = f"https://{HOST}/restconf/data"
AUTH = ("admin", "C1sco12345")
 
HEADERS = {
    "Accept": "application/yang-data+json",
    "Content-Type": "application/yang-data+json",
}

GET — Retrieve Interfaces

# ── All interfaces (ietf-interfaces model) ──────────────────────
response = requests.get(
    f"{BASE_URL}/ietf-interfaces:interfaces",
    auth=AUTH,
    headers=HEADERS,
    verify=False,
)
print(response.status_code)  # 200
interfaces = response.json()
 
for intf in interfaces["ietf-interfaces:interfaces"]["interface"]:
    print(f"  {intf['name']:30s} {intf.get('description', 'N/A')}")

GET — Single Interface by Key

# ── Get specific interface (list key = interface name) ──────────
response = requests.get(
    f"{BASE_URL}/ietf-interfaces:interfaces/interface=GigabitEthernet1",
    auth=AUTH,
    headers=HEADERS,
    verify=False,
)
intf = response.json()["ietf-interfaces:interface"]
print(f"Name: {intf['name']}")
print(f"Type: {intf['type']}")

GET — Native Model (Cisco-IOS-XE-native)

# ── Get hostname using Cisco native YANG model ─────────────────
response = requests.get(
    f"{BASE_URL}/Cisco-IOS-XE-native:native/hostname",
    auth=AUTH,
    headers=HEADERS,
    verify=False,
)
print(response.json())  # {"Cisco-IOS-XE-native:hostname": "csr1000v"}

PATCH — Update Interface Description

# ── Update description on GigabitEthernet1 ──────────────────────
payload = {
    "ietf-interfaces:interface": {
        "name": "GigabitEthernet1",
        "description": "Updated via RESTCONF PATCH",
    }
}
 
response = requests.patch(
    f"{BASE_URL}/ietf-interfaces:interfaces/interface=GigabitEthernet1",
    auth=AUTH,
    headers=HEADERS,
    json=payload,
    verify=False,
)
print(f"PATCH status: {response.status_code}")  # 204 No Content = success

PUT — Replace Entire Interface Config

# ── Replace Loopback200 entirely ────────────────────────────────
payload = {
    "ietf-interfaces:interface": {
        "name": "Loopback200",
        "description": "Replaced via RESTCONF PUT",
        "type": "iana-if-type:softwareLoopback",
        "enabled": True,
        "ietf-ip:ipv4": {
            "address": [
                {
                    "ip": "10.200.200.1",
                    "netmask": "255.255.255.0",
                }
            ]
        },
    }
}
 
response = requests.put(
    f"{BASE_URL}/ietf-interfaces:interfaces/interface=Loopback200",
    auth=AUTH,
    headers=HEADERS,
    json=payload,
    verify=False,
)
print(f"PUT status: {response.status_code}")  # 201 Created or 204 No Content

POST — Create New Resource

# ── Create a new loopback (POST to the collection) ─────────────
payload = {
    "ietf-interfaces:interface": {
        "name": "Loopback201",
        "description": "Created via RESTCONF POST",
        "type": "iana-if-type:softwareLoopback",
        "enabled": True,
        "ietf-ip:ipv4": {
            "address": [
                {
                    "ip": "10.201.201.1",
                    "netmask": "255.255.255.0",
                }
            ]
        },
    }
}
 
response = requests.post(
    f"{BASE_URL}/ietf-interfaces:interfaces",
    auth=AUTH,
    headers=HEADERS,
    json=payload,
    verify=False,
)
print(f"POST status: {response.status_code}")  # 201 Created

DELETE — Remove a Resource

# ── Delete Loopback201 ─────────────────────────────────────────
response = requests.delete(
    f"{BASE_URL}/ietf-interfaces:interfaces/interface=Loopback201",
    auth=AUTH,
    headers=HEADERS,
    verify=False,
)
print(f"DELETE status: {response.status_code}")  # 204 No Content

YANG Module Paths (Exam Favourites)

YANG ModuleRESTCONF PathUsage
ietf-interfaces/restconf/data/ietf-interfaces:interfacesStandard interface config
Cisco-IOS-XE-native/restconf/data/Cisco-IOS-XE-native:nativeFull IOS XE native config
openconfig-interfaces/restconf/data/openconfig-interfaces:interfacesOpenConfig interface model
ietf-routing/restconf/data/ietf-routing:routingRouting tables
Cisco-IOS-XE-native.../native/router/bgpBGP configuration

4 – Ansible for Device-Level Automation

Exam Topic: 2.4

Ansible uses SSH (network_cli connection plugin) for device-level automation. Know the key modules, the difference between imperative and declarative approaches, and the inventory format.

Connection Plugin: network_cli

Unlike controller-based Ansible collections (which use httpapi), device-level automation uses network_cli — a persistent SSH connection to the device CLI.

┌──────────────────────────────────────────┐
│           Ansible Control Node           │
│  ansible-playbook site.yml               │
└────────────────┬─────────────────────────┘
                 │ SSH (network_cli)
                 ▼
┌──────────────────────────────────────────┐
│           IOS XE Device                  │
│  cisco.ios.ios_command / ios_config       │
└──────────────────────────────────────────┘

Connection Plugins Comparison

PluginTransportUsed For
network_cliSSHDevice-level (CLI) — exam topic 2.4
netconfSSH (port 830)NETCONF-based modules
httpapiHTTPSController APIs (Catalyst Center, Meraki, SD-WAN)
localN/ARunning API calls from the control node

Inventory File

# inventory.ini
[iosxe]
sandbox-iosxe-latest-1.cisco.com
 
[iosxe:vars]
ansible_network_os=cisco.ios.ios
ansible_user=admin
ansible_password=C1sco12345
ansible_connection=network_cli
ansible_become=yes
ansible_become_method=enable
ansible_become_password=C1sco12345

Key Modules

ModulePurposeApproach
cisco.ios.ios_commandRun show commandsImperative
cisco.ios.ios_configPush config linesImperative
cisco.ios.ios_factsGather structured factsRead-only
cisco.ios.ios_interfacesManage interfacesDeclarative
cisco.ios.ios_l3_interfacesManage L3 interface IPsDeclarative
cisco.ios.ios_vlansManage VLANsDeclarative
cisco.ios.ios_static_routesManage static routesDeclarative
cisco.ios.ios_aclsManage ACLsDeclarative

Imperative vs Declarative

Exam Favourite: Know the Difference

AspectImperative (ios_config)Declarative (ios_interfaces)
You specifyExact CLI commands to pushDesired end state
IdempotencyManual (check before push)Built-in (module checks current state)
State paramNostate: merged/replaced/overridden/deleted
RollbackManualSome modules support state: deleted
ComplexitySimple, directMore abstracted

ios_command — Run Show Commands

- name: Run show commands
  hosts: iosxe
  gather_facts: false
  tasks:
    - name: Get show version
      cisco.ios.ios_command:
        commands:
          - show version
          - show ip interface brief
      register: output
 
    - name: Display output
      ansible.builtin.debug:
        msg: "{{ output.stdout_lines }}"

ios_config — Push Configuration

- name: Configure device
  hosts: iosxe
  gather_facts: false
  tasks:
    - name: Configure loopback interface
      cisco.ios.ios_config:
        lines:
          - description Configured by Ansible
          - ip address 10.99.99.1 255.255.255.0
          - no shutdown
        parents: interface Loopback99
        save_when: modified  # save only if changes were made
 
    - name: Configure NTP
      cisco.ios.ios_config:
        lines:
          - ntp server 10.0.0.1

ios_config Key Parameters

ParameterPurpose
linesList of config commands to push
parentsParent command(s) to enter before lines (e.g., interface Gi1)
save_whenalways, modified, changed, never — when to save config
beforeCommands to run before lines
afterCommands to run after lines
backupyes — backup running config before changes
matchline, strict, exact, none — how to compare existing config

ios_facts — Gather Structured Facts

- name: Gather facts
  hosts: iosxe
  gather_facts: false
  tasks:
    - name: Gather all IOS facts
      cisco.ios.ios_facts:
        gather_subset:
          - all  # or: min, hardware, interfaces, config
      register: facts
 
    - name: Show hostname
      debug:
        msg: "Hostname: {{ ansible_net_hostname }}"
 
    - name: Show interfaces
      debug:
        msg: "{{ ansible_net_interfaces }}"

ios_interfaces — Declarative Interface Management

- name: Declarative interface config
  hosts: iosxe
  gather_facts: false
  tasks:
    - name: Configure interfaces (merged)
      cisco.ios.ios_interfaces:
        config:
          - name: Loopback99
            description: Managed by Ansible (declarative)
            enabled: true
          - name: GigabitEthernet2
            description: WAN uplink
            enabled: true
            speed: "1000"
            duplex: full
        state: merged  # merged | replaced | overridden | deleted

Declarative State Parameter Values

StateBehavior
mergedAdd/update specified attributes. Leave everything else untouched.
replacedReplace config for specified interfaces only. Unspecified interfaces untouched.
overriddenReplace config for all interfaces. Unspecified interfaces reset to defaults.
deletedRemove config for specified interfaces (or all if no config given).

Exam trap: overridden affects ALL interfaces, not just the ones listed. Use replaced for targeted changes.

Full Playbook Example

---
# playbook: device_automation.yml
- name: Device-Level Automation Demo
  hosts: iosxe
  gather_facts: false
 
  tasks:
    # ── Step 1: Gather facts ────────────────────────────────────
    - name: Gather IOS facts
      cisco.ios.ios_facts:
        gather_subset:
          - min
          - interfaces
      register: facts
 
    - name: Display device info
      debug:
        msg: >
          Device: {{ ansible_net_hostname }}
          Model: {{ ansible_net_model }}
          Version: {{ ansible_net_version }}
 
    # ── Step 2: Configure interfaces (declarative) ──────────────
    - name: Ensure Loopback99 exists with correct config
      cisco.ios.ios_interfaces:
        config:
          - name: Loopback99
            description: Ansible Declarative
            enabled: true
        state: merged
 
    - name: Assign IP to Loopback99
      cisco.ios.ios_l3_interfaces:
        config:
          - name: Loopback99
            ipv4:
              - address: 10.99.99.1/24
        state: merged
 
    # ── Step 3: Verify ──────────────────────────────────────────
    - name: Verify Loopback99
      cisco.ios.ios_command:
        commands:
          - show ip interface brief | include Loopback99
      register: verify
 
    - name: Show verification
      debug:
        msg: "{{ verify.stdout_lines }}"

Cross-Reference

For controller-based Ansible automation (cisco.dnac, cisco.meraki, cisco.ise, cisco.catalystwan), see 3.4_Ansible_Controller_Automation.


5 – Day-0 Provisioning

Exam Topic: 2.5

Day-0 provisioning automates initial device setup without manual CLI access. Know ZTP (script-based) vs PnP (controller-based) and when each is used.

ZTP (Zero Touch Provisioning) on IOS XE

ZTP is a device-level provisioning method — the device downloads and executes a Python script on first boot, with no controller required.

ZTP Workflow

┌───────────────┐     ┌───────────────┐     ┌───────────────┐
│  New IOS XE   │     │  DHCP Server  │     │  HTTP/TFTP    │
│  Device Boot  │────>│  Option 67:   │────>│  Server       │
│  (no config)  │     │  ztp.py URL   │     │  ztp.py file  │
└───────────────┘     └───────────────┘     └───────┬───────┘
                                                     │
                            ┌────────────────────────┘
                            ▼
                   ┌────────────────┐
                   │ Device runs    │
                   │ ztp.py script  │
                   │ - Set hostname │
                   │ - Config mgmt  │
                   │ - Enable SSH   │
                   │ - Enable NETCONF│
                   └────────────────┘
  1. Device boots with no startup configuration.
  2. DHCP server assigns an IP and provides Option 67 (bootfile name) pointing to a Python script URL.
  3. Device downloads and executes the Python script in Guest Shell.
  4. Script configures the device via the cli() function.

DHCP Server Configuration (Example)

ip dhcp pool ZTP-POOL
 network 10.0.0.0 255.255.255.0
 default-router 10.0.0.1
 option 67 ascii "http://10.0.0.100/ztp.py"

ZTP Python Script Example

#!/usr/bin/env python3
"""
ZTP script for IOS XE Day-0 provisioning.
Runs automatically on first boot via DHCP Option 67.
"""
 
import cli   # IOS XE built-in module — only available on-box
 
# ── Set hostname ────────────────────────────────────────────────
cli.configure([
    "hostname BRANCH-SW-01",
])
 
# ── Configure management interface ─────────────────────────────
cli.configure([
    "interface GigabitEthernet1",
    "ip address 10.0.0.10 255.255.255.0",
    "no shutdown",
])
 
# ── Enable SSH and NETCONF ──────────────────────────────────────
cli.configure([
    "ip domain-name lab.local",
    "crypto key generate rsa modulus 2048",
    "ip ssh version 2",
    "username admin privilege 15 secret C1sco12345",
    "line vty 0 4",
    "login local",
    "transport input ssh",
    "netconf-yang",
    "restconf",
])
 
# ── Save configuration ─────────────────────────────────────────
cli.cli("write memory")
 
print("ZTP provisioning complete!")

ZTP Key Points for the Exam

  • ZTP uses a Python script, not a config file (unlike some other vendors).
  • The script runs in Guest Shell on IOS XE.
  • The cli module (cli.configure(), cli.cli()) is only available on-box.
  • DHCP Option 67 provides the script URL.
  • ZTP is device-level provisioning — no controller needed.

PnP (Plug and Play) — Device Side

PnP is a controller-based provisioning method. The device discovers and contacts a PnP server (Catalyst Center) which pushes configuration to it.

PnP Discovery Methods (Device Side)

MethodHow It WorksPriority
DHCP Option 43DHCP server provides PnP server IP in option 431st (highest)
DNSDevice queries _cisco-pnp._tcp.{domain} SRV record2nd
Cloud redirectDevice contacts devicehelper.cisco.com → redirected to controller3rd

PnP Workflow (Device Perspective)

┌───────────────┐     ┌───────────────┐     ┌───────────────────┐
│  New Device   │     │  DHCP/DNS     │     │  Catalyst Center  │
│  Boot (PnP    │────>│  Option 43 or │────>│  PnP Service      │
│   agent)      │     │  SRV record   │     │  /dna/intent/api/ │
└───────────────┘     └───────────────┘     │  v1/onboarding/   │
                                             │  pnp-device       │
                                             └─────────┬─────────┘
                                                       │
                                              Push config/image
                                                       │
                                                       ▼
                                             ┌─────────────────┐
                                             │ Device claimed   │
                                             │ and provisioned  │
                                             └─────────────────┘

ZTP vs PnP

AspectZTPPnP
Controller requiredNoYes (Catalyst Center)
TriggerDHCP Option 67DHCP Option 43 / DNS / Cloud
Config sourcePython script on HTTP/TFTP serverCatalyst Center template
Image upgradeManual in scriptIntegrated (SWIM)
ScalabilityGood for small deploymentsEnterprise-scale
Exam topic2.5 (device-level)3.1 (controller-based)

For controller-side PnP details, see 8 – Day-0 PnP Provisioning.

iPXE Boot

iPXE is used for automated OS installation on bare-metal or virtual network devices.

Device power-on → iPXE firmware → DHCP (option 67 = iPXE script URL)
→ Download boot image → Install OS → Boot into provisioned state

iPXE is primarily relevant for virtualized environments (CSR1000v, Catalyst 8000v) and data center provisioning. ZTP and PnP are more commonly tested.


6 – Troubleshooting NETCONF, RESTCONF, and YANG

Exam Topic: 2.6

Expect exam questions that present error messages or logs and ask you to identify the root cause. Know the common errors for both NETCONF and RESTCONF.

Common NETCONF Errors

ErrorCauseFix
Connection refusedSSH not enabled, wrong port, netconf-yang not configuredEnable with netconf-yang on device, verify port 830
Capability mismatchYANG model not supported on device/versionCheck m.server_capabilities, upgrade IOS XE
lock-deniedAnother session holds the lockClose other NETCONF sessions, or use <kill-session>
data-missingTrying to delete data that does not existVerify target data exists first with get_config
invalid-valueWrong data type or value in XML payloadCheck YANG model constraints (range, enum, pattern)
malformed-messageInvalid XML syntaxValidate XML before sending (close all tags, correct namespaces)
access-deniedInsufficient privilegesUse admin-level credentials
TimeoutDevice too slow or large responseIncrease timeout in manager.connect(timeout=60)

NETCONF Error Response Example

<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <rpc-error>
    <error-type>application</error-type>
    <error-tag>invalid-value</error-tag>
    <error-severity>error</error-severity>
    <error-message>
      "Loopback" is not a valid interface type string
    </error-message>
  </rpc-error>
</rpc-reply>

Common RESTCONF Errors

HTTP StatusMeaningCommon Cause
400 Bad RequestInvalid requestWrong YANG path, malformed JSON, invalid value
401 UnauthorizedAuthentication failedWrong credentials, auth not configured
403 ForbiddenAuthorized but not permittedUser lacks privilege for this operation
404 Not FoundResource does not existWrong URL, module not enabled (restconf not configured)
405 Method Not AllowedHTTP method not valid for resourcePOST to existing resource, DELETE on non-deletable
409 ConflictResource already existsPOST when resource exists — use PUT or PATCH instead
415 Unsupported Media TypeWrong Content-TypeUse application/yang-data+json not application/json

404 vs 409 — Exam Trap

  • 404: The URL path itself is wrong (typo in YANG module, wrong container name).
  • 409: The URL is correct but the operation conflicts (e.g., POST an interface that already exists).

Fix for 409: Change POST to PUT (replace) or PATCH (merge).

YANG Model Troubleshooting

Using pyang to Visualize YANG Models

# Install pyang
pip install pyang
 
# Download YANG model
# From: https://github.com/YangModels/yang
 
# Generate tree view
pyang -f tree ietf-interfaces.yang
 
# Output:
# module: ietf-interfaces
#   +--rw interfaces
#      +--rw interface* [name]
#         +--rw name                        string
#         +--rw description?                string
#         +--rw type                        identityref
#         +--rw enabled?                    boolean

Checking Capabilities at Runtime

from ncclient import manager
 
m = manager.connect(
    host="sandbox-iosxe-latest-1.cisco.com",
    port=830,
    username="admin",
    password="C1sco12345",
    hostkey_verify=False,
)
 
# Check if a specific YANG model is supported
for cap in m.server_capabilities:
    if "openconfig" in cap:
        print(cap)
 
# Check NETCONF base version
for cap in m.server_capabilities:
    if "base" in cap:
        print(cap)
# urn:ietf:params:netconf:base:1.0
# urn:ietf:params:netconf:base:1.1

YANG Suite

YANG Suite

YANG Suite is a Cisco tool for exploring YANG models visually. It helps:

  • Browse YANG model trees interactively
  • Generate NETCONF/RESTCONF payloads from models
  • Test API calls directly
  • Validate XML/JSON payloads against YANG constraints

Install: pip install yangsuite or use the Docker container.

Debugging Tips

# ── NETCONF: Enable verbose logging ────────────────────────────
import logging
logging.basicConfig(level=logging.DEBUG)
 
# This will show the raw XML RPC exchange
 
# ── RESTCONF: Inspect full response ────────────────────────────
response = requests.get(url, auth=AUTH, headers=HEADERS, verify=False)
print(f"Status: {response.status_code}")
print(f"Headers: {dict(response.headers)}")
print(f"Body: {response.text}")
 
# ── On device: Enable NETCONF/RESTCONF debugging ───────────────
# debug netconf all
# debug restconf all
# terminal monitor

IOS XE Prerequisite Commands

If NETCONF/RESTCONF is not working, verify these are configured on the device:

netconf-yang
restconf
ip http server
ip http secure-server
ip http authentication local

7 – On-Box Automation (EEM, Guest Shell, On-Box Python)

Exam Topic: 2.7

On-box automation runs directly on the network device rather than from an external script. Know EEM applets, Guest Shell, and the on-box Python cli module.

EEM (Embedded Event Manager)

EEM is IOS/IOS XE’s built-in event-driven automation framework. It detects events and executes actions — no external tools required.

EEM Architecture

┌─────────────────────────────────────────────┐
│              IOS XE Device                  │
│                                             │
│  Event Detectors          Actions           │
│  ┌──────────────┐        ┌───────────────┐  │
│  │ cli           │───────>│ cli command   │  │
│  │ syslog        │       │ syslog msg    │  │
│  │ timer         │       │ mail          │  │
│  │ interface     │       │ info          │  │
│  │ snmp          │       │ reload        │  │
│  │ track         │       │ Python script │  │
│  └──────────────┘        └───────────────┘  │
└─────────────────────────────────────────────┘

EEM Applet: Interface Down Detection

! Detect when any GigabitEthernet interface goes down
event manager applet INTF_DOWN
 event syslog pattern "Interface GigabitEthernet.*, changed state to down"
 action 1.0 syslog msg "EEM: Interface went DOWN — investigating"
 action 2.0 cli command "enable"
 action 3.0 cli command "show interface brief"
 action 4.0 syslog msg "EEM: Interface status captured"

EEM Applet: Scheduled Config Backup

! Run every day at 02:00
event manager applet DAILY_BACKUP
 event timer cron cron-entry "0 2 * * *" name BACKUP_TIMER
 action 1.0 cli command "enable"
 action 2.0 cli command "copy running-config tftp://10.0.0.100/backup.cfg"
 action 3.0 syslog msg "EEM: Daily config backup completed"

EEM Applet: CLI Event Detection

! Detect when someone runs "show running-config"
event manager applet AUDIT_SHOW_RUN
 event cli pattern "show running-config" sync yes
 action 1.0 syslog priority notifications msg "AUDIT: User ran show run"

EEM Event Types for the Exam

EventTrigger
event syslog pattern "..."Syslog message matches pattern
event cli pattern "..."CLI command matches pattern
event timer watchdog time <sec>Repeating timer (every N seconds)
event timer cron cron-entry "..."Cron-style schedule
event interface name <intf> parameter ...Interface counter threshold
event track <id> state ...Tracked object state change
event noneManual trigger only (event manager run <applet>)

EEM with Python Script

! Run a Python script when triggered
event manager applet RUN_PYTHON
 event none
 action 1.0 cli command "enable"
 action 2.0 cli command "guestshell run python3 /home/guestshell/check_compliance.py"

Guest Shell

Guest Shell is a Linux container (LXC) running on IOS XE that provides a full Python environment with access to the device CLI.

Enable Guest Shell

! On IOS XE device
iox
app-hosting appid guestshell
 app-vnic gateway1 virtualportgroup 0 guest-interface 0
  guest-ipaddress 192.168.1.2 netmask 255.255.255.0
 app-default-gateway 192.168.1.1 guest-interface 0
 name-server0 8.8.8.8
guestshell enable

Or on newer IOS XE (simplified):

guestshell enable

Using Guest Shell

# Enter guest shell from IOS XE CLI
Device# guestshell
 
# You are now in a Linux shell
[guestshell@guestshell ~]$ python3 --version
Python 3.6.8
 
# Install packages
[guestshell@guestshell ~]$ pip3 install requests
 
# Run a script
[guestshell@guestshell ~]$ python3 /home/guestshell/backup.py
 
# Run from IOS XE CLI directly (without entering guest shell)
Device# guestshell run python3 /home/guestshell/backup.py

Guest Shell Python: Accessing IOS CLI

#!/usr/bin/env python3
"""
Guest Shell script: backup running config to TFTP server.
Uses the cli module available only inside Guest Shell.
"""
 
import cli
import time
 
# ── Get hostname ────────────────────────────────────────────────
hostname = cli.execute("show run | include hostname").split()[-1]
 
# ── Generate timestamped filename ───────────────────────────────
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"{hostname}_{timestamp}.cfg"
 
# ── Copy running config to TFTP ────────────────────────────────
cli.execute(f"copy running-config tftp://10.0.0.100/{filename}")
 
print(f"Backup saved: {filename}")

cli Module Methods

MethodBehavior
cli.cli("show version")Returns output as a string (includes command echo)
cli.execute("show version")Returns output as a string (cleaner, no echo)
cli.configure(["hostname R1", "int Lo0"])Enters config mode, runs commands, exits
cli.configurep(["hostname R1"])Same as configure but prints output

Exam trap: cli.cli() includes the command echo in output; cli.execute() does not.

On-Box Python (Direct Interpreter)

IOS XE 16.x+ includes a Python 3 interpreter accessible from the CLI without Guest Shell.

! Start Python interpreter directly on IOS XE
Device# python3

Python 3.6.8 (default)
>>> import cli
>>> output = cli.execute("show ip route summary")
>>> print(output)

On-Box Compliance Check Example

#!/usr/bin/env python3
"""
On-box compliance check: verify required configurations exist.
Run via: guestshell run python3 /home/guestshell/compliance.py
"""
 
import cli
 
REQUIRED_CONFIGS = [
    "service timestamps log datetime msec",
    "logging buffered 16384",
    "ntp server 10.0.0.1",
    "ip ssh version 2",
    "netconf-yang",
]
 
# ── Get running config ──────────────────────────────────────────
running_config = cli.execute("show running-config")
 
# ── Check compliance ────────────────────────────────────────────
non_compliant = []
for config_line in REQUIRED_CONFIGS:
    if config_line not in running_config:
        non_compliant.append(config_line)
 
if non_compliant:
    print("NON-COMPLIANT — Missing configurations:")
    for item in non_compliant:
        print(f"  - {item}")
 
    # Auto-remediate
    print("\nApplying missing configurations...")
    cli.configure(non_compliant)
    print("Remediation complete.")
else:
    print("COMPLIANT — All required configurations present.")

8 – Common Patterns and Gotchas

Tool Comparison Table

AspectNetmikoncclientRESTCONFAnsible
ProtocolSSH (22)SSH/NETCONF (830)HTTPS (443)SSH (22) via network_cli
Data formatUnstructured textXMLJSON or XMLYAML playbooks
Data modelCLI screen-scrapingYANG modelsYANG modelsAbstracted modules
AuthUsername/passwordUsername/passwordBasic Auth (IOS XE)Inventory credentials
IdempotencyManualManual (check before edit)Manual (PATCH is safe)Built-in (declarative modules)
TransactionNoYes (lock/commit)NoNo (per-task)
Structured outputTextFSM (add-on)Native XMLNative JSONios_facts
Best forQuick scripts, legacyConfig mgmt, transactionsREST integration, CI/CDLarge-scale orchestration
Exam topic2.12.22.32.4

When to Use Which Tool

Decision Matrix

  • One-off show commands on a few devices → Netmiko
  • Configuration changes requiring transactions (lock/edit/commit) → ncclient (NETCONF)
  • Integration with CI/CD pipelines or REST-based workflows → RESTCONF
  • Large-scale, repeatable automation across many devices → Ansible
  • Event-driven automation on the device itself → EEM
  • Complex on-box scripts with Python packages → Guest Shell

Common Exam Traps

Top Exam Gotchas

  1. NETCONF port is 830, not 22. Netmiko uses 22.
  2. RESTCONF Content-Type is application/yang-data+json, not application/json.
  3. POST fails with 409 if resource exists — use PUT or PATCH instead.
  4. Ansible overridden state resets ALL interfaces, not just listed ones.
  5. ZTP uses Python scripts (DHCP Option 67), PnP uses a controller (Option 43).
  6. cli.cli() includes command echo, cli.execute() does not.
  7. NETCONF edit_config(target="running") — IOS XE writes to running directly. IOS XR uses candidate + commit.
  8. RESTCONF on IOS XE requires both restconf AND ip http secure-server to be configured.
  9. Netmiko send_command() is for show commands. send_config_set() is for config commands. Mixing them up causes errors.
  10. Ansible network_cli is for device-level. httpapi is for controllers.

9 – Exam-Style Scenarios

Scenario 1 — Netmiko (Exam Topic: 2.1)

A network engineer writes the following Netmiko script but it hangs indefinitely:

net_connect = ConnectHandler(**device)
output = net_connect.send_command("reload")

What is the most likely cause?

A) The reload command requires enable mode B) The device disconnects during reload, breaking the SSH session C) send_command() is waiting for the device prompt, but reload shows a confirmation dialog D) Netmiko does not support the reload command


Scenario 2 — NETCONF (Exam Topic: 2.2)

An engineer tries to edit the running config via NETCONF but receives a lock-denied error. What should they check?

A) The device does not support NETCONF B) Another NETCONF session holds the lock on the running datastore C) The YANG model is not installed on the device D) The XML payload is malformed


Scenario 3 — RESTCONF (Exam Topic: 2.3)

A Python script sends a POST request to create a loopback interface but receives a 409 Conflict response. The interface already exists. Which HTTP method should be used instead to update it?

A) GET B) PUT C) PATCH D) DELETE


Scenario 4 — Ansible (Exam Topic: 2.4)

An Ansible playbook uses cisco.ios.ios_interfaces with state: overridden and specifies only Loopback0 in the config. After running the playbook, all other interfaces lose their descriptions and are set to defaults. Why?

A) The overridden state has a bug B) The overridden state replaces config for ALL interfaces, resetting unspecified ones to defaults C) The playbook did not include gather_facts: true D) The network_cli connection does not support declarative modules


Scenario 5 — ZTP (Exam Topic: 2.5)

A new IOS XE switch boots with no configuration. The DHCP server is configured with Option 67 pointing to http://10.0.0.100/ztp.py. The switch downloads the script but fails to execute it. Which is the most likely cause?

A) ZTP requires DHCP Option 43, not Option 67 B) The script uses import cli but cli is not available outside Guest Shell C) The script has a syntax error or the file is not valid Python 3 D) ZTP only supports TFTP, not HTTP


Scenario 6 — EEM and Guest Shell (Exam Topic: 2.7)

An engineer wants to run a Python script on an IOS XE device every time a syslog message containing “DUPLEX_MISMATCH” appears. Which combination of technologies achieves this?

A) EEM applet with event syslog trigger and action cli command "guestshell run python3 /home/guestshell/fix_duplex.py" B) Netmiko script polling the device log every 60 seconds C) RESTCONF webhook subscription for syslog events D) Ansible playbook with ios_command running show logging


10 – Quick Reference Cheat Sheet

Netmiko (2.1)

OperationCode
ConnectConnectHandler(device_type="cisco_ios", host=H, username=U, password=P)
Show commandnet_connect.send_command("show ip int brief")
Structured outputnet_connect.send_command("show ip int brief", use_textfsm=True)
Config pushnet_connect.send_config_set(["int Lo0", "desc Test"])
Config from filenet_connect.send_config_from_file("config.txt")
Enable modenet_connect.enable()
Save confignet_connect.save_config()
Disconnectnet_connect.disconnect()

ncclient / NETCONF (2.2)

OperationCode
Connectmanager.connect(host=H, port=830, username=U, password=P, hostkey_verify=False)
Get full configm.get_config(source="running")
Get filtered configm.get_config(source="running", filter=xml_filter)
Edit configm.edit_config(target="running", config=xml_payload)
Lock datastorem.lock(target="running")
Unlock datastorem.unlock(target="running")
Commit (candidate)m.commit()
Close sessionm.close_session()

RESTCONF (2.3)

OperationHTTP MethodSuccess Code
ReadGET200 OK
Create newPOST201 Created
Create or replacePUT201 / 204
Update (merge)PATCH204 No Content
DeleteDELETE204 No Content
Base URLhttps://{host}/restconf/data/
Content-Typeapplication/yang-data+json
AuthBasic Auth (IOS XE)

Ansible Device-Level (2.4)

ModulePurposeApproach
cisco.ios.ios_commandRun show commandsImperative
cisco.ios.ios_configPush config lines (lines, parents, save_when)Imperative
cisco.ios.ios_factsGather structured device factsRead-only
cisco.ios.ios_interfacesInterface management (state: merged/replaced/overridden/deleted)Declarative
Connectionansible_connection=network_cli
Network OSansible_network_os=cisco.ios.ios

Day-0 (2.5)

MethodTriggerScript/Config Source
ZTPDHCP Option 67Python script (HTTP/TFTP)
PnPDHCP Option 43 / DNS SRV / CloudCatalyst Center template
iPXEDHCP Option 67 (iPXE script)Network boot image

On-Box Automation (2.7)

TechnologyTriggerUse Case
EEM appletsyslog, cli, timer, interface, trackEvent-driven actions
Guest ShellManual / EEM / cronPython scripts with pip packages
On-box PythonManual / EEMQuick CLI automation
cli modulecli.execute() — clean output; cli.cli() — includes echo; cli.configure([]) — config mode

Port and Protocol Summary

ProtocolPortTransportPython Library
SSH (CLI)22TCPNetmiko / Paramiko
NETCONF830SSH subsystemncclient
RESTCONF443HTTPSrequests

Study Path

This guide covers device-level automation (Topic 2.0). For controller-based automation (Topic 3.0), see:

See Also