Exam Topics Covered: 5.2 Utilize RESTCONF to configure a network device including interfaces, static routes, and VLANs (IOS XE only)
Figuring out the URL path and required JSON payload from YANG modules manually can be challenging. There is some guess work that has to happen to select the correct YANG module file with the configuration parameters you are looking for, and then once again if augmentation of the feature being configured is in use. In that case there will actually be two YANG modules that you need to look at, as will be seen in the upcoming sections. There's a great utility called YANG Suite that is very helpful with figuring out the RESTCONF requests. See my blog post for more info on using that. That will likely be no help on the exam, however. For exam study purposes, the below sections work through figuring out the URL path and JSON payload manually by examining the YANG modules in tree format.
To determine the URL path, examine the ietf-interfaces
YANG module. A utility called pyang
can be used to view the model in tree format as shown below, using the command pyang --format tree [module_filename]
. Module files can be found in the YANG github repo
The URL path will follow the format: https://%7Bhostname%7D/restconf/data/%7Bmodule%7D:%7Bcontainer%7D/%7Bresource%7D. Looking at the below YANG model, the module is ietf-interfaces
, the container is interfaces
and the resource is interface
. The *
next to interface indicates that it is a leaf list, meaning there can be multiple interfaces. In this case the path would be https://%7Bhostname%7D/restconf/data/ietf-interfaces:interfaces/%7Binterface%7D.
When creating a new interface, the resource part of the URL is specified in the payload. To create a new interface, we need to send a payload consisting of the following to https://%7Bhostname%7D/restconf/data/ietf-interfaces:interfaces.
{
"interface": {
"name": "Loopback101",
"description": "Created by RESTCONF",
"type": "iana-if-type:softwareLoopback",
"enabled": True
}
The ? next to an attribute in the pyang tree indicates that the attribute is optional.
When retrieving a specific interface, we can specify the interface
resource in the URL path and match on the key which is denoted by [name]
in the pyang tree output. The path to retrieve interface Loopback101 would be https://%7Bhostname%7D/restconf/data/ietf-interfaces:interfaces/interface=Loopback101.
module: ietf-interfaces
+--rw interfaces
| +--rw interface* [name]
| +--rw name string
| +--rw description? string
| +--rw type identityref
| +--rw enabled? boolean
| +--rw link-up-down-trap-enable? enumeration {if-mib}?
| +--ro admin-status enumeration {if-mib}?
| +--ro oper-status enumeration
| +--ro last-change? yang:date-and-time
| +--ro if-index int32 {if-mib}?
| +--ro phys-address? yang:phys-address
| +--ro higher-layer-if* interface-ref
| +--ro lower-layer-if* interface-ref
| +--ro speed? yang:gauge64
| +--ro statistics
| +--ro discontinuity-time yang:date-and-time
| +--ro in-octets? yang:counter64
| +--ro in-unicast-pkts? yang:counter64
| +--ro in-broadcast-pkts? yang:counter64
| +--ro in-multicast-pkts? yang:counter64
| +--ro in-discards? yang:counter32
| +--ro in-errors? yang:counter32
| +--ro in-unknown-protos? yang:counter32
| +--ro out-octets? yang:counter64
| +--ro out-unicast-pkts? yang:counter64
| +--ro out-broadcast-pkts? yang:counter64
| +--ro out-multicast-pkts? yang:counter64
| +--ro out-discards? yang:counter32
| +--ro out-errors? yang:counter32
x--ro interfaces-state
x--ro interface* [name]
x--ro name string
x--ro type identityref
x--ro admin-status enumeration {if-mib}?
x--ro oper-status enumeration
x--ro last-change? yang:date-and-time
x--ro if-index int32 {if-mib}?
x--ro phys-address? yang:phys-address
x--ro higher-layer-if* interface-state-ref
x--ro lower-layer-if* interface-state-ref
x--ro speed? yang:gauge64
x--ro statistics
x--ro discontinuity-time yang:date-and-time
x--ro in-octets? yang:counter64
x--ro in-unicast-pkts? yang:counter64
x--ro in-broadcast-pkts? yang:counter64
x--ro in-multicast-pkts? yang:counter64
x--ro in-discards? yang:counter32
x--ro in-errors? yang:counter32
x--ro in-unknown-protos? yang:counter32
x--ro out-octets? yang:counter64
x--ro out-unicast-pkts? yang:counter64
x--ro out-broadcast-pkts? yang:counter64
x--ro out-multicast-pkts? yang:counter64
x--ro out-discards? yang:counter32
x--ro out-errors? yang:counter32
import requests
import logging
import json
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
requests.packages.urllib3.disable_warnings()
# DevNet IOS XE Sandbox
api_path = "https://sandbox-iosxe-latest-1.cisco.com/restconf"
# DevNet Creds using HTTP Basic Authentication
auth = ("admin", "C1sco12345")
def create_intf(name: str):
post_hdrs = {
"Content-Type": "application/yang-data+json",
"Accept": "application/yang-data+json, application/yang-data.errors+json"
}
# Headers for POST (interface creation)
intf = {
"interface":
{
"enabled": True,
"name": name,
"type": "iana-if-type:softwareLoopback"
}
}
post_url = f"{api_path}/data/ietf-interfaces:interfaces"
logging.info(f"Creating interface {name}...")
resp = requests.post(post_url, auth=auth, headers=post_hdrs, verify=False, json=intf)
logging.info(f"Status Code: {resp.status_code}, Reason: {resp.reason}\n")
def get_intf(name: str):
# Headers for GET request
get_hdrs = {"Accept": "application/yang-data+json"}
get_url = f"{api_path}/data/ietf-interfaces:interfaces/interface={name}"
logging.info(f"Get interface {name}...")
resp = requests.get(get_url, auth=auth, headers=get_hdrs, verify=False)
logging.info(json.dumps(resp.json(), indent=4))
def intf_state(name: str, enabled: bool):
hdrs = {
"Content-Type": "application/yang-data+json",
"Accept": "application/yang-data+json, application/yang-data.errors+json"
}
url = f"{api_path}/data/ietf-interfaces:interfaces/interface={name}"
payload = {
"ietf-interfaces:interface": {
"enabled": enabled
}
}
if enabled:
logging.info(f"Enabling interface {name}...")
else:
logging.info(f"Disabling interface {name}...")
resp = requests.patch(url, auth=auth, headers=hdrs, verify=False, json=payload)
logging.info(f"Status Code: {resp.status_code}, Reason: {resp.reason}\n")
def del_intf(name: str):
# Headers for DELETE request
delete_hdrs = {"Accept": "application/yang-data+json"}
delete_url = f"{api_path}/data/ietf-interfaces:interfaces/interface={name}"
logging.info(f"Delete interface {name}...")
resp = requests.delete(delete_url, auth=auth, headers=delete_hdrs, verify=False)
logging.info(f"Status Code: {resp.status_code}, Reason: {resp.reason}\n")
if __name__ == "__main__":
while True:
print("1. Create Interface")
print("2. Get Interface")
print("3. Delete Interface")
print("4. Disable Interface")
print("5. Enable Interface")
print("6. Quit")
answer = input("Enter selection: ")
if not answer == "6":
intf_name = input("Interface name: ")
if answer == "1":
create_intf(intf_name)
if answer == "2":
get_intf(intf_name)
if answer == "3":
del_intf(intf_name)
if answer == "4":
intf_state(intf_name, enabled=False)
if answer == "5":
intf_state(intf_name, enabled=True)
elif answer == "6":
break
else:
"Invalid selection!"
To determine the URL path and request format, examine the ietf-ip
YANG module. Since the ietf-ip
module augments the interfaces
module, the module, container, and resource are appended to the interface path. The path then becomes https://hostname/restconf/data/ietf-interface:interfaces/interface=%5Bifname%5D/ietf-ip:ipv4/address.
Using the example from the last section, to create an IP address on Loopback101, the URL would be https://hostname/restconf/data/ietf-interface/interfaces/interface=Loopback101/ietf-ip:ipv4/address. The payload would be:
{
"address":
{
"ip": "10.1.255.1",
"netmask": "255.255.255.0"
}
}
module: ietf-ip
augment /if:interfaces/if:interface:
+--rw ipv4!
| +--rw enabled? boolean
| +--rw forwarding? boolean
| +--rw mtu? uint16
| +--rw address* [ip]
| | +--rw ip inet:ipv4-address-no-zone
| | +--rw (subnet)
| | +--:(prefix-length)
| | | +--rw prefix-length? uint8
| | +--:(netmask)
| | +--rw netmask? yang:dotted-quad {ipv4-non-contiguous-netmasks}?
import requests
import logging
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
requests.packages.urllib3.disable_warnings()
# DevNet IOS XE Sandbox
api_path = "https://sandbox-iosxe-latest-1.cisco.com/restconf"
# DevNet Creds using HTTP Basic Authentication
auth = ("admin", "C1sco12345")
def assign_ip(if_name: str, ip_addr: str, netmask: str):
hdrs = {
"Content-Type": "application/yang-data+json",
"Accept": "application/yang-data+json, application/yang-data.errors+json"
}
url = f"{api_path}/data/ietf-interfaces:interfaces/interface={if_name}/ietf-ip:ipv4/address"
payload = {
"address":
{
"ip": ip_addr,
"netmask": netmask
}
}
logging.info(f"Assigning IP address {ip_addr} to {if_name}...")
resp = requests.patch(url, auth=auth, headers=hdrs, verify=False, json=payload)
logging.info(f"Status Code: {resp.status_code}, Reason: {resp.reason}\n")
def remove_ip(if_name: str, ip_addr: str):
url = f"{api_path}/data/ietf-interfaces:interfaces/interface={if_name}/ietf-ip:ipv4/address={ip_addr}"
hdrs = {
"Content-Type": "application/yang-data+json",
"Accept": "application/yang-data+json, application/yang-data.errors+json"
}
logging.info(f"Deleting IP address {ip_addr} from interface {if_name}")
resp = requests.delete(url, auth=auth, headers=hdrs, verify=False)
logging.info(f"Status Code: {resp.status_code}, Reason: {resp.reason}\n")
def update_ip(if_name: str, old_ip: str, new_ip: str, netmask: str):
url = f"{api_path}/data/ietf-interfaces:interfaces/interface={if_name}/ietf-ip:ipv4/address={old_ip}"
logging.info(f"Updating IP address {old_ip} on {if_name}...")
remove_ip(if_name, old_ip)
assign_ip(if_name, new_ip, netmask)
if __name__ == "__main__":
while True:
print("1. Assign IP Address")
print("2. Update IP Address")
print("3. Remove IP Address")
print("4. Quit")
answer = input("Enter selection: ")
if not answer == "4":
intf_name = input("Interface name: ")
if answer == "1":
ip_addr = input("IP Address: ")
netmask = input("Subnet Mask: ")
assign_ip(intf_name, ip_addr, netmask)
if answer == "2":
old_ip = input("Old IP: ")
new_ip = input("New IP: ")
netmask = input("Subnet Mask: ")
update_ip(intf_name, old_ip, new_ip, netmask)
if answer == "3":
ip_addr = input("IP Address: ")
remove_ip(intf_name, ip_addr)
elif answer == "4":
break
else:
"Invalid selection!"
For static routes, We need to first look at the routing container in the ietf-routing
module. In order to view the routing table for a particular instance (VRF), we can specify the instance name as the key. Here we'll use the default VRF. As shown below in the YANG tree, routing-protocol is a key which can be selected by specifying both a type and a name. Static routes have a type of static
, and with a name of 1. Therefore, the path to get the static routes in the default VRF:
module: ietf-routing
# Removed the routing-state container for brevity
+--rw routing
+--rw routing-instance* [name]
+--rw name string
+--rw type? identityref
+--rw enabled? boolean
+--rw router-id? yang:dotted-quad {router-id}?
+--rw description? string
+--rw interfaces
| +--rw interface* if:interface-ref
+--rw routing-protocols
| +--rw routing-protocol* [type name]
| +--rw type identityref
| +--rw name string
| +--rw description? string
| +--rw static-routes
+--rw ribs
+--rw rib* [name]
+--rw name string
+--rw address-family? identityref
+--rw description? string
Notice in the above tree there is no further info under static-routes
. This is because the ietf-routing module is augmented by the ietf-ipv4-unicast-routing
module. In order to create routes, we need to examine the ietf-ipv4-unicast-routing
module.
The module ietf-ipv4-unicast-routing
and container ipv4
must be appended to the end of the path, so the URL path for creating a new static route is:
The JSON payload required is:
{
"route": {
"destination-prefix": "1.1.1.0/24",
"description": "optional description",
"next-hop": {
"next-hop-address": "10.10.20.1"
}
}
}
module: ietf-ipv4-unicast-routing
augment /rt:routing/rt:routing-instance/rt:routing-protocols/rt:routing-protocol/rt:static-routes:
+--rw ipv4
+--rw route* [destination-prefix]
+--rw destination-prefix inet:ipv4-prefix
+--rw description? string
+--rw next-hop
+--rw (next-hop-options)
+--:(simple-next-hop)
| +--rw outgoing-interface? string
+--:(special-next-hop)
| +--rw special-next-hop? enumeration
+--:(next-hop-address)
+--rw next-hop-address? inet:ipv4-address
augment /rt:fib-route/rt:input/rt:destination-address:
+-- address? inet:ipv4-address
augment /rt:fib-route/rt:output/rt:route:
+-- destination-prefix? inet:ipv4-prefix
augment /rt:fib-route/rt:output/rt:route/rt:next-hop/rt:next-hop-options/rt:simple-next-hop:
+-- next-hop-address? inet:ipv4-address
To delete a static route, the route can be specified by the destination-prefix in the URL path. This is because destination-prefix is the key, which is denoted in the above output by [destination-prefix]. Since the prefix uses slash notation, we have to encode the slash so that the API does not interpret it as being part of the path. For example, a prefix of 1.1.1.0/24
becomes 1.1.1.0%2F24
. This is accomplished in the below code using the urllib
module. To delete a static route, a delete request can be sent to the following URL:
import requests
import logging
import json
import urllib
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
requests.packages.urllib3.disable_warnings()
# DevNet IOS XE Sandbox
api_path = "https://sandbox-iosxe-latest-1.cisco.com/restconf"
# DevNet Creds using HTTP Basic Authentication
auth = ("admin", "C1sco12345")
def list_routes(vrf: str="default"):
hdrs = {"Accept": "application/yang-data+json"}
url = f"{api_path}/data/ietf-routing:routing/routing-instance={vrf}/routing-protocols/routing-protocol=static,1"
resp = requests.get(url, headers=hdrs, auth=auth, verify=False)
logging.info(f"Status Code: {resp.status_code} Reason: {resp.reason}\n")
logging.info(json.dumps(resp.json(), indent=4))
def create_static(dest_prefix: str, nexthop_addr: str, vrf: str="default"):
hdrs = {
"Content-Type": "application/yang-data+json",
"Accept": "application/yang-data+json, application/yang-data.errors+json"
}
url = f"{api_path}/data/ietf-routing:routing/routing-instance={vrf}/routing-protocols/routing-protocol=static,1" \
"/static-routes/ietf-ipv4-unicast-routing:ipv4"
payload = {
"route": {
"destination-prefix": dest_prefix,
"next-hop": {
"next-hop-address": nexthop_addr
}
}
}
logging.info(f"Creating route to {dest_prefix} with next hop {nexthop_addr}...")
resp = requests.post(url, headers=hdrs, auth=auth, verify=False, json=payload)
logging.info(f"Status Code: {resp.status_code} Reason: {resp.reason}")
def delete_static(dest_prefix: str, vrf: str="default"):
hdrs = {
"Accept": "application/yang-data+json, application/yang-data.errors+json"
}
# It is necessary to urlencode the slash in the prefix so that it can be used in the path
# The urllib module is used to accomplish this
encoded_prefix = urllib.parse.quote(dest_prefix, safe='')
url = f"{api_path}/data/ietf-routing:routing/routing-instance={vrf}/routing-protocols/routing-protocol=static,1" \
f"/static-routes/ietf-ipv4-unicast-routing:ipv4/route={encoded_prefix}"
logging.info(f"Deleting route {dest_prefix}")
resp = requests.delete(url, auth=auth, headers=hdrs, verify=False)
logging.info(f"Status Code: {resp.status_code} Reason: {resp.reason}")
if __name__ == "__main__":
while True:
print("1. List static routes")
print("2. Create static route")
print("3. Delete static route")
print("4. Quit")
answer = input("Enter selection: ")
if not answer == "4":
if answer == "1":
list_routes()
if answer == "2":
dest_prefix = input("Destination prefix: ")
next_hop = input("Next hop address: ")
create_static(dest_prefix=dest_prefix, nexthop_addr=next_hop)
if answer == "3":
dest_prefix = input("Destination prefix: ")
delete_static(dest_prefix)
elif answer == "4":
break
else:
"Invalid selection!"
There are two YANG models to be concerned with for managing the VLAN database on IOS XE switches, Cisco-IOS-XE-native
and Cisco-IOS-XE-vlan
which augments the native module. The path to retrieve the configured VLANs is:
https://hostname/restconf/data/Cisco-IOS-XE-native:native/vlan
Note that this is not pulling from the VLAN database, but rather the configuration. This means that if VTP is being used and the switch is not a Server, or in Transparent mode, there will be no VLANs in the running configuration and the API call will return an empty list. A workaround would be to get the operational VLANs using the url: /restconf/data/Cisco-IOS-XE-vlan-oper:vlans/vlan
.
The container in the Cisco-IOS-XE-vlan module is the vlan-list
container, and it can be searched using the VLAN id as indicated by [id]
in the below pyang tree output.
+--rw vlan-list* [id]
| +--rw id uint16
| +--rw remote-span? empty
| +--rw private-vlan {ios-features:private-vlan}?
| | +--rw primary? empty
| | +--rw association? string
| | +--rw community? empty
| | +--rw isolated? empty
| +--rw name? string
| +--rw state? enumeration
| o--rw lldp
| | o--rw run? empty
| +--rw uni-vlan? enumeration
| +--rw shutdown? empty
Given this schema, the URL to create a VLAN would be:
https://hostname/restconf/data/Cisco-IOS-XE-native:native/vlan
The payload would be as follows:
{
"Cisco-IOS-XE-vlan:vlan-list": [
{
"id": 100,
"name": "Test"
}
]
}
Currently it is not possible to validate using publicly available DevNet systems as there are none currently that have Layer 2 VLAN capability. However, the above solution is mentioned in this Cisco Community post.