The Ansible post pushed config as code, but it was still screen-scraping the CLI underneath. Model-driven programmability is the grown-up version: structured data validated against a schema, changed transactionally, over NETCONF or RESTCONF. This is how you talk to modern IOS-XE like an API instead of pretending to be a human at a terminal.
CLI automation — even good CLI automation — has a ceiling: you're parsing human-readable output that was never meant to be parsed, and pushing lines that either apply cleanly or leave the box half-configured. Model-driven programmability removes the guesswork. Config and state are described by YANG models (a schema); you read and write that structured data over NETCONF (XML over SSH, port 830) or RESTCONF (JSON/XML over HTTPS). Because it's schema-validated, an invalid change is rejected as a whole rather than half-applied, and reading state gives you clean JSON instead of show output to regex.
The trade-off versus the Ansible/CLI approach: model-driven is more rigid and has a learning curve (you must learn the models), but it's transactional, idempotent by nature, and truly programmable. For a fleet you drive from software — a NetBox-to-network pipeline, a self-service portal — this is the interface you want. Requires modern IOS-XE; classic IOS doesn't have it.
ncclient (Python) and/or curl01
Two features, one AAA user. NETCONF listens on SSH 830; RESTCONF rides the HTTPS server. Enable what you'll use.
R1(config)# aaa new-model R1(config)# username auto privilege 15 secret <strong> # NETCONF over SSH (tcp/830): R1(config)# netconf-yang # RESTCONF over the secure HTTP server (tcp/443): R1(config)# ip http secure-server R1(config)# restconf # Give both a few seconds to initialise (the YANG stack starts lazily). R1# show netconf-yang datastores
NETCONF (830) and RESTCONF (443) are full configuration channels — everything the management-plane hardening post says applies here. Restrict them to your automation host with a control-plane/VTY ACL, use real AAA (ideally TACACS+), and never expose them to anything untrusted. A model-driven API is more powerful than the CLI, which cuts both ways.
02
If you can curl, you can drive RESTCONF. Read structured state as JSON, and PATCH a change with a small body — no SSH session, no expect scripts.
# READ: every interface as clean JSON (ietf-interfaces model). $ curl -sk -u auto:secret https://203.0.113.1/restconf/data/ietf-interfaces:interfaces \ -H 'Accept: application/yang-data+json' | jq . # CHANGE: set a description on Gi2 with a PATCH (merge, idempotent). $ curl -sk -u auto:secret -X PATCH \ https://203.0.113.1/restconf/data/ietf-interfaces:interfaces/interface=GigabitEthernet2 \ -H 'Content-Type: application/yang-data+json' \ -d '{"ietf-interfaces:interface":{"name":"GigabitEthernet2","description":"uplink-to-core"}}' # Re-running the same PATCH is a no-op — idempotent by construction.
03
NETCONF's edge is the datastore model: on platforms that support a candidate config, you stage changes and commit them atomically — all or nothing — which is exactly what you want for multi-part changes.
from ncclient import manager m = manager.connect(host="203.0.113.1", port=830, username="auto", password="secret", hostkey_verify=False) # READ running config for one model subtree: filt = "<interfaces xmlns='urn:ietf:params:xml:ns:yang:ietf-interfaces'/>" print(m.get_config(source="running", filter=("subtree", filt))) # WRITE: edit-config, then commit (candidate-capable platforms): cfg = """<config><interfaces xmlns='urn:ietf:params:xml:ns:yang:ietf-interfaces'> <interface><name>GigabitEthernet2</name> <description>managed-by-netconf</description></interface> </interfaces></config>""" m.edit_config(target="candidate", config=cfg) m.commit() # atomic — rejected as a whole if any part is invalid
There are three model families and you'll meet all of them: IETF (ietf-interfaces — portable, lowest common denominator), OpenConfig (vendor-neutral, richer), and Cisco-IOS-XE-native (everything the CLI can do, but Cisco-specific). Portable code targets IETF/OpenConfig; anything the standard models don't cover forces you into the native model. Also, not every platform/version advertises a writable candidate datastore — check the capabilities the device returns on connect and fall back to running if needed. Don't assume; the device tells you what it supports.
04
Both configure the box; they're different tools for different jobs. Here's the honest comparison so you pick right.
| Dimension | CLI / Ansible ios_config |
|---|---|
| Data | Unstructured text you parse/generate |
| Validation | Applied line by line; can half-apply |
| Reads | show output → regex |
| Idempotency | You engineer it |
| Best for | Quick wins, brownfield, any IOS |
| Dimension | NETCONF / RESTCONF |
|---|---|
| Data | Structured, schema-validated (YANG) |
| Validation | Transactional — all or nothing |
| Reads | Clean JSON/XML |
| Idempotency | Inherent (merge to desired state) |
| Best for | Software-driven pipelines, modern IOS-XE |
Takeaways
curl, you can read clean JSON and PATCH idempotent changes.commit atomically, so multi-part changes never half-apply.show output?NOCTIS builds model-driven automation for Cisco and MikroTik estates across Crete — NETCONF/RESTCONF pipelines, structured state, and transactional changes that don't half-apply — not brittle screen-scraping.
Book a Discovery Call →