Libnetconf transAPI is a framework designed to save developers time and let them focus on configuring and managing their device instead of fighting with the NETCONF protocol.
It allows a developer to choose parts of a configuration that can be easily configured as a single block. Based on a list of so called 'sensitive paths' the generator creates C code containing a single callback function for every 'sensitive path'. Whenever something changes in the configuration file, the appropriate callback function is called and it is supposed to reflect configuration changes in the actual device behavior.
Additionaly, transAPI provides an opportunity to implement behavior of NETCONF RPC operation defined in the data model. In case lnctool(1) finds an RPC definition inside the provided data model, it generates callbacks for it too. Whenever a server calls ncds_apply_rpc() or ncds_apply_rpc2all() with RPC message containing such defined RPC operation, libnetconf uses callback function implemented in the module.
Understanding callback parameters
Every transapi callback function has fixed set of parameters. Function header looks like this:
int callback_path_into_configuration_xml(
void **data,
XMLDIFF_OP op, xmlNodePtr node,
struct nc_err **error)
void **data
This parameter was added to provide a way to share any data between callbacks. libnetconf never change (or even access) content of this parameter. Initialy content of 'data' is NULL. transapi module may use 'data' as it like but is also fully responsible for correct memory handling and freeing of no longer needed memory referenced by 'data'.
XMLDIFF_OP op
Parameter op indicates what event(s) was occured on node. All events are bitwise ored. To test if certaint event occured on node use bitwise and (&).
- Node can be added or removed.
- XMLDIFF_ADD = Node was added.
- XMLDIFF_REM = Node was removed.
- Nodes of type leaf can be changed.
- XMLDIFF_MOD = node content was changed
- Container nodes are informed about events occured on descendants. It can be distinguished whether the event was processed or not.
- XMLDIFF_MOD = Some of node children was changed and there is not callback specified for it.
- XMLDIFF_CHAIN = Some of node children was changed and associated callback was called.
- Additionaly, user-ordered lists and leaf-lists are notified when change in order occurs.
- XMLDIFF_SIBLING = Change in order. Some of siblings was added, removed or changed place.
- XMLDIFF_REORDER = Undrelying user-ordered list has changed order.
Valid combinations of events
- XMLDIFF_ADD and XMLDIFF_REM can never be specified simutaneously.
- other restrictions depend on node type:
- Leaf: exactly one of XMLDIFF_ADD, XMLDIFF_REM, XMLDIFF_MOD
- Container: at least one of XMLDIFF_ADD, XMLDIFF_REM, XMLDIFF_MOD, XMLDIFF_CHAIN and posibly XMLDIFF_REORDER when node holds user-ordered list
- List (system-ordered): at least one of XMLDIFF_ADD, XMLDIFF_REM, XMLDIFF_MOD, XMLDIFF_CHAIN and posibly XMLDIFF_REORDER when node holds user-ordered list
- List (user-ordered): at least one of XMLDIFF_ADD, XMLDIFF_REM, XMLDIFF_MOD, XMLDIFF_CHAIN, XMLDIFF_SIBLING and posibly XMLDIFF_REORDER when node holds user-ordered list
- Leaf-list (system-ordered): exactly one of XMLDIFF_ADD, XMLDIFF_REM
- Leaf-list (user-ordered): at least one of XMLDIFF_ADD, XMLDIFF_REM, XMLDIFF_SIBLING
Ex.: Leaf processing
int callback_some_leaf(
void **data,
XMLDIFF_OP op, xmlNodePtr node,
struct nc_err **error)
{
} else {
return(EXIT_FAILURE);
}
return(EXIT_SUCCESS);
}
xmlNodePtr node
Pointer to a particular node instance in configuration document where the event was detected. When the node was removed pointer is set to its instance in old configuration snapshot.
strict nc_err **error
libnetconf's error structure. May (and should) be used to specify error when it occurs and callback returns EXIT_FAILURE. Error description is forwarded to client.
History of the transAPI versions
Each transAPI module source code is generated with the transapi_version
variable set to the transAPI version supported by the code generator (lnctool(1)). libnetconf requires exactly the same transAPI version in the modules as it supports itself. However, some of the transAPI versions are kind of backward compatible, so it is possible to simply change the value of the transapi_version
variable in the module source code. In that case no additional changes to the transAPI module source code are required.
Here is the list of transAPI versions with notes to the changed things and to the backward compatibility.
- version 1
- version 2
- Allow callbacks to modify configuration data. This action is announced by the callback via the
config_modified
variable.
- Changes prototype of the transAPI callbacks. It allows to return NETCONF error description structure from the callbacks.
- Backward incompatible.
- version 3
- Changes prototype of the
transapi_init()
function. It allows the module can announce to libnetconf the initial configuration of the device when the module is loaded.
- Changes prototype of the transAPI callbacks. The configuration data are passed to the callbacks only as the libxml2 structures. Callbacks variant passing configuration data as strings are no longer available.
- Backward incompatible.
- version 4
- Callback order - the module can change the order of executing callbacks from the default 'from leafs to root
to 'from root to leafs
. This is done via the callbacks_order
variable. If the variable is not defined (such as in a transAPI v3 module), the default callback order is used.
- Backward compatible.
- version 5
- Adds support for monitoring external files.
- Backward compatible.
transAPI Tutorial
On this page we will show how to write a simple module for controlling example toaster.
- Note
- To install libnetconf follow the instructions on the Compilation and Installation page.
Preparations
In this example we will work with the data model of the toaster provided by Andy Bierman at NETCONF CENTRAL (http://dld.netconfcentral.org/src/toaster@2009-11-20.yang).
First, we need to identify important parts of the configuration data. Since the toaster data model describes only one configurable element, we have an easy choice. So, we can create the 'paths_file' file containing the specification of our chosen element and mapping prefixes with URIs for any used namespace.
Our file may look like this (irrespective of order):
1 toaster=http://netconfcentral.org/ns/toaster
Generating code
- Create a new directory for the toaster module and move the data model and the path file into it:
1 $ mkdir toaster && cd toaster/
2 $ mv ../toaster@2009-11-20.yang ../paths_file .
- Run lnctool(1) for transapi:
1 $ lnctool --model ./toaster@2009-11-20.yang transapi --paths ./paths_file
Besides the generated source code of our transAPI module and GNU Build System files (Makefile.in, configure.in,...), lnctool(1) also generates YIN format of the data model and validators accepted by the libnetconf's ncds_new_transapi() and ncds_set_validation() functions:
- *.yin - YIN format of the data model
- *.rng - RelagNG schema for syntax validation
- *-schematron.xsl - Schematron XSL stylesheet for semantics validation
The data model can define various feature
s and use them via the if-feature
clauses. By default, all features are enabled for the validators. If you plan to to implement only a specific set (or none) of features, specify it to using the --feature
` option (that can be used multiple times). The value has the following syntax:
1 --feature module_name:feature_to_enable
If you want to disable all features of the module, use the following syntax:
Augmenting module
When you are adding a model augmenting the original model, you have generally 2 ways of doing so:
- Create a new transAPI module implementing the original model with any augments, basically treating it as a single model. This way you receive a standalone transAPI module that will make the original module obsolete. lnctool(1) command:
1 $ lnctool --model <original_model> --augment-model <augment_model> transapi --path <paths_for_original_and_augment_model>
- Create a new transAPI module implementing only the augmented parts. This way you receive an additional module that will be used together with the original module, which does not need to be modified in any way. lnctool(1) command:
1 $ lnctool --model <augment_model> transapi --path <paths_for_augment_model>
However, the case when a model is augmenting na RPC in the original model must be treated specially. Firstly, ONLY the first way of augmenting a module can and MUST be used. Secondly, after issuing the lnctool(1) command, the generated code will be INCORRECT and must be changed manually for it to work properly. Illustrated on an example:
The original model has an RPC 'my-rpc' with a single argument 'arg1'. Augment model is adding another argument 'arg2'. The original module 'my-rpc' code and the newly generated code will be the same:
nc_reply *rpc_my_rpc(xmlNodePtr input[])
{
xmlNodePtr arg1 = input[0];
return NULL;
}
.callbacks = {
{.name="my-rpc", .func=rpc_my_rpc, .arg_count=1, .arg_order={"arg1"}}
}
};
To be able to work with the second argument 'arg2', the code must be changed to:
nc_reply *rpc_my_rpc(xmlNodePtr input[])
{
xmlNodePtr arg1 = input[0];
xmlNodePtr arg2 = input[1];
return NULL;
}
.callbacks = {
{.name="my-rpc", .func=rpc_my_rpc, .arg_count=2, .arg_order={"arg1", "arg2"}}
}
};
This pattern can be used with several augment models, all changing a single RPC.
Filling up functionality
Here we show the simplest example of a toaster simulating module. It is working but does not deal with multiple access and threads correctly. Better example may can be found in the netopeer-server-sl source codes located in the Netopeer project repository (server-sl/toaster/toaster.c).
- Open 'toaster.c' file with your favorite editor:
- Add global variables and auxiliary functions. This is completely up to you, libnetconf does not work with this anyway.
enum {ON, OFF, BUSY} status;
pthread_t thread;
void * auxiliary_make_toast(void * time)
{
sleep(*(int*)time);
if (status == BUSY) {
status = ON;
}
return(NULL);
}
- Complete the 'transapi_init()' function with actions that will be run right after the module loads and before any other function in the module is called.
The 'running' parameter can optionally return the current configuration state of the device as the 'transapi_init()' detects it. The configuration must correspond with the device data model and it is supposed to contain only the configuration data (defined with 'config true`). The returned data are then compared with the startup configuration and only the diverging values are set according to the startup content using the appropriate transAPI callback functions.
We ignore it in our example - the toaster is on each start without the root element, which means that it is supposed to be switched off. Then it is up to the startup content if the toaster will be turned on.
int transapi_init(xmlDocPtr * running)
{
status = OFF;
printf("Toaster initialized!\n");
return(EXIT_SUCCESS);
}
- Locate the 'transapi_close()' function and fill it with actions that will be run just before the module unloads. No other function of the transAPI module is called after the 'transapi_close()'.
void transapi_close()
{
printf("Toaster ready for unplugging!\n");
}
- Fill 'get_state_data()' function. This function returns (only!) the state data (defined with 'config false').
char * get_state_data(char * model, char * running, struct nc_err **err)
{
return strdup("<?xml version="1.0"?><toaster xmlns="http:
}
- Complete the configuration callbacks (they have the
callback_
prefix). The 'op' parameter can be used to determine operation which was done with the node. Parameter 'node' holds a copy of node after change (or before change if op == XMLDIFF_REM).
More detailed information about the callback parameters can be found above in the Understanding callback parameters section.
int callback_toaster_toaster (
void ** data,
XMLDIFF_OP op, xmlNodePtr node,
struct nc_err** error)
{
if (op & XMLDIFF_ADD) {
status = ON;
} else if (op & XMLDIFF_REM) {
status = OFF;
} else {
return(EXIT_FAILURE);
}
return(EXIT_SUCCESS);
}
- Fill the RPC message callback functions with the code that will be run when an RPC message with the defined operation arrives.
nc_reply * rpc_make_toast (xmlNodePtr input[])
{
xmlNodePtr toasterDoneness = input[0];
xmlNodePtr toasterToastType = input[1];
int doneness = atoi(xmlNodeGetContent(toasterDoneness));
if (status == ON) {
status = BUSY;
pthread_create(&thread, NULL, auxiliary_make_toast, (void*)&doneness);
pthread_detach(thread);
} else {
}
return(reply);
}
nc_reply * rpc_cancel_toast (xmlNodePtr input[])
{
if (status == BUSY) {
status = ON;
} else {
}
return(reply);
}
- Optionally, you can set monitoring for some external configuration file.
Let's say, that our toaster has a textual configuration located in the /etc/toaster.conf
file. libnetconf can monitor this file for modification and whenever an external application changes content of the file, the specified callback is executed. It's up to the callback function to open the file for reading and update get the current configuration data.
int example_callback(const char *filepath, xmlDocPtr *running, int* execflag)
{
*running = NULL;
*execflag = 0;
return(EXIT_SUCCESS);
}
.callbacks = {{.path = "/etc/toaster.conf", .func = example_callback}}
};
Here is the description of the callback function parameters:
- const char *filepath - input parameter providing the path to the changed file
- xmlDocPtr *edit_config - output parameter to return content for the
edit-config
operation to change the content of the NETCONF running datastore.
- int *exec - output parameter to set if the performed changes should cause execution of the regular transAPI callbacks. If set to
0
, the changes are only reflected in the running configuration datastore, but no transAPI callback is executed.
- Done
Compiling module
Following sequence of commands will produce the shared library 'toaster.so' which may be loaded into libnetconf:
Integrating to a server
In a server we use libnetconf's function ncds_new_transapi() instead of ncds_new() to create a transAPI-capable data store. Then, you do not need to process any data-writing (edit-config, copy-config, delete-config, lock, unlock), data-reading (get, get-config) or module data-model-defined RPC operations. All these operations are processed inside the ncds_apply_rpc2all() function.