566 lines
20 KiB
ReStructuredText
566 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.
|