565 lines
20 KiB
ReStructuredText
565 lines
20 KiB
ReStructuredText
Operational Data, RPCs and Notifications
|
||
========================================
|
||
|
||
.. contents:: Table of contents
|
||
:local:
|
||
:backlinks: entry
|
||
:depth: 1
|
||
|
||
Operational data
|
||
~~~~~~~~~~~~~~~~
|
||
|
||
Writing API-agnostic code for YANG-modeled operational data is
|
||
challenging. Sysrepo, for instance, has completely different API to
|
||
fetch operational data. So how can we write API-agnostic callbacks
|
||
that can be used by both the Sysrepo plugin, and any other northbound
|
||
client that might be written in the future?
|
||
|
||
As an additional requirement, the callbacks must be designed in a way
|
||
that makes in-place XPath filtering possible. As an example, a
|
||
management client might want to retrieve only a subset of a large YANG
|
||
list (e.g. a BGP table), and for optimal performance it should be
|
||
possible to filter out the unwanted elements locally in the managed
|
||
devices instead of returning all elements and performing the filtering
|
||
on the management application.
|
||
|
||
To meet all these requirements, the four callbacks below were introduced
|
||
in the northbound architecture:
|
||
|
||
.. code:: c
|
||
|
||
/*
|
||
* Operational data callback.
|
||
*
|
||
* The callback function should return the value of a specific leaf or
|
||
* inform if a typeless value (presence containers or leafs of type
|
||
* empty) exists or not.
|
||
*
|
||
* xpath
|
||
* YANG data path of the data we want to get
|
||
*
|
||
* list_entry
|
||
* pointer to list entry
|
||
*
|
||
* Returns:
|
||
* pointer to newly created yang_data structure, or NULL to indicate
|
||
* the absence of data
|
||
*/
|
||
struct yang_data *(*get_elem)(const char *xpath, void *list_entry);
|
||
|
||
/*
|
||
* Operational data callback for YANG lists.
|
||
*
|
||
* The callback function should return the next entry in the list. The
|
||
* 'list_entry' parameter will be NULL on the first invocation.
|
||
*
|
||
* list_entry
|
||
* pointer to a list entry
|
||
*
|
||
* Returns:
|
||
* pointer to the next entry in the list, or NULL to signal that the
|
||
* end of the list was reached
|
||
*/
|
||
void *(*get_next)(void *list_entry);
|
||
|
||
/*
|
||
* Operational data callback for YANG lists.
|
||
*
|
||
* The callback function should fill the 'keys' parameter based on the
|
||
* given list_entry.
|
||
*
|
||
* list_entry
|
||
* pointer to a list entry
|
||
*
|
||
* keys
|
||
* structure to be filled based on the attributes of the provided
|
||
* list entry
|
||
*
|
||
* Returns:
|
||
* NB_OK on success, NB_ERR otherwise
|
||
*/
|
||
int (*get_keys)(void *list_entry, struct yang_list_keys *keys);
|
||
|
||
/*
|
||
* Operational data callback for YANG lists.
|
||
*
|
||
* The callback function should return a list entry based on the list
|
||
* keys given as a parameter.
|
||
*
|
||
* keys
|
||
* structure containing the keys of the list entry
|
||
*
|
||
* Returns:
|
||
* a pointer to the list entry if found, or NULL if not found
|
||
*/
|
||
void *(*lookup_entry)(struct yang_list_keys *keys);
|
||
|
||
These callbacks were designed to provide maximum flexibility. Each
|
||
callback does one and only one task, they are indivisible primitives
|
||
that can be combined in several different ways to iterate over operational
|
||
data. The extra flexibility certainly has a performance cost, but it’s the
|
||
price to pay if we want to expose FRR operational data using several
|
||
different management interfaces (e.g. Sysrepo+Netopeer2). In the
|
||
future it might be possible to introduce optional callbacks that do
|
||
things like returning multiple objects at once. They would provide
|
||
enhanced performance when iterating over large lists, but their use
|
||
would be limited by the northbound plugins that can be integrated with
|
||
them.
|
||
|
||
The [[Plugins - Writing Your Own]] page explains how the northbound
|
||
plugins can fetch operational data using the aforementioned northbound
|
||
callbacks, and how in-place XPath filtering can be implemented.
|
||
|
||
Example
|
||
^^^^^^^
|
||
|
||
Now let’s move to an example to show how these callbacks are implemented
|
||
in practice. The following YANG container is part of the *ietf-rip*
|
||
module and contains operational data about RIP neighbors:
|
||
|
||
.. code:: yang
|
||
|
||
container neighbors {
|
||
description
|
||
"Neighbor information.";
|
||
list neighbor {
|
||
key "address";
|
||
description
|
||
"A RIP neighbor.";
|
||
leaf address {
|
||
type inet:ipv4-address;
|
||
description
|
||
"IP address that a RIP neighbor is using as its
|
||
source address.";
|
||
}
|
||
leaf last-update {
|
||
type yang:date-and-time;
|
||
description
|
||
"The time when the most recent RIP update was
|
||
received from this neighbor.";
|
||
}
|
||
leaf bad-packets-rcvd {
|
||
type yang:counter32;
|
||
description
|
||
"The number of RIP invalid packets received from
|
||
this neighbor which were subsequently discarded
|
||
for any reason (e.g. a version 0 packet, or an
|
||
unknown command type).";
|
||
}
|
||
leaf bad-routes-rcvd {
|
||
type yang:counter32;
|
||
description
|
||
"The number of routes received from this neighbor,
|
||
in valid RIP packets, which were ignored for any
|
||
reason (e.g. unknown address family, or invalid
|
||
metric).";
|
||
}
|
||
}
|
||
}
|
||
|
||
We know that this is operational data because the ``neighbors``
|
||
container is within the ``state`` container, which has the
|
||
``config false;`` property (which is applied recursively).
|
||
|
||
As expected, the ``gen_northbound_callbacks`` tool also generates
|
||
skeleton callbacks for nodes that represent operational data:
|
||
|
||
.. code:: c
|
||
|
||
{
|
||
.xpath = "/frr-ripd:ripd/state/neighbors/neighbor",
|
||
.cbs.get_next = ripd_state_neighbors_neighbor_get_next,
|
||
.cbs.get_keys = ripd_state_neighbors_neighbor_get_keys,
|
||
.cbs.lookup_entry = ripd_state_neighbors_neighbor_lookup_entry,
|
||
},
|
||
{
|
||
.xpath = "/frr-ripd:ripd/state/neighbors/neighbor/address",
|
||
.cbs.get_elem = ripd_state_neighbors_neighbor_address_get_elem,
|
||
},
|
||
{
|
||
.xpath = "/frr-ripd:ripd/state/neighbors/neighbor/last-update",
|
||
.cbs.get_elem = ripd_state_neighbors_neighbor_last_update_get_elem,
|
||
},
|
||
{
|
||
.xpath = "/frr-ripd:ripd/state/neighbors/neighbor/bad-packets-rcvd",
|
||
.cbs.get_elem = ripd_state_neighbors_neighbor_bad_packets_rcvd_get_elem,
|
||
},
|
||
{
|
||
.xpath = "/frr-ripd:ripd/state/neighbors/neighbor/bad-routes-rcvd",
|
||
.cbs.get_elem = ripd_state_neighbors_neighbor_bad_routes_rcvd_get_elem,
|
||
},
|
||
|
||
The ``/frr-ripd:ripd/state/neighbors/neighbor`` list within the
|
||
``neighbors`` container has three different callbacks that need to be
|
||
implemented. Let’s start with the first one, the ``get_next`` callback:
|
||
|
||
.. code:: c
|
||
|
||
static void *ripd_state_neighbors_neighbor_get_next(void *list_entry)
|
||
{
|
||
struct listnode *node;
|
||
|
||
if (list_entry == NULL)
|
||
node = listhead(peer_list);
|
||
else
|
||
node = listnextnode((struct listnode *)list_entry);
|
||
|
||
return node;
|
||
}
|
||
|
||
Given a list entry, the job of this callback is to find the next element
|
||
from the list. When the ``list_entry`` parameter is NULL, then the first
|
||
element of the list should be returned.
|
||
|
||
*ripd* uses the ``rip_peer`` structure to represent RIP neighbors, and
|
||
the ``peer_list`` global variable (linked list) is used to store all RIP
|
||
neighbors.
|
||
|
||
In order to be able to iterate over the list of RIP neighbors, the
|
||
callback returns a ``listnode`` variable instead of a ``rip_peer``
|
||
variable. The ``listnextnode`` macro can then be used to find the next
|
||
element from the linked list.
|
||
|
||
Now the second callback, ``get_keys``:
|
||
|
||
.. code:: c
|
||
|
||
static int ripd_state_neighbors_neighbor_get_keys(void *list_entry,
|
||
struct yang_list_keys *keys)
|
||
{
|
||
struct listnode *node = list_entry;
|
||
struct rip_peer *peer = listgetdata(node);
|
||
|
||
keys->num = 1;
|
||
(void)inet_ntop(AF_INET, &peer->addr, keys->key[0].value,
|
||
sizeof(keys->key[0].value));
|
||
|
||
return NB_OK;
|
||
}
|
||
|
||
This one is easy. First, we obtain the RIP neighbor from the
|
||
``listnode`` structure. Then, we fill the ``keys`` parameter according
|
||
to the attributes of the RIP neighbor. In this case, the ``neighbor``
|
||
YANG list has only one key: the neighbor IP address. We then use the
|
||
``inet_ntop()`` function to transform this binary IP address into a
|
||
string (the lingua franca of the FRR northbound).
|
||
|
||
The last callback for the ``neighbor`` YANG list is the ``lookup_entry``
|
||
callback:
|
||
|
||
.. code:: c
|
||
|
||
static void *
|
||
ripd_state_neighbors_neighbor_lookup_entry(struct yang_list_keys *keys)
|
||
{
|
||
struct in_addr address;
|
||
|
||
yang_str2ipv4(keys->key[0].value, &address);
|
||
|
||
return rip_peer_lookup(&address);
|
||
}
|
||
|
||
This callback is the counterpart of the ``get_keys`` callback: given an
|
||
array of list keys, the associated list entry should be returned. The
|
||
``yang_str2ipv4()`` function is used to convert the list key (an IP
|
||
address) from a string to an ``in_addr`` structure. Then the
|
||
``rip_peer_lookup()`` function is used to find the list entry.
|
||
|
||
Finally, each YANG leaf inside the ``neighbor`` list has its associated
|
||
``get_elem`` callback:
|
||
|
||
.. code:: c
|
||
|
||
/*
|
||
* XPath: /frr-ripd:ripd/state/neighbors/neighbor/address
|
||
*/
|
||
static struct yang_data *
|
||
ripd_state_neighbors_neighbor_address_get_elem(const char *xpath,
|
||
void *list_entry)
|
||
{
|
||
struct rip_peer *peer = list_entry;
|
||
|
||
return yang_data_new_ipv4(xpath, &peer->addr);
|
||
}
|
||
|
||
/*
|
||
* XPath: /frr-ripd:ripd/state/neighbors/neighbor/last-update
|
||
*/
|
||
static struct yang_data *
|
||
ripd_state_neighbors_neighbor_last_update_get_elem(const char *xpath,
|
||
void *list_entry)
|
||
{
|
||
/* TODO: yang:date-and-time is tricky */
|
||
return NULL;
|
||
}
|
||
|
||
/*
|
||
* XPath: /frr-ripd:ripd/state/neighbors/neighbor/bad-packets-rcvd
|
||
*/
|
||
static struct yang_data *
|
||
ripd_state_neighbors_neighbor_bad_packets_rcvd_get_elem(const char *xpath,
|
||
void *list_entry)
|
||
{
|
||
struct rip_peer *peer = list_entry;
|
||
|
||
return yang_data_new_uint32(xpath, peer->recv_badpackets);
|
||
}
|
||
|
||
/*
|
||
* XPath: /frr-ripd:ripd/state/neighbors/neighbor/bad-routes-rcvd
|
||
*/
|
||
static struct yang_data *
|
||
ripd_state_neighbors_neighbor_bad_routes_rcvd_get_elem(const char *xpath,
|
||
void *list_entry)
|
||
{
|
||
struct rip_peer *peer = list_entry;
|
||
|
||
return yang_data_new_uint32(xpath, peer->recv_badroutes);
|
||
}
|
||
|
||
These callbacks receive the list entry as parameter and return the
|
||
corresponding data using the ``yang_data_new_*()`` wrapper functions.
|
||
Not much to explain here.
|
||
|
||
Iterating over operational data without blocking the main pthread
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
One of the problems we have in FRR is that some “show” commands in the
|
||
CLI can take too long, potentially long enough to the point of
|
||
triggering some protocol timeouts and bringing sessions down.
|
||
|
||
To avoid this kind of problem, northbound clients are encouraged to do
|
||
one of the following:
|
||
|
||
* Create a separate pthread for handling requests to fetch operational data.
|
||
|
||
* Iterate over YANG lists and leaf-lists asynchronously, returning a maximum
|
||
number of elements per time instead of returning all elements in one shot.
|
||
|
||
In order to handle both cases correctly, the ``get_next`` callbacks need
|
||
to use locks to prevent the YANG lists from being modified while they
|
||
are being iterated over. If that is not done, the list entry returned by
|
||
this callback can become a dangling pointer when used in another
|
||
callback.
|
||
|
||
Currently the Sysrepo plugin runs only in the main pthread. The plan in the
|
||
short-term is to introduce a separate pthread only for handling operational
|
||
data, and use the main pthread only for handling configuration changes,
|
||
RPCs and notifications.
|
||
|
||
RPCs and Actions
|
||
~~~~~~~~~~~~~~~~
|
||
|
||
The FRR northbound supports YANG RPCs and Actions through the ``rpc()``
|
||
callback, which is documented as follows in the *lib/northbound.h* file:
|
||
|
||
.. code:: c
|
||
|
||
/*
|
||
* RPC and action callback.
|
||
*
|
||
* Both 'input' and 'output' are lists of 'yang_data' structures. The
|
||
* callback should fetch all the input parameters from the 'input' list,
|
||
* and add output parameters to the 'output' list if necessary.
|
||
*
|
||
* xpath
|
||
* xpath of the YANG RPC or action
|
||
*
|
||
* input
|
||
* read-only list of input parameters
|
||
*
|
||
* output
|
||
* list of output parameters to be populated by the callback
|
||
*
|
||
* Returns:
|
||
* NB_OK on success, NB_ERR otherwise
|
||
*/
|
||
int (*rpc)(const char *xpath, const struct list *input,
|
||
struct list *output);
|
||
|
||
Note that the same callback is used for both RPCs and actions, which are
|
||
essentially the same thing. In the case of YANG actions, the ``xpath``
|
||
parameter can be consulted to find the data node associated to the
|
||
operation.
|
||
|
||
As part of the northbound retrofitting process, it’s suggested to model
|
||
some EXEC-level commands using YANG so that their functionality is
|
||
exposed to other management interfaces other than the CLI. As an
|
||
example, if the ``clear bgp`` command is modeled using a YANG RPC, and a
|
||
corresponding ``rpc`` callback is written, then it should be possible to
|
||
clear BGP neighbors using NETCONF and RESTCONF with that RPC (the Sysrepo
|
||
plugin has full support for YANG RPCs and actions).
|
||
|
||
Here’s an example of a very simple RPC modeled using YANG:
|
||
|
||
.. code:: yang
|
||
|
||
rpc clear-rip-route {
|
||
description
|
||
"Clears RIP routes from the IP routing table and routes
|
||
redistributed into the RIP protocol.";
|
||
}
|
||
|
||
This RPC doesn’t have any input or output parameters. Below we can see
|
||
the implementation of the corresponding ``rpc`` callback, whose skeleton
|
||
was automatically generated by the ``gen_northbound_callbacks`` tool:
|
||
|
||
.. code:: c
|
||
|
||
/*
|
||
* XPath: /frr-ripd:clear-rip-route
|
||
*/
|
||
static int clear_rip_route_rpc(const char *xpath, const struct list *input,
|
||
struct list *output)
|
||
{
|
||
struct route_node *rp;
|
||
struct rip_info *rinfo;
|
||
struct list *list;
|
||
struct listnode *listnode;
|
||
|
||
/* Clear received RIP routes */
|
||
for (rp = route_top(rip->table); rp; rp = route_next(rp)) {
|
||
list = rp->info;
|
||
if (list == NULL)
|
||
continue;
|
||
|
||
for (ALL_LIST_ELEMENTS_RO(list, listnode, rinfo)) {
|
||
if (!rip_route_rte(rinfo))
|
||
continue;
|
||
|
||
if (CHECK_FLAG(rinfo->flags, RIP_RTF_FIB))
|
||
rip_zebra_ipv4_delete(rp);
|
||
break;
|
||
}
|
||
|
||
if (rinfo) {
|
||
RIP_TIMER_OFF(rinfo->t_timeout);
|
||
RIP_TIMER_OFF(rinfo->t_garbage_collect);
|
||
listnode_delete(list, rinfo);
|
||
rip_info_free(rinfo);
|
||
}
|
||
|
||
if (list_isempty(list)) {
|
||
list_delete_and_null(&list);
|
||
rp->info = NULL;
|
||
route_unlock_node(rp);
|
||
}
|
||
}
|
||
|
||
return NB_OK;
|
||
}
|
||
|
||
If the ``clear-rip-route`` RPC had any input parameters, they would be
|
||
available in the ``input`` list given as a parameter to the callback.
|
||
Similarly, the ``output`` list can be used to append output parameters
|
||
generated by the RPC, if any are defined in the YANG model.
|
||
|
||
The northbound clients (CLI and northbound plugins) have the
|
||
responsibility to create and delete the ``input`` and ``output`` lists.
|
||
However, in the cases where the RPC or action doesn’t have any input or
|
||
output parameters, the northbound client can pass NULL pointers to the
|
||
``rpc`` callback to avoid creating linked lists unnecessarily. We can
|
||
see this happening in the example below:
|
||
|
||
.. code:: c
|
||
|
||
/*
|
||
* XPath: /frr-ripd:clear-rip-route
|
||
*/
|
||
DEFPY (clear_ip_rip,
|
||
clear_ip_rip_cmd,
|
||
"clear ip rip",
|
||
CLEAR_STR
|
||
IP_STR
|
||
"Clear IP RIP database\n")
|
||
{
|
||
return nb_cli_rpc("/frr-ripd:clear-rip-route", NULL, NULL);
|
||
}
|
||
|
||
``nb_cli_rpc()`` is a helper function that merely finds the appropriate
|
||
``rpc`` callback based on the XPath provided in the first argument, and
|
||
map the northbound error code from the ``rpc`` callback to a vty error
|
||
code (e.g. ``CMD_SUCCESS``, ``CMD_WARNING``). The second and third
|
||
arguments provided to the function refer to the ``input`` and ``output``
|
||
lists. In this case, both arguments are set to NULL since the YANG RPC
|
||
in question doesn’t have any input/output parameters.
|
||
|
||
Notifications
|
||
~~~~~~~~~~~~~
|
||
|
||
YANG notifations are sent using the ``nb_notification_send()`` function,
|
||
documented in the *lib/northbound.h* file as follows:
|
||
|
||
.. code:: c
|
||
|
||
/*
|
||
* Send a YANG notification. This is a no-op unless the 'nb_notification_send'
|
||
* hook was registered by a northbound plugin.
|
||
*
|
||
* xpath
|
||
* xpath of the YANG notification
|
||
*
|
||
* arguments
|
||
* linked list containing the arguments that should be sent. This list is
|
||
* deleted after being used.
|
||
*
|
||
* Returns:
|
||
* NB_OK on success, NB_ERR otherwise
|
||
*/
|
||
extern int nb_notification_send(const char *xpath, struct list *arguments);
|
||
|
||
The northbound doesn’t use callbacks for notifications because
|
||
notifications are generated locally and sent to the northbound clients.
|
||
This way, whenever a notification needs to be sent, it’s possible to
|
||
call the appropriate function directly instead of finding a callback
|
||
based on the XPath of the YANG notification.
|
||
|
||
As an example, the *ietf-rip* module contains the following
|
||
notification:
|
||
|
||
.. code:: yang
|
||
|
||
notification authentication-failure {
|
||
description
|
||
"This notification is sent when the system
|
||
receives a PDU with the wrong authentication
|
||
information.";
|
||
leaf interface-name {
|
||
type string;
|
||
description
|
||
"Describes the name of the RIP interface.";
|
||
}
|
||
}
|
||
|
||
The following convenience function was implemented in *ripd* to send
|
||
*authentication-failure* YANG notifications:
|
||
|
||
.. code:: c
|
||
|
||
/*
|
||
* XPath: /frr-ripd:authentication-failure
|
||
*/
|
||
void ripd_notif_send_auth_failure(const char *ifname)
|
||
{
|
||
const char *xpath = "/frr-ripd:authentication-failure";
|
||
struct list *arguments;
|
||
char xpath_arg[XPATH_MAXLEN];
|
||
struct yang_data *data;
|
||
|
||
arguments = yang_data_list_new();
|
||
|
||
snprintf(xpath_arg, sizeof(xpath_arg), "%s/interface-name", xpath);
|
||
data = yang_data_new_string(xpath_arg, ifname);
|
||
listnode_add(arguments, data);
|
||
|
||
nb_notification_send(xpath, arguments);
|
||
}
|
||
|
||
Now sending the *authentication-failure* YANG notification should be as
|
||
simple as calling the above function and provide the appropriate
|
||
interface name. The notification will be processed by all northbound
|
||
plugins that subscribed a callback to the ``nb_notification_send`` hook.
|
||
The Sysrepo plugin, for instance, uses this hook to relay the notifications
|
||
to the *sysrepod* daemon, which can generate NETCONF notifications to subscribed
|
||
clients. When no northbound plugin is loaded, ``nb_notification_send()`` doesn’t
|
||
do anything and the notifications are ignored.
|