Table of Contents
- 1 Useful Snippets
- 1.1 Enable Debugging
- 1.2 Pretty Printing XML
- 1.3 Enable netconf
- 2 Connecting to a Device
- 2.1 Capabilities
- 2.2 YANG data model Schema
- 3 Device Configuration
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