NETCONF with ncclient on Cisco IOS-XE

SSH screen‑scraping and REST APIs both have limits. Screen‑scraping is fragile — output formats change between IOS versions. REST is clean but not every platform exposes it. NETCONF sits in between: a standardized protocol (RFC 6241) for structured, transactional configuration over SSH, using XML payloads and YANG data models. It runs on IOS‑XE, NX‑OS, Junos, and IOS‑XR. The Python library is ncclient. Today is an introduction to what NETCONF offers and how to drive it — the datastores, the operations, and a real config change on IOS‑XE.

This is Day 18 of the 21‑post Python for Network Engineers series. NETCONF is more involved than the earlier tools, so today favors understanding the model over memorizing every XML tag.

Why NETCONF Exists

Three properties make NETCONF worth the extra complexity:

  • Structured data. Config and state are XML matching a YANG model, not free‑form text. No regex, no format drift between releases.
  • Datastores. Many platforms separate running (live) from candidate (a staging area). Changes go to candidate, then a single commit applies them atomically — the same safe workflow NAPALM wrapped on Day 14.
  • Transactions. A commit either fully applies or fully fails. No half‑applied config from a dropped SSH session.

Install and Connect

pip install ncclient
from ncclient import manager

conn = manager.connect(
    host="sandbox-iosxe-latest-1.cisco.com",
    port=830,                     # NETCONF's standard port
    username="admin",
    password="C1sco12345",
    hostkey_verify=False,         # lab only; verify keys in production
    device_params={"name": "iosxe"},
)

print("Connected. Server capabilities:")
for cap in list(conn.server_capabilities)[:5]:
    print(" ", cap)

conn.close_session()

Port 830 is NETCONF’s dedicated SSH port — distinct from port 22. The device_params={"name": "iosxe"} tells ncclient which platform quirks to apply. On connect, the device advertises its capabilities: the YANG models and NETCONF features it supports. Those capabilities are the menu of what can be configured.

Reading Config with get-config

NETCONF requests are XML. A filter narrows the response to the subtree of interest, rather than pulling the entire config:

from ncclient import manager
import xml.dom.minidom

filter_xml = """

  
    
  

"""

with manager.connect(host="sandbox-iosxe-latest-1.cisco.com", port=830,
                     username="admin", password="C1sco12345",
                     hostkey_verify=False,
                     device_params={"name": "iosxe"}) as conn:
    reply = conn.get_config(source="running", filter=filter_xml)
    # Pretty-print the XML that comes back
    print(xml.dom.minidom.parseString(reply.xml).toprettyxml(indent="  "))

Two things worth noting. The with block closes the session automatically — the same context‑manager habit from files and sockets. And the xmlns= attribute is the YANG namespace (here, the standard ietf-interfaces model); it is required, and it identifies which model the request targets.

Making a Change with edit-config

To configure, send an edit-config with a payload describing the desired state. The operation="merge" attribute means “add or update these fields,” leaving everything else untouched:

from ncclient import manager

config = """

  
    
      Loopback100
      
        ianaift:softwareLoopback
      true
      
        
10.100.100.1 255.255.255.255
""" with manager.connect(host="sandbox-iosxe-latest-1.cisco.com", port=830, username="admin", password="C1sco12345", hostkey_verify=False, device_params={"name": "iosxe"}) as conn: reply = conn.edit_config(target="running", config=config) print(reply.ok) # True if the edit was accepted

The XML is verbose — that is the honest cost of NETCONF. In exchange, the change is structured and validated against the YANG model instead of hoping a CLI string parses. Note this example edits running directly; platforms that support a candidate datastore allow target="candidate" followed by conn.commit() for the safer transactional flow.

The candidate + commit Workflow

# On a platform with a candidate datastore:
with manager.connect(...) as conn:
    conn.edit_config(target="candidate", config=config)
    conn.commit()          # atomically apply; rolls back automatically on error

This is the NETCONF equivalent of NAPALM’s stage‑diff‑commit. The change lands in candidate, and commit() moves it to running in one transaction. A failure anywhere means nothing is applied — no partial config.

A Pragmatic Note

Hand‑writing NETCONF XML is tedious, and most engineers do not do it raw for long. Higher‑level tools build on it: NAPALM uses NETCONF under the hood for some drivers, and libraries like ncclient pair well with YANG tooling that generates the XML from Python objects. The value of learning the raw layer — exactly as with Paramiko under Netmiko — is that when a higher‑level tool fails, the underlying request and reply are legible instead of mysterious.

Exercises

The DNA Center / IOS‑XE always‑on DevNet sandbox (sandbox-iosxe-latest-1.cisco.com) supports NETCONF on port 830 for these.

  1. Warm-up. Connect to an IOS‑XE device over NETCONF and print how many server capabilities it advertises (hint: len(list(conn.server_capabilities))).
  2. Read. Use get_config with source running and no filter to pull the full config, then print just the first 500 characters of reply.xml.
  3. Filtered read. Build a filter that returns only the ietf-interfaces subtree and pretty‑print the result.
  4. Write. Use edit_config to create Loopback101 with IP 10.101.101.1/32 and confirm reply.ok is True.
  5. Challenge. Write netconf_backup(host, user, pw) that connects, pulls the full running config via get_config, pretty‑prints the XML, and writes it to {host}_running.xml — a structured backup analogous to the CLI backup from Day 12, but format‑stable across IOS versions.

Answers

Show answers

1. Warm-up

from ncclient import manager
with manager.connect(host="sandbox-iosxe-latest-1.cisco.com", port=830,
                     username="admin", password="C1sco12345",
                     hostkey_verify=False,
                     device_params={"name": "iosxe"}) as conn:
    print(len(list(conn.server_capabilities)))

2. Read

with manager.connect(...) as conn:
    reply = conn.get_config(source="running")
    print(reply.xml[:500])

3. Filtered read

import xml.dom.minidom
filt = """

  

"""
with manager.connect(...) as conn:
    reply = conn.get_config(source="running", filter=filt)
    print(xml.dom.minidom.parseString(reply.xml).toprettyxml(indent="  "))

4. Write

config = """

  
    
      Loopback101
      
        ianaift:softwareLoopback
      true
      
        
10.101.101.1255.255.255.255
""" with manager.connect(...) as conn: print(conn.edit_config(target="running", config=config).ok)

5. Challenge

import xml.dom.minidom
from ncclient import manager

def netconf_backup(host, user, pw):
    with manager.connect(host=host, port=830, username=user, password=pw,
                         hostkey_verify=False,
                         device_params={"name": "iosxe"}) as conn:
        reply = conn.get_config(source="running")
        pretty = xml.dom.minidom.parseString(reply.xml).toprettyxml(indent="  ")
        with open(f"{host}_running.xml", "w") as f:
            f.write(pretty)
        print(f"Saved {host}_running.xml ({len(pretty)} chars)")

The advantage over the Day 12 CLI backup: this XML is schema‑defined and stable across releases, so a diff between two backups reflects real config changes rather than cosmetic output‑format differences.


Previously: REST APIs with requests. Coming tomorrow — TextFSM and ntc-templates: turning ordinary show output into structured data with vendor‑maintained templates, no regex required.

Leave a Reply