Merge branch dispatcher and broke out pagination callbacks to use it

* Merge branch 'dcornejo-master'
* Broke out pagination callback API from state data callbacks
  * New pagination callback API uses new dispatcher from netgate, thanks @dcornejo
   * Register callback with: `clixon_pagination_cb_register()`
   * Use accessor functions `pagination_offset()`, `pagination_limit()`, etc
 * Reverted state data callback API to pre-5.3 (see C/CLI API changes below)
This commit is contained in:
Olof hagsand 2021-10-07 09:17:25 +02:00
commit ce06f25be7
19 changed files with 996 additions and 123 deletions

1
.gitignore vendored
View file

@ -73,3 +73,4 @@ test/vagrant/site.mk
test/cicd/site.mk
doc/html
.idea/

View file

@ -34,6 +34,16 @@
## 5.4.0
Expected: November, 2021
Thanks netgate for providing the dispatcher code!
### New features
* Broke out pagination callback API from state data callbacks
* New pagination callback API uses new dispatcher from netgate, thanks @dcornejo
* Register callback with: `clixon_pagination_cb_register()`
* Use accessor functions `pagination_offset()`, `pagination_limit()`, etc
* Reverted state data callback API to pre-5.3 (see C/CLI API changes below)
### API changes on existing protocol/config features
Users may have to change how they access the system
@ -41,8 +51,24 @@ Users may have to change how they access the system
* NETCONF hello errors, such as wrong session-id, prefix, namespace terminates session
* Instead of returning an rpc-error reply
### C/CLI-API changes on existing features
Developers may need to change their code
* Statedata plugin callbacks are reverted to pre-5.3:
* This has been done as a consequence of breaking out the pagination state API as a separate API.
* The reverted state data callback signature is as follows:
```
int statedata(clicon_handle h,
cvec *nsc,
char *xpath,
cxobj *xstate)
```
### Minor features
* Added set/get pointer API to clixon_data:
* clicon_ptr_get(), clicon_ptr_set(),
* Restconf YANG PATCH according to RFC 8072
* Changed YANG PATCH enabling:
* Now: `./configure --enable-yang-patch`
@ -109,6 +135,7 @@ Users may have to change how they access the system
Developers may need to change their code
* You need to change all statedata plugin callback for the new pagination feature
* NOTE THIS CHANGE IS REVERTED IN 5.4
* If you dont use pagination you can ignore the values of the new parameters
* The updated callback signature is as follows:
```

View file

@ -212,7 +212,6 @@ get_client_statedata(clicon_handle h,
/* Add default global state */
if (xml_global_defaults(h, *xret, nsc, xpath, yspec, 1) < 0)
goto done;
if (clicon_option_bool(h, "CLICON_STREAM_DISCOVERY_RFC5277")){
if ((ymod = yang_find_module_by_name(yspec, "clixon-rfc5277")) == NULL){
clicon_err(OE_YANG, ENOENT, "yang module clixon-rfc5277 not found");
@ -258,7 +257,7 @@ get_client_statedata(clicon_handle h,
goto fail;
}
/* Use plugin state callbacks */
if ((ret = clixon_plugin_statedata_all(h, yspec, nsc, xpath, PAGINATION_NONE, 0, 0, NULL, xret)) < 0)
if ((ret = clixon_plugin_statedata_all(h, yspec, nsc, xpath, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
@ -490,7 +489,7 @@ get_list_pagination(clicon_handle h,
}
if (yang_keyword_get(ylist) != Y_LIST &&
yang_keyword_get(ylist) != Y_LEAF_LIST){
if (netconf_invalid_value(cbret, "application", "list-pagination is enabled but target is not leaf or leaf-list") < 0)
if (netconf_invalid_value(cbret, "application", "list-pagination is enabled but target is not list or leaf-list") < 0)
goto done;
goto ok;
}
@ -539,35 +538,37 @@ get_list_pagination(clicon_handle h,
/* where */
if (ret && (x = xml_find_type(xe, NULL, "where", CX_ELMNT)) != NULL)
where = xml_body(x);
/* Build a "predicate" cbuf
* This solution uses xpath predicates to translate "limit" and "offset" to
* relational operators <>.
*/
if ((cbpath = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
/* This uses xpath. Maybe limit should use parameters */
if (xpath)
cprintf(cbpath, "%s", xpath);
else
cprintf(cbpath, "/");
if (where)
cprintf(cbpath, "[%s]", where);
if (offset){
cprintf(cbpath, "[%u <= position()", offset);
if (limit)
cprintf(cbpath, " and position() < %u", limit+offset);
cprintf(cbpath, "]");
}
else if (limit)
cprintf(cbpath, "[position() < %u]", limit);
/* Append predicate to original xpath and replace it */
xpath2 = cbuf_get(cbpath);
/* Read config */
switch (content){
case CONTENT_CONFIG: /* config data only */
case CONTENT_ALL: /* both config and state */
/* Build a "predicate" cbuf
* This solution uses xpath predicates to translate "limit" and "offset" to
* relational operators <>.
*/
if ((cbpath = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
/* This uses xpath. Maybe limit should use parameters */
if (xpath)
cprintf(cbpath, "%s", xpath);
else
cprintf(cbpath, "/");
if (where)
cprintf(cbpath, "[%s]", where);
if (offset){
cprintf(cbpath, "[%u <= position()", offset);
if (limit)
cprintf(cbpath, " and position() < %u", limit+offset);
cprintf(cbpath, "]");
}
else if (limit)
cprintf(cbpath, "[position() < %u]", limit);
/* Append predicate to original xpath and replace it */
xpath2 = cbuf_get(cbpath);
/* specific xpath */
if (xmldb_get0(h, db, YB_MODULE, nsc, xpath2?xpath2:"/", 1, &xret, NULL, NULL) < 0) {
if ((cbmsg = cbuf_new()) == NULL){
@ -603,11 +604,12 @@ get_list_pagination(clicon_handle h,
pagmode = PAGINATION_LOCK;
else
pagmode = PAGINATION_STATELESS;
/* Use plugin state callbacks */
if ((ret = clixon_plugin_statedata_all(h, yspec, nsc, xpath,
pagmode,
offset, limit, &remaining, &xret)) < 0)
if ((ret = clixon_pagination_cb_call(h, xpath, pagmode,
offset, limit, &remaining,
xret)) < 0)
goto done;
if (ret == 0)
goto ok;
}
/* Help function to filter out anything that is outside of xpath */
if (filter_xpath_again(h, yspec, xret, xpath, nsc, &x1) < 0)

View file

@ -253,10 +253,6 @@ clixon_plugin_statedata_one(clixon_plugin_t *cp,
clicon_handle h,
cvec *nsc,
char *xpath,
pagination_mode_t pagmode,
uint32_t offset,
uint32_t limit,
uint32_t *remaining,
cxobj **xp)
{
int retval = -1;
@ -267,7 +263,7 @@ clixon_plugin_statedata_one(clixon_plugin_t *cp,
if ((fn = clixon_plugin_api_get(cp)->ca_statedata) != NULL){
if ((x = xml_new(DATASTORE_TOP_SYMBOL, NULL, CX_ELMNT)) == NULL)
goto done;
if (fn(h, nsc, xpath, pagmode, offset, limit, remaining, x) < 0){
if (fn(h, nsc, xpath, x) < 0){
if (clicon_errno < 0)
clicon_log(LOG_WARNING, "%s: Internal error: State callback in plugin: %s returned -1 but did not make a clicon_err call",
__FUNCTION__, clixon_plugin_name_get(cp));
@ -306,10 +302,6 @@ clixon_plugin_statedata_all(clicon_handle h,
yang_stmt *yspec,
cvec *nsc,
char *xpath,
pagination_mode_t pagmode,
uint32_t offset,
uint32_t limit,
uint32_t *remaining,
cxobj **xret)
{
int retval = -1;
@ -321,8 +313,7 @@ clixon_plugin_statedata_all(clicon_handle h,
clicon_debug(1, "%s", __FUNCTION__);
while ((cp = clixon_plugin_each(h, cp)) != NULL) {
if ((ret = clixon_plugin_statedata_one(cp, h, nsc, xpath, pagmode,
offset, limit, remaining, &x)) < 0)
if ((ret = clixon_plugin_statedata_one(cp, h, nsc, xpath, &x)) < 0)
goto done;
if (ret == 0){
if ((cberr = cbuf_new()) == NULL){
@ -455,6 +446,63 @@ clixon_plugin_lockdb_all(clicon_handle h,
return retval;
}
/*! Traverse state data callbacks
*
* @param[in] h Clixon handle
* @param[in] xpath Registered XPath using canonical prefixes
*/
int
clixon_pagination_cb_call(clicon_handle h,
char *xpath,
pagination_mode_t pagmode,
uint32_t offset,
uint32_t limit,
uint32_t *remaining,
cxobj *xstate)
{
int retval = -1;
pagination_data_t pd = {pagmode, offset, limit, 0, xstate};
dispatcher_entry_t *htable = NULL;
clicon_ptr_get(h, "pagination-entries", (void**)&htable);
if (htable && dispatcher_call_handlers(htable, h, xpath, &pd) < 0)
goto done;
if (remaining)
*remaining = pd.pd_remaining;
retval = 1;
done:
return retval;
}
/*! Register a state data callback
*
* @param[in] h Clixon handle
* @param[in] fn Callback
* @param[in] xpath Registered XPath using canonical prefixes
* @param[in] arg Domain-specific argument to send to callback
*/
int
clixon_pagination_cb_register(clicon_handle h,
handler_function fn,
char *xpath,
void *arg)
{
int retval = -1;
dispatcher_definition x = {xpath, fn};
dispatcher_entry_t *htable = NULL;
clicon_ptr_get(h, "pagination-entries", (void**)&htable);
if (dispatcher_register_handler(&htable, &x) < 0){
clicon_err(OE_PLUGIN, errno, "dispatcher");
goto done;
}
if (clicon_ptr_set(h, "pagination-entries", htable) < 0)
goto done;
retval = 0;
done:
return retval;
}
/*! Create and initialize a validate/commit transaction
* @retval td New alloced transaction,
* @retval NULL Error
@ -885,4 +933,3 @@ plugin_transaction_abort_all(clicon_handle h,
retval = 0;
return retval;
}

View file

@ -51,6 +51,7 @@
* It is up to the validate callbacks to ensure that these changes are OK
* It is up to the commit callbacks to enforce these changes in the "state" of
* the system.
* see also transaction_data in clixon_plugin.h
*/
typedef struct {
uint64_t td_id; /* Transaction id */
@ -66,6 +67,21 @@ typedef struct {
int td_clen; /* Changed xml vector length */
} transaction_data_t;
/*! Pagination userdata
* @param[in] pagmode List pagination mode
* @param[in] offset Offset, for list pagination
* @param[in] limit Limit, for list pagination
* @param[out] remaining Remaining elements (if limit is non-zero)
* see also pagination_data in clixon_plugin.h
*/
typedef struct {
pagination_mode_t pd_pagmode; /* Pagination mode, stateless or locked */
uint32_t pd_offset; /* Start of pagination interval */
uint32_t pd_limit; /* Number of elemenents (limit) */
uint32_t pd_remaining; /* If limit, then remaining nr of elements */
cxobj *pd_xstate; /* Returned xml state tree */
} pagination_data_t;
/*
* Prototypes
*/
@ -76,11 +92,14 @@ int clixon_plugin_pre_daemon_all(clicon_handle h);
int clixon_plugin_daemon_all(clicon_handle h);
int clixon_plugin_statedata_all(clicon_handle h, yang_stmt *yspec, cvec *nsc, char *xpath,
pagination_mode_t pagmode,
uint32_t offset, uint32_t limit, uint32_t *remaining,
cxobj **xtop);
int clixon_plugin_lockdb_all(clicon_handle h, char *db, int lock, int id);
int clixon_pagination_cb_register(clicon_handle h, handler_function fn, char *path, void *arg);
int clixon_pagination_cb_call(clicon_handle h, char *xpath, pagination_mode_t pagmode,
uint32_t offset, uint32_t limit, uint32_t *remaining,
cxobj *xstate);
transaction_data_t * transaction_new(void);
int transaction_free(transaction_data_t *);

View file

@ -284,3 +284,59 @@ transaction_log(clicon_handle h,
cbuf_free(cb);
return 0;
}
/*! Get pagination data: mode parameter
*
* @param[in] pd Pagination userdata
* @retval mode Pagination mode, stateless or locked
*/
pagination_mode_t
pagination_pagmode(pagination_data pd)
{
return ((pagination_data_t *)pd)->pd_pagmode;
}
/*! Get pagination data: offset parameter
*
* @param[in] pd Pagination userdata
* @retval offset Start of pagination interval
*/
uint32_t
pagination_offset(pagination_data pd)
{
return ((pagination_data_t *)pd)->pd_offset;
}
/*! Get pagination data: limit parameter
*
* @param[in] pd Pagination userdata
* @retval limit Number of elemenents (limit)
*/
uint32_t
pagination_limit(pagination_data pd)
{
return ((pagination_data_t *)pd)->pd_limit;
}
/*! Set pagination data: remaining nr of elements
*
* @param[in] pd Pagination userdata
* @param[in] remaining If limit, then remaining nr of elements
*/
int
pagination_remaining_set(pagination_data pd,
uint32_t remaining)
{
return ((pagination_data_t *)pd)->pd_remaining = remaining;
}
/*! Get pagination data: Returned xml state tree
*
* @param[in] pd Pagination userdata
* @retval xstate Returned xml state tree
*/
cxobj*
pagination_xstate(pagination_data pd)
{
return ((pagination_data_t *)pd)->pd_xstate;
}

View file

@ -46,7 +46,6 @@
* Prototypes
*/
/* Transaction callback data accessors for client plugins
* (defined in config_dbdep.c)
* @see transaction_data_t internal structure
*/
uint64_t transaction_id(transaction_data td);
@ -65,4 +64,13 @@ size_t transaction_clen(transaction_data td);
int transaction_print(FILE *f, transaction_data th);
int transaction_log(clicon_handle h, transaction_data th, int level, const char *id);
/* Pagination callbacks
* @see pagination_data_t internal structure
*/
pagination_mode_t pagination_pagmode(pagination_data pd);
uint32_t pagination_offset(pagination_data pd);
uint32_t pagination_limit(pagination_data pd);
int pagination_remaining_set(pagination_data pd, uint32_t remaining);
cxobj *pagination_xstate(pagination_data pd);
#endif /* _CLIXON_BACKEND_TRANSACTION_H_ */

View file

@ -66,7 +66,7 @@
#include <clixon/clixon_backend.h>
/* Command line options to be passed to getopt(3) */
#define BACKEND_EXAMPLE_OPTS "rsS:iuUt:v:"
#define BACKEND_EXAMPLE_OPTS "rsS:x:iuUt:v:"
/*! Variable to control if reset code is run.
* The reset code inserts "extra XML" which assumes ietf-interfaces is
@ -87,6 +87,13 @@ static int _state = 0;
*/
static char *_state_file = NULL;
/*! XPath to register for pagination state XML from file,
* if _state is true -- -sS <file> -x <xpath>
* Primarily for testing
* Start backend with -- -sS <file> -x <xpath>
*/
static char *_state_xpath = NULL;
/*! Read state file init on startup instead of on request
* Primarily for testing
* Start backend with -- -siS <file>
@ -353,10 +360,6 @@ example_copy_extra(clicon_handle h, /* Clicon handle */
* @param[in] h Clicon handle
* @param[in] nsc External XML namespace context, or NULL
* @param[in] xpath String with XPATH syntax. or NULL for all
* @param[in] pagmode List pagination (not used here)
* @param[in] offset Offset, for list pagination
* @param[in] limit Limit, for list pagination
* @param[out] remaining Remaining elements (if limit is non-zero)
* @param[out] xstate XML tree, <config/> on entry.
* @retval 0 OK
* @retval -1 Error
@ -376,10 +379,6 @@ int
example_statedata(clicon_handle h,
cvec *nsc,
char *xpath,
pagination_mode_t pagmode,
uint32_t offset,
uint32_t limit,
uint32_t *remaining,
cxobj *xstate)
{
int retval = -1;
@ -462,9 +461,6 @@ example_statedata(clicon_handle h,
* @param[in] h Clicon handle
* @param[in] nsc External XML namespace context, or NULL
* @param[in] xpath String with XPATH syntax. or NULL for all
* @param[in] pagmode List pagination mode
* @param[in] offset Offset, for list pagination
* @param[in] limit Limit, for list pagination
* @param[out] xstate XML tree, <config/> on entry. Copy to this
* @retval 0 OK
* @retval -1 Error
@ -472,14 +468,10 @@ example_statedata(clicon_handle h,
* @see example_statefile where state is programmatically added
*/
int
example_statefile(clicon_handle h,
cvec *nsc,
char *xpath,
pagination_mode_t pagmode,
uint32_t offset,
uint32_t limit,
uint32_t *remaining,
cxobj *xstate)
example_statefile(clicon_handle h,
cvec *nsc,
char *xpath,
cxobj *xstate)
{
int retval = -1;
cxobj **xvec = NULL;
@ -489,8 +481,6 @@ example_statefile(clicon_handle h,
yang_stmt *yspec = NULL;
FILE *fp = NULL;
cxobj *x1;
uint32_t lower;
uint32_t upper;
int ret;
/* If -S is set, then read state data from file */
@ -511,6 +501,100 @@ example_statefile(clicon_handle h,
if (_state_file_cached)
_state_xml_cache = xt;
}
if (_state_file_cached)
xt = _state_xml_cache;
if (xpath_vec(xt, nsc, "%s", &xvec, &xlen, xpath) < 0)
goto done;
/* Mark elements to copy:
* For every node found in x0, mark the tree as changed
*/
for (i=0; i<xlen; i++){
if ((x1 = xvec[i]) == NULL)
break;
xml_flag_set(x1, XML_FLAG_MARK);
xml_apply_ancestor(x1, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_CHANGE);
}
if (xml_copy_marked(xt, xstate) < 0) /* Copy the marked elements */
goto done;
/* Unmark original tree */
if (xml_apply(xt, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, (void*)(XML_FLAG_MARK|XML_FLAG_CHANGE)) < 0)
goto done;
/* Unmark returned state tree */
if (xml_apply(xstate, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, (void*)(XML_FLAG_MARK|XML_FLAG_CHANGE)) < 0)
goto done;
if (_state_file_cached)
xt = NULL; /* ensure cache is not cleared */
ok:
retval = 0;
done:
if (fp)
fclose(fp);
if (xt)
xml_free(xt);
if (xvec)
free(xvec);
return retval;
}
/*! Example of state pagination callback and how to use pagination_data
*
* @param[in] h Generic handler
* @param[in] xpath Registered XPath using canonical prefixes
* @param[in] userargs Per-call user arguments
* @param[in] arg Per-path user argument
*/
int
example_pagination(void *h0,
char *xpath,
pagination_data pd,
void *arg)
{
int retval = -1;
clicon_handle h = (clicon_handle)h0;
pagination_mode_t pagmode;
uint32_t offset;
uint32_t limit;
uint32_t remaining;
cxobj *xstate;
cxobj **xvec = NULL;
size_t xlen = 0;
int i;
cxobj *xt = NULL;
yang_stmt *yspec = NULL;
FILE *fp = NULL;
cxobj *x1;
uint32_t lower;
uint32_t upper;
int ret;
cvec *nsc;
/* If -S is set, then read state data from file */
if (!_state || !_state_file)
goto ok;
pagmode = pagination_pagmode(pd);
offset = pagination_offset(pd);
limit = pagination_limit(pd);
xstate = pagination_xstate(pd);
/* Get canonical namespace context */
if (xml_nsctx_yangspec(yspec, &nsc) < 0)
goto done;
yspec = clicon_dbspec_yang(h);
/* Read state file if either not cached, or the cache is NULL */
if (_state_file_cached == 0 ||
_state_xml_cache == NULL){
if ((fp = fopen(_state_file, "r")) == NULL){
clicon_err(OE_UNIX, errno, "open(%s)", _state_file);
goto done;
}
if ((xt = xml_new("config", NULL, CX_ELMNT)) == NULL)
goto done;
if ((ret = clixon_xml_parse_file(fp, YB_MODULE, yspec, &xt, NULL)) < 0)
goto done;
if (_state_file_cached)
_state_xml_cache = xt;
}
if (_state_file_cached)
xt = _state_xml_cache;
if (xpath_vec(xt, nsc, "%s", &xvec, &xlen, xpath) < 0)
@ -530,7 +614,8 @@ example_statefile(clicon_handle h,
else{
if ((upper = offset+limit)>xlen)
upper = xlen;
*remaining = xlen - upper;
remaining = xlen - upper;
pagination_remaining_set(pd, remaining);
}
break;
}
@ -1166,7 +1251,9 @@ clixon_plugin_init(clicon_handle h)
break;
case 'S': /* state file (requires -s) */
_state_file = optarg;
api.ca_statedata = example_statefile; /* Switch state data callback */
break;
case 'x': /* state xpath (requires -sS) */
_state_xpath = optarg;
break;
case 'i': /* read state file on init not by request (requires -sS <file> */
_state_file_cached = 1;
@ -1185,6 +1272,18 @@ clixon_plugin_init(clicon_handle h)
break;
}
if (_state_file){
api.ca_statedata = example_statefile; /* Switch state data callback */
if (_state_xpath){
/* State pagination callbacks */
if (clixon_pagination_cb_register(h,
example_pagination,
_state_xpath,
NULL) < 0)
goto done;
}
}
/* Example stream initialization:
* 1) Register EXAMPLE stream
* 2) setup timer for notifications, so something happens on stream

View file

@ -180,10 +180,6 @@ int
nacm_statedata(clicon_handle h,
cvec *nsc,
char *xpath,
pagination_mode_t pagmode,
uint32_t offset,
uint32_t limit,
uint32_t *remaining,
cxobj *xstate)
{
int retval = -1;
@ -210,7 +206,7 @@ static clixon_plugin_api api = {
clixon_plugin_init, /* init */
NULL, /* start */
NULL, /* exit */
.ca_statedata=nacm_statedata, /* statedata */
.ca_statedata=nacm_statedata, /* statedata */
.ca_trans_begin=nacm_begin, /* trans begin */
.ca_trans_validate=nacm_validate, /* trans validate */
.ca_trans_complete=nacm_complete, /* trans complete */

View file

@ -108,6 +108,7 @@ extern "C" {
#include <clixon/clixon_xml_nsctx.h>
#include <clixon/clixon_xml_vec.h>
#include <clixon/clixon_client.h>
#include <clixon/clixon_dispatcher.h>
/*
* Global variables generated by Makefile

View file

@ -61,10 +61,15 @@ typedef struct {
/*
* Prototypes
*/
/* Generic clixon data API the form <name>=<val> where <val> is string */
int clicon_data_get(clicon_handle h, const char *name, char **val);
int clicon_data_set(clicon_handle h, const char *name, char *val);
int clicon_data_del(clicon_handle h, const char *name);
int clicon_ptr_get(clicon_handle h, const char *name, void **ptr);
int clicon_ptr_set(clicon_handle h, const char *name, void *ptr);
int clicon_ptr_del(clicon_handle h, const char *name);
cvec *clicon_data_cvec_get(clicon_handle h, const char *name);
int clicon_data_cvec_set(clicon_handle h, const char *name, cvec *cvv);
int clicon_data_cvec_del(clicon_handle h, const char *name);

View file

@ -0,0 +1,75 @@
/*
* Copyright 2021 Rubicon Communications LLC (Netgate)
* @see https://github.com/dcornejo/dispatcher
*/
#ifndef DISPATCH_DISPATCHER_H
#define DISPATCH_DISPATCHER_H
/*! prototype for a function to handle a path
* minimally needs the path it's working on, but probably
* we want to hand down cached data somehow
* @param[in] h Generic handler
* @param[in] xpath Registered XPath using canonical prefixes
* @param[in] userargs Per-call user arguments
* @param[in] arg Per-path user argument
*/
typedef int (*handler_function)(void *handle, char *path, void *userargs, void *arg);
/*
* this structure is used to map a handler to a path
*/
typedef struct {
char *dd_path;
handler_function dd_handler;
} dispatcher_definition;
/*
* the dispatcher_entry_t is the structure created from
* the registered dispatcher_definitions
*/
struct _dispatcher_entry;
typedef struct _dispatcher_entry dispatcher_entry_t;
struct _dispatcher_entry {
/*
* the name of this node, NOT the complete path
*/
char *node_name;
/*
* peer points at peer to the right of this one
* if NULL then this is the rightmost and last on list
*/
dispatcher_entry_t *peer;
/*
* peer_head points at leftmost peer at this level
* if NULL, then this is the leftmost and first on the list
*/
dispatcher_entry_t *peer_head;
/*
* points at peer_head of children list
* if NULL, then no children
*/
dispatcher_entry_t *children;
/*
* pointer to handler function for this node
*/
handler_function handler;
/*
* End-user argument
*/
void *arg;
};
/*
* Prototypes
*/
int dispatcher_register_handler(dispatcher_entry_t **root, dispatcher_definition *x);
int dispatcher_call_handlers(dispatcher_entry_t *root, void *handle, char *path, void *user_args);
#endif /* DISPATCH_DISPATCHER_H */

View file

@ -217,24 +217,21 @@ enum pagination_mode{
typedef enum pagination_mode pagination_mode_t;
/* Plugin statedata
* @param[in] Clicon handle
* @param[in] h Clicon handle
* @param[in] xpath Part of state requested
* @param[in] nsc XPATH namespace context.
* @param[in] pagmode List pagination mode
* @param[in] offset Offset, for list pagination
* @param[in] limit Limit, for list pagination
* @param[out] remaining Remaining elements (if limit is non-zero)
* @param[out] xtop XML tree where statedata is added
* @retval -1 Fatal error
* @retval 0 OK
*/
typedef int (plgstatedata_t)(clicon_handle h, cvec *nsc, char *xpath,
pagination_mode_t pagmode,
uint32_t offset, uint32_t limit,
uint32_t *remaining,
cxobj *xtop);
typedef int (plgstatedata_t)(clicon_handle h, cvec *nsc, char *xpath, cxobj *xtop);
/*! Lock databse status has changed status
/* Pagination-data type
* @see pagination_data_t in clixon_backend_transaction.h for full pagination API
*/
typedef void *pagination_data;
/*! Lock database status has changed status
* @param[in] h Clixon handle
* @param[in] db Database name (eg "running")
* @param[in] lock Lock status: 0: unlocked, 1: locked

View file

@ -84,7 +84,8 @@ SRC = clixon_sig.c clixon_uid.c clixon_log.c clixon_err.c clixon_event.c \
clixon_xpath.c clixon_xpath_ctx.c clixon_xpath_eval.c clixon_xpath_function.c \
clixon_xpath_optimize.c clixon_xpath_yang.c \
clixon_datastore.c clixon_datastore_write.c clixon_datastore_read.c \
clixon_netconf_lib.c clixon_stream.c clixon_nacm.c clixon_client.c clixon_netns.c
clixon_netconf_lib.c clixon_stream.c clixon_nacm.c clixon_client.c clixon_netns.c \
clixon_dispatcher.c
YACCOBJS = lex.clixon_xml_parse.o clixon_xml_parse.tab.o \
lex.clixon_yang_parse.o clixon_yang_parse.tab.o \

View file

@ -134,6 +134,66 @@ clicon_data_del(clicon_handle h,
return clicon_hash_del(cdat, (char*)name);
}
/*! Get generic clixon data on the form <name>=<ptr> where <ptr> is void*
* @param[in] h Clicon handle
* @param[in] name Data name
* @param[out] val Data value as string
* @retval 0 OK
* @retval -1 Not found (or error)
* @see clicon_option_str
*/
int
clicon_ptr_get(clicon_handle h,
const char *name,
void **ptr)
{
clicon_hash_t *cdat = clicon_data(h);
void *p;
size_t vlen;
if (clicon_hash_lookup(cdat, (char*)name) == NULL)
return -1;
if (ptr){
p = clicon_hash_value(cdat, (char*)name, &vlen);
memcpy(ptr, p, vlen);
}
return 0;
}
/*! Set generic clixon data on the form <name>=<ptr> where <ptr> is void*
* @param[in] h Clicon handle
* @param[in] name Data name
* @param[in] val Data value as null-terminated string
* @retval 0 OK
* @retval -1 Error
* @see clicon_option_str_set
*/
int
clicon_ptr_set(clicon_handle h,
const char *name,
void *ptr)
{
clicon_hash_t *cdat = clicon_data(h);
return clicon_hash_add(cdat, (char*)name, &ptr, sizeof(ptr))==NULL?-1:0;
}
/*! Delete generic clixon data
* @param[in] h Clicon handle
* @param[in] name Data name
* @retval 0 OK
* @retval -1 Error
* @see clicon_option_del
*/
int
clicon_ptr_del(clicon_handle h,
const char *name)
{
clicon_hash_t *cdat = clicon_data(h);
return clicon_hash_del(cdat, (char*)name);
}
/*! Get generic cligen variable vector (cvv) on the form <name>=<val> where <val> is cvv
*
* @param[in] h Clicon handle
@ -201,7 +261,7 @@ clicon_data_cvec_del(clicon_handle h,
return clicon_hash_del(cdat, (char*)name);
}
/*!
/*! Get data yangspec, yspec
* @param[in] h Clicon handle
* @retval yspec Yang spec
* @see clicon_config_yang for the configuration yang

411
lib/src/clixon_dispatcher.c Normal file
View file

@ -0,0 +1,411 @@
/*
* Copyright 2021 Rubicon Communications LLC (Netgate)
* @see https://github.com/dcornejo/dispatcher
*/
/*
* we start with a series of dispatcher_definitions, which are a
* path and handler.
*
* we break the path up into elements and build a tree out of them
* example:
*
* we start with two paths /a/b/c and /a/d with handler_c() and
* handler_d() as their handlers respectively.
*
* this produces a tree like this:
*
* [/] root_handler()
* [a] NULL
* [b] NULL
* [c] handler_c()
* [d] handler_d()
*
* NULL means that there is no handler defined - if the terminal
* element of the path has a NULL handler then you look for the
* closest ancestor that does.
*
* for example, if I lookup /a/b I get back a pointer to root_handler()
* if i lookup /a/d, I get handler_d().
*
* if a element has a key (/a/b=c) then the list element is
* marked with an = sign and without the key
* so /a/b=c creates multiple entries:
*
* [/]
* [a]
* [b=]
* [b]
*
* NOTE 1: there is not a mechanism to free the created structures since
* it is intended that this tree is created only at startup. if use case
* changes, this function is trivial.
*
* NOTE 2: there is no attempt to optimize list searching here, sorry. I
* do not think that the known use cases will get big enough to make the
* tree get too large. I do not recommend that you encode every possible
* path, just top level key handlers.
*
* there are 2 functions to the API:
* clixon_register_handler(): build the dispatcher table
* clixon_call_handlers(): query the dispatcher table
*/
/*
* Important for writing handlers: a handler must return a complete
* valid response. It must operate in isolation, it must not expect
* any ordering in the calls and [under review] it should not call
* another handler directly or indirectly. Responses must me bound
* to a yang model and properly sorted and indexed.
*/
#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <stdlib.h>
#include <errno.h>
#include "clixon_dispatcher.h"
/* ===== utility routines ==== */
#define PATH_CHUNKS 32
/**
* spilt a path into elements
*
* given an api-path, break it up into chunks separated by '/'
* characters. it is expected that api-paths are URI encoded, so no need
* to deal with unescaped special characters like ', ", and /
*
* @param path [input] path string
* @param plist [output] pointer to split path array
* @param plist_len [output] pointer to storage space for path array length
*/
static void split_path(char *path, char ***plist, size_t *plist_len)
{
size_t allocated = PATH_CHUNKS;
/* don't modify the original copy */
char *work = strdup(path);
char **list = malloc(allocated * sizeof(char *));
memset(list, 0, allocated * sizeof(char *));
size_t len = 0;
char *ptr = work;
if (*ptr == '/') {
char *new_element = strdup("/");
list[len++] = new_element;
ptr++;
}
ptr = strtok(ptr, "/");
while (ptr != NULL) {
if (len > allocated) {
/* we've run out of space, allocate a bigger list */
allocated += PATH_CHUNKS;
list = realloc(list, allocated * sizeof(char *));
}
char *new_element = strdup(ptr);
list[len++] = new_element;
ptr = strtok(NULL, "/");
}
*plist = list;
*plist_len = len;
free(work);
}
/**
* free a split path structure
*
* @param list [input] pointer to split path array
* @param len [input] length of split path array
*/
static void split_path_free(char **list, size_t len)
{
size_t i;
for (i = 0; i < len; i++) {
free(list[i]);
}
free(list);
}
/**
* find a peer of this node by name
* search through the list pointed at by peer
*
* @param node [input] pointer to a node in the peer list
* @param node_name [input] name of node we're looking for
* @return pointer to found node or NULL
*/
static dispatcher_entry_t *find_peer(dispatcher_entry_t *node, char *node_name)
{
if ((node == NULL) || (node_name == NULL)) {
/* protect against idiot users */
return NULL;
}
dispatcher_entry_t *i = node->peer_head;
while (i != NULL) {
if (strcmp(node_name, i->node_name) == 0) {
break;
}
i = i->peer;
}
return i;
}
/**
* add a node as the last node in peer list
*
* @param node [input] pointer to an element of the peer list
* @param name [input] name of new node
* @return pointer to added/existing node
*/
static dispatcher_entry_t *add_peer_node(dispatcher_entry_t *node, char *name)
{
dispatcher_entry_t *new_node = malloc(sizeof(dispatcher_entry_t));
memset(new_node, 0, sizeof(dispatcher_entry_t));
if (node == NULL) {
/* this is a new node */
new_node->node_name = strdup(name);
new_node->peer = NULL;
new_node->children = NULL;
new_node->peer_head = new_node;
return new_node;
} else {
/* possibly adding to the list */
/* search for existing, or get tail end of list */
dispatcher_entry_t *eptr = node->peer_head;
while (eptr->peer != NULL) {
if (strcmp(eptr->node_name, name) == 0) {
return eptr;
}
eptr = eptr->peer;
}
// if eptr->node_name == name, we done
if (strcmp(eptr->node_name, name) == 0) {
return eptr;
}
new_node->node_name = strdup(name);
new_node->peer = NULL;
new_node->children = NULL;
new_node->peer_head = node->peer_head;
eptr->peer = new_node;
return new_node;
}
}
/**
* add a node as a child of this node
*
* this is different from add_peer_node() in that it returns a
* pointer to the head_peer of the children list where the node was
* added.
*
* @param node [input] pointer to parent node of children list
* @param name [input] name of child node
* @return pointer to head of children list
*/
static dispatcher_entry_t *add_child_node(dispatcher_entry_t *node, char *name)
{
dispatcher_entry_t *child_ptr = add_peer_node(node->children, name);
node->children = child_ptr->peer_head;
return child_ptr;
}
/**
*
* @param root
* @param path
* @return
*/
static dispatcher_entry_t *get_entry(dispatcher_entry_t *root, char *path)
{
char **split_path_list = NULL;
size_t split_path_len = 0;
dispatcher_entry_t *ptr = root;
dispatcher_entry_t *best = root;
/* cut the path up into individual elements */
split_path(path, &split_path_list, &split_path_len);
/* some elements may have keys defined, strip them off */
for (int i = 0; i < split_path_len; i++) {
char *kptr = strchr(split_path_list[i], '=');
if ((kptr != NULL) && (*kptr == '=')) {
*(kptr + 1) = 0;
}
}
/* search down the tree */
for (int i = 0; i < split_path_len; i++) {
char *query = split_path_list[i];
ptr = find_peer(ptr, query);
if (ptr == NULL) {
/* we ran out of matches, use last found handler */
return best;
}
if (ptr->handler != NULL) {
/* if handler is defined, save it */
best = ptr;
}
/* skip to next element */
ptr = ptr->children;
}
return best;
}
/**
* given a pointer to an entry, call the handler and all
* descendant and peer handlers.
*
* @param entry
* @param path
* @return
*/
static int
call_handler_helper(dispatcher_entry_t *entry,
void *handle,
char *path,
void *user_args)
{
if (entry->children != NULL) {
call_handler_helper(entry->children, handle, path, user_args);
}
if (entry->peer != NULL) {
call_handler_helper(entry->peer, handle, path, user_args);
}
if (entry->handler != NULL) {
(entry->handler)(handle, path, user_args, entry->arg);
}
return 1;
}
/*
* ===== PUBLIC API FUNCTIONS =====
*/
/*! Register a dispatcher handler
*
* called from initialization code to build a dispatcher tree
*
* @param[in] root Pointer to pointer to dispatch tree
* @param[in] x Handler to registration data
* @retval 0 OK
* @retval -1 Error
*/
int
dispatcher_register_handler(dispatcher_entry_t **root,
dispatcher_definition *x)
{
char **split_path_list = NULL;
size_t split_path_len = 0;
if (*x->dd_path != '/') {
errno = EINVAL;
fprintf(stderr, "%s: part '%s' must start at root\n", __func__, x->dd_path);
return -1;
}
/*
* get the path from the dispatcher_definition, break it
* up to create the elements of the dispatcher table
*/
split_path(x->dd_path, &split_path_list, &split_path_len);
/*
* the first element is always a peer to the top level
*/
dispatcher_entry_t *ptr = *root;
ptr = add_peer_node(ptr, split_path_list[0]);
if (*root == NULL) {
*root = ptr;
}
for (size_t i = 1; i < split_path_len; i++) {
ptr = add_child_node(ptr, split_path_list[i]);
}
/* when we get here, ptr points at last entry added */
if (x->dd_handler != NULL) {
/*
* we're adding/changing a handler
* you could make this an error optionally
*/
if (ptr->handler != NULL) {
printf("%s: warning: replacing existing handler: (%s) %p -> %p\n", __func__,
ptr->node_name, ptr->handler, x->dd_handler);
}
ptr->handler = x->dd_handler;
}
/* clean up */
split_path_free(split_path_list, split_path_len);
return 0;
}
/*! Call the handler and all its descendant handlers
*
* NOTE: There is no guarantee of the order in which handlers
* are called! Any handler must assume that it is called in
* isolation, even if this duplicates work. The right to
* reorder calls by this code is reserved.
*
* @param[in] handle
* @param[in] root
* @param[in] path
* @retval 1 OK
* @retval 0 Invalid
* @retval -1 Error
*/
int
dispatcher_call_handlers(dispatcher_entry_t *root,
void *handle,
char *path,
void *user_args)
{
int ret = 0;
dispatcher_entry_t *best = get_entry(root, path);
if (best->children != NULL) {
call_handler_helper(best->children, handle, path, user_args);
}
if (best->handler != NULL) {
ret = (*best->handler)(handle, path, user_args, best->arg);
}
return ret;
}

View file

@ -2,6 +2,7 @@
# Example-social from draft-netconf-list-pagination-00.txt appendix A.1
# Assumes variable fexample is set to name of yang file
# Note audit-logs/audit-log/outcome is changed from mandatory to default
# Also: leaf-list member/state/numbers is added
cat <<EOF > $fexample
module example-social {
@ -214,6 +215,10 @@ cat <<EOF > $fexample
config false;
description
"Operational state members values.";
leaf-list numbers {
description "config false extension";
type int32;
}
leaf joined {
type yang:date-and-time;
mandatory true;

View file

@ -140,8 +140,8 @@ if [ $BE -ne 0 ]; then
if [ $? -ne 0 ]; then
err
fi
new "start backend -s init -f $cfg -- -sS $fstate"
start_backend -s init -f $cfg -- -sS $fstate
new "start backend -s init -f $cfg -- -sS $fstate -x /lib:global-state"
start_backend -s init -f $cfg -- -sS $fstate -x /lib:global-state
fi
new "waiting"
wait_backend

View file

@ -1,21 +1,24 @@
#!/usr/bin/env bash
# List pagination tests loosely based on draft-wwlh-netconf-list-pagination-00
# The example-social yang file is used
# Three tests to get state pagination data:
# 1. NETCONF get a specific list (alice->numbers)
# 2. NETCONF get two listsspecific list (alice+bob->numbers)
# 3. CLI get audit logs (only interactive)
# This tests contains a large state list: audit-logs from the example
# Only CLI is used
# Magic line must be first in script (see README.md)
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
echo "...skipped: Must run interactvely"
if [ "$s" = $0 ]; then exit 0; else return 0; fi
APPNAME=example
cfg=$dir/conf.xml
fexample=$dir/example-social.yang
fstate=$dir/mystate.xml
xpath=/es:audit-logs/es:audit-log
# For 1M test,m use an external file since the generation takes considerable time
#fstate=~/tmp/mystate.xml
@ -26,8 +29,7 @@ fstate=$dir/mystate.xml
: ${validatexml:=false}
# Number of audit-log entries
: ${perfnr:=20000}
: ${perfnr:=1000}
cat <<EOF > $cfg
<clixon-config xmlns="http://clicon.org/config">
@ -51,8 +53,31 @@ cat <<EOF > $cfg
EOF
# See draft-wwlh-netconf-list-pagination-00 A.2 (only stats and audit-log)
# XXX members not currently used, only audit-logs as generated below
cat<<EOF > $fstate
<members xmlns="http://example.com/ns/example-social">
<member>
<member-id>alice</member-id>
<stats>
<numbers>3</numbers>
<numbers>4</numbers>
<numbers>5</numbers>
<numbers>6</numbers>
<numbers>7</numbers>
<numbers>8</numbers>
</stats>
</member>
<member>
<member-id>bob</member-id>
<stats>
<numbers>13</numbers>
<numbers>14</numbers>
<numbers>15</numbers>
<numbers>16</numbers>
<numbers>17</numbers>
<numbers>18</numbers>
</stats>
</member>
</members>
EOF
# Append generated state data to $fstate file
@ -68,44 +93,82 @@ for (( i=0; i<$perfnr; i++ )); do
echo " <request>POST</request>" >> $fstate
echo " </audit-log>" >> $fstate
done
echo -n "</audit-logs>" >> $fstate # No CR
new "test params: -f $cfg -s init -- -siS $fstate"
# start backend with specific xpath
function testrun_start()
{
xpath=$1
if [ $BE -ne 0 ]; then
new "kill old backend"
sudo clixon_backend -zf $cfg
if [ $? -ne 0 ]; then
err
new "test params: -f $cfg -s init -- -siS $fstate -x $xpath"
if [ $BE -ne 0 ]; then
new "kill old backend"
sudo clixon_backend -zf $cfg
if [ $? -ne 0 ]; then
err
fi
sudo pkill -f clixon_backend # to be sure
new "start backend -s init -f $cfg -- -siS $fstate -X $xpath"
start_backend -s init -f $cfg -- -siS $fstate -x $xpath
fi
sudo pkill -f clixon_backend # to be sure
new "start backend -s init -f $cfg -- -siS $fstate"
start_backend -s init -f $cfg -- -siS $fstate
fi
new "wait backend"
wait_backend
}
new "wait backend"
wait_backend
function testrun_stop()
{
if [ $BE -ne 0 ]; then
new "Kill backend"
# Check if premature kill
pid=$(pgrep -u root -f clixon_backend)
if [ -z "$pid" ]; then
err "backend already dead"
fi
# kill backend
stop_backend -f $cfg
fi
}
testrun_start "/es:members/es:member[es:member-id='alice']/es:stats/es:numbers"
new "NETCONF get leaf-list member/numbers 0-10 alice"
expecteof "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO<rpc $DEFAULTNS><get content=\"nonconfig\"><filter type=\"xpath\" select=\"$xpath\" xmlns:es=\"http://example.com/ns/example-social\"/><list-pagination xmlns=\"http://clicon.org/clixon-netconf-list-pagination\">true</list-pagination><offset xmlns=\"http://clicon.org/clixon-netconf-list-pagination\">0</offset><limit xmlns=\"http://clicon.org/clixon-netconf-list-pagination\">10</limit></get></rpc>]]>]]>" "^<rpc-reply $DEFAULTNS><data><members xmlns=\"http://example.com/ns/example-social\"><member><member-id>alice</member-id><privacy-settings><post-visibility>public</post-visibility></privacy-settings><stats><numbers>3</numbers><numbers>4</numbers><numbers>5</numbers><numbers>6</numbers><numbers>7</numbers><numbers>8</numbers></stats></member></members></data></rpc-reply>]]>]]>$"
# negative
new "NETCONF get container, expect fail"
expecteof "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO<rpc $DEFAULTNS><get content=\"nonconfig\"><filter type=\"xpath\" select=\"/es:members/es:member[es:member-id='alice']/es:stats\" xmlns:es=\"http://example.com/ns/example-social\"/><list-pagination xmlns=\"http://clicon.org/clixon-netconf-list-pagination\">true</list-pagination><offset xmlns=\"http://clicon.org/clixon-netconf-list-pagination\">0</offset><limit xmlns=\"http://clicon.org/clixon-netconf-list-pagination\">10</limit></get></rpc>]]>]]>" "^<rpc-reply $DEFAULTNS><rpc-error><error-type>application</error-type><error-tag>invalid-value</error-tag><error-severity>error</error-severity><error-message>list-pagination is enabled but target is not list or leaf-list</error-message></rpc-error></rpc-reply>]]>]]>$"
testrun_stop
#----------------------------
testrun_start "/es:members/es:member/es:stats/es:numbers"
new "NETCONF get leaf-list member/numbers 0-10 alice"
expecteof "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO<rpc $DEFAULTNS><get content=\"nonconfig\"><filter type=\"xpath\" select=\"$xpath\" xmlns:es=\"http://example.com/ns/example-social\"/><list-pagination xmlns=\"http://clicon.org/clixon-netconf-list-pagination\">true</list-pagination><offset xmlns=\"http://clicon.org/clixon-netconf-list-pagination\">0</offset><limit xmlns=\"http://clicon.org/clixon-netconf-list-pagination\">10</limit></get></rpc>]]>]]>" "^<rpc-reply $DEFAULTNS><data><members xmlns=\"http://example.com/ns/example-social\"><member><member-id>alice</member-id><privacy-settings><post-visibility>public</post-visibility></privacy-settings><stats><numbers>3</numbers><numbers>4</numbers><numbers>5</numbers><numbers>6</numbers><numbers>7</numbers><numbers>8</numbers></stats></member><member><member-id>bob</member-id><privacy-settings><post-visibility>public</post-visibility></privacy-settings><stats><numbers>13</numbers><numbers>14</numbers><numbers>15</numbers><numbers>16</numbers></stats></member></members></data></rpc-reply>]]>]]>$"
testrun_stop
#----------------------------
echo "...skipped: Must run interactvely"
if false; then
testrun_start "/es:audit-logs/es:audit-log"
# XXX How to run without using a terminal? Maybe use expect/unbuffer
new "cli show"
echo "$clixon_cli -1 -f $cfg -l o show pagination xpath /es:audit-logs/es:audit-log cli"
$clixon_cli -1 -f $cfg -l o show pagination xpath /es:audit-logs/es:audit-log cli
#expectpart "$(echo -n | unbuffer -p $clixon_cli -1 -f $cfg -l o show pagination xpath /es:audit-logs/es:audit-log cli)" 0 foo
$clixon_cli -1 -f $cfg -l o show pagination xpath $xpath cli
#expectpart "$(echo -n | unbuffer -p $clixon_cli -1 -f $cfg -l o show pagination xpath $xpath cli)" 0 foo
if [ $BE -ne 0 ]; then
new "Kill backend"
# Check if premature kill
pid=$(pgrep -u root -f clixon_backend)
if [ -z "$pid" ]; then
err "backend already dead"
fi
# kill backend
stop_backend -f $cfg
fi
testrun_stop
fi # interactive
unset validatexml
unset perfnr
unset xpath
rm -rf $dir