Introduction to YANG, NETCONF and Cisco Catalyst 9800 wireless controller

This year I have been learning network programmability through Cisco DevNet, which is a program for developing integration with Cisco platforms and APIs. In this article, I tried to introduce how to use NETCONF to retrieve and to update the configuration of the latest generation of Cisco Wireless Controllers.

The Cisco Catalyst 9800 Wireless Controller is based on IOS XE, which runs as a daemon and modular sub systems that are built on a Linux kernel. IOS XE supports Application Hosting, can run Guest Shell (a virtualized Linux-based environment) and provides a NETCONF interface for network programmability.

There are some immediate benefits in using NETCONF interface instead of the GUI or the CLI. For example, some repetitive tasks (provisioning, configuration at scale) can be automated. Automation provides consistency to configuration and reduces errors. NETCONF can also be used for telemetry and active monitoring. And programmability is a key part in continuous cycle of development and controls aiming at higher software quality.

DevNet has a tremendous amount of information online and provides many examples for IOS-XE platforms. But this example focuses on the Catalyst 9800.

The wireless configuration model on IOS XE is also new compared to the previous AireOS platforms, and introduces two concepts:

  • Tags: including policy-tags, site-tags and rf-tags that are used to define the policies, site and radios that are directly associated to an Access Point.

  • Profiles: including wlan-profiles, policy-profiles, ap-join-profiles, flex-profiles and rf-profiles that define the attributes and parameters that are used by tags.

The objective of this article is to retrieve and to update the tags (policy tag, site tag and RF tag) of Access Points using Python language and NETCONF.

WARNING: do not run a script in production environment without testing and without understanding the consequences. For example, editing the Access Point tags may result in an Access Point reboot.

Configure NETCONF on IOS XE

Network Configuration Protocol (NETCONF) provides a mechanism to install, manipulate, and delete the configuration of network devices. NETCONF v1.1 is defined in IETF RFC 6241, which was published in 2011. I understand the protocol as a replacement of the Simple Network Management Protocol (SNMP).

NETCONF uses Remote Protocol Calls (RPC) messages encoded in XML to realise various operations (get configuration, edit configuration, etc) and to provide configuration or operational data. Messages are then securely transported through SSH using a NETCONF port (TCP 830 by default).

Another element of NETCONF are the datastores, which represents a configuration storage location in the network device. Operations can retrieve, configure, copy and delete configuration datastores. For example, a "candidate" datastore allows to manipulate a copy of the device's current configuration without impacting the current configuration. "running" is the mandatory datastore for implementations.

The following NETCONF operations are supported:

  • Get: retrieve configuration from the "running" datastore

  • Get-config: retrieve all or part of a configuration datastore

  • Edit-config: load all or part of a configuration to a target datastore

  • Copy-config: replace an entire datastore with the content of another

  • Delete-config: delete a datastore

  • Lock: lock the datastore to be manipulate by the user-only

  • Unlock: unlock the datastore

  • Close-session: gracefully terminate the NETCONF session

  • Kill-session: force the termination of the NETCONF session

At the beginning, NETCONF must be enabled on the NETCONF server, which is the 9800 wireless controller (disabled by default).

Using CLI:

C9800-CL# netconf-yang

The wireless controller must also have a user with privilege level of 15 and must support SSH version 2.

Using the local user database on the wireless controller:

C9800-CL# username <USER> privilege 15 secret <PASSWORD>

C9800-CL# aaa authentication login default local

C9800-CL# aaa authorization exec default local

Preparing your environment of development

At first, you will need to have Python installed on your computer. Once Python is installed, it is recommended to use virtual environments using the virtualenv library in order to keep isolated the library environment of your projects. Cisco DevNet provides very detailed guides (https://developer.cisco.com/learning-labs/setup/) on how to setup a development environment whether you are using Windows, Mac or Linux.

In order to use NETCONF protocol in a Python script, we will use the ncclient library (https://github.com/ncclient/ncclient), as suggested on Cisco Devnet.

To install the ncclient library in a new virtual environment:

$ virtualenv venv

$ source venv/Scripts/activate

(venv)

$ sudo pip install ncclient

Let’s verify that the PIP importation has been successful using Python in interactive mode:

$ python -i

Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)] on win32

Type "help", "copyright", "credits" or "license" for more information.

>>> import ncclient

>>> dir(ncclient)

['NCClientError', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_version', 'sys']

Those Python commands import the ncclient library using the import statement. The dir() function shows the list of attributes contained in the ncclient library.

Opening a NETCONF session and retrieving the 9800 NETCONF capabilities

Once NETCONF is enabled on the Catalyst 9800 and the ncclient library is installed, we start creating a first Python script to connect to the NETCONF interface of the 9800.

from ncclient import manager

 

m = manager.connect(host="192.168.203.10",

                    port=830,

                    username="netconf",

                    password="changeme",

                    hostkey_verify=False)

The first line from <package> import <module> imports the manager module from the ncclient library. Manager is the high-level API that provides the NETCONF operations.

The second line manager.connect() establishes a NETCONF session with the Catalyst 9800 (host 192.168.203.10) over SSH on the default port 830. It uses the user netconf or any username that you have created earlier. It returns a manager object called m, which will be used in every other script later.

 

When the NETCONF session is opened, both client and server exchange a Hello message containing the list of the peer's NETCONF capabilities.

During the establishment of the session, the manager object has retrieved the NETCONF capabilities of the server and added it to the server_capabilities attribute. A NETCONF capability describes the operations and the information that the server offers, and extends the standard features of NETCONF. A NETCONF capability is identified by a URI. The following example shows several capabilities including "http://cisco.com/ns/yang/Cisco-IOS-XE-aaa-oper?module=Cisco-IOS-XE-aaa-oper&revision=2019-05-01", which allows to retrieve and to edit the AAA configuration on the server.

To print the list of the NETCONF capabilities of the Catalyst 9800:

print("Printing server capabilities:")

 

capabilities = []

for capability in m.server_capabilities:

    print("\t{}".format(capability))

    capabilities.append(capability)

Output:

Printing server capabilities:

    urn:ietf:params:netconf:base:1.0

    urn:ietf:params:netconf:base:1.1

    http://cisco.com/ns/yang/Cisco-IOS-XE-aaa?module=Cisco-IOS-XE-aaa&revision=2020-03-01

    http://cisco.com/ns/yang/Cisco-IOS-XE-aaa-oper?module=Cisco-IOS-XE-aaa-oper&revision=2019-    05-01

    http://cisco.com/ns/yang/Cisco-IOS-XE-acl?module=Cisco-IOS-XE-acl&revision=2020-03-01

    http://cisco.com/ns/yang/Cisco-IOS-XE-acl-oper?module=Cisco-IOS-XE-acl-oper&revision=2020-03-01

    http://cisco.com/ns/yang/Cisco-IOS-XE-app-hosting-cfg?module=Cisco-IOS-XE-app-hosting-cfg&revision=2020-03-01

You can also directly open an SSH client to the NETCONF interface port 830 using the netconf username. The console will return an XML-formatted Hello message that contains the list of capabilities from the server.

login as: netconf

netconf@192.168.203.10'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>http://cisco.com/ns/yang/Cisco-IOS-XE-aaa?module=Cisco-IOS-XE-aaa&amp;revision=2020-03-01</capability>

<capability>http://cisco.com/ns/yang/Cisco-IOS-XE-aaa-oper?module=Cisco-IOS-XE-aaa-oper&amp;revision=2019-05-01</capability>

<capability>http://cisco.com/ns/yang/Cisco-IOS-XE-acl?module=Cisco-IOS-XE-acl&amp;revision=2020-03-01</capability>

<capability>http://cisco.com/ns/yang/Cisco-IOS-XE-acl-oper?module=Cisco-IOS-XE-acl-oper&amp;revision=2020-03-01</capability>

Introducing YANG

The non-standard NETCONF capabilities are often defined by a data modelling language called YANG (Yet Another Next Generation). YANG data models are defined in RFC 6020 (YANG 1.0) and RFC 7950 (YANG 1.1). YANG defines the language structure and semantics to represent configuration and state data and may be formatted in XML or JSON by network management protocols such as NETCONF.

We will keep working on our script to retrieve all the YANG data models that are advertised by the server into .yang files into a subdirectory called YANG_schemas:

import re

 

# Scan the capabilities and extract modules using regular expression

print("Extracting YANG module names:")

 

modules = []

for capability in capabilities:

    supported_model = re.search("module=([a-zA-Z0-9_.-]*)", capability)

    if supported_model is not None:

        print("\t{}".format(supported_model.groups(0)[0]))

        modules.append(supported_model.groups(0)[0])

 

# Create subdirectory if not exits

if not os.path.exists("YANG_schemas"):

    os.makedirs("YANG_schemas")

 

# For each module, import the schema into text files

print("Importing YANG schemas from NETCONF server...")

for model in modules:

    schema = m.get_schema(model)

    # Open new file handle.

    with open("YANG_schemas/{}".format(model), "w") as f:

        f.write(schema.data)

print("Importing YANG schemas from NETCONF server finished")

Output:

Extracting YANG module names:

    Cisco-IOS-XE-aaa

    Cisco-IOS-XE-aaa-oper

    Cisco-IOS-XE-acl

    Cisco-IOS-XE-acl-oper

    Cisco-IOS-XE-app-hosting-cfg

Importing YANG schemas from NETCONF server...

Importing YANG schemas from NETCONF server finished

In this script, the list of server capabilities that was returned from the NETCONF connection is parsed using a regular expression that search for the YANG data model name that is after "module=".

Then we iterate through the list of YANG data model name to request the YANG data model to the 9800 wireless controller using the get_schema() method. The YANG schema is then written into a file with the YANG data model name.

 

For example, we are going to use the file that is called Cisco-IOS-XE-wireless-ap-cfg. It shows the YANG module that allows us to configure the Access Point tags:

module Cisco-IOS-XE-wireless-ap-cfg {

  yang-version 1;

  namespace "http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-ap-cfg";

  prefix wireless-ap-cfg;

 

  import Cisco-IOS-XE-wireless-ap-types {

    prefix wireless-ap-types;

  }

  import ietf-yang-types {

    prefix yang;

  }

  import cisco-semver {

    prefix cisco-semver;

  }

 

  organization

    "Cisco Systems, Inc.";

  contact

    "Cisco Systems, Inc.

     Customer Service

     

     Postal: 170 W Tasman Drive

     San Jose, CA 95134

     

     Tel: +1 1800 553-NETS

     

     E-mail: cs-yang@cisco.com";

  description

    "Model for managing AP configurations.

     Copyright (c) 2016-2019 by Cisco Systems, Inc.

     All rights reserved.";

 

  revision 2019-06-25 {

    description

      "- Deprecating filter priority, replacing filter priority with

         rule priority with range [0-1023].

       - Added semantic version.

       - Fix constraint for tag source priority.";

    reference "5.0.0";

    cisco-semver:module-version "5.0.0";

  }

 

  grouping ap-tag {

    description

      "Configuration of ap tag";

    leaf ap-mac {

      type yang:mac-address;

      description

        "mac address of Access Point";

    }

    leaf policy-tag {

      type string;

      default "default-policy-tag";

      description

        "Configuration of policy tag";

    }

    leaf site-tag {

      type string;

      default "default-site-tag";

      description

        "Configuration of site tag";

    }

    leaf rf-tag {

      type string;

      default "default-rf-tag";

      description

        "Configuration of rf tag";

    }

  }

 

 

  container ap-cfg-data {

    description

      "AP tag configuration";

    container ap-tags {

      description

        "Configuration of AP tags";

      list ap-tag {

        key "ap-mac";

        description

          "List of AP tags";

        uses wireless-ap-cfg:ap-tag;

      }

    }

  }

}

 

A YANG module contains header statements, revision statements and definition statements.

The header statements provide information about the model including:

  • The YANG version of the model identified by yang-version

  • The namespace URI http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-ap-cfg identifies the model and is used in the advertised capabilities of the NETCONF server.

  • The import statement allows to use data from other modules.

  • The prefix statement wireless-ap-cfg can be used inside the module statement or inside the import statements. It references the module in which the definitions are used.

The module defines a hierarchy of nodes which can be represented as a tree. The language allows four types of nodes:

  • Leaf nodes: contains simple data values and has no child node.

  • List nodes: defines a sequence of child nodes in which each entry of the list is uniquely identified by the values of one or more key leafs.

  • Container nodes: group one or more child nodes of any type (including leafs, lists, containers, and leaf-lists) and has no value.

  • Leaf-list nodes: defines a sequence of leaf nodes with exactly one value of a particular type per leaf.

In our example, the module defines a container called ap-tags, which contains the ap-tag list.

The ap-tag list defines a list of leafs (ap-mac, policy-tag, site-tag and rf-tag) that is defined by the grouping wireless-ap-cfg:ap-tag, and that is uniquely identified by the ap-mac key. The grouping statement is not defined as a node statement but describes a set of nodes which may be reused in the module as a copy and paste.

ap-mac, policy-tag, site-tag and rf-tag are leaf nodes of various types (yang:mac-address and string).

 

For better visualization, we can use the Python library called Pyang (https://github.com/mbj4668/pyang) to generate a tree representation of the YANG models. Pyang is a YANG validator and convertor that transform YANG modules into other formats such as YIN, DSDL or XSD.

Open your bash interpreter and install Pyang using PIP. Then run the pyang command to develop the tree from the downloaded YANG data model Cisco-IOS-XE-wireless-ap-cfg.yang:

(venv)

$ sudo pip install pyang

 

$ pyang -f tree Cisco-IOS-XE-wireless-ap-cfg.yang

Output:

module: Cisco-IOS-XE-wireless-ap-cfg

    +--rw ap-cfg-data

       +--rw location-entries

       |  +--rw location-entry* [location-name]

       |     +--rw location-name     string

       |     +--rw description?      string

       |     +--rw tag-info

       |     |  +--rw policy-tag?   string

       |     |  +--rw site-tag?     string

       |     |  +--rw rf-tag?       string

       |     +--rw associated-aps

       |        +--rw associated-ap* [ap-mac]

       |           +--rw ap-mac    yang:mac-address

       +--rw tag-source-priority-configs

       |  +--rw tag-source-priority-config* [priority]

       |     +--rw priority    uint8

       |     +--rw tag-src?    wireless-ap-types:enm-ap-tag-source

       +--rw ap-filter-configs

       |  +--rw ap-filter-config* [filter-name]

       |     +--rw filter-name        string

       |     +--rw filter-string?     string

       |     +--rw filter-priority?   uint8

       |     +--rw apply-tag-list

       |        +--rw policy-tag?   string

       |        +--rw site-tag?     string

       |        +--rw rf-tag?       string

       o--rw ap-filter-priority-cfg-entries

       |  +--rw ap-filter-priority-cfg-entry* [priority]

       |     +--rw priority       uint8

       |     +--rw filter-name?   string

       +--rw ap-rule-priority-cfg-entries

       |  +--rw ap-rule-priority-cfg-entry* [priority]

       |     +--rw priority       uint32

       |     +--rw filter-name?   string

       +--rw ap-tags

          +--rw ap-tag* [ap-mac]

             +--rw ap-mac        yang:mac-address

             +--rw policy-tag?   string

             +--rw site-tag?     string

             +--rw rf-tag?       string

The module Cisco-IOS-XE-wireless-ap-cfg is represented into a node tree. Each node is printed as following:

<status> <flags> <name> <opts> <type> <if-features>

Where:

  • Status is + for current, o for obsolete and x for deprecated

  • Flags is rw for configuration data, ro for non-configuration data, -x for rpcs and actions and -n for notifications

  • Name for the node: (<name>) for choice node, :(<name>) for case node, <prefix>:<name> for node that are imported from another module

  • Options are:

    • ? for an optional leaf, choice, anydata or anyxml

    • ! for a presence container (can exist without any child node defined)

    • * for a leaf-list or list

    • [<keys>] for a list's keys

  • Node type for leafs and leaf-list

The tree representation is easier to read than the YANG definitions. The first node ap-cfg-data is a container (there is no data type and value). Its container branch ap-tags is also a container. Its subbranch ap-tag is a list (*) using the key ap-mac (identified by the square bracket []). The list contains four leaf nodes ap-mac, policy-tag, site-tag and rf-tag which types are yang:mac-address and string.

Retrieving the full configuration from the running datastore

Let's go back to our Python script and retrieve all the configuration of the running datastore from the Catalyst 9800 using the ncclient library. To do so, we use the get_config() method. Be cautious before running the script, as it could be a lot of configuration data.

import xml.dom.minidom

 

# Execute NETCONF get-config() to retrieve all the configuration data

result = m.get_config(source="running")

 

# Parse the XML file

xml_doc = xml.dom.minidom.parseString(result.xml)

 

# Print all the configuration

print("NETCONF full configuration using get-config() method:\n{}\n".format(xml_doc.toprettyxml()))

Output:

NETCONF full configuration using get-config() method:

<?xml version="1.0" ?>

<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="urn:uuid:1b93d81e-dd19-48d2-8a88-78a086157784">

        <data>

                <native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">

                        <version>17.2</version>

                        <boot-start-marker/>

                        <boot-end-marker/>

                        <memory>

                                <free>

                                        <low-watermark>

                                                <processor>72701</processor>

                                        </low-watermark>

                                </free>

                        </memory>

                <ap-cfg-data xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-ap-cfg">

                        <ap-tags>

                                <ap-tag>

                                        <ap-mac>00:aa:bb:cc:dd:ee</ap-mac>

                                        <policy-tag>Corporate-PolicyTag</policy-tag>

                                        <site-tag>Brisbane</site-tag>

                                        <rf-tag>HighDensity-RFtag</rf-tag>

                                </ap-tag>

                        </ap-tags>

                </ap-cfg-data>

        </data>

</rpc-reply>

The NETCONF reply is XML-formatted and we use the xml.dom.minidom library to parse and pretty print the XML message. The XML tags that contain data associated to a YANG model are identified with an xmlns attribute which refers to namespace URI of the YANG model. For example, the <ap-cfg-data> element with xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-ap-cfg" contains the data associated with the Cisco-IOS-XE-wireless-ap-cfg YANG model.

Retrieving the static configuration of Access Point tags

The full configuration of the running datastore may be a lot of data to retrieve. It is often preferred to use a NETCONF XML filter in order to get a specific part of the configuration. The filter starts with a <filter> root element.

We continue to develop our script to retrieve some configuration data using the Get() operation:

import xmltodict

import csv

from datetime import datetime

from collections import OrderedDict

 

# Create an XML filter

filter = """

<filter>

    <ap-cfg-data xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-ap-cfg">

        <ap-tags>

            <ap-tag></ap-tag>

        </ap-tags>

    </ap-cfg-data>

</filter>

"""

 

# Execute the NETCONF get() to retrieve all the AP tag configuration data

result = m.get(filter)

 

# Parse the XML file

xml_doc = xml.dom.minidom.parseString(result.xml)

 

# Print the configuration

print("NETCONF AP tag configuration using get() method and XML filter:\n{}\n".format(xml_doc.toprettyxml()))

 

# Create a list of AP tag configuration

netconf_data = xmltodict.parse(result.xml)["rpc-reply"]["data"]

if netconf_data == None:

    # No AP configuration has been returned

    ap_list = []

elif isinstance(netconf_data["ap-cfg-data"]["ap-tags"]["ap-tag"], OrderedDict):

    # Only one AP configuration has been returned with a type of OrderedDict. Wrapping it into a list

    ap_list = [netconf_data["ap-cfg-data"]["ap-tags"]["ap-tag"]]

else:

    # Multiple AP configurations have been returned

    ap_list = netconf_data["ap-cfg-data"]["ap-tags"]["ap-tag"]

 

# Get the current date and time

today = datetime.today()

 

# Create a Comma Separated Value file with the AP tag configuration

with open("ap-tags-config-{}.csv".format(today.strftime("%Y-%m-%d-%H-%M-%S")), mode="w", newline="") as csv_file:

 

    csv_writer = csv.writer(csv_file, delimiter=",", quotechar="'", quoting=csv.QUOTE_MINIMAL)

 

    print("Writing AP tag configuration to CSV file named {}:".format(csv_file.name))

 

    for ap in ap_list:

        # Print the AP tag configuration

        print("\tAP MAC:{}, policy-tag:{}, site-tag:{}, rf-tag:{}".format(ap["ap-mac"],

           ap["policy-tag"] if "policy-tag" in ap else "",

           ap["site-tag"] if "site-tag" in ap else "",

           ap["rf-tag"] if "rf-tag" in ap else ""))

 

        # Write the AP tag configuration as a row in the CSV file

        csv_writer.writerow([ap["ap-mac"],

                             ap["policy-tag"] if "policy-tag" in ap else "",

                             ap["site-tag"] if "site-tag" in ap else "",

                             ap["rf-tag"] if "rf-tag" in ap else ""])

Output:

NETCONF AP tag configuration using get() method and XML filter:

<?xml version="1.0" ?>

<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="urn:uuid:db5d7a9d-c06c-4a7c-a07e-9ce3e413d3a6">

        <data>

                <ap-cfg-data xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-ap-cfg">

                        <ap-tags>

                                <ap-tag>

                                        <ap-mac>11:22:33:44:55:66</ap-mac>

                                        <policy-tag>Brisbane</policy-tag>

                                </ap-tag>

                                <ap-tag>

                                        <ap-mac>22:33:44:55:66:77</ap-mac>

                                        <site-tag>Sydney</site-tag>

                                </ap-tag>

                        </ap-tags>

                </ap-cfg-data>

        </data>

</rpc-reply>

 

Writing AP tag configuration to CSV file named ap-tags-config-2020-10-12-18-27-23.csv

    AP MAC:11:22:33:44:55:66, policy-tag:Brisbane, site-tag:, rf-tag:

    AP MAC:22:33:44:55:66:77, policy-tag:, site-tag:Sydney, rf-tag:

The script uses the NETCONF get() method to retrieve the data associated with the Cisco-IOS-XE-wireless-ap-cfg YANG model into an XML-formatted message.

We use the xmltodict library to parse the XML result into a dictionary and create a list of the configured Access Point tags by accessing the nested dictionary elements through the following keys ["rpc-reply"]["data"]["ap-cfg-data"]["ap-tags"]["ap-tag"].

Additionally the NETCONF data that is returned using the keys ["rpc-reply"]["data"] can have no value if there is no Access Point tag configuration, can be an OrderedList type if there is only one AP, or can be a List type if there are several APs. So, we create a list of Access Point tags configuration based on those conditions.

Then we print the related information for each Access Point by iterating the items of the Access Point tags list using a for loop.

The script also write the Access Point tags configuration into a new Comma Separated Value (CSV) file. We use the csv library and the datetime library to create a unique timestamped filename.

Note that the static configuration of Policy tag, Site tag and RF tag of an Access Point are not necessarily configured in the running datastore. If not configured, our script will write an empty string for the missing tags in the CSV file.

 

The YANG data models are differentiated in two categories: configuration data models and operational data models. Configuration data are data related to the "config" commands, whereas operational data are related to the "show" commands.

In our case, we have retrieved the static configuration of the Access Point tags as written in the running configuration. But the actual Access Point tags may be different. For example, if the connected Access Point does not even have any statically assigned tag, it may be allocated default or rule-based tags. In this case, it will not appear in the NETCONF configuration data but will be in the operational data.

To see the operational tags of all connected CAPWAP Access Point, we would need to look for the YANG model Cisco-IOS-XE-wireless-access-point-oper.yang.

$ pyang -f tree Cisco-IOS-XE-wireless-access-point-oper.yang

module: Cisco-IOS-XE-wireless-access-point-oper

    +--ro access-point-oper-data

       +--ro capwap-data* [wtp-mac]

       |  +--ro wtp-mac                  yang:mac-address

       |  +--ro ip-addr?                 string

       |  +--ro name?                    string

       |  +--ro device-detail

       |  +--ro tag-info

       |  |  +--ro policy-tag-info

       |  |  |  +--ro policy-tag-name?   string

       |  |  +--ro site-tag

       |  |  |  +--ro site-tag-name?   string

       |  |  +--ro rf-tag

       |  |  |  +--ro rf-tag-name?   string

       |  |  +--ro filter-info

       |  |     +--ro filter-name?   string

A different template filter can be used in a script to retrieve the tags of all connected Access Points.

filter = '''

<filter>

    <access-point-oper-data xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-access-point-oper">

        <capwap-data>

            <wtp-mac></wtp-mac>

            <tag-info></tag-info>

        </capwap-data>

    </access-point-oper-data>

</filter>

'''

Editing the tags of Access Points

NETCONF can also be used to edit the configuration of a datastore. In the following example, we will set the Site tag of a specific Access Point to "Sydney". We use the same YANG configuration model and create a template that specifies the identifier of the Access Point (AP MAC address) and its targeted Site tag.

# Create a NETCONF template

netconf_template = """

<config>

    <ap-cfg-data xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-ap-cfg">

        <ap-tags>

            <ap-tag>

                <ap-mac>11:22:33:44:55:66</ap-mac>

                <site-tag>Sydney</site-tag>

            </ap-tag>

        </ap-tags>

    </ap-cfg-data>

</config>

"""

 

# Execute NETCONF edit_config() to edit the AP tag configuration

result = m.edit_config(netconf_template, target = "running")

 

# Parse the XML file

xml_doc = xml.dom.minidom.parseString(result.xml)

 

# Print the configuration

print("NETCONF response for editing the AP tag configuration:\n{}".format(xml_doc.toprettyxml()))

Output:

NETCONF response for editing the AP tag configuration:

<?xml version="1.0" ?>

<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="urn:uuid:846f0fc2-a20e-4a45-aba4-dd3415a753e4">

        <ok/>

</rpc-reply>

At first, we create a XML template that is similar to the XML configuration that was returned by the get-config() method for the Access Point configuration YANG model. We want to add an Access Point with the MAC identifier 11:22:33:44:55:66 for <ap-mac> tag and "Sydney" as <site-tag>.

Then we use the edit-config() method to send the configuration to the running datastore.

The NETCONF reply returns whether the operation has been successful or not.

Be careful: changing the Access Point tag configuration may result in a reboot of the Access Point.

 

Now let's continue our script to edit the Access Point tags configuration based on a CSV file. In the previous section, we have exported the Access Point tags into a CSV file. We can similarly make a script that imports the CSV file that is named "ap-config.csv" and change the tags of multiple Access Points into the 9800 wireless controller.

ap_tags = ""

 

# Import the AP tag configuration from the "ap-config.csv" file

print("Importing ap-config.csv file")

with open("ap-config.csv") as csv_file:

    csv_reader = csv.reader(csv_file, delimiter=",")

    line_count = 0

    for row in csv_reader:

        print(f"\t{row[0]}, {row[1]}, {row[2]}, {row[3]}")

        ap_tags += """

            <ap-tag>

                <ap-mac>""" + row[0] + """</ap-mac>"""

        if row[1] != "":

            ap_tags += """

                <policy-tag>""" + row[1] + """</policy-tag>"""

        if row[2] != "":

            ap_tags += """

                <site-tag>""" + row[2] + """</site-tag>"""

        if row[3] != "":

            ap_tags += """

                <rf-tag>""" + row[3] + """</rf-tag>"""

        ap_tags += """

            </ap-tag>"""

        line_count += 1

    print(f"Processed {line_count} lines.\n")

 

# Create a NETCONF template

netconf_template = """

<config>

    <ap-cfg-data xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-ap-cfg">

        <ap-tags>""" + ap_tags + """

        </ap-tags>

    </ap-cfg-data>

</config>

"""

 

# Print the NETCONF template

print("NETCONF template for editing the AP tag configuration:\n{}".format(netconf_template))

 

# Execute NETCONF edit_config() to edit the AP tag configuration

result = m.edit_config(netconf_template, target = "running")

 

# Parse the XML file

xml_doc = xml.dom.minidom.parseString(result.xml)

 

# Print the configuration

print("NETCONF response for editing the AP tag configuration:\n{}".format(xml_doc.toprettyxml()))

Before running the script, create a CSV file named "ap-config.csv" with some AP tags configuration:

00:11:22:33:44:55,Corporate-PolicyTag,,

00:aa:bb:cc:dd:ee,Corporate-PolicyTag,Sydney,HighDensity-RFtag

01:02:03:04:05:06,Corporate-PolicyTag,Brisbane,HighDensity-RFtag

Output:

Importing ap-config.csv file

        00:11:22:33:44:55, Corporate-PolicyTag, ,

        00:aa:bb:cc:dd:ee, Corporate-PolicyTag, Sydney, HighDensity-RFtag

        01:02:03:04:05:06, Corporate-PolicyTag, Brisbane, HighDensity-RFtag

Processed 3 lines.

 

NETCONF template for editing the AP tag configuration:

 

    <config>

        <ap-cfg-data xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-ap-cfg">

            <ap-tags>

                <ap-tag>

                    <ap-mac>00:11:22:33:44:55</ap-mac>

                    <policy-tag>Corporate-PolicyTag</policy-tag>

                </ap-tag>

                <ap-tag>

                    <ap-mac>00:aa:bb:cc:dd:ee</ap-mac>

                    <policy-tag>Corporate-PolicyTag</policy-tag>

                    <site-tag>Sydney</site-tag>

                    <rf-tag>HighDensity-RFtag</rf-tag>

                </ap-tag>

                <ap-tag>

                    <ap-mac>01:02:03:04:05:06</ap-mac>

                    <policy-tag>Corporate-PolicyTag</policy-tag>

                    <site-tag>Brisbane</site-tag>

                    <rf-tag>HighDensity-RFtag</rf-tag>

                </ap-tag>

            </ap-tags>

        </ap-cfg-data>

    </config>

 

NETCONF response for editing the AP tag configuration:

<?xml version="1.0" ?>

<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="urn:uuid:3ea1dfb2-1eb3-48e1-a5ce-bdb503ac522f">

        <ok/>

</rpc-reply>

In this script, we use the csv library to open a CSV file that contains the requested Access Point tags configuration.

Each line is parsed to build the XML template. We create the <ap-mac> tag, and then add the <policy-tag>, <site-tag> and <rf-tag> if they exist in the CSV row.

Finally we use the edit-config() method to send the configuration to the NETCONF server. ANd the RPC Request is successfully acknowledged.

Saving the configuration and closing the NETCONF session

After editing the running configuration, we must save the configuration by sending a "write memory" RPC request to the Catalyst 9800:

from ncclient import xml_

 

# Build XML Payload for the RPC

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

 

# Send the RPC to the device

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

 

# Parse the XML file

xml_doc = xml.dom.minidom.parseString(result.xml)

 

print("NETCONF save config response:\n{}\n".format(xml_doc.toprettyxml()))

 

# Close the session

m.close_session()

Output:

NETCONF Save config result:

<?xml version="1.0" ?>

<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="urn:uuid:1d39bb24-d467-4d2a-8272-bb3ea8dbc058">

        <result xmlns="http://cisco.com/yang/cisco-ia">Save running-config successful</result>

</rpc-reply>

The NETCONF Remote Procedure Call (RPC) save-config from the cisco-ia YANG data model is sent using the NETCONF dispatch() method to save the running configuration to the persistent startup configuration.

At the end we use the close_session() method to properly terminate the NETCONF session.

 

In summary, we introduced Python and NETCONF to retrieve operational data and to perform basic configuration on Cisco IOS-XE. Using a programming language helps to break down the network configuration into individual tasks, and to think about different scenarios or conditions that would impact the operation and change the output. Improving the code took (and will take) actually more time than building a first release that was working.

Note: exporting the Access Point tag configuration and importing a CSV file of Access Point tags may not be the most useful example. It takes only two clicks to do so in the 9800 wireless controller GUI!

 

The whole script can be downloaded at https://github.com/dot11zen/9800-NETCONF-script.

Additional Resources

IETF RFC 6241 Network Configuration Protocol (NETCONF):

https://tools.ietf.org/html/rfc6241

IETF RFC 6020 YANG - A Data Modeling Language for the Network Configuration Protocol (NETCONF):

https://tools.ietf.org/html/rfc6020

IETF RFC 7950 The YANG 1.1 Data Modeling Language:

https://tools.ietf.org/html/rfc7950

Pyang documentation wiki:

https://github.com/mbj4668/pyang/wiki/Documentation

YANG data models GitHub repository:

https://github.com/YangModels/yang

Cisco Catalyst 9800 Series Wireless Controller Programmability Guide:

https://www.cisco.com/c/en/us/td/docs/wireless/controller/9800/programmability-guide/b_c9800_programmability_cg.html

Cisco DevNet Learning lab - Introducing YANG Data Modeling for the Network:

https://developer.cisco.com/learning/lab/intro-yang/step/1

Cisco DevNet Learning lab - Exploring IOS XE YANG Data Models with NETCONF:

https://developer.cisco.com/learning/lab/intro-netconf/step/1

Cisco DevNet Learning lab - Telemetry Streaming with Catalyst 9800 WLC:

https://developer.cisco.com/learning/lab/wireless-01-9800-telemetry-streaming/step/1

Cisco DevNet video course - Learn network programmability basics:

https://developer.cisco.com/video/net-prog-basics

Configure NETCONF/YANG and Validate Example for Cisco IOS XE 16.x Platforms:

https://www.cisco.com/c/en/us/support/docs/storage-networking/management/200933-YANG-NETCONF-Configuration-Validation.html

NETCONF /YANG with a 9800 Controller blog article series:

https://thewlan.com.au/2020/07/15/netconf-yang-with-a-9800-controller

Configuring NETCONF: Cisco C9800 WLC blog article:

https://rowelldionicio.com/configuring-netconf-cisco-c9800-wlc

Monitoring IOS-XE device with Netconf API + Grafana + InfluxDB blog article:

https://lihaifeng.net/?p=821