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 dataplane 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 constantly 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 which 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. If you consider that your hundreds of Branches/Spokes are in a completely different prefix blocks which is impossible to summarize then you are in trouble to define access-lists with hundreds of destinations, this is definitely not manageable and time consuming process which does not scale!

Given that, something more is needed to create 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 route.
If we match 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 Site-ID, however this feature is not available in native-GUI and python code is needed to take out necessary information from the controller with REST-API, process it and push back with the updated data.

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

I’ve created data policy which matches 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 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 vManage to get all the OMP routes from vSmart ( DeviceId is actual system-ip of the vSmart node) and store it in 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 to 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. At the end 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, 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 vSmart policy, template configuration must be re-applied to propagate policy down to Spoke routers which will result in a access restriction between the spokes.
For the sake of simplicity, i’ve used CLI vSMart template but this can be 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)

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s