Remote configuration via NETCONF

Virtual Service Router can be configured remotely thanks to the NETCONF protocol.

XML request format

NETCONF uses XML requests to communicate with Virtual Service Router. The easiest way to write those requests is to use the CLI.

Configuration requests

To write a configuration request, generate the wanted configuration with the CLI, and call show config xml absolute:

vsr> edit running
vsr running config# / system hostname myhostname
vsr running config# show / system hostname myhostname
vsr running config# show config xml absolute system hostname
<config xmlns="urn:6wind:vrouter">
  <system xmlns="urn:6wind:vrouter/system">
    <hostname>myhostname</hostname>
  </system>
</config>

This result of the last command must be encapsulated in the NETCONF configuration tag, like this:

<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <config xmlns="urn:6wind:vrouter">
    <system xmlns="urn:6wind:vrouter/system">
      <hostname>myhostname</hostname>
    </system>
  </config>
</config>

State requests

To write a state request, retrieve the desired state with the CLI, and call show state xml absolute:

vsr> show state xml absolute system hostname
<state xmlns="urn:6wind:vrouter">
  <system xmlns="urn:6wind:vrouter/system">
    <hostname>vsr</hostname>
  </system>
</state>

Then, generate a filter by removing the leaf value, like this:

<state xmlns="urn:6wind:vrouter">
  <system xmlns="urn:6wind:vrouter/system">
    <hostname></hostname>
  </system>
</state>

RPC requests

To write a cmd, flush, or show rpc request, use the dry-run option in the CLI:

vsr> show dry-run xml product version
<show-product xmlns="urn:6wind:vrouter/system">
  <version/>
</show-product>

Note

Some rpcs are long to run, and are designed to be used by the CLI. They will return other rpcs that should be called, like this:

<?xml version='1.0' encoding='UTF-8'?>
  <nc:rpc-reply xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="urn:uuid:31d28770-078b-494e-90b5-e2ba9d5042d1">
    <status-rpc xmlns="urn:6wind:vrouter/bgp"><get-command-status xmlns="urn:6wind:vrouter/commands"><uid>26</uid></get-command-status></status-rpc>
    <refresh-rpc xmlns="urn:6wind:vrouter/bgp"><refresh-command xmlns="urn:6wind:vrouter/commands"><uid>26</uid></refresh-command></refresh-rpc>
    <stop-rpc xmlns="urn:6wind:vrouter/bgp"><stop-command xmlns="urn:6wind:vrouter/commands"><uid>26</uid></stop-command></stop-rpc>
    <buffer xmlns="urn:6wind:vrouter/bgp"/>
</nc:rpc-reply>

The refresh-rpc must be called from time to time to keep the command alive, the stop-rpc should be called if the rpc should be stopped, and the status-rpc will accumulate the rpc output in the buffer attribute.

Those rpcs are marked with the long-cmd-status and long-cmd-output extensions in the YANG model.

We don’t recommend calling those rpcs using the NETCONF api.

Example of use

We will use ncclient as a NETCONF client. On another machine that will configure the router, install ncclient dependencies.

root@local# python3 -m pip install ncclient

Create a netconf.py file with this content. This script will use the following NETCONF operations:

  • lock, unlock

  • get

  • edit-config

  • validate

  • commit

It will configure the hostname twice, and it will check whether the system state has properly changed after each change.

#!/usr/bin/env python3
# pip install xmltodict ncclient

import json
from ncclient import manager
import time
import xmltodict

def connect(host, user, password):
    conn = manager.connect(host=host,
                           username=user,
                           password=password,
                           timeout=10,
                           hostkey_verify=False)

    state = """
<state xmlns="urn:6wind:vrouter">
  <system xmlns="urn:6wind:vrouter/system">
    <hostname></hostname>
  </system>
</state>
"""

    conf = """
<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <config xmlns="urn:6wind:vrouter">
    <system xmlns="urn:6wind:vrouter/system">
      <hostname>vsr</hostname>
    </system>
  </config>
</config>
"""

    new_hostname_conf = """
<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <config xmlns="urn:6wind:vrouter">
    <system xmlns="urn:6wind:vrouter/system">
      <hostname>myhostname</hostname>
    </system>
  </config>
</config>
"""

    conn.lock()

    get_state = conn.get(filter=('subtree', state))
    print('***************** hostname before configuration ****************')
    print(json.dumps(xmltodict.parse(get_state.data_xml), indent=2))

    print('***************** set hostname to "myhostname" ****************')
    send_config = conn.edit_config(target='running', config=new_hostname_conf, default_operation='merge')

    check_config = conn.validate()

    conn.commit()

    get_state = conn.get(filter=('subtree', state))
    print('***************** hostname is now "myhostname" ****************')
    print(json.dumps(xmltodict.parse(get_state.data_xml), indent=2))

    print('***************** revert to "vsr" ****************')
    send_config = conn.edit_config(target='running', config=conf, default_operation='merge')

    conn.commit()
    get_state = conn.get(filter=('subtree', state))
    print('***************** hostname is now "vsr" ****************')
    print(json.dumps(xmltodict.parse(get_state.data_xml), indent=2))

    conn.unlock()
    conn.close_session()

if __name__ == '__main__':
    connect('<myip>', 'root', '<rootpass>')

Update the connect line to put the router IP, and the root password of the router, and launch the script. The following output is displayed.

root@local# python3 netconf_hostname.py
***************** hostname before configuration ****************
{
  "data": {
    "@xmlns": "urn:ietf:params:xml:ns:netconf:base:1.0",
    "@xmlns:nc": "urn:ietf:params:xml:ns:netconf:base:1.0",
    "state": {
      "@xmlns": "urn:6wind:vrouter",
      "system": {
        "@xmlns": "urn:6wind:vrouter/system",
        "hostname": "vsr"
      }
    }
  }
}
***************** set hostname to "myhostname" ****************
***************** hostname is now "myhostname" ****************
{
  "data": {
    "@xmlns": "urn:ietf:params:xml:ns:netconf:base:1.0",
    "@xmlns:nc": "urn:ietf:params:xml:ns:netconf:base:1.0",
    "state": {
      "@xmlns": "urn:6wind:vrouter",
      "system": {
        "@xmlns": "urn:6wind:vrouter/system",
        "hostname": "myhostname"
      }
    }
  }
}
***************** revert to "vsr" ****************
***************** hostname is now "vsr" ****************
{
  "data": {
    "@xmlns": "urn:ietf:params:xml:ns:netconf:base:1.0",
    "@xmlns:nc": "urn:ietf:params:xml:ns:netconf:base:1.0",
    "state": {
      "@xmlns": "urn:6wind:vrouter",
      "system": {
        "@xmlns": "urn:6wind:vrouter/system",
        "hostname": "vsr"
      }
    }
  }
}

Remote subscriptions via NETCONF

Virtual Service Router can send notifications using the NETCONF protocol.

Basic example

This chapter expects that the requirements from the previous chapter are met.

We will start a script that will subscribe to NETCONF notifications, and display the notification when one is received.

Create a netconf_subscribe.py file with this content

#!/usr/bin/env python3
import json
from ncclient import manager
from ncclient.xml_ import to_ele
import xmltodict

create_subscription="""<create-subscription xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0"/>"""

def connect(host, user, password):
    conn = manager.connect(host=host,
                           username=user,
                           password=password,
                           timeout=10,
                           hostkey_verify=False)

    conn.dispatch(to_ele(create_subscription))

    print('Waiting for events')
    while True:
        notification = conn.take_notification()
        print(json.dumps(xmltodict.parse(notification.notification_xml), indent=2))

if __name__ == '__main__':
    connect('<myip>', 'root', '<rootpass>')

Update the connect line to put the router IP, and the root password of the router, and launch the script. Then, on the router, log as admin, and logout. The following output is displayed.

root@local# python3 netconf_subscribe.py
Waiting for events
{
  "notification": {
    "@xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0",
    "eventTime": "2022-10-05T14:05:10.125719825+00:00",
    "netconf-session-start": {
      "@xmlns": "urn:ietf:params:xml:ns:yang:ietf-netconf-notifications",
      "username": "admin",
      "session-id": "12"
    }
  }
}
{
  "notification": {
    "@xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0",
    "eventTime": "2022-10-05T14:05:22.267006931+00:00",
    "netconf-session-end": {
      "@xmlns": "urn:ietf:params:xml:ns:yang:ietf-netconf-notifications",
      "username": "admin",
      "session-id": "12",
      "termination-reason": "closed"
    }
  }
}

The subscription can be targeted to a particular type of subscription, using a filter. For instance, the following filter targets ietf-yang-push push updates, ietf-alarms alarm notifications, and ietf-netconf-notifications netconf-session-start, netconf-session-end and netconf-config-change.

create_subscription="""
<create-subscription xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
  <filter xmlns:netconf="urn:ietf:params:xml:ns:netconf:base:1.0" netconf:type="subtree"
       xmlns:yp="urn:ietf:params:xml:ns:yang:ietf-yang-push"
       xmlns:al="urn:ietf:params:xml:ns:yang:ietf-alarms"
       xmlns:nn="urn:ietf:params:xml:ns:yang:ietf-netconf-notifications">
    <yp:push-update/>
    <yp:push-change-update/>
    <al:alarm-notification/>
    <nn:netconf-session-start/>
    <nn:netconf-session-end/>
    <nn:netconf-config-change/>
  </filter>
</create-subscription>"""

The subscriptions for the ietf-netconf-notification and ietf-alarms can also be replayed. For instance, the following request will replay the NETCONF notifications since 2022-03-02T15:25:00+00:00.

create_subscription="""
<create-subscription xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
   <startTime>2022-03-02T15:25:00+00:00</startTime>
</create-subscription>"""

Yang push example

Virtual Service Router supports the yang push functionality (RFC 8641), which triggers notifications on a periodic basic, or on change in the NETCONF datastores.

The following script subscribes to changes on the /state/vrf/interface/physical/enabled leaf, and requests periodic updates every 30s for the /state/vrf/interface/physical/counters container.

#!/usr/bin/env python3
import json
from ncclient import manager
from ncclient.xml_ import to_ele
import xmltodict

yang_push_operational_periodic="""
 <establish-subscription xmlns="urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications">
  <datastore xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-push" xmlns:ds="urn:ietf:params:xml:ns:yang:ietf-datastores">ds:operational</datastore>
  <datastore-xpath-filter xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-push">/state/vrf/interface/physical/counters</datastore-xpath-filter>
  <periodic xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-push">
   <period>3000</period>
  </periodic>
 </establish-subscription>
"""

yang_push_operational_on_change="""
 <establish-subscription xmlns="urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications">
   <datastore xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-push" xmlns:ds="urn:ietf:params:xml:ns:yang:ietf-datastores">ds:operational</datastore>
   <datastore-xpath-filter xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-push">/state/vrf/interface/physical/enabled</datastore-xpath-filter>
   <on-change xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-push"/>
 </establish-subscription>
"""

yang_push_state="""
 <streams xmlns="urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications"/>
 <subscriptions xmlns="urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications"/>
"""

create_subscription="""
 <create-subscription xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
  <filter xmlns:netconf="urn:ietf:params:xml:ns:netconf:base:1.0" netconf:type="subtree" xmlns:yp="urn:ietf:params:xml:ns:yang:ietf-yang-push">
    <yp:push-update/>
  </filter>
 </create-subscription>
"""

def connect(host, user, password):
    with manager.connect(host=host,
                           username=user,
                           password=password,
                           timeout=10,
                           hostkey_verify=False) as conn:

        # conn.create_subscription(start_time="2020-03-02T15:25:00+00:00") does not work, use dispatch
        # See: https://github.com/ncclient/ncclient/issues/224
        conn.dispatch(to_ele(create_subscription))

        conn.dispatch(to_ele(yang_push_operational_on_change))
        conn.dispatch(to_ele(yang_push_operational_periodic))

        get_state = conn.get(filter=('subtree', yang_push_state))
        print('*********** Dump subscribed notifications ***********')
        print(json.dumps(xmltodict.parse(get_state.data_xml), indent=2))

        print('*****************************************************')
        print('Waiting for events')
        while True:
            notification = conn.take_notification()
            print(json.dumps(xmltodict.parse(notification.notification_xml), indent=2))

if __name__ == '__main__':
    connect('<myip>', 'root', '<rootpass>')

The following output is displayed:

*********** Dump subscribed notifications ***********
{
  "data": {
    "@xmlns": "urn:ietf:params:xml:ns:netconf:base:1.0",
    "subscriptions": {
      "@xmlns": "urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications",
      "subscription": [
        {
          "id": "19",
          "receivers": {
            "receiver": {
              "name": "NETCONF session 14",
              "sent-event-records": "1",
              "excluded-event-records": "0",
              "state": "active"
            }
          }
        },
        {
          "id": "20",
          "receivers": {
            "receiver": {
              "name": "NETCONF session 14",
              "sent-event-records": "1",
              "excluded-event-records": "0",
              "state": "active"
            }
          }
        }
      ]
    }
  }
}
*****************************************************
Waiting for events
{
  "notification": {
    "@xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0",
    "eventTime": "2022-10-06T06:44:20.110765456+00:00",
    "push-update": {
      "@xmlns": "urn:ietf:params:xml:ns:yang:ietf-yang-push",
      "id": "19",
      "datastore-contents": {
        "state": {
          "@xmlns": "urn:6wind:vrouter",
          "vrf": {
            "name": "main",
            "interface": {
              "@xmlns": "urn:6wind:vrouter/interface",
              "physical": {
                "name": "ens3",
                "enabled": "true"
              }
            }
          }
        }
      }
    }
  }
}
{
  "notification": {
    "@xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0",
    "eventTime": "2022-10-06T06:44:20.258909358+00:00",
    "push-update": {
      "@xmlns": "urn:ietf:params:xml:ns:yang:ietf-yang-push",
      "id": "20",
      "datastore-contents": {
        "state": {
          "@xmlns": "urn:6wind:vrouter",
          "vrf": {
            "name": "main",
            "interface": {
              "@xmlns": "urn:6wind:vrouter/interface",
              "physical": {
                "name": "ens3",
                "counters": {
                  "in-octets": "30923",
                  "in-unicast-pkts": "175",
                  "in-discards": "0",
                  "in-errors": "0",
                  "out-octets": "6943",
                  "out-unicast-pkts": "55",
                  "out-discards": "0",
                  "out-errors": "0"
                }
              }
            }
          }
        }
      }
    }
  }
}

The on-change functionality is not available for all the leaves. The ones that support it are marked as “(pushed)” in the command-reference.

Alarm example

Virtual Service Router supports the alarm functionality (RFC 8632), which triggers notifications when an alarm is triggered.

The following script subscribes to all the alarms on the system.

#!/usr/bin/env python3
import json
from ncclient import manager
from ncclient.xml_ import to_ele
import xmltodict

create_subscription="""
 <create-subscription xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
  <filter xmlns:netconf="urn:ietf:params:xml:ns:netconf:base:1.0" netconf:type="subtree"
       xmlns:al="urn:ietf:params:xml:ns:yang:ietf-alarms">
    <al:alarm-notification/>
  </filter>
 </create-subscription>
"""

def connect(host, user, password):
    with manager.connect(host=host,
                           username=user,
                           password=password,
                           timeout=10,
                           hostkey_verify=False) as conn:

        # conn.create_subscription(start_time="2020-03-02T15:25:00+00:00") does not work, use dispatch
        # See: https://github.com/ncclient/ncclient/issues/224
        conn.dispatch(to_ele(create_subscription))

        print('Waiting for events')
        while True:
            notification = conn.take_notification()
            print(json.dumps(xmltodict.parse(notification.notification_xml), indent=2))

if __name__ == '__main__':
    connect('<myip>', 'root', '<rootpass>')

The following output is displayed in case the ntp service is configured, but a crash occurs:

{
  "notification": {
    "@xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0",
    "eventTime": "2022-10-06T06:55:14.351217458+00:00",
    "alarm-notification": {
      "@xmlns": "urn:ietf:params:xml:ns:yang:ietf-alarms",
      "resource": "/vrf[name=\"main\"]/ntp/enabled",
      "alarm-type-id": {
        "@xmlns:vrouter-alarm": "urn:6wind:vrouter/alarm",
        "#text": "vrouter-alarm:service-alarm"
      },
      "alarm-type-qualifier": "ntp",
      "time": "2022-10-06T06:55:14.350704+00:00",
      "perceived-severity": "major",
      "alarm-text": "The ntp service is down when configured up in vrf main."
    }
  }
}