First Plugin

First plugin

In this part of the tutorial, we will take an example oven (opens in a new tab) plugin and go over parts of the code which make up the plugin. The code here will be slightly modified for better understanding of sysrepo features such as module changes and operational callbacks. You can keep adding pieces of code shown here to your local file for code completion features and for getting a better look at the code.

Build system

First we need to set up our sysrepo plugin project and it's build system. As mentioned before, we will use CMake. The only thing we need is a CMakeLists.txt file and our source - main.c.

CMakeLists.txt
cmake_minimum_required(VERSION 3.0.0)
project(first-plugin VERSION 0.1.0)
 
add_executable(first-plugin main.c)
main.c
#include <stdio.h>
int main() {}

Once we have that, we can configure our CMake project:

cmake -S . -B build -DCMAKE_PREFIX_PATH=~/sysrepo-dev/libyang2 -DCMAKE_INSTALL_PREFIX:PATH=~/sysrepo-dev/libyang2 -DCMAKE_C_COMPILER=clang -DCMAKE_BUILD_TYPE=Debug

As we can see, we are using the CMAKE_PREFIX_PATH variable to point to our local root - the folder used in sysrepo-dev-env (opens in a new tab) repository for setting up the development environment. We are using the libyang2 folder for plugin development. The same applies for the CMAKE_INSTALL_PREFIX. Used C compiler here is clang and the app will be built in debug mode.

Once configured, we can build our plugin by running the following command:

cmake --build build/

The first step towards building our plugin is finished. Now we have a working environment and are ready to start adding our dependencies such as sysrepo and libyang to the picture.

Dependencies

In CMake, we can have modules for finding our dependencies. These are called Find Modules and we'll now add two of them - one for sysrepo and one for libyang.

First we need to tell CMake where to look for these find modules. We'll make a directory called CMakeModules/ and add two files to it: FindSYSREPO.cmake and FindLIBYANG.cmake. Your directory structure should now look something like this:

Now we can add the finding package code to the find script for sysrepo:

FindSYSREPO.cmake
if (SYSREPO_LIBRARIES AND SYSREPO_INCLUDE_DIRS)
    set(SYSREPO_FOUND TRUE)
else ()
 
    find_path(
        SYSREPO_INCLUDE_DIR
        NAMES sysrepo.h
        PATHS /usr/include /usr/local/include /opt/local/include /sw/include ${CMAKE_INCLUDE_PATH} ${CMAKE_INSTALL_PREFIX}/include
    )
 
    find_library(
        SYSREPO_LIBRARY
        NAMES sysrepo
        PATHS /usr/lib /usr/lib64 /usr/local/lib /usr/local/lib64 /opt/local/lib /sw/lib ${CMAKE_LIBRARY_PATH} ${CMAKE_INSTALL_PREFIX}/lib
    )
 
    if (SYSREPO_INCLUDE_DIR AND SYSREPO_LIBRARY)
        set(SYSREPO_FOUND TRUE)
    else (SYSREPO_INCLUDE_DIR AND SYSREPO_LIBRARY)
        set(SYSREPO_FOUND FALSE)
    endif (SYSREPO_INCLUDE_DIR AND SYSREPO_LIBRARY)
 
    set(SYSREPO_INCLUDE_DIRS ${SYSREPO_INCLUDE_DIR})
    set(SYSREPO_LIBRARIES ${SYSREPO_LIBRARY})
endif ()

This code looks at include/ and lib/ directories in our filesystem root directory and also in directory which is set by the prefix path that we set when we configured the CMake project. If the script finds the needed header and library files, it sets our needed variables ${SYSREPO_INCLUDE_DIRS} and ${SYSREPO_LIBRARIES} to the valid directories where our include files and libraries are found.

The script is the same for libyang:

FindLIBYANG.cmake
if (LIBYANG_LIBRARIES AND LIBYANG_INCLUDE_DIRS)
    set(LIBYANG_FOUND TRUE)
else ()
 
    find_path(
        LIBYANG_INCLUDE_DIR
        NAMES libyang/libyang.h
        PATHS /usr/include /usr/local/include /opt/local/include /sw/include ${CMAKE_INCLUDE_PATH} ${CMAKE_INSTALL_PREFIX}/include
    )
 
    find_library(
        LIBYANG_LIBRARY
        NAMES yang
        PATHS /usr/lib /usr/lib64 /usr/local/lib /usr/local/lib64 /opt/local/lib /sw/lib ${CMAKE_LIBRARY_PATH} ${CMAKE_INSTALL_PREFIX}/lib
    )
 
    if (LIBYANG_INCLUDE_DIR AND LIBYANG_LIBRARY)
        set(LIBYANG_FOUND TRUE)
    else (LIBYANG_INCLUDE_DIR AND LIBYANG_LIBRARY)
        set(LIBYANG_FOUND FALSE)
    endif (LIBYANG_INCLUDE_DIR AND LIBYANG_LIBRARY)
 
    set(LIBYANG_INCLUDE_DIRS ${LIBYANG_INCLUDE_DIR})
    set(LIBYANG_LIBRARIES ${LIBYANG_LIBRARY})
endif ()

Now that we have our find modules ready, we need to tell CMake where to find them. We can do that in the terminal or in the CMakeLists.txt. In this case, we'll set our module path in the CMakeLists.txt file. We do that by modifying the CMAKE_MODULE_PATH variable and adding our CMakeModules/ directory to it:

CMakeLists.txt
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/CMakeModules")

After setting the modules directory, we can find our packages with the find_package() command:

CMakeLists.txt
find_package(SYSREPO REQUIRED)
find_package(LIBYANG REQUIRED)

Once the packages are found, we should have access to the variables set by the package find scripts - ${LIBRARY_INCLUDE_DIRS} and ${LIBRARY_LIBRARIES}, where LIBRARY can be replaced with either SYSREPO or LIBYANG.

We can include our found include directories:

CMakeLists.txt
include_directories(
    ${SYSREPO_INCLUDE_DIRS}
    ${LIBYANG_INCLUDE_DIRS}
)

And also we can link our libraries to our executable:

CMakeLists.txt
target_link_libraries(
    first-plugin
    ${SYSREPO_LIBRARIES}
    ${LIBYANG_LIBRARIES}
)

Once we're finished, our CMakeLists.txt file should look something like this:

CMakeLists.txt
cmake_minimum_required(VERSION 3.0.0)
project(first-plugin VERSION 0.1.0)
 
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/CMakeModules")
 
find_package(SYSREPO REQUIRED)
find_package(LIBYANG REQUIRED)
 
include_directories(
    ${SYSREPO_INCLUDE_DIRS}
    ${LIBYANG_INCLUDE_DIRS}
)
 
add_executable(first-plugin main.c)
 
target_link_libraries(
    first-plugin
    ${SYSREPO_LIBRARIES}
    ${LIBYANG_LIBRARIES}
)

We can now configure our CMake to make sure our libraries are found and after that we can build our executable again:

$ cmake -S . -B build -DCMAKE_PREFIX_PATH=~/sysrepo-dev/libyang2 -DCMAKE_INSTALL_PREFIX:PATH=~/sysrepo-dev/libyang2 -DCMAKE_C_COMPILER=clang -DCMAKE_BUILD_TYPE=Debug

-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/sysrepo-dev/repos/plugins/first-plugin/build

$ cmake --build build/

Consolidate compiler generated dependencies of target first-plugin
[ 50%] Building C object CMakeFiles/first-plugin.dir/main.c.o
[100%] Linking C executable first-plugin
[100%] Built target first-plugin

We are now finished with adding our dependencies. We can now start building our first plugin.

Oven plugin

You can start this section by going over the example plugin written alongisde sysrepo documentation - oven plugin (opens in a new tab). We will be adding our own code starting from the main() function which the oven plugin does not have since it's intended to be used as a sysrepo-plugind plugin, not as a standalone daemon. In our case, we'll have our own main() function which will start the sysrepo connection and a session and we'll then call plugin init and cleanup callbacks ourselves.

First we can start by copying the oven YANG module to our own yang/oven.yang file:

module oven {
    namespace "urn:sysrepo:oven";
    prefix ov;
    revision 2018-01-19 {
        description "Initial revision.";
    }
    typedef oven-temperature {
        description "Temperature range that is accepted by the oven.";
        type uint8 {
            range "0..250";
        }
    }
    container oven {
        description "Configuration container of the oven.";
        leaf turned-on {
            description "Main switch determining whether the oven is on or off.";
            type boolean;
            default false;
        }
        leaf temperature {
            description "Slider for configuring the desired temperature.";
            type oven-temperature;
            default 0;
        }
    }
    container oven-state {
        description "State data container of the oven.";
        config false;
        leaf temperature {
            description "Actual temperature inside the oven.";
            type oven-temperature;
        }
        leaf food-inside {
            description "Informs whether the food is inside the oven or not.";
            type boolean;
        }
    }
    rpc insert-food {
        description "Operation to order the oven to put the prepared food inside.";
        input {
            leaf time {
                description "Parameter determining when to perform the operation.";
                type enumeration {
                    enum now {
                        description "Put the food in the oven immediately.";
                    }
                    enum on-oven-ready {
                        description
                            "Put the food in once the temperature inside
                             the oven is at least the configured one. If it
                             is already, the behaviour is similar to 'now'.";
                    }
                }
            }
        }
    }
    rpc remove-food {
        description "Operation to order the oven to take the food out.";
    }
    notification oven-ready {
        description
            "Event of the configured temperature matching the actual
             temperature inside the oven. If the configured temperature
             is lower than the actual one, no notification is generated
             when the oven cools down to the configured temperature.";
    }
}

As we can see in the module, it contains a oven container for setting the temperature and turned-on leafs, oven-state container for providing the state data (indicated also by the config false; statement), remove-food RPC and a oven-ready notification. First we'll create our main() function and after that our plugin init and cleanup callbacks where we'll set up our subscriptions to changes for the oven container, operational callback for the oven-state container and also our RPC callback for the remove-food RPC.

We can start with the main() function:

main.c
#define PLUGIN_NAME "oven-plugin"
 
volatile int exit_application = 0;
 
int main(void) {
  int error = SR_ERR_OK;
  sr_conn_ctx_t *connection = NULL;
  sr_session_ctx_t *session = NULL;
  void *private_data = NULL;
 
  sr_log_stderr(SR_LL_DBG);
 
  /* connect to sysrepo */
  error = sr_connect(SR_CONN_DEFAULT, &connection);
  if (error) {
    SRPLG_LOG_ERR(PLUGIN_NAME, "sr_connect error (%d): %s", error,
                  sr_strerror(error));
    goto out;
  }
 
  error = sr_session_start(connection, SR_DS_RUNNING, &session);
  if (error) {
    SRPLG_LOG_ERR(PLUGIN_NAME, "sr_session_start error (%d): %s", error,
                  sr_strerror(error));
    goto out;
  }
 
  error = sr_plugin_init_cb(session, &private_data);
  if (error) {
    SRPLG_LOG_ERR(PLUGIN_NAME, "sr_plugin_init_cb error");
    goto out;
  }
 
  /* loop until ctrl-c is pressed / SIGINT is received */
  signal(SIGINT, sigint_handler);
  signal(SIGPIPE, SIG_IGN);
  while (!exit_application) {
    sleep(1);
  }
 
out:
  sr_plugin_cleanup_cb(session, private_data);
  sr_disconnect(connection);
 
  return error ? -1 : 0;
}

Here we first create a connection to sysrepo and after that we start our session to the running datastore. If no error occurs, we then call our plugin init callback - sr_plugin_init_cb() and check the error code it returned. If an error occured in the plugin init callback, for example if a subscription couldn't be created, we return an error code different from 0 and in our main() we go directly to the out: label. If on the other hand our init callback ran correctly, we add a SIGINT signal handler - sigint_handler() to handle our CTRL-C key press to exit the application. In the meantime we are running an infinite loop while sysrepo can call our change/operational/RPC callbacks which we will set up in the plugin init callback. On the out: label, we free up our used resources - we call the plugin cleanup callback and disconnect our created connection.

sigint_handler():

main.c
static void sigint_handler(__attribute__((unused)) int signum) {
  SRPLG_LOG_INF(PLUGIN_NAME, "Sigint called, exiting...");
  exit_application = 1;
}

We use a volatile global variable to signal when to stop the inifinite loop in our main function.

Plugin init and cleanup callbacks:

main.c
int sr_plugin_init_cb(sr_session_ctx_t *session, void **private_data) {
  int error = SR_ERR_OK;
  return error;
}
 
void sr_plugin_cleanup_cb(sr_session_ctx_t *session, void *private_data) {}

Now we can build and run our application and we should see a successful connection to sysrepo:

❯ ./build/first-plugin
[INF] Connection 2 created.
[INF] Session 2 (user "user", CID 2) created.
.
.
.
^C[INF] oven-plugin: Sigint called, exiting...
[INF] No datastore changes to apply.

We are now finished with the base of the plugin. We can now start adding our module change, operational and RPC callbacks and subscribe them to the oven YANG module leafs/containers/lists etc.

Adding the oven YANG module to sysrepo

Before subscribing to module changes for the oven YANG module, we need to install it to sysrepo. We can do that by running the following command:

sysrepoctl -i yang/oven.yang

By running the list subcommand for sysrepoctl, we can confirm it was installed correctly:

sysrepoctl -l

Sysrepo repository: /home/user/sysrepo-dev/libyang2/repos/sysrepo/build/repository

Module Name                | Revision   | Flags | Owner             | Startup Perms | Submodules                      | Features                                                                         
-----------------------------------------------------------------------------------------------------------------------
oven                       | 2018-01-19 | I     | user:user | 600   |               |                                 |

Flags meaning: I - Installed/i - Imported; R - Replay support

Once installed, we can now write our subscriptions to the oven YANG module.

Module changes

We can start by adding a module change subscription and adding our callback for handling module changes. Our callback for handling module changes:

main.c
int oven_change_cb(sr_session_ctx_t *session, uint32_t sub_id,
                   const char *module_name, const char *xpath, sr_event_t event,
                   uint32_t request_id, void *private_data) {
  int error = SR_ERR_OK;
  return error;
}

In the plugin init callback, we can now subscribte to the changes for the oven container found in the oven YANG module:

main.c
int sr_plugin_init_cb(sr_session_ctx_t *session, void **private_data) {
  int error = SR_ERR_OK;
  int rc = 0;
 
  sr_subscription_ctx_t *subscription = NULL;
 
  /* subscribe for oven module changes - also causes startup oven data to be
   * copied into running and enabling the module */
  rc = sr_module_change_subscribe(session, "oven", "/oven:oven", oven_change_cb,
                                  NULL, 0, SR_SUBSCR_DEFAULT, &subscription);
  if (rc != SR_ERR_OK) {
    goto error;
  }
 
  goto out;
 
error:
  error = SR_ERR_CALLBACK_FAILED;
 
out:
  return error;
}

We subscribe to the oven YANG module and specifically on the /oven:oven XPath. When a change is added to sysrepo, our callback will be called if the change contains edit for the given XPath in the YANG tree. As for the plugin, we will also mock real usage with global variables:

main.c
volatile int temperature = 25;
volatile int turned_on = 0;

We will now see how to react to changes, specifically we will iterate over all changes given to us by sysrepo using the changes iterator API. First, we can check the event for which the callback was called. Our callback can be called on multiple events such as DONE, CHANGE, UPDATE etc. We can check the event callback parameter:

main.c
int oven_change_cb(sr_session_ctx_t *session, uint32_t sub_id,
                   const char *module_name, const char *xpath, sr_event_t event,
                   uint32_t request_id, void *private_data) {
  int error = SR_ERR_OK;
 
  switch (event) {
  case SR_EV_CHANGE:
    // handle change event
    break;
  default:
    break;
  }
 
  return error;
}

In this case, we'll only react to the CHANGE event and handle an edit which occured. You can have a look at changes iterator API (opens in a new tab) - functions such as sr_get_changes_iter() and sr_get_changes_tree_next(). Code used for iterating over changes:

main.c
int oven_change_cb(sr_session_ctx_t *session, uint32_t sub_id,
                   const char *module_name, const char *xpath, sr_event_t event,
                   uint32_t request_id, void *private_data) {
  int error = SR_ERR_OK;
 
  int rc = 0;
 
  // sysrepo
  sr_change_iter_t *changes_iterator = NULL;
 
  // helper struct for storing single change data
  struct change_ctx {
    const struct lyd_node *node; ///< Current changed libyang node.
    const char *previous_value;  ///< Previous node value.
    const char *previous_list;   ///< Previous list keys predicate.
    int previous_default;        ///< Previous value default flag.
    sr_change_oper_t operation;  ///< Operation being applied on the node.
  };
 
  struct change_ctx change = {0};
 
  switch (event) {
  case SR_EV_CHANGE:
    // handle change event
 
    // get changes iterator
    rc = sr_get_changes_iter(session, "/oven:oven/*", &changes_iterator);
    if (rc != SR_ERR_OK) {
      SRPLG_LOG_ERR(PLUGIN_NAME, "sr_get_changes_iter error (%d): %s", rc,
                    sr_strerror(rc));
      goto error;
    }
 
    // iterate over all changes
    while (sr_get_change_tree_next(session, changes_iterator, &change.operation,
                                   &change.node, &change.previous_value,
                                   &change.previous_list,
                                   &change.previous_default) == SR_ERR_OK) {
      // handle a change
    }
 
    break;
  default:
    break;
  }
 
  goto out;
error:
  error = SR_ERR_CALLBACK_FAILED;
 
out:
  return error;
}

First we create a helper struct used for storing single change data. Single change contains info about a previous datastore value, current node which is being changed from which a path or a node name can be extracted and also an operation on the node which can be CREATED, MODIFIED, DELETED and MOVED. node parameter is a data node which also means it contains some libyang data. If we extract the value of that node, it will contain the current change data i.e. the data which is being applied.

We create an iterator on the following path: /oven:oven/*. The reason for the * element is so that we can react to all changes for the inner nodes of the oven container.

The change handling code:

main.c
// iterate over all changes
while (sr_get_change_tree_next(session, changes_iterator, &change.operation,
                                &change.node, &change.previous_value,
                                &change.previous_list,
                                &change.previous_default) == SR_ERR_OK) {
    // handle a change
    const char *node_name = LYD_NAME(change.node);
    const char *node_value = lyd_get_value(change.node);
 
    SRPLG_LOG_INF(PLUGIN_NAME, "Change: %s, %s, %s", node_name, node_value,
                    change.previous_value);
 
    if (!strcmp(node_name, "temperature")) {
    // handle temperature change
    } else if (!strcmp(node_name, "turned-on")) {
    // handle turned-on change
    }
}

We check which change is being currently applied because we subscribed to the whole oven container. If we wanted separate callbacks for each leaf such as temperature and turned-on, we could've created them and subscribed in the plugin init callback to each leaf change using their respective XPaths - /oven:oven/temperature and /oven:oven/turned-on, but in this tutorial this approach was chosen for simplicity.

Now we can create our own API functions for handling these changes. Something along the lines of the following:

main.c
// temperature
int handle_temperature_change(const char *previous_value,
                              const char *current_value,
                              sr_change_oper_t operation) {
  int error = 0;
  return error;
}
 
// turned-on
int handle_turned_on_change(const char *previous_value,
                            const char *current_value,
                            sr_change_oper_t operation) {
  int error = 0;
  return error;
}

We can call the change handlers now in our change iteration loop:

main.c
// iterate over all changes
while (sr_get_change_tree_next(session, changes_iterator, &change.operation,
                                &change.node, &change.previous_value,
                                &change.previous_list,
                                &change.previous_default) == SR_ERR_OK) {
    // handle a change
    const char *node_name = LYD_NAME(change.node);
    const char *node_value = lyd_get_value(change.node);
 
    SRPLG_LOG_INF(PLUGIN_NAME, "Change: %s, %s, %s", node_name, node_value,
                    change.previous_value);
 
    if (!strcmp(node_name, "temperature")) {
        // handle temperature change
        rc = handle_temperature_change(change.previous_value, node_value,
                                        change.operation);
        if (rc != SR_ERR_OK) {
            SRPLG_LOG_ERR(PLUGIN_NAME, "handle_temperature_change error (%d): %s",
                        rc, sr_strerror(rc));
            goto error;
        }
    } else if (!strcmp(node_name, "turned-on")) {
        // handle turned-on change
        rc = handle_turned_on_change(change.previous_value, node_value,
                                        change.operation);
        if (rc != SR_ERR_OK) {
            SRPLG_LOG_ERR(PLUGIN_NAME, "handle_turned_on_change error (%d): %s",
                        rc, sr_strerror(rc));
            goto error;
        }
    }
}

Now the only thing left to do is handle the change and modify our global variables in this case. In normal use cases this would require for example changing a part of a file or using some system calls to alter system data. Change handling code:

main.c
// temperature
int handle_temperature_change(const char *previous_value,
                              const char *current_value,
                              sr_change_oper_t operation) {
  int error = 0;
 
  // convert the value to int
  int current_temperature = atoi(current_value);
 
  switch (operation) {
  case SR_OP_CREATED:
  case SR_OP_MODIFIED:
    // set the temperature
    oven_temperature = current_temperature;
    break;
  case SR_OP_DELETED:
    // set to initial value of 25
    oven_temperature = 25;
    break;
  case SR_OP_MOVED:
    break;
  }
 
  SRPLG_LOG_INF(PLUGIN_NAME, "Temperature changed to %d", oven_temperature);
 
  return error;
}
 
// turned-on
int handle_turned_on_change(const char *previous_value,
                            const char *current_value,
                            sr_change_oper_t operation) {
  int error = 0;
 
  switch (operation) {
  case SR_OP_CREATED:
  case SR_OP_MODIFIED:
    if (!strcmp(current_value, "true")) {
      SRPLG_LOG_INF(PLUGIN_NAME, "Oven turned on!");
      turned_on = 1;
    } else {
      SRPLG_LOG_INF(PLUGIN_NAME, "Oven turned off!");
      turned_on = 0;
    }
    break;
  case SR_OP_DELETED:
    // set to initial value of false
    turned_on = 0;
    break;
  case SR_OP_MOVED:
    break;
  }
 
  return error;
}

We use a switch on the operation enum variable to check which change operation is being applied. If the value is being modified or created the first time, we set our global variable to the wanted value. If the value is being deleted, we fallback to the initial global variable value which we used in the plugin.

Finally, the code for handling changes should look like this:

main.c
// temperature
int handle_temperature_change(const char *previous_value,
                              const char *current_value,
                              sr_change_oper_t operation) {
  int error = 0;
 
  // convert the value to int
  int current_temperature = atoi(current_value);
 
  switch (operation) {
  case SR_OP_CREATED:
  case SR_OP_MODIFIED:
    // set the temperature
    oven_temperature = current_temperature;
    break;
  case SR_OP_DELETED:
    // set to initial value of 25
    oven_temperature = 25;
    break;
  case SR_OP_MOVED:
    break;
  }
 
  SRPLG_LOG_INF(PLUGIN_NAME, "Temperature changed to %d", oven_temperature);
 
  return error;
}
 
// turned-on
int handle_turned_on_change(const char *previous_value,
                            const char *current_value,
                            sr_change_oper_t operation) {
  int error = 0;
 
  switch (operation) {
  case SR_OP_CREATED:
  case SR_OP_MODIFIED:
    if (!strcmp(current_value, "true")) {
      SRPLG_LOG_INF(PLUGIN_NAME, "Oven turned on!");
      turned_on = 1;
    } else {
      SRPLG_LOG_INF(PLUGIN_NAME, "Oven turned off!");
      turned_on = 0;
    }
    break;
  case SR_OP_DELETED:
    // set to initial value of false
    turned_on = 0;
    break;
  case SR_OP_MOVED:
    break;
  }
 
  return error;
}
 
int oven_change_cb(sr_session_ctx_t *session, uint32_t sub_id,
                   const char *module_name, const char *xpath, sr_event_t event,
                   uint32_t request_id, void *private_data) {
  int error = SR_ERR_OK;
 
  int rc = 0;
 
  // sysrepo
  sr_change_iter_t *changes_iterator = NULL;
 
  // helper struct for storing single change data
  struct change_ctx {
    const struct lyd_node *node; ///< Current changed libyang node.
    const char *previous_value;  ///< Previous node value.
    const char *previous_list;   ///< Previous list keys predicate.
    int previous_default;        ///< Previous value default flag.
    sr_change_oper_t operation;  ///< Operation being applied on the node.
  };
 
  // use an instance of change context to store change data
  struct change_ctx change = {0};
 
  switch (event) {
  case SR_EV_CHANGE:
    // handle change event
 
    // get changes iterator
    rc = sr_get_changes_iter(session, "/oven:oven/*", &changes_iterator);
    if (rc != SR_ERR_OK) {
      SRPLG_LOG_ERR(PLUGIN_NAME, "sr_get_changes_iter error (%d): %s", rc,
                    sr_strerror(rc));
      goto error;
    }
 
    // iterate over all changes
    while (sr_get_change_tree_next(session, changes_iterator, &change.operation,
                                   &change.node, &change.previous_value,
                                   &change.previous_list,
                                   &change.previous_default) == SR_ERR_OK) {
      // handle a change
      const char *node_name = LYD_NAME(change.node);
      const char *node_value = lyd_get_value(change.node);
 
      SRPLG_LOG_INF(PLUGIN_NAME, "Change: %s, %s, %s", node_name, node_value,
                    change.previous_value);
 
      if (!strcmp(node_name, "temperature")) {
        // handle temperature change
        rc = handle_temperature_change(change.previous_value, node_value,
                                       change.operation);
        if (rc != SR_ERR_OK) {
          SRPLG_LOG_ERR(PLUGIN_NAME, "handle_temperature_change error (%d): %s",
                        rc, sr_strerror(rc));
          goto error;
        }
      } else if (!strcmp(node_name, "turned-on")) {
        // handle turned-on change
        rc = handle_turned_on_change(change.previous_value, node_value,
                                     change.operation);
        if (rc != SR_ERR_OK) {
          SRPLG_LOG_ERR(PLUGIN_NAME, "handle_turned_on_change error (%d): %s",
                        rc, sr_strerror(rc));
          goto error;
        }
      }
    }
 
    break;
  default:
    break;
  }
 
  goto out;
error:
  error = SR_ERR_CALLBACK_FAILED;
 
out:
  return error;
}

We can now build and run our plugin and try to configure the temperature and turn the oven off. We can do that by writing a simple XML document to represent our edit data and use sysrepocfg tool to create an edit in the running datastore. We will create a file edit/oven.xml:

edit/oven.xml
<oven xmlns="urn:sysrepo:oven">
    <temperature>30</temperature>
</oven>

We will start our plugin in one terminal session and will edit the data from the second terminal session. Started plugin:

❯ ./build/first-plugin
[INF] Connection 7 created.
[INF] Session 3 (user "user", CID 7) created.
[DBG] #SHM before (adding change sub)
[DBG] #SHM:
000000-000008 [     8]: ext structure

[DBG] #SHM after (adding change sub)
[DBG] #SHM:
000000-000008 [     8]: ext structure
000008-000048 [    40]: running change subs (1, mod "oven")
000048-000064 [    16]: running change sub xpath ("/oven:oven", mod "oven")

Running sysrepocfg:

sysrepocfg --edit=./edit/oven.xml -d running -m oven

On the plugin side, we get the following output:

[INF] EV LISTEN: "oven" "change" ID 1 priority 0 processing (remaining 1 subscribers).
[INF] oven-plugin: Change: temperature, 30, 0
[INF] oven-plugin: Temperature changed to 30
[INF] EV LISTEN: "oven" "change" ID 1 priority 0 success (remaining 0 subscribers).
[INF] EV LISTEN: "oven" "done" ID 1 priority 0 processing (remaining 1 subscribers).
[INF] EV LISTEN: "oven" "done" ID 1 priority 0 success (remaining 0 subscribers).

As we can see, our change handling function was called and successfully changed the "oven temperature" to 30. We can check the running datastore now and make sure the value was written to the datastore:

❯ sysrepocfg -X -d running -x "/oven:oven"
<oven xmlns="urn:sysrepo:oven">
  <temperature>30</temperature>
</oven>

Operational callbacks

We can start with the operational callbacks the same way we did with the module change callbacks - we'll define our operational callback and subscribe to the oven-state container using the sysrepo subscription API.

Our operational callback with the needed function signature:

main.c
int oven_state_cb(sr_session_ctx_t *session, uint32_t sub_id,
                  const char *module_name, const char *path,
                  const char *request_xpath, uint32_t request_id,
                  struct lyd_node **parent, void *private_data) {
  int error = SR_ERR_OK;
 
  SRPLG_LOG_INF(PLUGIN_NAME, "State data provider called");
 
  return error;
}

Now we can subscribe to the oven-state container with the created callback:

main.c
/* subscribe as state data provider for the oven state data */
rc = sr_oper_get_subscribe(session, "oven", "/oven:oven-state", oven_state_cb,
                            NULL, SR_SUBSCR_DEFAULT, &subscription);
if (rc != SR_ERR_OK) {
  goto error;
}

We can now run and compile our plugin to make sure our state data callback is being called. Started plugin:

❯ ./build/first-plugin
[INF] Datastore copied from <startup> to <running>.
[INF] Connection 1 created.
[INF] Session 1 (user "user", CID 1) created.
[DBG] #SHM before (adding change sub)
[DBG] #SHM:
000000-000008 [     8]: ext structure

[DBG] #SHM after (adding change sub)
[DBG] #SHM:
000000-000008 [     8]: ext structure
000008-000048 [    40]: running change subs (1, mod "oven")
000048-000064 [    16]: running change sub xpath ("/oven:oven", mod "oven")

[DBG] #SHM before (adding oper get sub)
[DBG] #SHM:
000000-000008 [     8]: ext structure
000008-000048 [    40]: running change subs (1, mod "oven")
000048-000064 [    16]: running change sub xpath ("/oven:oven", mod "oven")

[DBG] #SHM after (adding oper get sub)
[DBG] #SHM:
000000-000008 [     8]: ext structure
000008-000048 [    40]: running change subs (1, mod "oven")
000048-000064 [    16]: running change sub xpath ("/oven:oven", mod "oven")
000064-000096 [    32]: oper get subs (1, mod "oven")
000096-000120 [    24]: oper get sub xpath ("/oven:oven-state", mod "oven")

[DBG] #SHM before (adding oper get xpath sub)
[DBG] #SHM:
000000-000008 [     8]: ext structure
000008-000048 [    40]: running change subs (1, mod "oven")
000048-000064 [    16]: running change sub xpath ("/oven:oven", mod "oven")
000064-000096 [    32]: oper get subs (1, mod "oven")
000096-000120 [    24]: oper get sub xpath ("/oven:oven-state", mod "oven")

[DBG] #SHM after (adding oper get xpath sub)
[DBG] #SHM:
000000-000008 [     8]: ext structure
000008-000048 [    40]: running change subs (1, mod "oven")
000048-000064 [    16]: running change sub xpath ("/oven:oven", mod "oven")
000064-000096 [    32]: oper get subs (1, mod "oven")
000096-000120 [    24]: oper get sub xpath ("/oven:oven-state", mod "oven")
000120-000152 [    32]: oper get xpath subs (1, xpath "/oven:oven-state")

We can already see from the sysrepo debug logging that the operational callback has been added with the xpath value of /oven:oven-state.

Running sysrepocfg:

sysrepocfg -X -d operational -x "/oven:oven-state"

On the plugin side we can see the following output:

[INF] EV LISTEN: "/oven:oven-state" "oper get" ID 1 processing.
[INF] oven-plugin: State data provider called
[INF] EV LISTEN: "/oven:oven-state" "oper get" ID 1 processing success.

Now that we made sure our operational callback is being called, we can start writing the code for providing the libyang tree. As a part of the callback signature, we can see the struct lyd_node **parent parameter. We are supposed to populate this tree with the data for which the callback is intended for. In this case, we should add the temperature and food-inside leaf values to the tree. More info about the callback parameters can be found here (opens in a new tab). Note that, for the top level nodes (like oven-state container), the parent value will be NULL as said in the documentation.

Code for adding temperature data to the parent tree:

main.c
int oven_state_cb(sr_session_ctx_t *session, uint32_t sub_id,
                  const char *module_name, const char *path,
                  const char *request_xpath, uint32_t request_id,
                  struct lyd_node **parent, void *private_data) {
  int error = SR_ERR_OK;
  int rc = 0;
 
  const struct ly_ctx *ly_ctx;
 
  char value_buffer[100] = {0};
 
  SRPLG_LOG_INF(PLUGIN_NAME, "State data provider called");
 
  // add data to the parent tree
  ly_ctx = sr_acquire_context(sr_session_get_connection(session));
  if (!ly_ctx) {
    SRPLG_LOG_ERR(PLUGIN_NAME, "unable to get libyang context");
    goto error;
  }
 
  // add oven state data
  rc = snprintf(value_buffer, sizeof(value_buffer), "%d", oven_temperature);
  if (rc < 0) {
    SRPLG_LOG_ERR(PLUGIN_NAME, "snprintf() error: %d", rc);
    goto error;
  }
 
  // create oven-state container
  lyd_new_path(*parent, ly_ctx, "/oven:oven-state", NULL, 0, parent);
 
  // create temperature leaf
  rc = lyd_new_path(*parent, ly_ctx, "temperature", value_buffer, 0, NULL);
  if (rc != LY_SUCCESS) {
    SRPLG_LOG_ERR(PLUGIN_NAME, "lyd_new_path() error: %d", rc);
    goto error;
  }
 
  SRPLG_LOG_INF(PLUGIN_NAME, "State data provider finished");
 
  goto out;
 
error:
  error = SR_ERR_CALLBACK_FAILED;
 
out:
  return error;
}

First we create an oven-state container and store it in the parent variable. After that we use the parent node and use a relative path to the parent to add a child temperature leaf value. Once we compile the plugin and create an operational get call using sysrepocfg like in the last example, we get the following output:

Plugin:

[INF] EV LISTEN: "/oven:oven-state" "oper get" ID 1 processing.
[INF] oven-plugin: State data provider called
[INF] oven-plugin: State data provider finished
[INF] EV LISTEN: "/oven:oven-state" "oper get" ID 1 processing success.

sysrepocfg terminal session:

<oven-state xmlns="urn:sysrepo:oven">
  <temperature>25</temperature>
</oven-state>

As we can see, we are correctly returning the intended libyang tree data from the operational callback. The same approach would be used for other leafs/lists/leaf-lists. You can also choose to use full XPaths or store nodes in temporary variables and then use relative path approach.

RPC callbacks

We can now add RPC callbacks for the two RPCs found in the oven yang module: insert-food and remove-food. RPCs can have input data and output data. In our case, insert-food has an input tree while remove-food has neither input nor output specification. In this example we will only print the YANG input tree from the insert-food specification but in real plugin examples, these input values would be used to run a correct remote procedure on the machine running sysrepo.

We can start by adding our callback with the correct specification which can be found here (opens in a new tab):

main.c
int oven_insert_food_rpc_cb(sr_session_ctx_t *session, uint32_t sub_id,
                            const char *op_path, const struct lyd_node *input,
                            sr_event_t event, uint32_t request_id,
                            struct lyd_node *output, void *private_data) {
  int error = SR_ERR_OK;
 
  SRPLG_LOG_INF(PLUGIN_NAME, "Insert food RPC called");
 
  return error;
}
 
int oven_remove_food_rpc_cb(sr_session_ctx_t *session, uint32_t sub_id,
                            const char *op_path, const struct lyd_node *input,
                            sr_event_t event, uint32_t request_id,
                            struct lyd_node *output, void *private_data) {
  int error = SR_ERR_OK;
 
  SRPLG_LOG_INF(PLUGIN_NAME, "Remove food RPC called");
 
  return error;
}

In this case, we are using the callback signatures in which the input and output parameters are represented by libyang tree data nodes. There is also an option to use sysrepo union based values as input or output data which can be found here (opens in a new tab).

We can now subscribe our RPCs using the sr_rpc_subscribe_tree() since we're using the tree based RPC callbacks:

main.c
/* subscribe for insert-food RPC calls */
rc =
    sr_rpc_subscribe_tree(session, "/oven:insert-food",
                          oven_insert_food_rpc_cb, NULL, 0, 0, &subscription);
if (rc != SR_ERR_OK) {
  goto error;
}
 
/* subscribe for remove-food RPC calls */
rc =
    sr_rpc_subscribe_tree(session, "/oven:remove-food",
                          oven_remove_food_rpc_cb, NULL, 0, 0, &subscription);
if (rc != SR_ERR_OK) {
  goto error;
}

We can prepare our RPC call with a simple XML file rpc/insert-food.xml:

rpc/insert-food.xml
<insert-food xmlns="urn:sysrepo:oven">
    <time>now</time>
</insert-food>

When we compile and run our plugin we can notice the debug logs from sysrepo telling us that an RPC subscriptions have been created, for example:

[DBG] #SHM after (adding rpc sub)
[DBG] #SHM:
000000-000008 [     8]: ext structure
000008-000048 [    40]: running change subs (1, mod "oven")
000048-000064 [    16]: running change sub xpath ("/oven:oven", mod "oven")
000064-000096 [    32]: oper get subs (1, mod "oven")
000096-000120 [    24]: oper get sub xpath ("/oven:oven-state", mod "oven")
000120-000152 [    32]: oper get xpath subs (1, xpath "/oven:oven-state")
000152-000192 [    40]: rpc subs (1, path "/oven:insert-food")
000192-000216 [    24]: rpc sub xpath ("/oven:insert-food", path "/oven:insert-food")
000216-000256 [    40]: rpc subs (1, path "/oven:remove-food")
000256-000280 [    24]: rpc sub xpath ("/oven:remove-food", path "/oven:remove-food")

Now we can run our RPC call using the sysrepocfg utility:

sysrepocfg --rpc=rpc/insert-food.xml

On our plugin side, we get the following output:

[INF] EV LISTEN: "/oven:insert-food" "rpc" ID 1 priority 0 processing (remaining 1 subscribers).
[INF] oven-plugin: Insert food RPC called
[INF] EV LISTEN: "/oven:insert-food" "rpc" ID 1 priority 0 success (remaining 0 subscribers).

We can see that our RPC callback is being called correctly. The same applies for the remove-food RPC.

Now the only thing left to do is implement our RPC functionality. In this example, we will only check for our input variable and print its value as an XML structured tree but as said before, in a real plugin case we would use our input parameter and just like in our operational example, we would fill our provided output libyang tree node with needed data if any data was needed.

The code used for printing the RPC input libyang tree data to standard output:

main.c
int oven_insert_food_rpc_cb(sr_session_ctx_t *session, uint32_t sub_id,
                            const char *op_path, const struct lyd_node *input,
                            sr_event_t event, uint32_t request_id,
                            struct lyd_node *output, void *private_data) {
  int error = SR_ERR_OK;
  int rc = 0;
 
  SRPLG_LOG_INF(PLUGIN_NAME, "Insert food RPC called");
 
  if (input) {
    rc = lyd_print_file(stdout, input, LYD_XML, 0);
    if (rc != LY_SUCCESS) {
      return SR_ERR_INVAL_ARG;
    }
  }
 
  return error;
}

When we run our plugin and send an RPC call using sysrepocfg like in the example before, we get the following output:

[INF] EV LISTEN: "/oven:insert-food" "rpc" ID 1 priority 0 processing (remaining 1 subscribers).
[INF] oven-plugin: Insert food RPC called
<insert-food xmlns="urn:sysrepo:oven">
  <time>now</time>
</insert-food>
[INF] EV LISTEN: "/oven:insert-food" "rpc" ID 1 priority 0 success (remaining 0 subscribers).

As we can see, we were provided with the whole libyang tree which was sent using sysrepocfg. We would use libyang API to iterate over tree nodes and pull data which was needed for us to create some useful functionality.

Conclusion

In this part of the tutorial, we have learned how to create a base sysrepo plugin example. We have learned how to add CMake find modules and how to use them to find our dependencies in a CMake project. We have also seen how to create a standalone application which can connect to sysrepo, run a sysrepo session to the running datastore and use our plugin init and cleanup callbacks to create a minimal plugin.

After that we continued to work on the plugin by adding useful functionality like module change subscriptions, operational data providers and RPC callbacks. These three parts make up most of the plugin functionality and the other part of the plugins is the actual implementation of these callbacks which will add changes to the running machine, provide data from the machine or do both at the same time using the RPC functionality.

After finishing this part of the tutorial, you should now be able to create a simple plugin from scratch and add module change, operational and RPC callbacks to it. Now that you're familiar with most of the process of the plugin development, you can have a look at the next steps.