Netconf/YANG

The Netconf protocol and YANG data modeling language together provide a standardized management interface to access a device’s configuration and operational data programmatically. This feature guide first explains how to set up RADKit Service to access devices over the Netconf protocol. Then it explains how to query nodes from the YANG data model of a device from the Client and how to process the results.

Warning

The Netconf feature as it stands today suffers from a number of limitations. It should be considered experimental until it gets reworked in a future RADKit release.

Service configuration

Netconf configuration

To enable Netconf support, when either adding a new device, or editing an existing one, check the Netconf box under Available Management Protocols. Subsequently, the Netconf configuration section will appear:

../_images/service_netconf_device.png

The Username and Password are user credentials for Netconf access to the device, and Port is the Netconf port, which is usually the standard port of 830.

Client operation

Devices Netconf status

When the Client loads the service inventory, it gets an internal attribute for each device called netconf_config which is set to either True or False, depending on whether the Service has Netconf credentials configured for that device:

>>> csr1.attributes.internal
<radkit_client.sync.device._AttributesDict object at 0x1106135e0>

key                  value
-------------------  -------------
description          csr1
device_type          IOS_XE
forwarded_tcp_ports  443
host                 172.18.124.37
http_config          False
netconf_config       True
snmp_version         None
swagger_config       False
terminal_config      True

It is easy to select all devices that support Netconf by using the following filter:

>>> nc = service.inventory.filter("netconf_config", "True")

Updating the Netconf API

Every Device object has a .netconf attribute which represents the Netconf API for that device. By default this radkit_client.sync.netconf.netconf_api.NetconfAPI object has a status of UNKNOWN:

>>> csr1.netconf
[UNKNOWN] <radkit_client.sync.netconf.netconf_api.SingleDeviceNetconfAPI object at 0x110a90ac0>

For a list of all available statuses for the Netconf API, see radkit_client.sync.netconf.netconf_api.NetconfAPIStatus.

If the device has Netconf credentials configured on the Service side, its capabilities can be retrieved from the Client using Device.update_netconf():

>>> csr1.update_netconf().wait()
[SUCCESS] <radkit_client.sync.request.TransformedFillerRequest object at 0xffff68ed2040>

--------------  ---------------------------------
sent_timestamp  2022-08-09 22:05:37
request_type    Netconf capabilities
identity        radkit-user@example.com
service_serial  xxxx-yyyy-zzzz
result          <radkit_client.async_.device.AsyncDeviceCapabilitiesUpdate object at 0xffff5b...
forwarder       wss://prod.radkit-cloud.cisco.com/forwarder-1/
--------------  ---------------------------------

If the retrieval is successful, the Netconf API status changes to AVAILABLE:

>>> csr1.netconf
[AVAILABLE] <radkit_client.sync.netconf.netconf_api.SingleDeviceNetconfAPI object at 0x110c009d0>

We can also display the Netconf capabilities hash with:

>>> csr1.netconf.capabilities
<radkit_client.sync.netconf.netconf_api.NetconfCapabilities object at 0x1111727c0>

-----------  ----------------------------------------------------------------
hash         b0f602db8ffb3d18f023f660a265c0a217813ad30a703e9fc6ba0a4065610623
#namespaces  529
-----------  ----------------------------------------------------------------

This Netconf capabilities hash identifies the exact set of YANG models available for that device, as well as its namespace configuration. Generally that hash will be the same for all devices of the same type and running the exact same software. We will come back to this hash in Netconf on multiple devices.

Querying a YANG node

To query a certain node in the data model, call its get() method. Please note that the first-level nodes representing the YANG modules cannot be queried:

>>> csr1.netconf.yang['ietf-interfaces'].get()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<frozen radkit_client.sync.netconf.yang_model>", line 286, in get
File "<frozen radkit_client.sync.netconf.yang_model>", line 299, in _validate_xpath
ValueError: A namespace node cannot be queried (go 1 level deeper)

A namespace node cannot be queried (go 1 level deeper)

Instead, you have to go at least one level deeper, for example:

>>> req = csr1.netconf.yang['ietf-interfaces']['interfaces'].get().wait()

>>> type(req)
<class 'radkit_client.sync.request.TransformedFillerRequest'>

>>> pprint(req.result.yang)
{'interfaces': {'@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-interfaces',
        'interface': {'GigabitEthernet1': {'enabled': 'true',
                'ipv4': {'@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-ip',
                        'address': {'ip': '172.18.124.37',
                                'netmask': '255.255.255.0'}},
                'ipv6': {'@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-ip'},
                'name': 'GigabitEthernet1',
                'type': {'#text': 'ianaift:ethernetCsmacd',
                        '@xmlns:ianaift': 'urn:ietf:params:xml:ns:yang:iana-if-type'}},
...

The .get() operation returns an instance of TransformedFillerRequest. That object represents the Netconf request sent to the Service to run the query on the device:

>>> req
[SUCCESS] <radkit_client.sync.request.TransformedFillerRequest object at 0x11536e2e0>

--------------  --------------------------------------------------------------------------------
sent_timestamp  2022-08-10 11:16:37
request_type    Netconf XPath query
identity        radkit-user@example.com
service_serial  xxxx-yyyy-zzzz
result          <radkit_client.sync.netconf.xpath_results.GetSingleXPathResult object at 0x1154f0fa0>
forwarder       wss://prod.radkit-cloud.cisco.com/forwarder-1/
--------------  --------------------------------------------------------------------------------

Querying a specific XPath

Another way to run a Netconf query is to manually specify the XPath without going through the YANG model tree. This is done using the get_xpaths() method of the device’s Netconf API, which accepts either a single XPath as a string, or multiple XPaths as a list of strings:

  • Single XPath:

    req = csr1.netconf.get_xpaths("/openconfig-system:system/state/current-datetime")
    
  • Multiple Xpaths:

    req = csr1.netconf.get_xpaths([
        "/openconfig-system:system/state/current-datetime",
        "/ietf-interfaces:interfaces/interface[name='Loopback0']/ipv4"
    ]).wait()
    

Note how the second XPath query includes a predicate to filter by interface name, as in our example we are only interested in the Loopback0 interface:

"/ietf-interfaces:interfaces/interface[name='Loopback0']/ipv4"

Note

get_xpaths() automatically populates the list of required namespaces based on the XPath(s) being queried and the device capabilities, if known. In case the device capabilities are not known, or if any namespaces are referenced from deep inside the XPath, you may need to provide additional namespaces as the second argument. Please see the Client API reference for details.

Response processing

When the Netconf response comes back, the RADKit Client performs some post-processing on the data to make it easier to consume; the main thing it does is turn list of dicts into dicts of dicts.

For example, this query retrieves information about IPv4 configuration on loopback interfaces:

>>> req = csr1.netconf.get_xpaths("/ietf-interfaces:interfaces/interface[starts-with(name, 'Loopback')]/ipv4").wait()

The raw response from the device is stored under req.result.raw. It contains a list of dicts, one for each loopback:

>>> pprint(req.result.raw)
{'interfaces': {'@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-interfaces',
                'interface': [{'ipv4': {'@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-ip',
                                        'address': {'ip': '1.1.1.1',
                                                    'netmask': '255.255.255.255'}},
                            'name': 'Loopback0'},
                            {'ipv4': {'@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-ip',
                                        'address': {'ip': '3.3.3.3',
                                                    'netmask': '255.255.255.255'}},
                            'name': 'Loopback3'}]}}

This raw response is not very convenient to navigate, as there is no way to directly access the data for Loopback0 without first looking up the corresponding index in the list (0, in this case):

>>> req.result.raw['interfaces']['interface'][0]['ipv4']['address']
{'ip': '1.1.1.1', 'netmask': '255.255.255.255'}

To make things easier, the RADKit Client goes through the list and tries to guess which field in the dict can be used as a key to turn the list into a dict. In this case, name is picked and the list is turned into a dict indexed by interface name. This post-processed result is stored under req.result.yang:

>>> pprint(req.result.yang)
{'interfaces': {'@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-interfaces',
                'interface': {'Loopback0': {'ipv4': {'@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-ip',
                                                    'address': {'ip': '1.1.1.1',
                                                                'netmask': '255.255.255.255'}},
                                            'name': 'Loopback0'},
                            'Loopback3': {'ipv4': {'@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-ip',
                                                    'address': {'ip': '3.3.3.3',
                                                                'netmask': '255.255.255.255'}},
                                            'name': 'Loopback3'}}}}

The data for Loopback0 can now be accessed without the need for a lookup:

>>> req.result.yang['interfaces']['interface']['Loopback0']['ipv4']['address']
{'ip': '1.1.1.1', 'netmask': '255.255.255.255'}

Note

There may be situations where this logic fails and no candidate key is found. In those rare situations, the post-processed data will still contain a list. Conversely, if multiple candidate keys are found, if there is one named name it will be preferred; else the first candidate key that was found will be selected. If in your particular case the wrong key is selected, you can always fall back on .raw.

Netconf on multiple devices

When dealing with multiple devices in a DeviceDict, Netconf can only be used on all devices in the dict if their capabilities are exactly the same:

  • all devices in the DeviceDict must support Netconf;

  • their capabilities must have been downloaded by the Client; and

  • their capabilities hashes must all be identical.

The DeviceDict object has a Netconf API of its own, which initially has the status HETEROGENOUS i.e., the Netconf capabilities of the devices it contains are either missing or incompatible.

>>> csrs
<radkit_client.sync.device.DeviceDict object at 0x10fc972b0>

name    host           device_type    Terminal    Netconf    Swagger    HTTP    description    failed
------  -------------  -------------  ----------  ---------  ---------  ------  -------------  --------
csr1    172.18.124.37  IOS_XE         True        True       False      False   csr1           False
csr2    172.18.124.38  IOS_XE         True        True       False      False                  False

>>> csrs.netconf
[HETEROGENOUS] <radkit_client.sync.netconf.netconf_api.NetconfAPI object at 0x10faa54c0>

--------  ------------
status    HETEROGENOUS
#devices  2
--------  ------------

The homogeneity of the dict’s Netconf API is re-evaluated every time the Netconf capabilities of any of the devices it contains are updated, and every time a device is added to or removed from the dict:

>>> csrs.update_netconf().wait()
[SUCCESS] FillerRequest(status='SUCCESS', rpc_name='get-netconf-capabilities')

--------------  ---------------------------------
sent_timestamp  2022-08-11 09:53:57
request_type    Netconf capabilities
identity        radkit-user@example.com
service_serial  xxxx-yyyy-zzzz
result          {'csr1': <radkit_client.async_.device.AsyncDeviceCapabilitiesUpdate o...
forwarder       wss://prod.radkit-cloud.cisco.com/forwarder-4/
--------------  ---------------------------------

The result attribute of this request contains more detailed information about the capabilities update:

>>> csrs.update_netconf().wait().result
>>> s.inventory.update_netconf().wait().result
[SUCCESS] <radkit_client.sync.device.DeviceDictCapabilitiesUpdate object at 0xffff68f02d00>
device_name    status    result
-------------  --------  --------------------------------------------------
csr1           SUCCESS   Netconf capabilities loaded.
csr2           SUCCESS   Netconf capabilities loaded.

In this case after the update, the Netconf capabilities of the two devices in the dict are identical, so the status of the dict’s Netconf API object changes to AVAILABLE:

>>> csrs.netconf
[AVAILABLE] <radkit_client.sync.netconf.netconf_api.NetconfAPI object at 0x1148600a0>

--------  ---------
status    AVAILABLE
#devices  2
--------  ---------

>>> csrs.netconf.capabilities.hash
'b0f602db8ffb3d18f023f660a265c0a217813ad30a703e9fc6ba0a4065610623'

It is now possible to access the YANG model tree under the dict’s Netconf API object and query multiple devices in a single request:

>>> req = csrs.netconf.yang['openconfig-system']['system']['state']['current-datetime'].get().wait()

>>> pprint(req.result['csr1'].yang)
{'system': {'@xmlns': 'http://openconfig.net/yang/system',
            'state': {'current-datetime': '2022-08-11T14:01:56.661Z+00:00'}}}

>>> pprint(req.result['csr2'].yang)
{'system': {'@xmlns': 'http://openconfig.net/yang/system',
            'state': {'current-datetime': '2022-08-11T14:01:52.107Z+00:00'}}}

Querying heterogenous sets

If a set of devices is heterogenous but you know that they all support a given XPath, it is possible to query those devices manually as a set by using the get_xpaths() method of the DeviceDict’s Netconf API.

In this case, you will have to specify the namespace(s) manually, as those cannot be automatically populated based on the device capabilities (see Querying a specific XPath earlier in this document for details).