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) fromcandidate(a staging area). Changes go to candidate, then a singlecommitapplies 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.
- Warm-up. Connect to an IOS‑XE device over NETCONF and print how many server capabilities it advertises (hint:
len(list(conn.server_capabilities))). - Read. Use
get_configwith sourcerunningand no filter to pull the full config, then print just the first 500 characters ofreply.xml. - Filtered read. Build a filter that returns only the
ietf-interfacessubtree and pretty‑print the result. - Write. Use
edit_configto createLoopback101with IP10.101.101.1/32and confirmreply.okisTrue. - Challenge. Write
netconf_backup(host, user, pw)that connects, pulls the full running config viaget_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.1 255.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.