Cisco SD-WAN Policy automation with REST API

I’ve been working with Cisco SD-WAN recently and decided to summarize my experience to demonstrate REST API capabilities in Viptela vManage (The management plane component).

Interaction to networking components with out-of-band SDN controllers is much robust than working with devices directly. Everything from the operational and greenfield deployment perspective is possible via vManage REST API. It is very flexible to Automate repetitive tasks or initial configuration or even expand SD-WAN capabilities to bring the new features to it!

Short introduction of the scenario.

Typically when we need to filter access to something, what we do is to create FW rules, Access-lists, route-maps, etc., which looks at the Packet/Frame in the data plane to match SRC/DST, Protocol or port numbers, pretty much anything that each of the packet or frame contains.

What I wanted to do is to create continuously updated dynamic access-lists with the latest information available in Control-plane to apply in Dataplane.

Below is the simplified topology illustrated:

With topology shown above, I want to create a policy that will block inter-spoke communication but forwards traffic to internet destined resources.

You might think that its easy peasy to do with Traditional access-control mechanisms in SD-WAN or Classic VPN routing schemes, but again it depends on the conditions. Imagine that your hundreds of Branches/Spokes are in a completely different prefix block, which is impossible to summarize. You are in trouble to define access-lists with hundreds of destinations. This is not a manageable and time-consuming process that does not scale!
Given that, something more is needed to create a single-lined and simple data policy, for this reason, I’ve decided to use SITE-ID, which is Cisco SD-WAN specific and is advertised via OMP along with the route.
If we match the Site-id range to cover all the Branches/Spokes, then we will be able to create dynamic data policy ( with data prefix-list), which blocks inter-spoke traffic based on control-plane information (Site-ID). However, this feature is not available in native-GUI, and python code is needed to take out all the necessary information from the controller with REST-API, process it, and push it back with the updated data.

Here is the list of the SITE-IDs and VPNs defined, which of course could be created via REST but for now i will be focusing on the functionality described above.

I’ve created a data policy that matches the destination data prefix-list “BLOCK_INTER_SPOKE,” which we need to work with to update with proper data! Topology is Hub-Spoke. As shown above, vSmart would not reflect routes received from one spoke to another via OMP, but there is a default route in which traffic within the VPN follows it to reach the other end of the spoke. Therefore route-control alone is not enough to restrict inter-spoke communication.

 

SPOKE1# show ip routes omp
100 0.0.0.0/0 omp – – – – 10.100.100.113 gold ipsec F,S

SPOKE1# ping 192.168.1.1 source 172.16.1.1 vpn 100
Ping in VPN 100
PING 192.168.1.1 (192.168.1.1) from 172.16.1.1 : 56(84) bytes of data.
64 bytes from 192.168.1.1: icmp_seq=1 ttl=63 time=218 ms
64 bytes from 192.168.1.1: icmp_seq=2 ttl=63 time=350 ms
64 bytes from 192.168.1.1: icmp_seq=3 ttl=63 time=388 ms

With the following ugly python code below, first, I request a vManage controller to get all the OMP routes from the vSmart controller ( DeviceId is actual system-ip of the vSmart node) and store it in the file.

response = req.get("https://192.168.10.119/dataservice/device/omp/routes/received?deviceId=10.100.100.117",auth=('admin', 'admin'), verify=False)

with open('rest-data.txt', 'w') as f: 
json.dump(response.json(), f, indent=4)

Below is the portion of response data in JSON Format:

    "data": [
        {
            "overlay-id": "1",
            "color": "gold",
            "vdevice-name": "10.100.100.117",
            "prefix": "0.0.0.0/0",
            "ip": "10.100.100.113",
            "from-peer": "10.100.100.113",
            "label": "1005",
            "encap": "ipsec",
            "site-id": "200",
            "originator": "10.100.100.113",
            "vpn-id": "100",
            "vdevice-host-name": "vsmart",
            "path-id": "75",
            "protocol": "static",
            "vdevice-dataKey": "10.100.100.117-ipv4-100",
            "metric": "0",
            "lastupdated": 1580152072459,
            "attribute-type": "installed",
            "address-family": "ipv4",
            "status": "C R"
        },
        {
            "overlay-id": "1",
            "color": "gold",
            "vdevice-name": "10.100.100.117",
            "prefix": "192.0.2.0/24",
            "ip": "10.100.100.113",
            "from-peer": "10.100.100.113",
            "label": "1005",
            "encap": "ipsec",
            "site-id": "200",
            "originator": "10.100.100.113",
            "vpn-id": "100",
            "vdevice-host-name": "vsmart",
            "path-id": "75",
            "protocol": "connected",
            "vdevice-dataKey": "10.100.100.117--100",
            "metric": "0",
            "lastupdated": 1580152072459,
            "attribute-type": "installed",
            "status": "C R"
        },
        {
            "overlay-id": "1",
            "color": "gold",
            "vdevice-name": "10.100.100.117",
            "prefix": "172.16.1.0/24",
            "ip": "10.100.100.114",
            "from-peer": "10.100.100.114",
            "label": "1005",
            "encap": "ipsec",
            "site-id": "201",
            "originator": "10.100.100.114",
            "vpn-id": "100",
            "vdevice-host-name": "vsmart",
            "path-id": "75",
            "protocol": "connected",
            "vdevice-dataKey": "10.100.100.117--100",
            "metric": "0",
            "lastupdated": 1580152072460,
            "attribute-type": "installed",
            "status": "C R"
        },
        {
            "overlay-id": "1",
            "color": "gold",
            "vdevice-name": "10.100.100.117",
            "prefix": "192.168.1.0/24",
            "ip": "10.100.100.115",
            "from-peer": "10.100.100.115",
            "label": "1005",
            "encap": "ipsec",
            "site-id": "202",
            "originator": "10.100.100.115",
            "vpn-id": "100",
            "vdevice-host-name": "vsmart",
            "path-id": "75",
            "protocol": "connected",
            "vdevice-dataKey": "10.100.100.117--100",
            "metric": "0",
            "lastupdated": 1580152072460,
            "attribute-type": "installed",
            "status": "C R"
        }
    ]

 

Clearly, we can spot OMP prefix and site-id associated with it, but we have to sort it to match the routes advertised by Spokes only.

This portion of code opens previously created file ( rest-data.txt), looks for the Key ‘site-id’ in nested JSON, and appends prefixes within the range of 201-202 site-id and appends it to python list. In the end, the JSON dictionary is populated with data which can be used later to create prefix-list

prefixes = []

with open('rest-data.txt', 'r') as data_file:
    data = json.load(data_file)
    for d in data['data']:
        if int(d['site-id']) >= 201 and int(d['site-id']) <= 202:  # Range of Site-ID
            prefixes.append(d['prefix'])

json_dict = {
    "name": "BLOCK_INTER_SPOKE",  # Name of the Data Prefix-list
    "entries": [{'ipPrefix': prefixes} for prefixes in prefixes]
}
with open('prefixes.json', 'w') as f:
    json.dump(json_dict, f, indent=4)

Print output of prefixes.json file.

{
    "name": "BLOCK_INTER_SPOKE",
    "entries": [
        {
            "ipPrefix": "172.16.1.0/24"
        },
        {
            "ipPrefix": "192.168.1.0/24"
        }
    ]

Now we have Spoke prefixes in JSON format, the only thing left is to update existing policy with provided data.
First List-Id is required to update right prefix-list which can be retrieved with quick postman call to

https://{{vmanage}}:{{port}}/dataservice/template/policy/list/dataprefix
Screen Shot 2020-01-27 at 7.58.18 PMFollowing post request updates prefix-list “BLOCK_INTER_SPOKE” with the entries described in prefixes.json file.

headers = {'Content-Type' : 'application/json'}

p = req.put("https://192.168.10.119/dataservice/template/policy/list/dataprefix/3c2742b7-d449-4345-a8df-4a27df28711c",auth=('admin', 'admin'), data=open('prefixes.json', 'rb'), verify=False, headers=headers)

Quick check to validate if prefix-list is updated with right input.
Screen Shot 2020-01-28 at 11.27.09 AM

Perfect, now vSmart policy needs to be activated, and configuration template re-applied to propagate updated data policy down to vEdge/cEdge.

activate_policy.json contains empty brackets “{}”

#Activate Data Policy after updating prefix-list
headers = {'Content-Type' : 'application/json'}
r = req.post("https://192.168.10.119/dataservice/template/policy/vsmart/activate/3128f641-deeb-4355-9c6e-3d7556423a30",auth=('admin', 'admin'), data=open('activate_policy.json', 'r'), verify=False, headers=headers, cookies=s.cookies)

Again,  policy ID can be retrieved via GET request in Postman or Curl to the following address
https://{{vmanage}}:{{port}}/dataservice/template/policy/vsmart

Upon activation of the vSmart policy, template configuration must be re-applied to propagate policy down to Spoke routers, which will result in an access restriction between the spokes.
For the sake of simplicity, I’ve used the CLI vSMart template, but this can be a regular configuration template.

headers = {'Content-Type' : 'application/json'}
r = req.post("https://192.168.10.119/dataservice/template/device/config/attachcli",auth=('admin', 'admin'), data=open('tempcli.json', 'r'), verify=False, headers=headers, cookies=s.cookies)

tempcli.json file contains:
TemplateID – Configuration Template
csv-DeviceId – vSmart DeviceID
csv-DeviceIP – vSmart Device IP
csv-Hostname- vSmart Hostname

{
    "deviceTemplateList": [
        {
            "templateId": "a9b74c36-9b80-41cb-8e53-aa6ae47ac9c0",
            "device": [
                {
                    "csv-status": "complete",
                    "csv-deviceId": "2e196eb0-dd1b-4878-9e04-397d77aa4ff2",
                    "csv-deviceIP": "10.100.100.117",
                    "csv-host-name": "vsmart",
                    "csv-templateId": "a9b74c36-9b80-41cb-8e53-aa6ae47ac9c0"
                }
            ],
            "isEdited": false
        }
    ]
}

That’s it all, policy is successfully applied to Spoke routers.

SPOKE2# show policy from-vsmart
from-vsmart data-policy _100_DATA_POLICY
 direction from-service
 vpn-list 100
  sequence 1
   match
    destination-data-prefix-list BLOCK_INTER_SPOKE
   action drop
  default-action accept
from-vsmart lists vpn-list 100
 vpn 100
from-vsmart lists data-prefix-list BLOCK_INTER_SPOKE
 ip-prefix 172.16.1.0/24
 ip-prefix 192.168.1.0/24

Below is a full code.
Keep in mind to maintain JSESSIONID Cookie for each request.

import requests as req
import json
headers = {'Content-Type' : 'application/json'}

s = req.Session()
s = req.get("https://192.168.10.119/dataservice/device/omp/routes/received?deviceId=10.100.100.117",
auth=('admin', 'admin'), verify=False)

with open('rest-data.txt', 'w') as f:
    json.dump(s.json(), f, indent=4)


prefixes = []

with open('rest-data.txt', 'r') as data_file:
    data = json.load(data_file)
    for d in data['data']: # Nested JSON , Information is Under Data!
        if int(d['site-id']) >= 201 and int(d['site-id']) <= 202:  # Range of Site-ID
            prefixes.append(d['prefix'])

json_dict = {
    "name": "BLOCK_INTER_SPOKE",  # Name of the Data Prefix-list
    "description": "",
    "type": "dataPrefix",
    "listId": "3c2742b7-d449-4345-a8df-4a27df28711c",
    "entries": [{'ipPrefix': prefixes} for prefixes in prefixes]
}
with open('prefixes.json', 'w') as f:
    json.dump(json_dict, f, indent=4)

# Update Prefix List
r = req.put("https://192.168.10.119/dataservice/template/policy/list/dataprefix/3c2742b7-d449-4345-a8df-4a27df28711c",
auth=('admin', 'admin'), data=open('prefixes.json', 'r'), verify=False, headers=headers, cookies=s.cookies)

#Activate Data Policy after updating prefix-list
r = req.post("https://192.168.10.119/dataservice/template/policy/vsmart/activate/3128f641-deeb-4355-9c6e-3d7556423a30",auth=('admin', 'admin'), data=open('activate_policy.json', 'r'), verify=False, headers=headers, cookies=s.cookies)


#Attach Template
r = req.post("https://192.168.10.119/dataservice/template/device/config/attachcli",auth=('admin', 'admin'), data=open('tempcli.json', 'r'), verify=False, headers=headers, cookies=s.cookies)