NETCONF with ncclient (Python)

Traditional network management — SSHing into a device, typing CLI commands, and screen-scraping the output — does not scale. When a change needs to be applied to 200 routers, or when you need to extract a specific value from the configuration of every switch in the network, text-based CLI automation is fragile, vendor-specific, and error-prone. NETCONF (Network Configuration Protocol, RFC 6241) is the industry answer: a structured, transaction-safe, XML-based protocol that lets management software read and write device configuration through well-defined data models rather than parsing human-readable text.

NETCONF runs over SSH on TCP port 830 and uses YANG (Yet Another Next Generation, RFC 6020/7950) data models to describe the structure of configuration and operational data. Every piece of configuration has a defined path in a YANG model — an interface's IP address, a static route, an OSPF area — and NETCONF operations use XML payloads that follow those paths precisely. The result is configuration management that is structured (no text parsing), transactional (changes either fully apply or roll back), and vendor-neutral (OpenConfig models work across vendors; vendor-native models cover vendor-specific features). For the broader automation landscape, see Network Automation Overview and Python for Networking.

ncclient is the most widely used Python library for NETCONF. It handles the SSH transport, the NETCONF session handshake, capability negotiation, and XML serialisation — giving you a clean Python API to send get, get-config, edit-config, commit, and lock operations without writing raw XML transport code. This lab covers the complete workflow: enabling NETCONF on IOS-XE, connecting with ncclient, reading configuration and operational state, and pushing configuration changes using both Cisco-native and OpenConfig YANG models.

Before starting this lab, ensure NETCONF's SSH transport prerequisites are in place at SSH Configuration. For comparison with CLI-based Python automation, see Python Netmiko Show Commands. For EEM-based on-device automation that complements NETCONF for event-driven config changes, see EEM — Embedded Event Manager Scripting.

1. NETCONF Architecture — Core Concepts

NETCONF Protocol Stack

  ┌──────────────────────────────────────────────────────┐
  │  Management Application  (Python + ncclient)         │
  ├──────────────────────────────────────────────────────┤
  │  NETCONF Protocol Layer  (RFC 6241)                  │
  │  Operations: get, get-config, edit-config, commit... │
  ├──────────────────────────────────────────────────────┤
  │  XML Encoding + YANG Data Models                     │
  │  (Cisco-IOS-XE-native, OpenConfig, IETF)             │
  ├──────────────────────────────────────────────────────┤
  │  SSH Transport  (TCP port 830)                       │
  └──────────────────────────────────────────────────────┘

  NETCONF Datastores:
  ┌──────────────┬────────────────────────────────────────────┐
  │  running     │ The active configuration (always present)  │
  │  startup     │ Config saved to NVRAM (loaded at boot)     │
  │  candidate   │ Staging area — edit here, then commit      │
  │              │ to running. Requires :candidate capability │
  └──────────────┴────────────────────────────────────────────┘

  IOS-XE supports all three datastores.
  Most operations in this lab use the running datastore directly.
  

NETCONF Operations Reference

Operation Description ncclient Method Analogous CLI
<get-config> Retrieve configuration data from a datastore (running, startup, or candidate) m.get_config() show running-config
<get> Retrieve both configuration data AND operational/state data (interface counters, BGP state, ARP table) m.get() show interfaces, show ip route
<edit-config> Modify a datastore — merge, replace, create, delete, or remove configuration elements m.edit_config() configure terminal + config commands
<commit> Apply changes from the candidate datastore to the running datastore. Only needed with candidate datastore workflow m.commit() copy running-config startup-config (conceptually)
<lock> / <unlock> Lock a datastore to prevent other sessions from modifying it during a configuration transaction m.lock() / m.unlock() No direct CLI equivalent
<validate> Validate configuration in the candidate datastore against YANG constraints before committing m.validate() No direct CLI equivalent
<copy-config> Copy one complete datastore to another (e.g., candidate to running, running to startup) m.copy_config() copy running-config startup-config
<delete-config> Delete a datastore (cannot delete the running datastore) m.delete_config() write erase (on startup)

YANG Data Model Types on IOS-XE

Model Type Namespace Prefix Coverage Use When
Cisco-IOS-XE-native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native" Full IOS-XE CLI feature set — 1:1 mapping to every CLI command. Most comprehensive coverage Configuring any IOS-XE-specific feature (VRF, GRE, HSRP, QoS, NAT, EEM). When OpenConfig does not cover the feature needed
OpenConfig xmlns="http://openconfig.net/yang/interfaces" (varies by module) Vendor-neutral models for interfaces, BGP, IS-IS, OSPF, VLANs, and more. Same model works across Cisco, Juniper, Arista Writing multi-vendor automation scripts. When portability across vendors is required
IETF xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces" IETF-standardised models for interfaces, IP, routing, NETCONF itself Standards-compliant automation. Retrieving interface and IP address data portably

edit-config Operation Attributes

Attribute Value Behaviour Analogous CLI
nc:operation merge (default) Add or update the element — existing config is preserved, new values are added or overwritten interface gi1description New-Desc
replace Replace the entire element with the provided content — all child elements not in the payload are deleted Removing and re-entering the entire interface config
create Create the element only if it does not already exist — returns an error if it exists Creating a new VLAN that must not already exist
delete Delete the element — returns an error if it does not exist no interface loopback 99
remove Delete the element if it exists — no error if it does not exist (idempotent delete) no interface loopback 99 (idempotent)

2. Lab Topology & Environment

  Python Workstation                  NetsTuts_R1 (IOS-XE 17.x)
  192.168.1.100                       Management: 192.168.1.1
       |                              GigabitEthernet1: 192.168.1.1/24
       |   SSH / NETCONF              GigabitEthernet2: 10.0.0.1/24
       └──── TCP 830 ────────────────► GigabitEthernet3: 10.0.1.1/24
                                       Loopback0: 1.1.1.1/32

  Python environment:
    Python 3.10+
    ncclient 0.6.13+
    lxml 4.9+           (XML parsing)
    xmltodict 0.13+     (XML to Python dict conversion)

  IOS-XE Requirements:
    NETCONF enabled (netconf-yang)
    SSH version 2
    Local user with privilege 15
    NETCONF port 830 reachable from workstation
  

3. Step 1 — Enable NETCONF on IOS-XE

NETCONF requires SSH to be configured first. The netconf-yang command enables the NETCONF agent and opens port 830. A dedicated NETCONF user with privilege 15 is required for edit-config write operations.

NetsTuts_R1>en
NetsTuts_R1#conf t

! ── Step 1: Configure hostname and domain (required for SSH keys)
NetsTuts_R1(config)#hostname NetsTuts_R1
NetsTuts_R1(config)#ip domain-name netstuts.lab

! ── Step 2: Generate RSA keys for SSH ─────────────────────
NetsTuts_R1(config)#crypto key generate rsa modulus 2048
% Generating 2048 bit RSA keys, keys will be non-exportable...
[OK] (elapsed time was 4 seconds)

! ── Step 3: Enable SSH version 2 ─────────────────────────
NetsTuts_R1(config)#ip ssh version 2
NetsTuts_R1(config)#ip ssh time-out 60
NetsTuts_R1(config)#ip ssh authentication-retries 3

! ── Step 4: Create NETCONF management user ────────────────
NetsTuts_R1(config)#username netconf privilege 15 secret NetC0nf$ecret

! ── Step 5: Enable local AAA authentication ───────────────
NetsTuts_R1(config)#aaa new-model
NetsTuts_R1(config)#aaa authentication login default local
NetsTuts_R1(config)#aaa authorization exec default local

! ── Step 6: Enable NETCONF-YANG agent ─────────────────────
NetsTuts_R1(config)#netconf-yang

! ── Step 7: Confirm NETCONF is listening on port 830 ──────
NetsTuts_R1(config)#end

NetsTuts_R1#show netconf-yang status

netconf-yang: enabled
netconf-yang candidate-datastore: disabled
netconf-yang side-effect-sync: enabled
netconf-yang SSH port: 830

! ── Step 8: Verify from workstation (optional quick check) ─
! ── (run from Linux/Mac terminal on workstation) ──────────
! nc -zv 192.168.1.1 830
! Connection to 192.168.1.1 830 port [tcp] succeeded!
  
The netconf-yang command enables the NETCONF agent alongside any existing SSH access on port 22 — they are independent. Port 830 is the IANA-assigned NETCONF port and is the default used by ncclient. The aaa new-model and authorization commands are required on IOS-XE for NETCONF sessions to authenticate successfully — without them, the SSH connection to port 830 is established but the NETCONF session hello exchange fails with an authentication error. See AAA Overview and SSH Configuration for full AAA and SSH prerequisites.

Optional: Enable Candidate Datastore

! ── Enable candidate datastore for staged config workflow ─
NetsTuts_R1(config)#netconf-yang feature candidate-datastore

NetsTuts_R1#show netconf-yang status
netconf-yang: enabled
netconf-yang candidate-datastore: enabled   ← now available
netconf-yang SSH port: 830
  
The candidate datastore allows a staged configuration workflow: edit the candidate datastore with multiple edit-config operations, validate the result, and then commit all changes atomically to the running datastore in a single commit operation. If any validation fails, the running configuration is unchanged. This is the preferred approach for production configuration changes. The labs in this guide use the running datastore directly for simplicity, with candidate datastore examples shown where relevant.

4. Step 2 — Install ncclient and Connect

Install Python Dependencies

# ── Run on the Python workstation ─────────────────────────
# ── Create and activate a virtual environment (recommended)
python3 -m venv netconf-lab
source netconf-lab/bin/activate          # Linux/Mac
# netconf-lab\Scripts\activate           # Windows

# ── Install required packages ─────────────────────────────
pip install ncclient lxml xmltodict

# ── Verify installation ───────────────────────────────────
python3 -c "import ncclient; print(ncclient.__version__)"
0.6.13
  

Script 1 — Connect and Inspect Device Capabilities

#!/usr/bin/env python3
"""
netconf_01_connect.py
Connect to IOS-XE via NETCONF and display device capabilities.
Capabilities describe which YANG models and NETCONF features
the device supports.
"""

from ncclient import manager
import xml.dom.minidom

# ── Device connection parameters ──────────────────────────
DEVICE = {
    "host":             "192.168.1.1",
    "port":             830,
    "username":         "netconf",
    "password":         "NetC0nf$ecret",
    "hostkey_verify":   False,     # Set True in production with known_hosts
    "device_params":    {"name": "iosxe"},   # Enables IOS-XE specific handling
    "manager_params":   {"timeout": 60},
}

def main():
    print(f"Connecting to {DEVICE['host']}:{DEVICE['port']} via NETCONF...")

    with manager.connect(**DEVICE) as m:
        print(f"\nConnected successfully.")
        print(f"Session ID : {m.session_id}")
        print(f"\n{'='*60}")
        print("DEVICE CAPABILITIES (YANG models supported):")
        print('='*60)

        # ── Print all capabilities advertised by the device ──
        for cap in sorted(m.server_capabilities):
            print(f"  {cap}")

        # ── Check for key capabilities ────────────────────────
        print(f"\n{'='*60}")
        print("KEY CAPABILITY CHECKS:")
        print('='*60)

        checks = {
            ":candidate":    "Candidate datastore",
            ":rollback-on-error": "Auto rollback on error",
            ":validate":     "Config validation",
            ":confirmed-commit": "Confirmed commit (auto-rollback)",
            "Cisco-IOS-XE-native": "Cisco IOS-XE native YANG models",
            "openconfig-interfaces": "OpenConfig interfaces model",
            "ietf-interfaces": "IETF interfaces model",
        }

        for cap_fragment, label in checks.items():
            found = any(cap_fragment in c for c in m.server_capabilities)
            status = "✓ Supported" if found else "✗ Not found"
            print(f"  {label:<40} {status}")

if __name__ == "__main__":
    main()
  
The with manager.connect(**DEVICE) as m: pattern automatically closes the NETCONF session when the block exits, even if an exception occurs. The device_params={"name": "iosxe"} argument enables ncclient's IOS-XE-specific behaviour — it handles IOS-XE's slightly non-standard NETCONF hello exchange and adds commit calls after edit-config on the running datastore automatically. Setting hostkey_verify=False is acceptable in a lab environment but should be set to True in production with the device's SSH host key added to ~/.ssh/known_hosts.

Expected Output

Connecting to 192.168.1.1:830 via NETCONF...

Connected successfully.
Session ID : 47

============================================================
DEVICE CAPABILITIES (YANG models supported):
============================================================
  urn:ietf:params:netconf:base:1.0
  urn:ietf:params:netconf:base:1.1
  urn:ietf:params:netconf:capability:candidate:1.0
  urn:ietf:params:netconf:capability:rollback-on-error:1.0
  urn:ietf:params:netconf:capability:validate:1.1
  urn:ietf:params:netconf:capability:confirmed-commit:1.1
  http://cisco.com/ns/yang/Cisco-IOS-XE-native?module=...
  http://openconfig.net/yang/interfaces?module=...
  urn:ietf:params:xml:ns:yang:ietf-interfaces?module=...
  ... (50+ more capability URIs)

============================================================
KEY CAPABILITY CHECKS:
============================================================
  Candidate datastore                      ✓ Supported
  Auto rollback on error                   ✓ Supported
  Config validation                        ✓ Supported
  Confirmed commit (auto-rollback)         ✓ Supported
  Cisco IOS-XE native YANG models          ✓ Supported
  OpenConfig interfaces model              ✓ Supported
  IETF interfaces model                    ✓ Supported
  

5. Step 3 — Retrieve Configuration with get-config

The get-config operation retrieves configuration data from a datastore. A subtree filter narrows the response to a specific section of the configuration tree, avoiding the overhead of retrieving the entire running configuration for every query.

Script 2 — Retrieve All Interface Configuration

#!/usr/bin/env python3
"""
netconf_02_get_interfaces.py
Retrieve all interface configuration using the Cisco-IOS-XE-native
YANG model with a subtree filter, then parse the XML response.
"""

from ncclient import manager
from lxml import etree
import xmltodict, json

DEVICE = {
    "host": "192.168.1.1", "port": 830,
    "username": "netconf", "password": "NetC0nf$ecret",
    "hostkey_verify": False, "device_params": {"name": "iosxe"},
}

# ── Subtree filter: retrieve only the  section ─
# ── from the Cisco-IOS-XE-native model ───────────────────
INTERFACE_FILTER = """

  
    
  

"""

def main():
    with manager.connect(**DEVICE) as m:

        # ── Send get-config with the subtree filter ────────
        response = m.get_config(source="running", filter=INTERFACE_FILTER)

        # ── response.xml is the raw XML string ────────────
        # ── response.data_ele is the parsed lxml element ──

        # ── Pretty-print the raw XML response ─────────────
        print("=== RAW XML RESPONSE ===")
        pretty_xml = etree.tostring(
            response.data_ele,
            pretty_print=True
        ).decode()
        print(pretty_xml[:3000])      # Truncate for display

        # ── Convert to Python dict for easier processing ──
        print("\n=== PARSED AS PYTHON DICT ===")
        response_dict = xmltodict.parse(response.xml)

        # ── Navigate to the interface list ────────────────
        try:
            native = response_dict["rpc-reply"]["data"]["native"]
            interfaces = native.get("interface", {})

            # ── Print GigabitEthernet interfaces ──────────
            gi_list = interfaces.get("GigabitEthernet", [])
            if isinstance(gi_list, dict):
                gi_list = [gi_list]    # Single interface — wrap in list

            print("\nGigabitEthernet Interfaces:")
            print("-" * 50)
            for gi in gi_list:
                name   = gi.get("name", "unknown")
                desc   = gi.get("description", "(no description)")
                ip_cfg = gi.get("ip", {})
                addr   = ip_cfg.get("address", {})
                pri    = addr.get("primary", {})
                ip     = pri.get("address", "not set")
                mask   = pri.get("mask", "")
                shut   = gi.get("shutdown")
                status = "shutdown" if shut is not None else "no shutdown"
                print(f"  Gi{name}: {ip}/{mask}  [{desc}]  {status}")

        except KeyError as e:
            print(f"Parse error — key not found: {e}")
            print("Full dict structure:")
            print(json.dumps(response_dict, indent=2)[:2000])

if __name__ == "__main__":
    main()
  
The subtree filter <native xmlns="..."><interface/></native> tells NETCONF: "return only the interface subtree within the native YANG model namespace." Without a filter, get_config(source="running") returns the entire running configuration as XML — hundreds of kilobytes for a complex device. The filter dramatically reduces response size and processing time. The xmltodict.parse() function converts the XML response into a nested Python dictionary, making it easy to navigate with standard Python key access. Note the isinstance(gi_list, dict) check — when a device has only one interface of a type, xmltodict returns a dict instead of a list; the check normalises this to always be a list.

Expected Output

=== RAW XML RESPONSE ===
<data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"
      xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0">
  <native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">
    <interface>
      <GigabitEthernet>
        <name>1</name>
        <description>Management Interface</description>
        <ip>
          <address>
            <primary>
              <address>192.168.1.1</address>
              <mask>255.255.255.0</mask>
            </primary>
          </address>
        </ip>
        <negotiation>
          <auto>true</auto>
        </negotiation>
      </GigabitEthernet>
      <GigabitEthernet>
        <name>2</name>
        <ip>...</ip>
      </GigabitEthernet>
      ...

=== PARSED AS PYTHON DICT ===

GigabitEthernet Interfaces:
--------------------------------------------------
  Gi1: 192.168.1.1/255.255.255.0  [Management Interface]  no shutdown
  Gi2: 10.0.0.1/255.255.255.0  [(no description)]  no shutdown
  Gi3: 10.0.1.1/255.255.255.0  [(no description)]  no shutdown
  

Script 3 — Retrieve Specific Interface by Name (Precise Filter)

#!/usr/bin/env python3
"""
netconf_03_get_one_interface.py
Use a precise subtree filter to retrieve only GigabitEthernet2.
Adding a value inside a filter element makes it an exact-match key.
"""

from ncclient import manager
import xmltodict

DEVICE = {
    "host": "192.168.1.1", "port": 830,
    "username": "netconf", "password": "NetC0nf$ecret",
    "hostkey_verify": False, "device_params": {"name": "iosxe"},
}

# ── Filter: match only GigabitEthernet with name = "2" ────
# ── The 2 inside  is a key ──
# ── selector — it returns ONLY that interface ─────────────
SINGLE_INTF_FILTER = """

  
    
      
        2
      
    
  

"""

def main():
    with manager.connect(**DEVICE) as m:
        response = m.get_config(source="running", filter=SINGLE_INTF_FILTER)
        data = xmltodict.parse(response.xml)

        try:
            gi = (data["rpc-reply"]["data"]["native"]
                     ["interface"]["GigabitEthernet"])

            print(f"Interface: GigabitEthernet{gi['name']}")
            print(f"  Description : {gi.get('description', '(none)')}")
            ip_addr = (gi.get("ip", {})
                          .get("address", {})
                          .get("primary", {})
                          .get("address", "not configured"))
            ip_mask = (gi.get("ip", {})
                          .get("address", {})
                          .get("primary", {})
                          .get("mask", ""))
            print(f"  IP Address  : {ip_addr} {ip_mask}")
            print(f"  Shutdown    : {'yes' if gi.get('shutdown') is not None else 'no'}")

        except (KeyError, TypeError) as e:
            print(f"Interface not found or parse error: {e}")

if __name__ == "__main__":
    main()
  

Script 4 — Retrieve Operational State with get (not get-config)

#!/usr/bin/env python3
"""
netconf_04_get_oper.py
Use <get> (not <get-config>) to retrieve OPERATIONAL state data —
interface counters, line protocol status, input/output rates.
Operational data is NOT in the running config datastore.
Uses the IETF interfaces operational YANG model.
"""

from ncclient import manager
from lxml import etree

DEVICE = {
    "host": "192.168.1.1", "port": 830,
    "username": "netconf", "password": "NetC0nf$ecret",
    "hostkey_verify": False, "device_params": {"name": "iosxe"},
}

# ── Filter: operational state of GigabitEthernet interfaces
# ── Uses ietf-interfaces-state (RFC 7223) ─────────────────
OPER_FILTER = """

  
    
      GigabitEthernet2
    
  

"""

def main():
    with manager.connect(**DEVICE) as m:

        # ── Use get() for operational data ────────────────
        # ── (get-config only returns config, not oper state)
        response = m.get(filter=("subtree", OPER_FILTER))

        print("=== OPERATIONAL STATE (IETF model) ===")
        print(etree.tostring(
            response.data_ele,
            pretty_print=True
        ).decode())

if __name__ == "__main__":
    main()
  
The get() operation retrieves both configuration data and operational state data — interface counters, protocol status, BGP session state, ARP tables, and routing tables. The get-config() operation retrieves only configuration data. This distinction maps to the YANG concept of config vs state nodes: config nodes appear in both get-config and get responses; state nodes (operational data) only appear in get responses. Note the filter syntax for get() uses a tuple: filter=("subtree", FILTER_STRING) while get_config() uses the filter=FILTER_STRING keyword directly. See show interfaces for the equivalent CLI operational data.

6. Step 4 — Push Configuration with edit-config

The edit-config operation modifies the device configuration by sending an XML payload that follows the YANG model structure. The payload specifies the target datastore, the default operation (merge, replace), and the configuration tree to apply.

Script 5 — Configure Interface Description and IP Address

#!/usr/bin/env python3
"""
netconf_05_edit_interface.py
Configure GigabitEthernet2 description and IP address using
the Cisco-IOS-XE-native YANG model with edit-config merge.
"""

from ncclient import manager
from ncclient.operations import RPCError

DEVICE = {
    "host": "192.168.1.1", "port": 830,
    "username": "netconf", "password": "NetC0nf$ecret",
    "hostkey_verify": False, "device_params": {"name": "iosxe"},
}

# ── XML payload: configure GigabitEthernet2 ───────────────
# ── operation="merge" adds/updates without removing other config
# ── The namespace must match the YANG model exactly ────────
INTF_CONFIG = """

  
    
      
        2
        WAN-Link-to-ISP-A
        
          
10.0.0.1
255.255.255.0
true
""" def main(): with manager.connect(**DEVICE) as m: print(f"Configuring GigabitEthernet2 on {DEVICE['host']}...") try: # ── Send edit-config to the running datastore ── response = m.edit_config( target="running", config=INTF_CONFIG ) # ── Check response status ────────────────────── if response.ok: print("✓ edit-config succeeded — GigabitEthernet2 configured") else: print(f"✗ edit-config returned errors: {response.errors}") except RPCError as e: # ── NETCONF server returned an <rpc-error> ────── print(f"✗ RPC Error: {e}") print(f" Error type : {e.type}") print(f" Error tag : {e.tag}") print(f" Error severity: {e.severity}") print(f" Error message : {e.message}") # ── Verify: read back the config we just pushed ─── print("\nVerifying by reading back GigabitEthernet2...") verify_filter = """ 2 """ verify_resp = m.get_config(source="running", filter=verify_filter) import xmltodict data = xmltodict.parse(verify_resp.xml) gi = (data["rpc-reply"]["data"]["native"] ["interface"]["GigabitEthernet"]) desc = gi.get("description", "(none)") addr = (gi.get("ip", {}).get("address", {}) .get("primary", {}).get("address", "not set")) mask = (gi.get("ip", {}).get("address", {}) .get("primary", {}).get("mask", "")) print(f" Description : {desc}") print(f" IP Address : {addr} {mask}") if __name__ == "__main__": main()
Always wrap edit_config() calls in a try/except RPCError block — the device returns an RPC error for invalid values, read-only nodes, or constraint violations (e.g., configuring a secondary IP before a primary). The error's tag attribute provides a standardised error type (bad-element, operation-failed, data-missing, etc.) and the message attribute contains the device's human-readable explanation. Always read back the configuration after pushing changes to confirm the device accepted and stored the values as intended — a successful response.ok means the RPC was accepted, not necessarily that the config matches what you intended.

Script 6 — Configure a Loopback Interface (Create)

#!/usr/bin/env python3
"""
netconf_06_create_loopback.py
Create Loopback99 with an IP address.
Uses nc:operation="create" — returns an error if it already exists.
Use nc:operation="merge" for idempotent create/update.
"""

from ncclient import manager
from ncclient.operations import RPCError

DEVICE = {
    "host": "192.168.1.1", "port": 830,
    "username": "netconf", "password": "NetC0nf$ecret",
    "hostkey_verify": False, "device_params": {"name": "iosxe"},
}

# ── nc: namespace prefix must be declared for operation attr
LOOPBACK_CREATE = """

  
    
      
        99
        NETCONF-Lab-Loopback
        
          
99.99.99.99
255.255.255.255
""" def main(): with manager.connect(**DEVICE) as m: print("Creating Loopback99...") try: resp = m.edit_config(target="running", config=LOOPBACK_CREATE) if resp.ok: print("✓ Loopback99 created: 99.99.99.99/32") else: print(f"✗ Errors: {resp.errors}") except RPCError as e: if e.tag == "data-exists": print("ℹ Loopback99 already exists — use merge to update") else: print(f"✗ RPC Error [{e.tag}]: {e.message}") if __name__ == "__main__": main()

Script 7 — Delete a Configuration Element

#!/usr/bin/env python3
"""
netconf_07_delete_loopback.py
Delete Loopback99 using nc:operation="remove"
(idempotent — no error if the interface does not exist).
"""

from ncclient import manager
from ncclient.operations import RPCError

DEVICE = {
    "host": "192.168.1.1", "port": 830,
    "username": "netconf", "password": "NetC0nf$ecret",
    "hostkey_verify": False, "device_params": {"name": "iosxe"},
}

# ── operation="remove" is idempotent: no error if not present
# ── operation="delete" raises data-missing error if not present
LOOPBACK_DELETE = """

  
    
      
        99
      
    
  

"""

def main():
    with manager.connect(**DEVICE) as m:
        print("Removing Loopback99...")
        try:
            resp = m.edit_config(target="running", config=LOOPBACK_DELETE)
            if resp.ok:
                print("✓ Loopback99 removed (or did not exist)")
        except RPCError as e:
            print(f"✗ RPC Error [{e.tag}]: {e.message}")

if __name__ == "__main__":
    main()
  

7. Step 5 — Practical YANG Model Payloads

This section provides ready-to-use XML payloads for common configuration tasks, covering both Cisco-IOS-XE-native and OpenConfig models. Each can be used directly as the config argument to m.edit_config().

Configure a Static Route (Cisco-IOS-XE-native)

STATIC_ROUTE = """

  
    
      
        
          0.0.0.0
          0.0.0.0
          
            10.0.0.254
          
        
      
    
  

"""

# ── Usage ──────────────────────────────────────────────────
# with manager.connect(**DEVICE) as m:
#     m.edit_config(target="running", config=STATIC_ROUTE)
  

Configure NTP Server (Cisco-IOS-XE-native)

NTP_CONFIG = """

  
    
      
        
          216.239.35.0
        
      
    
  

"""
  

Configure a VLAN (Cisco-IOS-XE-native)

VLAN_CONFIG = """

  
    
      
        100
        NETCONF-VLAN
      
    
  

"""
  

Configure Interface Using OpenConfig Model

#!/usr/bin/env python3
"""
netconf_08_openconfig_interface.py
Configure interface description using the OpenConfig interfaces
YANG model instead of the Cisco-native model.
OpenConfig payloads work across Cisco, Juniper, and Arista.
"""

from ncclient import manager
from ncclient.operations import RPCError

DEVICE = {
    "host": "192.168.1.1", "port": 830,
    "username": "netconf", "password": "NetC0nf$ecret",
    "hostkey_verify": False, "device_params": {"name": "iosxe"},
}

# ── OpenConfig namespace: http://openconfig.net/yang/interfaces ─
# ── Interface name format for IOS-XE in OpenConfig: ────────────
# ── "GigabitEthernet2" (full name, not just "2") ───────────────
OC_INTF_CONFIG = """

  
    
      GigabitEthernet2
      
        GigabitEthernet2
        WAN-to-ISP-OpenConfig
        true
      
    
  

"""

def main():
    with manager.connect(**DEVICE) as m:
        # ── Verify device supports OpenConfig interfaces ───
        oc_supported = any(
            "openconfig-interfaces" in c
            for c in m.server_capabilities
        )
        if not oc_supported:
            print("✗ OpenConfig interfaces model not supported on this device")
            return

        print("Configuring GigabitEthernet2 via OpenConfig model...")
        try:
            resp = m.edit_config(target="running", config=OC_INTF_CONFIG)
            if resp.ok:
                print("✓ Interface configured via OpenConfig")
        except RPCError as e:
            print(f"✗ RPC Error [{e.tag}]: {e.message}")

if __name__ == "__main__":
    main()
  
The key difference between Cisco-native and OpenConfig payloads is the XML namespace (xmlns) and the data model structure beneath it. OpenConfig uses a consistent <config> container inside each model element (e.g., <interface><config><description>) while Cisco-native maps more directly to CLI hierarchy (<interface><GigabitEthernet><description>). OpenConfig also uses the full interface name string (GigabitEthernet2) rather than the IOS-XE-native numeric-only format (2). For basic interface configuration reference, see Basic Interface Configuration.

Candidate Datastore Workflow — Staged Configuration

#!/usr/bin/env python3
"""
netconf_09_candidate_workflow.py
Best-practice production workflow using the candidate datastore:
1. Lock candidate datastore
2. Push multiple edit-config operations to candidate
3. Validate the candidate
4. Commit atomically to running
5. Unlock
If any step fails, discard changes — running config unchanged.
"""

from ncclient import manager
from ncclient.operations import RPCError

DEVICE = {
    "host": "192.168.1.1", "port": 830,
    "username": "netconf", "password": "NetC0nf$ecret",
    "hostkey_verify": False, "device_params": {"name": "iosxe"},
}

CHANGE_1 = """

  
    
      
        2
        WAN-Primary-Configured-via-NETCONF
      
    
  

"""

CHANGE_2 = """

  
    
      
        0
        Router-ID-Loopback
      
    
  

"""

def main():
    with manager.connect(**DEVICE) as m:

        # ── Verify candidate datastore is supported ────────
        if not any(":candidate" in c for c in m.server_capabilities):
            print("✗ Candidate datastore not supported — use running directly")
            return

        print("Starting candidate datastore workflow...")

        try:
            # ── Step 1: Lock the candidate datastore ──────
            print("  1. Locking candidate datastore...")
            m.lock(target="candidate")

            # ── Step 2: Push changes to candidate ─────────
            print("  2. Pushing change 1 to candidate...")
            resp1 = m.edit_config(target="candidate", config=CHANGE_1)
            if not resp1.ok:
                raise Exception(f"Change 1 failed: {resp1.errors}")

            print("  3. Pushing change 2 to candidate...")
            resp2 = m.edit_config(target="candidate", config=CHANGE_2)
            if not resp2.ok:
                raise Exception(f"Change 2 failed: {resp2.errors}")

            # ── Step 3: Validate before committing ────────
            print("  4. Validating candidate configuration...")
            m.validate(source="candidate")
            print("     ✓ Validation passed")

            # ── Step 4: Commit to running ──────────────────
            print("  5. Committing to running datastore...")
            m.commit()
            print("     ✓ Commit successful — changes are now live")

        except (RPCError, Exception) as e:
            # ── Rollback: discard candidate changes ────────
            print(f"\n✗ Error: {e}")
            print("  Rolling back — discarding candidate changes...")
            try:
                m.discard_changes()
                print("  ✓ Candidate discarded — running config unchanged")
            except RPCError as discard_err:
                print(f"  ✗ Discard failed: {discard_err}")

        finally:
            # ── Always unlock regardless of outcome ────────
            try:
                m.unlock(target="candidate")
                print("  Candidate datastore unlocked")
            except RPCError:
                pass    # May already be unlocked if lock failed

if __name__ == "__main__":
    main()
  
The candidate datastore workflow is the safest approach for production configuration changes. The lock() prevents other NETCONF sessions or CLI operators from modifying the candidate concurrently. The validate() call checks the full candidate configuration against all YANG constraints before any change reaches the running configuration. The discard_changes() in the exception handler guarantees that a failed workflow leaves the running configuration exactly as it was — no partial changes. The finally block ensures the lock is always released, even if the commit itself fails, preventing the candidate datastore from being permanently locked and blocking future NETCONF sessions.

8. Step 6 — Verification and Troubleshooting NETCONF Sessions

Verify NETCONF on IOS-XE

! ── Check NETCONF agent status ───────────────────────────
NetsTuts_R1#show netconf-yang status

netconf-yang: enabled
netconf-yang candidate-datastore: enabled
netconf-yang side-effect-sync: enabled
netconf-yang SSH port: 830

! ── List active NETCONF sessions ─────────────────────────
NetsTuts_R1#show netconf-yang sessions

R: global-lock
S: SID     Username    Transport    Source              Elapsed time
----------------------------------------------------------------------
S: 47      netconf     netconf-ssh  192.168.1.100:52413  00:00:15

! ── Show session details including YANG capabilities ──────
NetsTuts_R1#show netconf-yang sessions detail

session-id: 47
  transport:     netconf-ssh
  username:      netconf
  source-host:   192.168.1.100
  login-time:    2024-10-16T14:22:01+00:00
  in-rpcs:       12
  in-bad-rpcs:   0
  out-rpc-errors:0
  out-notifications: 0

! ── View NETCONF statistics ───────────────────────────────
NetsTuts_R1#show netconf-yang statistics

netconf-yang server statistics:
  in-sessions        : 8
  in-bad-hellos      : 0
  in-rpcs            : 47
  in-bad-rpcs        : 2
  out-rpc-errors     : 2
  out-notifications  : 0
  dropped-sessions   : 0

! ── Check NETCONF debug logs ─────────────────────────────
NetsTuts_R1#show logging | include NETCONF\|netconf\|yang

! ── Enable NETCONF debug (use only in lab — very verbose) ─
! NetsTuts_R1#debug netconf all
! NetsTuts_R1#undebug all
  

NETCONF Troubleshooting Reference

Problem Symptom Cause Fix
Connection refused on port 830 ncclient.transport.errors.SSHError: [Errno 111] Connection refused netconf-yang not enabled on the device, or port 830 blocked by an ACL between workstation and device Run netconf-yang in global config. Verify with show netconf-yang status. Check ACLs with show ip access-lists — ensure TCP port 830 is permitted inbound on the management interface
Authentication failure ncclient.transport.errors.AuthenticationError Wrong username/password, or aaa authorization exec default local not configured — NETCONF requires exec authorisation to establish the session Verify credentials. Add aaa new-model, aaa authentication login default local, and aaa authorization exec default local to the device config
RPC error: bad-element RPCError: tag=bad-element, message=element is not valid in this context The XML payload references a YANG node that does not exist, is misspelled, or uses a wrong namespace. The element name or namespace does not match the YANG model Verify the exact XML path using the YANG model explorer (YANG Suite, pyang, or netconf-console). Compare against known-working payloads. Check that the xmlns namespace matches the target model exactly
RPC error: operation-failed RPCError: tag=operation-failed, message=... The configuration value is semantically invalid — IP address format wrong, value out of range, YANG constraint violated (e.g., secondary IP without primary), or attempting a write on a read-only state node Check the error message for the specific constraint violated. Verify IP addresses, masks, and values match the YANG model's constraints. State nodes (operational data) cannot be written — verify you are using the config branch of the model
Empty response from get-config response.xml contains only the RPC reply envelope with an empty data element The subtree filter does not match anything in the running configuration — either the namespace is wrong, the element name is incorrect, or the key value (e.g., interface name) does not match Try the same filter without a key selector first (retrieve all of that type). Verify the namespace with show netconf-yang capabilities on the device. Check interface naming — IOS-XE native uses <name>2</name> while OpenConfig uses <name>GigabitEthernet2</name>
Candidate datastore lock failure RPCError: tag=lock-denied, message=access to requested lock is denied Another NETCONF session (or CLI operator) already holds the candidate lock, or a previous script crashed without calling unlock() Check active sessions with show netconf-yang sessions. Kill the offending session if appropriate. In the script, always place unlock() in a finally block to guarantee release even on exceptions
Changes lost after reload Configuration changes applied via NETCONF are present in running-config but disappear after a device reload edit-config to the running datastore modifies the running configuration but does not save to NVRAM (startup-config) After edit-config to running, also call m.copy_config(source="running", target="startup") or send the CLI equivalent via a separate SSH session. Alternatively use the candidate datastore workflow, which on IOS-XE can be configured to auto-save

9. Reusable NETCONF Helper Class

The following class wraps the most common NETCONF operations into a reusable, production-ready interface for building larger automation scripts and pipelines.

#!/usr/bin/env python3
"""
netconf_helper.py
Reusable NETCONF helper class for IOS-XE automation.
Wraps ncclient with error handling, logging, and convenience methods.
"""

from ncclient import manager
from ncclient.operations import RPCError
from lxml import etree
import xmltodict
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
log = logging.getLogger(__name__)


class NetconfDevice:
    """
    Context-manager wrapper around ncclient.manager for IOS-XE.
    Usage:
        with NetconfDevice("192.168.1.1", "netconf", "pass") as dev:
            data = dev.get_config_subtree(FILTER)
            dev.edit_running(PAYLOAD)
    """

    def __init__(self, host, username, password, port=830,
                 hostkey_verify=False):
        self.host     = host
        self.port     = port
        self.username = username
        self.password = password
        self.hostkey_verify = hostkey_verify
        self._manager = None

    # ── Context manager entry ──────────────────────────────
    def __enter__(self):
        log.info(f"Connecting to {self.host}:{self.port} via NETCONF")
        self._manager = manager.connect(
            host=self.host, port=self.port,
            username=self.username, password=self.password,
            hostkey_verify=self.hostkey_verify,
            device_params={"name": "iosxe"},
            manager_params={"timeout": 60},
        )
        log.info(f"Connected — session ID {self._manager.session_id}")
        return self

    # ── Context manager exit ───────────────────────────────
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self._manager and self._manager.connected:
            self._manager.close_session()
            log.info("NETCONF session closed")
        return False    # Do not suppress exceptions

    # ── get-config with subtree filter ────────────────────
    def get_config_subtree(self, filter_xml: str,
                           source: str = "running") -> dict:
        """
        Retrieve config data using a subtree filter.
        Returns parsed Python dict.
        """
        log.debug(f"get-config from {source}")
        resp = self._manager.get_config(
            source=source, filter=filter_xml
        )
        return xmltodict.parse(resp.xml)

    # ── get with subtree filter (config + oper state) ──────
    def get_oper(self, filter_xml: str) -> dict:
        """Retrieve operational state data using get()."""
        resp = self._manager.get(filter=("subtree", filter_xml))
        return xmltodict.parse(resp.xml)

    # ── edit-config to running ────────────────────────────
    def edit_running(self, config_xml: str) -> bool:
        """
        Push config to running datastore.
        Returns True on success, raises RPCError on failure.
        """
        log.info("Sending edit-config to running datastore")
        try:
            resp = self._manager.edit_config(
                target="running", config=config_xml
            )
            if resp.ok:
                log.info("edit-config succeeded")
                return True
            else:
                log.error(f"edit-config errors: {resp.errors}")
                return False
        except RPCError as e:
            log.error(f"RPCError [{e.tag}]: {e.message}")
            raise

    # ── Candidate datastore workflow ───────────────────────
    def push_candidate(self, *config_payloads: str,
                       validate: bool = True) -> bool:
        """
        Push one or more config payloads via candidate datastore.
        Validates and commits atomically. Discards on any error.
        Returns True on successful commit.
        """
        m = self._manager

        if not any(":candidate" in c for c in m.server_capabilities):
            log.warning("Candidate not supported — falling back to running")
            for payload in config_payloads:
                self.edit_running(payload)
            return True

        log.info("Starting candidate datastore workflow")
        try:
            m.lock(target="candidate")
            log.debug("Candidate locked")

            for i, payload in enumerate(config_payloads, 1):
                log.info(f"  Applying change {i}/{len(config_payloads)}")
                resp = m.edit_config(target="candidate", config=payload)
                if not resp.ok:
                    raise Exception(f"Change {i} failed: {resp.errors}")

            if validate:
                m.validate(source="candidate")
                log.info("Validation passed")

            m.commit()
            log.info("Committed to running successfully")
            return True

        except (RPCError, Exception) as e:
            log.error(f"Workflow failed: {e} — discarding candidate")
            try:
                m.discard_changes()
                log.info("Candidate discarded")
            except RPCError:
                pass
            raise

        finally:
            try:
                m.unlock(target="candidate")
                log.debug("Candidate unlocked")
            except RPCError:
                pass

    # ── Save running to startup ────────────────────────────
    def save_config(self) -> bool:
        """Copy running-config to startup-config (NVRAM)."""
        log.info("Saving running-config to startup-config")
        try:
            self._manager.copy_config(
                source="running", target="startup"
            )
            log.info("Config saved to NVRAM")
            return True
        except RPCError as e:
            log.error(f"Save failed [{e.tag}]: {e.message}")
            return False

    # ── Get server capabilities ────────────────────────────
    def capabilities(self) -> list:
        return list(self._manager.server_capabilities)

    # ── Pretty-print XML ───────────────────────────────────
    @staticmethod
    def pretty(xml_str: str) -> str:
        return etree.tostring(
            etree.fromstring(xml_str.encode()),
            pretty_print=True
        ).decode()


# ── Example usage ──────────────────────────────────────────
if __name__ == "__main__":

    LOOPBACK_PAYLOAD = """
    
      
        
          
            10
            Automation-Test
            
              
10.10.10.10
255.255.255.255
""" INTF_FILTER = """ 10 """ with NetconfDevice("192.168.1.1", "netconf", "NetC0nf$ecret") as dev: # ── Push config via candidate workflow ───────────── dev.push_candidate(LOOPBACK_PAYLOAD) # ── Read back and display ────────────────────────── data = dev.get_config_subtree(INTF_FILTER) lo = (data.get("rpc-reply", {}).get("data", {}) .get("native", {}).get("interface", {}) .get("Loopback", {})) print(f"Loopback{lo.get('name')}: " f"{lo.get('ip',{}).get('address',{}).get('primary',{}).get('address','?')}") # ── Save to NVRAM ────────────────────────────────── dev.save_config()

Key Points & Exam Tips

  • NETCONF (RFC 6241) is a structured configuration management protocol that runs over SSH on TCP port 830. It uses XML-encoded payloads and YANG data models to read and write device configuration in a structured, transactional way — replacing fragile CLI text parsing for network automation.
  • YANG (RFC 6020/7950) defines the schema (data types, hierarchy, constraints) for configuration and operational data. YANG models tell NETCONF what the XML payload structure must look like. The three main model families on IOS-XE are Cisco-IOS-XE-native (full feature coverage, IOS-specific), OpenConfig (vendor-neutral, portable across Cisco/Juniper/Arista), and IETF (standards-defined, interfaces and routing).
  • The four most important NETCONF operations: get-config (retrieve configuration from a datastore), get (retrieve configuration AND operational state), edit-config (modify a datastore), and commit (apply candidate to running). Know the difference between get-config (config only) and get (config + oper state).
  • The three datastores are running (active config), startup (NVRAM/boot config), and candidate (staging area). The candidate datastore enables staged, atomic, transactional changes: edit candidate → validate → commit to running → all changes apply or none do. edit-config target="running" changes take effect immediately but are not saved to NVRAM automatically.
  • Subtree filters in get-config and get narrow the XML response to only the requested portion of the configuration tree. Adding a value inside a filter element (e.g., <name>2</name>) makes it a key selector that returns only that specific entry. Without filters, get-config(source="running") returns the entire config tree.
  • The nc:operation attribute controls how edit-config handles existing data: merge (add/update, default), replace (replace entire subtree), create (only if not exists), delete (must exist, errors if absent), remove (delete if exists, idempotent). Use remove for idempotent deletion in automation scripts.
  • The XML namespace (xmlns) in NETCONF payloads must match the YANG model exactly. The most common cause of bad-element RPC errors is a wrong namespace, a misspelled element name, or using Cisco-native path structure with an OpenConfig namespace (or vice versa). Always verify the namespace against the device's capability list.
  • Always use a try/except RPCError block around edit-config operations and always place unlock() in a finally block when using locks. An uncaught exception that leaves a candidate lock held will block all subsequent NETCONF sessions until the lock is manually cleared.
  • For production automation: use the candidate datastore workflow (lock → edit → validate → commit → unlock), always verify changes by reading them back with get-config, and save to startup with copy_config(source="running", target="startup") after successful commits. Changes to running are lost on reload if not saved.
  • On the exam: know NETCONF's port (TCP 830), the three datastores, the key operations (get/get-config/edit-config/commit), the difference between Cisco-native and OpenConfig models, the role of YANG, and the IOS-XE prerequisites (netconf-yang, AAA, SSH v2). Compare NETCONF against RESTCONF (HTTPS/JSON alternative, RFC 8040) and CLI automation (Netmiko/screen-scraping).
Next Steps: For CLI-based Python automation that does not require NETCONF to be enabled, see Python Netmiko Show Commands. For fleet-level automation that orchestrates NETCONF across many devices, see Ansible IOS Configuration. For SNMP-based device monitoring that complements NETCONF configuration management, see SNMP v2c & v3 Configuration. For on-device event-driven automation using EEM applets that can trigger NETCONF changes via Python scripts, see EEM — Embedded Event Manager Scripting. For saving and verifying device configurations managed via NETCONF, see Saving & Managing Cisco Configurations. For the RESTCONF API with Python (HTTPS/JSON-based alternative to NETCONF), see RESTCONF Basics. For understanding the YANG data models that underpin NETCONF payloads in depth, see JSON, XML & YANG Data Models.

TEST WHAT YOU LEARNED

1. What is the fundamental difference between a NETCONF get-config operation and a get operation, and when would you use each?

Correct answer is B. This distinction is fundamental to YANG data modelling and NETCONF operations. In YANG, every node is classified as either a config node (writable configuration, stored in datastores) or a state node (read-only operational data, generated at runtime). Interface description and IP address are config nodes — they are written by edit-config and read by get-config. Interface input/output packet counters, current line protocol status, and learned MAC table entries are state nodes — they are generated by the device's hardware and software in real time and are never stored in a configuration datastore. The get operation returns both config nodes and state nodes in a single response, making it the equivalent of CLI show commands. The get-config operation returns only config nodes from the specified datastore, making it the equivalent of show running-config. In ncclient: m.get_config(source="running", filter=...) for configuration, m.get(filter=("subtree", ...)) for operational data.

2. An ncclient script sends an edit-config to the running datastore and receives response.ok = True. The engineer runs the script, it succeeds, but after a router reload the configuration change is gone. Why, and how should the script be corrected?

Correct answer is D. This is one of the most important operational considerations for NETCONF automation and directly parallels the CLI behaviour that every network engineer knows: changes to running-config do not survive a reload unless saved. NETCONF edit-config target="running" is exactly equivalent to typing commands at the CLI prompt — the change is active immediately in the running configuration, but the startup configuration (NVRAM) is unchanged. A reload discards running-config and loads startup-config. The IOS-XE ncclient device_params setting does add automatic commit calls after edit-config on the running datastore (for the candidate-to-running promotion step), but this is completely separate from the running-to-startup NVRAM save. The fix is explicit: call m.copy_config(source="running", target="startup") at the end of the script, or equivalently send a CLI command via a separate SSH session. In production automation workflows, it is common to include this save step as a final confirmation that the change is intended to be permanent.

3. A NETCONF edit-config payload returns an RPCError with tag="bad-element". The XML payload looks syntactically correct. What are the two most likely causes and how do you diagnose them?

Correct answer is C. The bad-element RPC error tag specifically means "this element is not valid in this context" — the NETCONF server received a syntactically valid XML document (well-formed XML) but could not find the element in the YANG model. This is a schema-level error, not a syntax error (which would be invalid-value or an SSH/parsing error before NETCONF even sees it). The two causes in option C are by far the most common. Namespaces are case-sensitive URI strings — the Cisco-native namespace is http://cisco.com/ns/yang/Cisco-IOS-XE-native with capital C, I, X, E in the model name. An element hierarchy error is also common: for example, placing <address> directly under <ip> when the YANG model requires <ip><address><primary><address>. The best diagnostic tool is the device's capability list: show netconf-yang capabilities | include [model-name] shows the exact namespace URI that must be used in the payload. YANG Suite (Cisco's free YANG model browser) allows interactive exploration of the model hierarchy to verify the correct XML path.

4. What is the candidate datastore, and why is the lock → edit → validate → commit → unlock workflow superior to directly editing the running datastore for production changes?

Correct answer is A. The candidate datastore is one of NETCONF's most powerful features and directly addresses the limitations of both CLI configuration and direct running-datastore editing. The four advantages in option A are precise and important. Atomicity: when you send multiple edit-config operations to the candidate, each one is staged but not yet live. If the second or third operation fails, the first is already in the candidate but not in running — discard_changes() cleans up the candidate completely, and running was never touched. This is impossible with direct running-datastore edits where each change is live the moment it is applied. Pre-commit validation: the validate operation checks the entire candidate configuration for YANG constraint consistency — it catches cross-reference errors (e.g., referencing an OSPF process that does not exist) that individual edit operations might not catch in isolation. Concurrency: on a busy production device, a CLI operator might be making changes simultaneously — the candidate lock ensures that the staged changes are evaluated in a consistent state. Option C is incorrect — IOS-XE does NOT require the candidate datastore for all operations; edit-config target="running" applies changes directly to running without any candidate involvement. The candidate datastore must be explicitly enabled with netconf-yang feature candidate-datastore on IOS-XE.

5. A subtree filter for get-config returns an empty data element — no configuration data at all. The interface being queried definitely exists in the running configuration. What are the two most likely causes?

Correct answer is D. Empty responses from get-config are the NETCONF equivalent of a regex that matches nothing — the query is syntactically valid but semantically matches no data. NETCONF subtree filtering works by matching XML element trees: if any element in the filter has a value (e.g., a namespace or a key value), the server only returns nodes that match that value exactly. A wrong namespace means the server looks for data under a namespace that does not exist in its YANG model — it finds nothing and returns empty. This is the most common cause for new NETCONF users. The interface name format difference between models is the second most common: the Cisco-IOS-XE-native model stores GigabitEthernet interfaces with their name as just the number (1, 2, 3) because the type (GigabitEthernet) is already encoded in the element name. The OpenConfig model uses the full interface name string including the type prefix. Using the full name in a Cisco-native filter will never match any interface because no interface has name "GigabitEthernet2" in that model — they have name "2". The diagnostic procedure: try removing the key selector entirely first (return all GigabitEthernet interfaces), then compare the names you see in the response with what you are using in the filter. If even the key-free filter returns empty, the namespace is wrong.

6. What is the difference between nc:operation="delete" and nc:operation="remove" in an edit-config payload, and which should be used in automated scripts?

Correct answer is B. The delete vs remove distinction is directly specified in RFC 6241 and both are correctly implemented on IOS-XE. The idempotency difference is operationally significant for automation. Consider a deployment script that runs as part of a CI/CD pipeline or a periodic reconciliation job — it needs to ensure a configuration element is absent. If it uses delete and the element was already removed by a previous run (or manually via CLI), the script fails with a data-missing error, causing the pipeline to alarm and requiring human investigation of a non-problem. With remove, the script succeeds cleanly regardless of whether the element existed before, making the "ensure this configuration is absent" operation truly idempotent. This is the same principle as using no interface loopback 99 in a bash script that pipes to SSH — no in IOS CLI succeeds silently if the object already does not exist, while NETCONF delete does not. For any automation script that may run more than once or may encounter pre-existing state, remove is the correct choice. delete is useful when you specifically need to verify that an element existed before deletion and treat "already deleted" as an unexpected error requiring attention.

7. What is the purpose of the device_params={"name": "iosxe"} argument in ncclient's manager.connect(), and what happens if it is omitted when connecting to an IOS-XE device?

Correct answer is C. ncclient includes device-specific handlers for several platforms (IOS-XE, Junos, Huawei, etc.) precisely because real-world NETCONF implementations have small but significant deviations from the RFC 6241 specification. The IOS-XE handler addresses known quirks in how IOS-XE formats its NETCONF hello and capability exchange. Without the device handler, ncclient's strict RFC 6241 parser may raise an exception or disconnect immediately after the hello exchange due to unexpected XML elements in IOS-XE's greeting. Experienced ncclient users will recognise the symptom: the SSH connection succeeds (TCP port 830 connects, credentials authenticate) but an immediate exception is raised during the capability negotiation phase before any operations can be sent. Setting device_params={"name": "iosxe"} is the correct solution. The auto-commit behaviour mentioned in option C's point 2 is also real: in some versions of the IOS-XE handler, ncclient automatically adds a commit after edit-config operations because IOS-XE's NETCONF implementation can behave subtly differently from strict RFC 6241 running-datastore semantics. The precise behaviour depends on the ncclient version, so always test in a lab environment to understand the exact behaviour of your specific version combination.

8. A script uses the OpenConfig interfaces model to configure GigabitEthernet2, but the payload with <name>2</name> returns a bad-element error. The same payload using <name>GigabitEthernet2</name> succeeds. Why does the interface name format differ between the Cisco-native and OpenConfig models?

Correct answer is A. This is a fundamental YANG data model design difference that every NETCONF practitioner must understand to avoid confusion when switching between model families. The Cisco-IOS-XE-native model mirrors the IOS CLI's interface hierarchy: the CLI has separate type namespaces (interface GigabitEthernet 2, interface Loopback 0), so the YANG model has separate list types (<GigabitEthernet>, <Loopback>) with numeric keys within each type. The OpenConfig interfaces model uses a single flat interface list (<interfaces><interface>) where every interface regardless of type is in the same list, and the list key (<name>) must be unique across all types — therefore it must include the type information in the name string. This is why OpenConfig interface names are always the complete string like GigabitEthernet2, Loopback0, or Management0. When writing NETCONF payloads, always verify the key format for the specific model being used. The quickest way to check: issue a filter-free get-config using the model's namespace and examine what the device actually returns as the key value — then use exactly that format in your filter and edit-config payloads.

9. Why does a NETCONF edit-config to the running datastore on IOS-XE require aaa authorization exec default local in addition to aaa authentication login default local?

Correct answer is D. This is the single most common IOS-XE NETCONF setup problem encountered in labs and production. The sequence is precise: when aaa new-model is enabled, IOS-XE enforces AAA for all session establishment. The authentication phase verifies credentials. The authorisation phase determines what the authenticated user can do — specifically, whether they are allowed to start an exec process. Without aaa authorization exec default local, IOS-XE has no authorisation policy to evaluate for the exec request, and the default behaviour with aaa new-model is to deny what is not explicitly authorised. The result: the SSH TCP handshake completes on port 830, the SSH authentication (username/password) succeeds, but the exec session that would host the NETCONF agent is denied — the connection drops immediately after authentication. ncclient raises an AuthenticationError or SSHError, which misleads engineers into thinking the password is wrong. The three mandatory IOS-XE AAA commands for NETCONF are: aaa new-model, aaa authentication login default local, and aaa authorization exec default local. Option C is partially correct in describing the mechanism (exec session) but option D is more precise and complete in explaining why authorisation is specifically required.

10. How does NETCONF with ncclient differ from SSH-based CLI automation with Netmiko for network configuration management, and when would you choose one over the other?

Correct answer is C. Understanding when to use each tool is as important as knowing how to use them. The comparison in option C covers the five key dimensions. Text vs structured: Netmiko sends CLI commands and receives free-text output that must be parsed — a show command output format change in a new IOS version can silently break all dependent scripts. NETCONF returns XML validated against a YANG schema — the data structure is guaranteed by the model. Transactionality: Netmiko changes are applied line by line with no rollback mechanism; a script that fails halfway leaves the device in a partially changed state. NETCONF with candidate datastore is all-or-nothing. Validation: Netmiko sends whatever text you provide and IOS accepts or rejects it at the CLI level with human-readable error messages that must be parsed. NETCONF's YANG constraints catch structural errors, value range violations, and cross-reference issues before commit. Prerequisites: Netmiko works on any device with SSH configured — including 15-year-old IOS 12.4 routers. NETCONF requires minimum IOS-XE 16.3+ (for full YANG model support), enabled netconf-yang, and AAA configuration. The practical guideline: use Netmiko for rapid development, legacy device support, and show-command extraction where TextFSM templates exist. Use NETCONF when building production-grade configuration management systems where correctness, atomicity, and auditability are non-negotiable requirements.