Chapter 5: Python Network Automation with RESTCONF

Learning Objectives

Pre-Chapter Quiz — Test Your Prior Knowledge

1. Which two HTTP headers are mandatory for every RESTCONF request to a Cisco IOS XE device?

2. What is the correct RESTCONF URI to target a specific interface named GigabitEthernet1 using the IETF interfaces model?

3. You want to update only the description field of an existing interface without touching its IP address configuration. Which HTTP method should you use?

4. What HTTP status code does a successful RESTCONF PUT operation return?

5. Which YANG module family should you target to read real-time interface traffic counters on IOS XE?


Section 1: RESTCONF with Python Requests

RESTCONF is an HTTPS-based protocol (RFC 8040) that exposes a Cisco IOS XE device's YANG data model as a set of addressable URLs. Python's requests library acts as the HTTP client, allowing you to read, create, update, and delete device configuration and state data using standard HTTP operations.

1.1 Enabling RESTCONF on IOS XE

Three IOS XE configuration commands are required before any Python script can reach the RESTCONF API:

ip http secure-server
ip http authentication local
restconf

username admin privilege 15 secret Cisco1234!

Verify the service is running with show platform software yang-management process and show restconf capabilities. If the yang-management process is active and capabilities are returned, the device is ready.

1.2 Python Environment and Headers

Always isolate RESTCONF projects in a virtual environment (pip install requests). Every RESTCONF script requires two elements:

RESTCONF_HEADERS = {
    'Accept': 'application/yang-data+json',
    'Content-Type': 'application/yang-data+json'
}
AUTH = HTTPBasicAuth(os.environ['RESTCONF_USER'], os.environ['RESTCONF_PASS'])

1.3 RESTCONF URI Construction

A RESTCONF URI is a direct serialization of the YANG model hierarchy. The formula is:

https://<device>/restconf/data/<module>:<container>/<list>=<key>/<leaf>

The IETF model uses interface=GigabitEthernet1 (single string key), while the Cisco native model splits on type: GigabitEthernet=1. Interface names containing forward slashes (e.g., GigabitEthernet1/0/1) must be percent-encoded using urllib.parse.quote(name, safe='') to prevent the slash from being interpreted as a path separator.

flowchart TD A[IOS XE Device] -->|HTTPS / TLS| B[RESTCONF API\n/restconf/data] B --> C{YANG Data Store} C --> D[Configuration Data\nread-write] C --> E[Operational Data\nread-only / config false] F[Python Script\nrequests library] -->|GET / PUT / PATCH\nPOST / DELETE| B F -->|HTTPBasicAuth\napplication/yang-data+json| B D -->|ietf-interfaces\nCisco-IOS-XE-native| F E -->|Cisco-IOS-XE-oper modules| F

1.4 Choosing the Right YANG Model

Model FamilyPrefixBest Use CaseLimitation
Cisco NativeCisco-IOS-XE-native:Full IOS feature set, vendor-specific configVersion-dependent, not portable
IETF Standardsietf-interfaces:, ietf-ip:Interfaces, IP addressing, standard featuresLimited to standardized features
OpenConfigopenconfig-interfaces:Multi-vendor scriptsLess granular than native

On IOS XE 17.7.1+, run show running-config | format restconf-json on the device CLI to instantly discover the correct YANG URI for any existing configuration element.

RESTCONF URI Construction — Step by Step
Base URL https://10.10.20.48
+ Root path https://10.10.20.48/restconf/data
+ YANG module https://10.10.20.48/restconf/data/ietf-interfaces:interfaces
+ List + key https://10.10.20.48/restconf/data/ietf-interfaces:interfaces/interface=GigabitEthernet1
+ Leaf node https://10.10.20.48/restconf/data/ietf-interfaces:interfaces/interface=GigabitEthernet1/description

Key Points — Section 1


Section 2: RESTCONF CRUD Operations

The five HTTP methods map directly onto database CRUD operations. Understanding the exact semantics of each — especially the difference between PUT and PATCH — is essential for both the exam and production automation.

HTTP MethodCRUDBehaviorSuccess Code
GETReadRetrieve resource; no body sent200 OK
POSTCreateAdd new resource under target container201 Created
PUTCreate or ReplaceFully replace target resource (idempotent)204 No Content
PATCHUpdate (merge)Merge payload into existing resource204 No Content
DELETEDeleteRemove target resource204 No Content

2.1 GET — Reading Configuration and State

GET retrieves the current value of any resource. Use the fields query parameter to fetch only specific leaves and dramatically reduce response size on devices with many interfaces:

# All interfaces
url = f"{DATA_URL}/ietf-interfaces:interfaces"

# Filtered response — only name and IPv4 address
url = f"{DATA_URL}/ietf-interfaces:interfaces?fields=interface/name;interface/ietf-ip:ipv4/address"

response = requests.get(url, headers=RESTCONF_HEADERS, auth=AUTH, verify=False)
response.raise_for_status()
data = response.json()

2.2 PUT vs. PATCH — The Critical Distinction

PUT fully replaces the target resource. If you PUT a payload that omits a field, that field is deleted from the device configuration. Use PUT for idempotent provisioning where you own the complete desired state.

PATCH merges the payload into the existing resource. Fields not present in the PATCH payload are left unchanged. Use PATCH for targeted single-field updates. Think of PUT as repainting an entire wall, PATCH as touching up a single scuff mark.

# PATCH — only updates description, leaves IP address untouched
payload = {
    "ietf-interfaces:interface": {
        "name": "GigabitEthernet1",
        "description": "Primary WAN — Updated 2024"
    }
}
response = requests.patch(url, headers=RESTCONF_HEADERS, auth=AUTH,
                          json=payload, verify=False)
# Returns 204 on success

2.3 POST and DELETE

POST creates a new child resource and returns 201 Created. If the resource already exists it returns 409 Conflict — always handle this in automation scripts or switch to PUT for create-or-replace semantics.

DELETE removes the target resource and returns 204 No Content. Target the leaf path precisely to avoid accidentally deleting a parent container and all its children.

2.4 Error Handling

Never assume a RESTCONF call succeeded without checking the status code. Build a reusable wrapper:

def restconf_request(method, url, payload=None):
    kwargs = {'headers': RESTCONF_HEADERS, 'auth': AUTH, 'verify': False}
    if payload:
        kwargs['json'] = payload
    response = requests.request(method, url, **kwargs)

    if response.status_code == 400:
        print(f"[ERROR 400] Bad request — check payload: {response.text}")
    elif response.status_code == 401:
        print("[ERROR 401] Authentication failed")
    elif response.status_code == 404:
        print(f"[ERROR 404] Path not found: {url}")
    elif response.status_code == 409:
        print("[ERROR 409] Resource already exists")
    return response
flowchart TD A[Need to interact with\na RESTCONF resource] --> B{What is your goal?} B -->|Read current state| C[GET\nReturns 200 + JSON body] B -->|Write / change config| D{Does the resource\nalready exist?} D -->|Unsure — safe to overwrite all| E[PUT\nCreate or full replace\nReturns 204] D -->|Yes — change one field only| F[PATCH\nPartial merge\nReturns 204] D -->|No — device assigns key| G[POST\nCreate new child\nReturns 201\nor 409 if exists] B -->|Remove config| H[DELETE\nReturns 204] C --> I{Status 200?} I -->|Yes| J[Parse JSON response body] I -->|No| K[Handle error:\n401 auth / 404 path / 400 payload] E --> L{Status 204?} F --> L G --> M{Status 201?} H --> L L -->|Yes| N[Success] M -->|409 Conflict| O[Switch to PUT for idempotency]
RESTCONF HTTP Methods at a Glance
GET Read any resource — returns full JSON body 200 OK
PUT Create or fully replace — DANGER: omitted fields are deleted 204 No Content
PATCH Partial merge — only named fields change 204 No Content
POST Create new child under container 201 Created / 409 Conflict
DELETE Remove target resource — no body returned 204 No Content

Key Points — Section 2


Section 3: Practical RESTCONF Automation Scenarios

This section applies CRUD primitives to four real-world IOS XE automation tasks aligned with the ENAUTO exam: interface fleet management, static route configuration, ACL provisioning, and VLAN management.

3.1 Interface Fleet Automation

For Day 2 automation across multiple devices, build thin wrapper functions and iterate over a device list. Use PUT for idempotent provisioning — the same script can be re-run safely:

DEVICES = ['10.10.20.48', '10.10.20.49', '10.10.20.50']

def configure_interface_on_device(device_ip, iface_config):
    base = f"https://{device_ip}/restconf/data"
    encoded = urllib.parse.quote(iface_config['name'], safe='')
    url = f"{base}/ietf-interfaces:interfaces/interface={encoded}"
    payload = {"ietf-interfaces:interface": iface_config}
    response = requests.put(url, headers=HEADERS, auth=AUTH,
                            json=payload, verify=False)
    status = "OK" if response.status_code == 204 else f"FAIL ({response.status_code})"
    print(f"  {device_ip} -> {iface_config['name']}: {status}")

3.2 Static Route Configuration

Static routes live under Cisco-IOS-XE-native:native/ip/route. Use PATCH to add a route without disturbing other existing routes — PATCHing the native container merges the new route into the existing configuration.

3.3 ACL Management

Named extended ACLs are managed via Cisco-IOS-XE-native:native/ip/access-list/extended=<name>. Use PUT to create or replace the entire ACL atomically. Use PATCH to add a single new ACE entry to an existing ACL without replacing the whole list.

3.4 Idempotent VLAN Provisioning

VLAN management uses the Cisco-IOS-XE-vlan model. PUT per VLAN ID is idempotent and safe to run repeatedly — it creates the VLAN if absent, or silently overwrites the name if the VLAN ID already exists, with no 409 conflict:

def provision_vlans(device_ip, vlans):
    base = f"https://{device_ip}/restconf/data"
    for vlan in vlans:
        url = f"{base}/Cisco-IOS-XE-native:native/vlan/vlan-list={vlan['id']}"
        payload = {
            "Cisco-IOS-XE-vlan:vlan-list": {
                "id": vlan['id'], "name": vlan['name']
            }
        }
        response = requests.put(url, headers=HEADERS, auth=AUTH,
                                json=payload, verify=False)
        result = "CREATED/UPDATED" if response.status_code == 204 else f"ERROR {response.status_code}"
        print(f"  VLAN {vlan['id']} ({vlan['name']}): {result}")
graph LR A[Automation Script] --> B{Task Type} B -->|Deploy interfaces\nto fleet| C[PUT per interface\nper device\nURL-encode name] B -->|Add static route| D[PATCH native container\nmerge-safe] B -->|Create / replace ACL| E[PUT to\naccess-list=NAME\nfull replacement] B -->|Add single ACE| F[PATCH to\nspecific sequence entry] B -->|Provision VLANs| G[PUT per vlan-list=ID\nidempotent] C --> H[204 = success\nre-runnable] D --> H E --> H F --> H G --> H

Key Points — Section 3


Section 4: RESTCONF Monitoring and Operational Data

4.1 Configuration Data vs. Operational Data

Configuration data represents intended state — what you have told the device to do. It is read-write, stored in the running configuration datastore, and accessible via all HTTP methods.

Operational data represents actual state — what the device is currently doing. It is read-only and generated in real time. In YANG schemas, operational data nodes are marked config false. Attempting PUT/PATCH/POST/DELETE on these nodes returns an error.

graph TD A[IOS XE YANG Data] --> B[Configuration Data\nread-write / rw] A --> C[Operational Data\nread-only / ro / config false] B --> D[ietf-interfaces:interfaces] B --> E[Cisco-IOS-XE-native:native] D --> D1[name, description\nenabled, ietf-ip:ipv4] E --> E1[ip/route, vlan\naccess-list, hostname] C --> G[Cisco-IOS-XE-interfaces-oper:interfaces] C --> H[Cisco-IOS-XE-bgp-oper:bgp-state-data] C --> I[Cisco-IOS-XE-platform-oper:components] C --> J[Cisco-IOS-XE-fib-oper:fib-oper-data] G --> G1[in-octets / out-octets\nin-errors / oper-status] H --> H1[session-state\nprefix counts / uptime] I --> I1[CPU load\nmemory / sensors] J --> J1[FIB forwarding table] style B fill:#d4edda,stroke:#28a745 style C fill:#cce5ff,stroke:#004085

4.2 Key Operational YANG Modules

YANG ModuleData Exposed
Cisco-IOS-XE-interfaces-operInterface statistics, link state, error counters, speed
Cisco-IOS-XE-bgp-operBGP neighbor state, prefix counts, session uptime
Cisco-IOS-XE-ospf-operOSPF neighbor adjacencies, LSA counts
Cisco-IOS-XE-fib-operFIB/CEF forwarding table entries
Cisco-IOS-XE-platform-operCPU load, memory usage, environmental sensors
Cisco-IOS-XE-mpls-operMPLS label forwarding table

4.3 Retrieving Interface Statistics

def get_interface_stats(device_ip, iface_name=None):
    base = f"https://{device_ip}/restconf/data"
    if iface_name:
        encoded = urllib.parse.quote(iface_name, safe='')
        url = f"{base}/Cisco-IOS-XE-interfaces-oper:interfaces/interface={encoded}"
    else:
        fields = "interface/name;interface/oper-status;interface/statistics"
        url = f"{base}/Cisco-IOS-XE-interfaces-oper:interfaces?fields={fields}"
    response = requests.get(url, headers=HEADERS, auth=AUTH, verify=False)
    response.raise_for_status()
    return response.json()

4.4 RESTCONF Polling vs. Model-Driven Telemetry

RESTCONF is a synchronous request-response protocol — it does not push data to you. For monitoring, you must poll at an interval. This has architectural implications:

ScenarioRESTCONF PollingNETCONF/gRPC Telemetry (MDT)
Frequency needed< 1 per minute> 1 per minute or sub-second
Number of devices< 20 devices20+ devices at scale
Event-driven alertingPoll-based workaroundNative push subscriptions
Implementation complexityLow — plain PythonHigher — requires collector

4.5 Capability Discovery

Before scripting against a specific YANG module, confirm it is loaded on the target device. Different IOS XE versions support different modules — targeting a missing module returns 404:

def list_yang_modules(device_ip, filter_prefix=None):
    url = f"https://{device_ip}/restconf/data/ietf-yang-library:modules-state"
    response = requests.get(url, headers=HEADERS, auth=AUTH, verify=False)
    modules = response.json().get('ietf-yang-library:modules-state', {}).get('module', [])
    if filter_prefix:
        modules = [m for m in modules if m.get('name', '').startswith(filter_prefix)]
    return [(m['name'], m.get('revision', 'unknown')) for m in modules]

Key Points — Section 4


Post-Chapter Quiz — Verify Your Understanding

1. Which two HTTP headers are mandatory for every RESTCONF request to a Cisco IOS XE device?

2. You run requests.put() on an interface resource with a payload that includes only the description field. What happens to the interface's IP address configuration?

3. An interface named GigabitEthernet1/0/1 needs to be targeted in a RESTCONF URI. What Python call produces the correct encoded path segment?

4. A script calls POST to create VLAN 100 on a device where VLAN 100 already exists. What status code does the device return?

5. You want to monitor BGP neighbor session state across 50 devices every 10 seconds. Which monitoring approach is most appropriate?

6. Which YANG module and URI prefix do you use to read real-time interface error counters on IOS XE?

7. What is the correct IOS XE CLI command to discover the RESTCONF URI for an existing configuration element?

8. A RESTCONF DELETE request targeting /ietf-interfaces:interfaces/interface=GigabitEthernet2/ietf-ip:ipv4/address returns 204. What does this mean?

9. Which Python call correctly authenticates a RESTCONF request using credentials stored in environment variables?

10. You need to add a new ACE (sequence 30) to an existing 10-entry ACL without disturbing the other entries. Which method and target are correct?

Your Progress

Answer Explanations