NetConf/Yang

Table of Contents

Useful Snippets

Note: All code snippets are available as a Jupyter Notebook in my GitHub Repo


Before we begin, let us define few useful snippets for debugging the netconf sessions and printing the raw XML content in more readable way.

The ncclient library generates huge amounts of debugging information. The below code fragment shows how to enable it.

Enable Debugging

import logging

handler = logging.StreamHandler()
for l in ['ncclient.transport.ssh', 'ncclient.transport.session', 'ncclient.operations.rpc']:
    logger = logging.getLogger(l)
    if not logger.hasHandlers():
        logger.addHandler(handler)
        logger.setLevel(logging.INFO)

print("Enabled the logging for ssh, session, rpc")

Pretty Printing XML

ncclient works only on XML data as NETCONF supports only XML content format. Below code snippet is to print the data part of the xml content in more readable format.

from lxml import etree

def pretty_print(element):
    # Netconf reply returns actual return data with a tag '<data></data>'
    data = list(element.data)
    items = len(data)
    for i, j in enumerate(data):
        if i > 0 and i < items:
            print('#' * 50)
        print(etree.tostring(j, pretty_print=True).decode('utf-8'))

print("Defined the pretty_print function")

Enable netconf

Configure command “netconf-yang” to enable the netconf on IOSXE device.

Router#conf t
Enter configuration commands, one per line.  End with CNTL/Z.
Router(config)#netconf-yang

Connecting to a Device

Verify if NETCONF is enabled by doing SSH to device on the NETCONF agent’s port. Netconf agent listens on port 830.

$ ssh -p 830 cisco@192.168.163.223 -s netconf
cisco@192.168.163.223's password:
<?xml version="1.0" encoding="UTF-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<capabilities>
<capability>urn:ietf:params:netconf:base:1.0</capability>
<capability>urn:ietf:params:netconf:base:1.1</capability>
<capability>urn:ietf:params:netconf:capability:writable-running:1.0</capability>
<capability>urn:ietf:params:netconf:capability:xpath:1.0</capability>
<capability>urn:ietf:params:netconf:capability:validate:1.0</capability>
<capability>urn:ietf:params:netconf:capability:validate:1.1</capability>

<snip>

<capability>urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults?module=ietf-netconf-with-defaults&revision=2011-06-01</capability>
</capabilities>
<session-id>24672</session-id></hello>]]>]]>

With -s option, we can provide the subsystem to be used.

Now, let’s define host variables and connect to device using ncclient package.

spoke = {
             "host": "127.0.0.1",
             "port": 2223,
             "username": "vagrant",
             "password": "vagrant"
          }

from ncclient import manager, xml_

m = manager.connect(host=spoke["host"], port=spoke["port"], username=spoke["username"], password=spoke["password"],
                    allow_agent=False,
                    look_for_keys=False,
                    hostkey_verify=False,
                    device_params={'name': 'csr'})

Capabilities

Every network device doesn’t support all features and mechanisms within the NETCONF protocol. During the hello message exchange with server, NETCONF provides the capabilities which are supported by it. In order to retrieve only capabilities supported by netconf server, we need to use the filter urn:ietf:params:netconf

Reference: http://www.netconfcentral.org/netconf_docs#capabilities

capabilities = m.server_capabilities

[i for i in capabilities if i.startswith('urn:ietf:params:netconf')]

Now let’s print the capabilities related to data model support.

import re

for i in capabilities:
    model = re.search('module=([^&]*)&', i)
    if model is not None:
        print("{}".format(model.group(1)))

YANG data model Schema

After parsing the capabilities we can also use ncclient to provide more details about the data models supported.

Let’s pick the crypto data model Cisco-IOS-XE-crypto and print the schema for it. The ncclient library provides getschema function for that. Here is snip of getschema output for pre-shared-key container.

Preshared key configuration on the device

crypto isakmp key cisco47 address 0.0.0.0 0.0.0.0

Snip of crypto data model which shows the container for pre-shared-key

    container pre-shared-key {
      description
        "Pre-Shared Key";
      container address {
        description
          "pre shared key by address";
        choice ipv4-ipv6 {
          case ipv4 {
            list ipv4 {
              description 
                "address prefix";
              key "ipv4-addr";
              leaf ipv4-addr {
                type inet:ipv4-address;
              }
              leaf mask {
                description
                    "address prefix mask";
                type inet:ipv4-address;
              }
              uses crypto-keyring-key-grouping;
            }
          }
          case ipv6 {
            list ipv6 {
              description
                "define shared key with IPv6 address";
              key "ipv6";
              leaf ipv6 {
                description
                  "IPv6 prefix mask";
                type ios-types:ipv6-prefix;
              }
              uses crypto-keyring-key-grouping;
            }
          } 
        }
      }

model_schema = m.get_schema('Cisco-IOS-XE-crypto')

print(model_schema)

Device Configuration

The ncclient library provides basic operations to get and modify the configuration.

get_config – takes a target data store and it also supports an optional filter

edit_config – takes a target data store and an XML content which represents configuration change.

Retrieve Configuration

Get the running configuration using get_config function

config = m.get_config(source='running')
pretty_print(config)

Using filters to retrieve specific configs

Using filters to get selected part of running configuration.

Below section of code shows how to get only interface details and hostname from configuration.

filter = '''
<native>
  <interface/>
</native>
'''

c = m.get_config(source='running', filter=('subtree', filter))
pretty_print(c)

filter = '''
<native>
  <hostname/>
</native>
'''
c = m.get_config(source='running', filter=('subtree', filter))
pretty_print(c)

Configure DMVPN Spoke

Generate Config With Templates

Jinja2 is widely used Templating Language. It supports various features like conditionals, loops, Variable insertion

Configuration details are isolated from the code and have been put in .yaml file for better code management.

Below is an example on how to generate required configuration in XML format from .j2 and .yaml file.

config_details.yaml file:

 VRFs:
  - name: RED
  - name: BLUE

crypto_config.j2 file:

    <vrf>
    {% for VRF in VRFs %}
    <definition>
        <name>{{VRF.name}}</name>
        <address-family>
            <ipv4>
            </ipv4>
        </address-family>
    </definition>
    {% endfor %}
    </vrf>

XML config generated with above template snip

    <vrf>

    <definition>
        <name>RED</name>
        <address-family>
            <ipv4>
            </ipv4>
        </address-family>
    </definition>

    <definition>
        <name>BLUE</name>
        <address-family>
            <ipv4>
            </ipv4>
        </address-family>
    </definition>

    </vrf>

import yaml
import xmltodict
from jinja2 import Template

print("Loading Network Configuration Details from YAML File")
with open("../local_spoke/config_details.yaml") as f:
    config = yaml.load(f.read())

with open("../local_spoke/crypto_config.j2") as f:
    crypto_template = Template(f.read())

with open("../local_spoke/interface_config.j2") as f:
    interface_template = Template(f.read())

print("Processing Device Configurations")

for device in config["devices"]:
    print("Device: {}".format(device["name"]))

    # Generate Device Specific Configurations

    print("Creating Device Specific Configurations from Interface and Crypto Template")

    with open("../local_spoke/{}_interface.cfg".format(device["name"]), "w") as f:
        interface_config = interface_template.render(interfaces=device["interfaces"])
        f.write(interface_config)

    with open("../local_spoke/{}_dmvpn.cfg".format(device["name"]), "w") as f:
        crypto_config = crypto_template.render(VRFs=device["VRFs"],TunnelInterfaces=device["TunnelInterfaces"],ospf=device["ospf"],Profiles=device["Profiles"])
        f.write(crypto_config)

print("Sending Configuration via NETCONF by using edit-config operation")

interface_resp = m.edit_config(interface_config, target="running")

crypto_resp = m.edit_config(crypto_config, target = "running")

interface_reply = xmltodict.parse(interface_resp.xml)

crypto_reply = xmltodict.parse(crypto_resp.xml)

print("Interface Config: {}".format(crypto_reply["rpc-reply"]))

print("DMVPN Config: {}".format(crypto_reply["rpc-reply"]))

print("-----------------------------------------------------------------")

Save Configuration

Configurations done via edit_config function will be in running configuration. We have to run below code to save the configuration to startup.

save_body = '<cisco-ia:save-config xmlns:cisco-ia="http://cisco.com/yang/cisco-ia"/>'

save_rpc = m.dispatch(xml_.to_ele(save_body))

save_reply = xmltodict.parse(save_rpc.xml)

print((save_reply["rpc-reply"]["result"]["#text"]))

Leave a comment

Blog at WordPress.com.

Up ↑

Design a site like this with WordPress.com
Get started