The flexibility of device templates in NSO (…and more)

Device templates in NSO is, as the name implies, configuration templates for devices. These templates can be applied to devices or device-groups.

Working on a larger configuration deployment I initially thought that I could only use these to deploy configurations that was common for all devices the templates was being deployed to. But in my opinion, considering the flexibility that NSO gives, that seemed a little weak. So I dug into it and apparently, like with XML service templates, you can reference XPATH variables from the current device that the template is being deployed on.

This gives us the possibility to, for instance, have a generic BGP router template that includes the BGP router-id based on a loopback IP:


<config xmlns="http://tail-f.com/ns/config/1.0">
  <devices xmlns="http://tail-f.com/ns/ncs">
    <template>
      <name>BGP_template</name>
      <config>
        <router xmlns="http://tail-f.com/ned/cisco-ios-xr">
          <bgp>
            <bgp-no-instance>
              <id>65000</id>
              <bgp>
                <router-id>{/devices/device[name=$DEVICE]/config/cisco-ios-xr:interface/Loopback[id='0']/ipv4/address/ip}</router-id>
              </bgp>
            </bgp-no-instance>
          </bgp>
        </router>
      </config>
    </template>
  </devices>
</config>

Some issues though, can’t always be resolved through static references. Like for instance the ISIS NET – which traditionally is a rewrite of the IPv4 loopback IP to the CLNS NET ID.

This had to be calculated through a script and added via the REST API:

Python function to extract Loopback IPs from a spreadsheet, calculate NET and send it via NSO API:

import openpyxl, sys, json, requests

from requests.auth import HTTPBasicAuth
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

BASE_URL = "https://192.168.1.100:8888/api"
USER = 'user'
PASS = 'pass' 

# ----

def NSO_PATCH_DEVICE(DEVICE, JSON):
  URL = BASE_URL + '/running/devices/device/' + DEVICE + '?no-networking=cli'
  HEADERS = {
    'Content-Type': 'application/vnd.yang.data+json',
    'Accept' : 'application/vnd.yang.data+json'
  }
  response = requests.patch(URL, auth=HTTPBasicAuth(USER,PASS), verify=False, headers=HEADERS, data=json.dumps(JSON))
  if not (response.status_code == 204 or response.status_code == 201):
    print(json.loads(str(response.text)))
  else:
    print("Patched device config for device : " + DEVICE)

# ---- 

def CALC_NET(ip):
    ipaddr = ip.split('.')
    net = list()
    for octet in ipaddr:
        net.append('{:03d}'.format(int(octet)))
    return (''.join(net)[:4] + '.' + ''.join(net)[4:8] + '.' + ''.join(net)[8:])

# ----

wb = openpyxl.load_workbook('Loopback_IPs.xlsx')
Routers = wb['Ark1']

for row in range(1, 200):
        r_hostname = str(Routers['A' + str(row)].value)
        r_lo0_ipv4 = str(Routers['B' + str(row)].value)

        device_cfg = {
            "tailf-ncs:device": {
                "name": str(r_hostname),
                "config": {
                    "tailf-ned-cisco-ios-xr:router": {
                        "isis": {
                            "tag": [
                                {
                                    "name": "1",
                                    "net": [
                                        {
                                            "id": "49.0001." + str(CALC_NET(r_lo0_ipv4)) + ".00"
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                }
             }
        }


        NSO_PATCH_DEVICE(r_hostname, device_cfg)