summaryrefslogtreecommitdiffstats
path: root/doc/developer/northbound/operational-data-rpcs-and-notifications.rst
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-09 13:16:35 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-09 13:16:35 +0000
commite2bbf175a2184bd76f6c54ccf8456babeb1a46fc (patch)
treef0b76550d6e6f500ada964a3a4ee933a45e5a6f1 /doc/developer/northbound/operational-data-rpcs-and-notifications.rst
parentInitial commit. (diff)
downloadfrr-e2bbf175a2184bd76f6c54ccf8456babeb1a46fc.tar.xz
frr-e2bbf175a2184bd76f6c54ccf8456babeb1a46fc.zip
Adding upstream version 9.1.upstream/9.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'doc/developer/northbound/operational-data-rpcs-and-notifications.rst')
-rw-r--r--doc/developer/northbound/operational-data-rpcs-and-notifications.rst565
1 files changed, 565 insertions, 0 deletions
diff --git a/doc/developer/northbound/operational-data-rpcs-and-notifications.rst b/doc/developer/northbound/operational-data-rpcs-and-notifications.rst
new file mode 100644
index 0000000..554bc17
--- /dev/null
+++ b/doc/developer/northbound/operational-data-rpcs-and-notifications.rst
@@ -0,0 +1,565 @@
+Operational data
+~~~~~~~~~~~~~~~~
+
+Writing API-agnostic code for YANG-modeled operational data is
+challenging. ConfD and Sysrepo, for instance, have completely different
+APIs to fetch operational data. So how can we write API-agnostic
+callbacks that can be used by both the ConfD and Sysrepo plugins, 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, and borrow
+a lot of ideas from the ConfD API. 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. NETCONF via either ConfD or 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.
+
+ NOTE: using the northbound callbacks as a base, the ConfD plugin can
+ provide up to 100 objects between each round trip between FRR and the
+ *confd* daemon. Preliminary tests showed FRR taking ~7 seconds
+ (asynchronously, without blocking the main pthread) to return a RIP
+ table containing 100k routes to a NETCONF client connected to *confd*
+ (JSON was used as the encoding format). Work needs to be done to find
+ the bottlenecks and optimize this operation.
+
+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 ConfD and Sysrepo plugins run 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 ConfD
+and Sysrepo plugins have 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 ConfD and Sysrepo plugins, for instance, use this hook to relay the
+notifications to the *confd*/*sysrepod* daemons, 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.